From 9bdcbb27c198974f491c16980283fb2b23e44d9b Mon Sep 17 00:00:00 2001 From: Andrew Liu <159852527+aliu39@users.noreply.github.com> Date: Fri, 15 May 2026 17:38:03 -0700 Subject: [PATCH 1/3] feat(explore): Track error outcome in trackAiQueryOutcome Extend trackAiQueryOutcome to emit an `error_on_load` outcome (with an optional error_reason attribute) when a table result fails to load. Previously, errored queries were silently bucketed as `empty_results`, which conflated empty data with failures. Wire up the new `error` param from spans, logs, and metrics analytics hooks using the existing tableError values, boxed to avoid extra effect re-fires. Co-Authored-By: Claude --- .../searchQueryBuilder/askSeerCombobox/utils.ts | 12 +++++++++++- static/app/views/explore/hooks/useAnalytics.tsx | 17 +++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/static/app/components/searchQueryBuilder/askSeerCombobox/utils.ts b/static/app/components/searchQueryBuilder/askSeerCombobox/utils.ts index 82718a1aebb8..3337da4d12a5 100644 --- a/static/app/components/searchQueryBuilder/askSeerCombobox/utils.ts +++ b/static/app/components/searchQueryBuilder/askSeerCombobox/utils.ts @@ -15,6 +15,7 @@ export function trackAiQueryOutcome({ referrer, resultCount, runId, + error = false, }: { dataset: 'spans' | 'errors' | 'logs' | 'tracemetrics' | 'issues'; mode: Mode | 'samples' | 'aggregate'; @@ -22,15 +23,24 @@ export function trackAiQueryOutcome({ referrer: string; resultCount: number; runId: number; + error?: string | boolean; }) { + const outcome = error + ? 'error_on_load' + : resultCount > 0 + ? 'has_results' + : 'empty_results'; + const errorReason = typeof error === 'string' ? error : undefined; const attributes = { dataset, mode: mode.toString(), org_slug: orgSlug, referrer, run_id: runId, - outcome: resultCount > 0 ? 'has_results' : 'empty_results', + outcome, + error_reason: errorReason, }; + Sentry.logger.info('assisted_query.outcome', { ...attributes, result_count: resultCount, diff --git a/static/app/views/explore/hooks/useAnalytics.tsx b/static/app/views/explore/hooks/useAnalytics.tsx index 8fe203becff4..d29c525f661c 100644 --- a/static/app/views/explore/hooks/useAnalytics.tsx +++ b/static/app/views/explore/hooks/useAnalytics.tsx @@ -109,6 +109,7 @@ function useTrackAnalytics({ : (spansTableResult.result.error?.message ?? ''); const chartError = timeseriesResult.error?.message ?? ''; const query_status = tableError || chartError ? 'error' : 'success'; + const tableErrorBox = useBox(tableError); const {isLoading: isLoadingSeerSetup} = useOrganizationSeerSetup({ enabled: !organization.hideAiFeatures, @@ -172,6 +173,7 @@ function useTrackAnalytics({ referrer: 'spans', resultCount: aggregatesTableResult.result.data?.length ?? 0, runId: aiQueryRunId, + error: tableErrorBox.current || false, }); } @@ -219,6 +221,7 @@ function useTrackAnalytics({ query, queryType, query_status, + tableErrorBox, timeseriesResult.data, timeseriesResult.isPending, title, @@ -304,6 +307,7 @@ function useTrackAnalytics({ referrer: 'spans', resultCount: spansTableResult.result.data?.length ?? 0, runId: aiQueryRunId, + error: tableErrorBox.current || false, }); } }, [ @@ -327,6 +331,7 @@ function useTrackAnalytics({ spansTableResult.result.data?.length, spansTableResult.result.isPending, spansTableResult.result.meta?.dataScanned, + tableErrorBox, timeseriesResult.data, timeseriesResult.isPending, title, @@ -495,6 +500,7 @@ function useTrackAnalytics({ referrer: 'traces', resultCount: tracesTableResult.result.data?.json?.data?.length ?? 0, runId: aiQueryRunId, + error: tableErrorBox.current || false, }); } }, [ @@ -513,6 +519,7 @@ function useTrackAnalytics({ query, queryType, query_status, + tableErrorBox, timeseriesResult.data, timeseriesResult.isPending, title, @@ -659,6 +666,7 @@ export function useLogAnalytics({ const tableError = logsTableResult.error?.message ?? ''; const query_status = tableError ? 'error' : 'success'; + const tableErrorBox = useBox(tableError); const autorefreshEnabled = useLogsAutoRefreshEnabled(); const autorefreshBox = useBox(autorefreshEnabled); // Boxed to avoid useEffect firing analytics on changes. const aggregatesResultLengthBox = useBox( @@ -756,6 +764,7 @@ export function useLogAnalytics({ referrer: 'logs', resultCount: resultLengthBox.current, runId: aiQueryRunId, + error: tableErrorBox.current || false, }); } }, [ @@ -778,6 +787,7 @@ export function useLogAnalytics({ mode, resultLengthBox, sortBysBox, + tableErrorBox, yAxesBox, getRunIdForAnalytics, ]); @@ -846,6 +856,7 @@ export function useLogAnalytics({ referrer: 'logs', resultCount: aggregatesResultLengthBox.current, runId: aiQueryRunId, + error: tableErrorBox.current || false, }); } }, [ @@ -868,6 +879,7 @@ export function useLogAnalytics({ query, query_status, search, + tableErrorBox, yAxes, getRunIdForAnalytics, ]); @@ -951,6 +963,7 @@ export function useMetricsPanelAnalytics({ ? (metricAggregatesTableResult.result.error?.message ?? '') : (metricSamplesTableResult.error?.message ?? ''); const query_status = tableError ? 'error' : 'success'; + const tableErrorBox = useBox(tableError); const aggregatesResultLengthBox = useBox( metricAggregatesTableResult.result.data?.length || 0 @@ -1031,6 +1044,7 @@ export function useMetricsPanelAnalytics({ referrer: 'tracemetrics', resultCount: resultLengthBox.current, runId: aiQueryRunId, + error: tableErrorBox.current || false, }); } }, [ @@ -1055,6 +1069,7 @@ export function useMetricsPanelAnalytics({ aggregateFunctionBox, groupBysBox, metricTypeBox, + tableErrorBox, getRunIdForAnalytics, ]); @@ -1086,6 +1101,7 @@ export function useMetricsPanelAnalytics({ referrer: 'tracemetrics', resultCount: aggregatesResultLengthBox.current, runId: aiQueryRunId, + error: tableErrorBox.current || false, }); } }, [ @@ -1107,6 +1123,7 @@ export function useMetricsPanelAnalytics({ metricNameBox, query, query_status, + tableErrorBox, getRunIdForAnalytics, ]); } From ea54c734904417ebf43b89b3444d22768263ada0 Mon Sep 17 00:00:00 2001 From: Andrew Liu <159852527+aliu39@users.noreply.github.com> Date: Fri, 15 May 2026 17:58:44 -0700 Subject: [PATCH 2/3] feat(explore): Track API error reason in trackAiQueryOutcome Extend trackAiQueryOutcome's `error` param to accept `Error` (in addition to `string | boolean`). The util now centralizes reason extraction: for `RequestError` it pulls `responseJSON.detail` (handling both string and `{message}` shapes), otherwise it falls back to `err.message`. Wire this through in two places: - Discover errors: add a second trackAiQueryOutcome call in fetchTotalCount's catch block so failed total-count requests emit an error_on_load outcome with the API reason. - Explore (spans, traces, logs, metrics): pass the raw `Error` object from the table result through useBox to the outcome call instead of pre-extracting `error?.message`. Co-Authored-By: Claude --- .../askSeerCombobox/utils.ts | 23 +++++++++++++++++-- static/app/views/discover/results.tsx | 17 ++++++++++++-- .../app/views/explore/hooks/useAnalytics.tsx | 20 ++++++++-------- 3 files changed, 46 insertions(+), 14 deletions(-) diff --git a/static/app/components/searchQueryBuilder/askSeerCombobox/utils.ts b/static/app/components/searchQueryBuilder/askSeerCombobox/utils.ts index 3337da4d12a5..1179c5a73a9d 100644 --- a/static/app/components/searchQueryBuilder/askSeerCombobox/utils.ts +++ b/static/app/components/searchQueryBuilder/askSeerCombobox/utils.ts @@ -6,8 +6,22 @@ import type { NoneOfTheseItem, QueryTokensProps, } from 'sentry/components/searchQueryBuilder/askSeerCombobox/types'; +import {RequestError} from 'sentry/utils/requestError/requestError'; import {Mode} from 'sentry/views/explore/contexts/pageParamsContext/mode'; +function extractErrorReason(err: Error): string { + if (err instanceof RequestError) { + const detail = err.responseJSON?.detail; + if (typeof detail === 'string') { + return detail; + } + if (detail?.message) { + return detail.message; + } + } + return err.message; +} + export function trackAiQueryOutcome({ dataset, mode, @@ -23,14 +37,19 @@ export function trackAiQueryOutcome({ referrer: string; resultCount: number; runId: number; - error?: string | boolean; + error?: string | boolean | Error; }) { const outcome = error ? 'error_on_load' : resultCount > 0 ? 'has_results' : 'empty_results'; - const errorReason = typeof error === 'string' ? error : undefined; + const errorReason = + typeof error === 'string' + ? error + : error instanceof Error + ? extractErrorReason(error) + : undefined; const attributes = { dataset, mode: mode.toString(), diff --git a/static/app/views/discover/results.tsx b/static/app/views/discover/results.tsx index 32656d643e8a..6caecc875926 100644 --- a/static/app/views/discover/results.tsx +++ b/static/app/views/discover/results.tsx @@ -371,6 +371,9 @@ export class Results extends Component { return; } + const aiQueryRunId = getAiQueryRunId?.() ?? null; + const mode = eventView.hasAggregateField() ? 'aggregate' : 'samples'; + try { const totals = await fetchTotalCount( api, @@ -379,11 +382,10 @@ export class Results extends Component { ); this.setState({totalValues: totals}); - const aiQueryRunId = getAiQueryRunId?.() ?? null; if (aiQueryRunId !== null) { trackAiQueryOutcome({ dataset: 'errors', - mode: eventView.hasAggregateField() ? 'aggregate' : 'samples', + mode, referrer: 'errors', resultCount: totals, orgSlug: organization.slug, @@ -392,6 +394,17 @@ export class Results extends Component { } } catch (err) { Sentry.captureException(err); + if (aiQueryRunId !== null) { + trackAiQueryOutcome({ + dataset: 'errors', + mode, + referrer: 'errors', + resultCount: 0, + orgSlug: organization.slug, + runId: aiQueryRunId, + error: err instanceof Error ? err : true, + }); + } } } diff --git a/static/app/views/explore/hooks/useAnalytics.tsx b/static/app/views/explore/hooks/useAnalytics.tsx index d29c525f661c..d69c91f3c354 100644 --- a/static/app/views/explore/hooks/useAnalytics.tsx +++ b/static/app/views/explore/hooks/useAnalytics.tsx @@ -103,12 +103,12 @@ function useTrackAnalytics({ const tableError = queryType === 'aggregate' - ? (aggregatesTableResult.result.error?.message ?? '') + ? aggregatesTableResult.result.error : queryType === 'traces' - ? (tracesTableResult?.error?.message ?? '') - : (spansTableResult.result.error?.message ?? ''); - const chartError = timeseriesResult.error?.message ?? ''; - const query_status = tableError || chartError ? 'error' : 'success'; + ? tracesTableResult?.error + : spansTableResult.result.error; + const chartError = timeseriesResult.error; + const query_status = tableError?.message || chartError?.message ? 'error' : 'success'; const tableErrorBox = useBox(tableError); const {isLoading: isLoadingSeerSetup} = useOrganizationSeerSetup({ @@ -664,8 +664,8 @@ export function useLogAnalytics({ const fields = useQueryParamsFields(); const page_source = source; - const tableError = logsTableResult.error?.message ?? ''; - const query_status = tableError ? 'error' : 'success'; + const tableError = logsTableResult.error; + const query_status = tableError?.message ? 'error' : 'success'; const tableErrorBox = useBox(tableError); const autorefreshEnabled = useLogsAutoRefreshEnabled(); const autorefreshBox = useBox(autorefreshEnabled); // Boxed to avoid useEffect firing analytics on changes. @@ -960,9 +960,9 @@ export function useMetricsPanelAnalytics({ const tableError = mode === Mode.AGGREGATE - ? (metricAggregatesTableResult.result.error?.message ?? '') - : (metricSamplesTableResult.error?.message ?? ''); - const query_status = tableError ? 'error' : 'success'; + ? metricAggregatesTableResult.result.error + : metricSamplesTableResult.error; + const query_status = tableError?.message ? 'error' : 'success'; const tableErrorBox = useBox(tableError); const aggregatesResultLengthBox = useBox( From 6dbaa62d92d3edfbab89c7a84c48dcafbfd5ba46 Mon Sep 17 00:00:00 2001 From: Andrew Liu <159852527+aliu39@users.noreply.github.com> Date: Fri, 15 May 2026 18:25:43 -0700 Subject: [PATCH 3/3] fix agg log error --- static/app/views/explore/hooks/useAnalytics.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/static/app/views/explore/hooks/useAnalytics.tsx b/static/app/views/explore/hooks/useAnalytics.tsx index d69c91f3c354..8a51fe84e340 100644 --- a/static/app/views/explore/hooks/useAnalytics.tsx +++ b/static/app/views/explore/hooks/useAnalytics.tsx @@ -664,7 +664,8 @@ export function useLogAnalytics({ const fields = useQueryParamsFields(); const page_source = source; - const tableError = logsTableResult.error; + const tableError = + mode === Mode.AGGREGATE ? logsAggregatesTableResult.error : logsTableResult.error; const query_status = tableError?.message ? 'error' : 'success'; const tableErrorBox = useBox(tableError); const autorefreshEnabled = useLogsAutoRefreshEnabled();