Compare commits

...

3 Commits

Author SHA1 Message Date
Gaurav Tewari
ea51e47504 chore: minor refactor 2026-08-24 01:12:22 +05:30
Gaurav Tewari
89f3be486e chore(querybuilder): exercise builder_ai_query from the traces explorer
DEMO SCAFFOLD — remove before merge.

The AI o11y explorer page does not exist yet, so there is nowhere to run
the builder_ai_query plumbing end to end. This wires the traces explorer
to it behind `?aiDemo=1`, leaving the page byte-identical without the
param.

With the param set:
- all four views send builderQueryType: 'builder_ai_query', so
  compositeQuery.queries[].type changes on /query_range
- QuerySearch sends &type=builder_ai_query to /fields/keys
- the All/Root/Entrypoint span-scope select hides in List and Trace,
  matching the AI explorer's per-view policy

Each view's hand-built queryKey gains the flag as a discriminator.
Without it the two modes share a react-query cache entry, since all four
views override options.queryKey and none of the existing entries
distinguish them.

The real AI explorer page will pass these values directly, at which
point this commit and src/container/TracesExplorer/useIsAIQueryDemo.ts
can be dropped.
2026-08-06 17:12:36 +05:30
Gaurav Tewari
4a0c34c236 feat(querybuilder): support builder_ai_query envelope type
Adds the plumbing the AI o11y explorer needs from the query builder,
with every addition defaulted so existing behaviour is unchanged. No
caller sets the new options yet, so this is a no-op for all nine
current QueryBuilderV2 call sites.

Payload:
- add 'builder_ai_query' to the QueryType union, plus a narrowed
  BuilderQueryEnvelopeType alias. Narrow on purpose: the other members
  take a different spec shape and the backend decodes with
  DisallowUnknownFields, so passing one with a builder spec is a 400.
- convertBuilderQueriesToV5 takes the envelope type as a defaulted 4th
  param instead of hardcoding 'builder_query'.
- GetQueryResultsProps gains builderQueryType, threaded through
  prepareQueryRangePayloadV5 (which destructures explicitly, so it has
  to be named there — a spread does not reach it).

Response:
- mapQueryFromV5 is an if/else-if chain with no fallback, so an
  unrecognised envelope type was silently dropped rather than defaulted.
  builder_ai_query shares the builder-query spec shape and name
  namespace, so it now hydrates into queryData the same way.

Query builder props:
- showSpanScopeSelector (default true) lets a page hide the
  All/Root/Entrypoint select. Gated in QueryV2's memo so both render
  sites are covered. QueryProps already declared this prop and nothing
  read it; this gives that declaration meaning.
- fieldKeysQueryType is forwarded verbatim to /fields/keys as `type` so
  the backend can scope the suggested key set. Appended to the URL only
  when set, keeping every existing request byte-identical.

Deliberately not included: the AI explorer page itself, per-query AI
state, and 'trace' as a field context (the last needs a backend
decision — see frontend/docs/ai-explorer-qb-changeset.md).
2026-08-06 17:06:08 +05:30
12 changed files with 65 additions and 13 deletions

View File

@@ -23,6 +23,7 @@ export const getKeySuggestions = (
fieldDataType = '',
signalSource = '',
metricNamespace = '',
type,
} = props;
const encodedSignal = encodeURIComponent(signal);
@@ -32,8 +33,10 @@ export const getKeySuggestions = (
const encodedFieldDataType = encodeURIComponent(fieldDataType);
const encodedSource = encodeURIComponent(signalSource);
const encodedMetricNamespace = encodeURIComponent(metricNamespace);
// Appended only when set, so existing request URLs stay byte-identical.
const typeParam = type ? `&type=${encodeURIComponent(type)}` : '';
return axios.get(
`/fields/keys?signal=${encodedSignal}&searchText=${encodedSearchText}&metricName=${encodedMetricName}&fieldContext=${encodedFieldContext}&fieldDataType=${encodedFieldDataType}&source=${encodedSource}&metricNamespace=${encodedMetricNamespace}`,
`/fields/keys?signal=${encodedSignal}&searchText=${encodedSearchText}&metricName=${encodedMetricName}&fieldContext=${encodedFieldContext}&fieldDataType=${encodedFieldDataType}&source=${encodedSource}&metricNamespace=${encodedMetricNamespace}${typeParam}`,
);
};

View File

@@ -13,6 +13,7 @@ import {
} from 'types/api/queryBuilder/queryBuilderData';
import {
BaseBuilderQuery,
BuilderQueryEnvelopeType,
FieldContext,
FieldDataType,
Filter,
@@ -322,6 +323,7 @@ export function convertBuilderQueriesToV5(
builderQueries: Record<string, any>,
requestType: RequestType,
panelType?: PANEL_TYPES,
builderQueryType: BuilderQueryEnvelopeType = 'builder_query',
): QueryEnvelope[] {
return Object.entries(builderQueries).map(
([queryName, queryData]): QueryEnvelope => {
@@ -363,10 +365,7 @@ export function convertBuilderQueriesToV5(
break;
}
return {
type: 'builder_query' as QueryType,
spec,
};
return { type: builderQueryType, spec };
},
);
}
@@ -558,6 +557,7 @@ export const prepareQueryRangePayloadV5 = ({
originalGraphType,
fillGaps,
dynamicVariables,
builderQueryType = 'builder_query',
}: GetQueryResultsProps): PrepareQueryRangePayloadV5Result => {
let legendMap: Record<string, string> = {};
const requestType = mapPanelTypeToRequestType(graphType);
@@ -594,6 +594,7 @@ export const prepareQueryRangePayloadV5 = ({
currentQueryData.data,
requestType,
graphType,
builderQueryType,
);
// Convert formulas as separate query type

View File

@@ -22,6 +22,8 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
isListViewPanel = false,
showOnlyWhereClause = false,
showTraceOperator = false,
showSpanScopeSelector = true,
fieldKeysQueryType,
version,
onSignalSourceChange,
signalSourceChangeEnabled = false,
@@ -209,6 +211,8 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
queryVariant={config?.queryVariant || 'dropdown'}
showOnlyWhereClause={showOnlyWhereClause}
isListViewPanel={isListViewPanel}
showSpanScopeSelector={showSpanScopeSelector}
fieldKeysQueryType={fieldKeysQueryType}
signalSource={currentQuery.builder.queryData[0].source as 'meter' | ''}
onSignalSourceChange={onSignalSourceChange || ((): void => {})}
signalSourceChangeEnabled={signalSourceChangeEnabled}
@@ -232,6 +236,8 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
queryVariant={config?.queryVariant || 'dropdown'}
showOnlyWhereClause={showOnlyWhereClause}
isListViewPanel={isListViewPanel}
showSpanScopeSelector={showSpanScopeSelector}
fieldKeysQueryType={fieldKeysQueryType}
signalSource={query.source as 'meter' | ''}
onSignalSourceChange={onSignalSourceChange || ((): void => {})}
signalSourceChangeEnabled={signalSourceChangeEnabled}

View File

@@ -47,7 +47,10 @@ import { validateQuery } from 'utils/queryValidationUtils';
import { unquote } from 'utils/stringUtils';
import { getRecentQueries } from 'lib/recentQueries/getRecentQueries';
import type { SignalType } from 'types/api/v5/queryRange';
import type {
BuilderQueryEnvelopeType,
SignalType,
} from 'types/api/v5/queryRange';
import {
queryExamples,
@@ -111,6 +114,8 @@ interface QuerySearchProps {
numberValues: number[];
complete: boolean;
}>;
/** Sent to `/fields/keys` as `type`; not named `type` — that's taken on the response side. */
fieldKeysQueryType?: BuilderQueryEnvelopeType;
}
function QuerySearch({
@@ -125,6 +130,7 @@ function QuerySearch({
initialExpression,
metricNamespace,
valueSuggestionsOverride,
fieldKeysQueryType,
}: QuerySearchProps): JSX.Element {
const isDarkMode = useIsDarkMode();
const [valueSuggestions, setValueSuggestions] = useState<any[]>([]);
@@ -326,6 +332,7 @@ function QuerySearch({
metricName: debouncedMetricName ?? undefined,
signalSource: signalSource as 'meter' | '',
metricNamespace,
type: fieldKeysQueryType,
});
if (response.data.data) {
@@ -363,6 +370,7 @@ function QuerySearch({
hardcodedAttributeKeys,
showFilterSuggestionsWithoutMetric,
metricNamespace,
fieldKeysQueryType,
],
);

View File

@@ -37,6 +37,9 @@ export const QueryV2 = forwardRef(function QueryV2(
hasTraceOperator = false,
version,
showOnlyWhereClause = false,
// Aliased: the local memo below is also called `showSpanScopeSelector`.
showSpanScopeSelector: allowSpanScopeSelector = true,
fieldKeysQueryType,
signalSource = '',
isMultiQueryAllowed = false,
onSignalSourceChange,
@@ -94,8 +97,8 @@ export const QueryV2 = forwardRef(function QueryV2(
);
const showSpanScopeSelector = useMemo(
() => dataSource === DataSource.TRACES,
[dataSource],
() => dataSource === DataSource.TRACES && allowSpanScopeSelector,
[dataSource, allowSpanScopeSelector],
);
const showInlineQuerySearch = useMemo(() => {
@@ -182,6 +185,7 @@ export const QueryV2 = forwardRef(function QueryV2(
queryData={query}
dataSource={dataSource}
signalSource={signalSource}
fieldKeysQueryType={fieldKeysQueryType}
/>
</div>
@@ -252,6 +256,7 @@ export const QueryV2 = forwardRef(function QueryV2(
queryData={query}
dataSource={dataSource}
signalSource={signalSource}
fieldKeysQueryType={fieldKeysQueryType}
/>
</div>

View File

@@ -2,6 +2,7 @@ import { ReactNode } from 'react';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { WhereClauseConfig } from 'hooks/queryBuilder/useAutoComplete';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { BuilderQueryEnvelopeType } from 'types/api/v5/queryRange';
import { DataSource } from 'types/common/queryBuilder';
import { OrderByFilterProps } from './filters/OrderByFilter/OrderByFilter.interfaces';
@@ -33,6 +34,10 @@ export type QueryBuilderProps = {
showOnlyTraceOperator?: boolean;
showTraceViewSelector?: boolean;
showTraceOperator?: boolean;
/** Traces only, default true; false hides the All/Root/Entrypoint span-scope select. */
showSpanScopeSelector?: boolean;
/** Forwarded verbatim to `/fields/keys` as `type`; never interpreted by the builder. */
fieldKeysQueryType?: BuilderQueryEnvelopeType;
version: string;
onChangeTraceView?: (view: TraceView) => void;
onSignalSourceChange?: (value: string) => void;

View File

@@ -1,6 +1,7 @@
import { IQueryBuilderState } from 'constants/queryBuilder';
import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interfaces';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { BuilderQueryEnvelopeType } from 'types/api/v5/queryRange';
export interface InitialStateI {
search: string;
@@ -30,6 +31,7 @@ export type QueryProps = {
showFunctions?: boolean;
version: string;
showSpanScopeSelector?: boolean;
fieldKeysQueryType?: BuilderQueryEnvelopeType;
showOnlyWhereClause?: boolean;
showTraceOperator?: boolean;
hasTraceOperator?: boolean;

View File

@@ -23,6 +23,7 @@ import { IDashboardVariable } from 'types/api/dashboard/getAll';
import { MetricQueryRangeSuccessResponse } from 'types/api/metrics/getQueryRange';
import { IBuilderQuery, Query } from 'types/api/queryBuilder/queryBuilderData';
import {
BuilderQueryEnvelopeType,
ExecStats,
MetricRangePayloadV5,
QueryRangeResponseV5,
@@ -399,4 +400,6 @@ export interface GetQueryResultsProps {
step?: number;
originalGraphType?: PANEL_TYPES;
dynamicVariables?: IDashboardVariable[];
/** Envelope type stamped on every builder query; defaults to `builder_query`. */
builderQueryType?: BuilderQueryEnvelopeType;
}

View File

@@ -30,19 +30,23 @@ const mapQueryFromV5 = (compositeQuery: ICompositeMetricQuery): Query => {
> = {};
const builderQueryTypes: Record<
string,
'builder_query' | 'builder_formula' | 'builder_trace_operator'
| 'builder_query'
| 'builder_ai_query'
| 'builder_formula'
| 'builder_trace_operator'
> = {};
const promQueries: IPromQLQuery[] = [];
const clickhouseQueries: IClickHouseQuery[] = [];
compositeQuery.queries?.forEach((q) => {
const spec = q.spec as BuilderQuery | PromQuery | ClickHouseQuery;
if (q.type === 'builder_query') {
// Shares builder_query's spec shape, so it hydrates identically; without this branch it's dropped.
if (q.type === 'builder_query' || q.type === 'builder_ai_query') {
if (spec.name) {
builderQueries[spec.name] = convertBuilderQueryToIBuilderQuery(
spec as BuilderQuery,
);
builderQueryTypes[spec.name] = 'builder_query';
builderQueryTypes[spec.name] = q.type;
}
} else if (q.type === 'builder_formula') {
if (spec.name) {

View File

@@ -15,7 +15,10 @@ export const transformQueryBuilderDataModel = (
data: BuilderQueryDataResourse,
queryTypes?: Record<
string,
'builder_query' | 'builder_formula' | 'builder_trace_operator'
| 'builder_query'
| 'builder_ai_query'
| 'builder_formula'
| 'builder_trace_operator'
>,
): QueryBuilderData => {
const queryData: QueryBuilderData['queryData'] = [];

View File

@@ -1,4 +1,7 @@
import { FieldDataType } from 'types/api/v5/queryRange';
import {
BuilderQueryEnvelopeType,
FieldDataType,
} from 'types/api/v5/queryRange';
export interface QueryKeyDataSuggestionsProps {
label: string;
@@ -35,6 +38,8 @@ export interface QueryKeyRequestProps {
metricName?: string;
metricNamespace?: string;
signalSource?: 'meter' | '';
/** Scopes the key set to a sub-variant of the signal: `builder_ai_query` narrows traces to gen_ai keys. */
type?: BuilderQueryEnvelopeType;
}
export interface QueryKeyValueSuggestionsProps {

View File

@@ -16,6 +16,7 @@ export type RequestType =
export type QueryType =
| 'builder_query'
| 'builder_ai_query'
| 'builder_trace_operator'
| 'builder_formula'
| 'builder_sub_query'
@@ -23,6 +24,12 @@ export type QueryType =
| 'clickhouse_sql'
| 'promql';
/** Envelope types `convertBuilderQueriesToV5` may emit; the rest take a different spec shape and 400. */
export type BuilderQueryEnvelopeType = Extract<
QueryType,
'builder_query' | 'builder_ai_query'
>;
export type OrderDirection = 'asc' | 'desc';
export type JoinType = 'inner' | 'left' | 'right' | 'full' | 'cross';