From 180b40af0031fbc47e765e11fddcb0a0e2062429 Mon Sep 17 00:00:00 2001 From: Aayush Seth Date: Wed, 31 Dec 2025 13:20:05 -0800 Subject: [PATCH 1/8] use agentic search endpoint --- .../explore/spans/spansTabSeerComboBox.tsx | 44 ++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/static/app/views/explore/spans/spansTabSeerComboBox.tsx b/static/app/views/explore/spans/spansTabSeerComboBox.tsx index 49f4c180885f73..fec662d9412172 100644 --- a/static/app/views/explore/spans/spansTabSeerComboBox.tsx +++ b/static/app/views/explore/spans/spansTabSeerComboBox.tsx @@ -47,6 +47,11 @@ interface TraceAskSeerSearchResponse { unsupported_reason: string | null; } +interface TraceAskSeerTranslateResponse { + query: string; + status: string; +} + export function SpansTabSeerComboBox() { const navigate = useNavigate(); const {projects} = useProjects(); @@ -60,6 +65,11 @@ export function SpansTabSeerComboBox() { enableAISearch, } = useSearchQueryBuilder(); + // Check if the new translation endpoint should be used (internal testing) + const useTranslateEndpoint = organization.features.includes( + 'gen-ai-explore-traces-translate' + ); + let initialSeerQuery = ''; const queryDetails = useMemo(() => { const queryToUse = committedQuery.length > 0 ? committedQuery : query; @@ -99,6 +109,35 @@ export function SpansTabSeerComboBox() { ? pageFilters.selection.projects : projects.filter(p => p.isMember).map(p => p.id); + // Use new translation endpoint if feature flag is enabled + if (useTranslateEndpoint) { + const data = await fetchMutation({ + url: `/organizations/${organization.slug}/search-agent/translate/`, + method: 'POST', + data: { + natural_language_query: queryToSubmit, + project_ids: selectedProjects, + }, + }); + + // Convert single query response to the expected format + return { + status: data.status, + unsupported_reason: null, + queries: [ + { + visualizations: [], + query: data.query, + sort: '', + groupBys: [], + statsPeriod: '', + mode: 'spans', + }, + ], + }; + } + + // Use the original endpoint const data = await fetchMutation({ url: `/organizations/${organization.slug}/trace-explorer-ai/query/`, method: 'POST', @@ -200,7 +239,10 @@ export function SpansTabSeerComboBox() { !organization?.hideAiFeatures && organization.features.includes('gen-ai-features'); - useTraceExploreAiQuerySetup({enableAISearch: areAiFeaturesAllowed}); + // Skip setup call when using the translation endpoint (it doesn't require setup) + useTraceExploreAiQuerySetup({ + enableAISearch: areAiFeaturesAllowed && !useTranslateEndpoint, + }); return ( Date: Thu, 1 Jan 2026 12:54:38 -0800 Subject: [PATCH 2/8] update token visuals --- .../askSeerCombobox/askSeerComboBox.tsx | 2 + .../askSeerCombobox/queryTokens.tsx | 58 +++++++++- .../askSeerCombobox/types.ts | 2 + .../askSeerCombobox/utils.ts | 49 +++++++- .../explore/spans/spansTabSeerComboBox.tsx | 107 +++++++++++++----- 5 files changed, 188 insertions(+), 30 deletions(-) diff --git a/static/app/components/searchQueryBuilder/askSeerCombobox/askSeerComboBox.tsx b/static/app/components/searchQueryBuilder/askSeerCombobox/askSeerComboBox.tsx index c18e43a0fa33d4..59a2dcb7f23b7a 100644 --- a/static/app/components/searchQueryBuilder/askSeerCombobox/askSeerComboBox.tsx +++ b/static/app/components/searchQueryBuilder/askSeerCombobox/askSeerComboBox.tsx @@ -249,6 +249,8 @@ export function AskSeerComboBox({ query={item?.query} groupBys={item?.groupBys} statsPeriod={item?.statsPeriod} + start={item?.start} + end={item?.end} visualizations={item?.visualizations} /> diff --git a/static/app/components/searchQueryBuilder/askSeerCombobox/queryTokens.tsx b/static/app/components/searchQueryBuilder/askSeerCombobox/queryTokens.tsx index bcd58ab90979d5..a150b20f3ad92e 100644 --- a/static/app/components/searchQueryBuilder/askSeerCombobox/queryTokens.tsx +++ b/static/app/components/searchQueryBuilder/askSeerCombobox/queryTokens.tsx @@ -6,11 +6,59 @@ import {ProvidedFormattedQuery} from 'sentry/components/searchQueryBuilder/forma import {parseQueryBuilderValue} from 'sentry/components/searchQueryBuilder/utils'; import {t} from 'sentry/locale'; +function formatDateRange(start: string, end: string): string { + // Treat UTC dates as local dates by removing the 'Z' suffix + // This ensures "2025-12-06T00:00:00Z" displays as Dec 6th in the user's timezone + const startLocal = start.endsWith('Z') ? start.slice(0, -1) : start; + const endLocal = end.endsWith('Z') ? end.slice(0, -1) : end; + + const startDate = new Date(startLocal); + const endDate = new Date(endLocal); + + // Check if times are at midnight (date-only range) + const startIsMidnight = + startDate.getHours() === 0 && + startDate.getMinutes() === 0 && + startDate.getSeconds() === 0; + const endIsMidnight = + endDate.getHours() === 0 && endDate.getMinutes() === 0 && endDate.getSeconds() === 0; + const endIsEndOfDay = + endDate.getHours() === 23 && + endDate.getMinutes() === 59 && + endDate.getSeconds() === 59; + + // Use date-only format if both are midnight or end of day + const useDateOnly = startIsMidnight && (endIsMidnight || endIsEndOfDay); + + const dateOptions: Intl.DateTimeFormatOptions = { + month: 'short', + day: 'numeric', + year: 'numeric', + }; + + const dateTimeOptions: Intl.DateTimeFormatOptions = { + month: 'short', + day: 'numeric', + year: 'numeric', + hour: 'numeric', + minute: '2-digit', + }; + + const formatOptions = useDateOnly ? dateOptions : dateTimeOptions; + + const startFormatted = startDate.toLocaleString('en-US', formatOptions); + const endFormatted = endDate.toLocaleString('en-US', formatOptions); + + return `${startFormatted} - ${endFormatted}`; +} + function QueryTokens({ groupBys, query, sort, statsPeriod, + start, + end, visualizations, }: QueryTokensProps) { const tokens = []; @@ -55,7 +103,15 @@ function QueryTokens({ ); } - if (statsPeriod && statsPeriod.length > 0) { + // Display absolute date range if start and end are provided + if (start && end) { + tokens.push( + + {t('Time Range')} + {formatDateRange(start, end)} + + ); + } else if (statsPeriod && statsPeriod.length > 0) { tokens.push( {t('Time Range')} diff --git a/static/app/components/searchQueryBuilder/askSeerCombobox/types.ts b/static/app/components/searchQueryBuilder/askSeerCombobox/types.ts index 2f7511fd1fe1be..227e99318d2b72 100644 --- a/static/app/components/searchQueryBuilder/askSeerCombobox/types.ts +++ b/static/app/components/searchQueryBuilder/askSeerCombobox/types.ts @@ -12,9 +12,11 @@ interface AskSeerSearchItem { export type AskSeerSearchItems = (AskSeerSearchItem & T) | NoneOfTheseItem; export interface QueryTokensProps { + end?: string | null; groupBys?: string[]; query?: string; sort?: string; + start?: string | null; statsPeriod?: string; visualizations?: Array<{chartType: ChartType; yAxes: string[]}>; } diff --git a/static/app/components/searchQueryBuilder/askSeerCombobox/utils.ts b/static/app/components/searchQueryBuilder/askSeerCombobox/utils.ts index ea23e82fedaaaa..06e0fe89d59e90 100644 --- a/static/app/components/searchQueryBuilder/askSeerCombobox/utils.ts +++ b/static/app/components/searchQueryBuilder/askSeerCombobox/utils.ts @@ -79,6 +79,50 @@ export function formatQueryToNaturalLanguage(query: string): string { return `${formattedQuery} `; } +function formatDateRangeForText(start: string, end: string): string { + // Treat UTC dates as local dates by removing the 'Z' suffix + const startLocal = start.endsWith('Z') ? start.slice(0, -1) : start; + const endLocal = end.endsWith('Z') ? end.slice(0, -1) : end; + + const startDate = new Date(startLocal); + const endDate = new Date(endLocal); + + // Check if times are at midnight (date-only range) + const startIsMidnight = + startDate.getHours() === 0 && + startDate.getMinutes() === 0 && + startDate.getSeconds() === 0; + const endIsMidnight = + endDate.getHours() === 0 && endDate.getMinutes() === 0 && endDate.getSeconds() === 0; + const endIsEndOfDay = + endDate.getHours() === 23 && + endDate.getMinutes() === 59 && + endDate.getSeconds() === 59; + + const useDateOnly = startIsMidnight && (endIsMidnight || endIsEndOfDay); + + const dateOptions: Intl.DateTimeFormatOptions = { + month: 'short', + day: 'numeric', + year: 'numeric', + }; + + const dateTimeOptions: Intl.DateTimeFormatOptions = { + month: 'short', + day: 'numeric', + year: 'numeric', + hour: 'numeric', + minute: '2-digit', + }; + + const formatOptions = useDateOnly ? dateOptions : dateTimeOptions; + + const startFormatted = startDate.toLocaleString('en-US', formatOptions); + const endFormatted = endDate.toLocaleString('en-US', formatOptions); + + return `${startFormatted} to ${endFormatted}`; +} + export function generateQueryTokensString(args: QueryTokensProps): string { const parts = []; @@ -103,7 +147,10 @@ export function generateQueryTokensString(args: QueryTokensProps): string { parts.push(`groupBys are '${groupByText}'`); } - if (args?.statsPeriod && args.statsPeriod.length > 0) { + // Prefer absolute date range over statsPeriod + if (args?.start && args?.end) { + parts.push(`time range is '${formatDateRangeForText(args.start, args.end)}'`); + } else if (args?.statsPeriod && args.statsPeriod.length > 0) { parts.push(`time range is '${args?.statsPeriod}'`); } diff --git a/static/app/views/explore/spans/spansTabSeerComboBox.tsx b/static/app/views/explore/spans/spansTabSeerComboBox.tsx index fec662d9412172..2a2e96d0a3a173 100644 --- a/static/app/views/explore/spans/spansTabSeerComboBox.tsx +++ b/static/app/views/explore/spans/spansTabSeerComboBox.tsx @@ -23,10 +23,12 @@ interface Visualization { } interface AskSeerSearchQuery { + end: string | null; groupBys: string[]; mode: string; query: string; sort: string; + start: string | null; statsPeriod: string; visualizations: Visualization[]; } @@ -48,8 +50,20 @@ interface TraceAskSeerSearchResponse { } interface TraceAskSeerTranslateResponse { - query: string; - status: string; + responses: Array<{ + end: string | null; + group_by: string[]; + mode: string; + query: string; + sort: string; + start: string | null; + stats_period: string; + unsupported_reason: string | null; + visualization: Array<{ + chart_type: number; + y_axes: string[]; + }>; + }>; } export function SpansTabSeerComboBox() { @@ -66,9 +80,10 @@ export function SpansTabSeerComboBox() { } = useSearchQueryBuilder(); // Check if the new translation endpoint should be used (internal testing) - const useTranslateEndpoint = organization.features.includes( - 'gen-ai-explore-traces-translate' - ); + // const useTranslateEndpoint = organization.features.includes( + // 'gen-ai-explore-traces-translate' + // ); + const useTranslateEndpoint = true; // TODO: Testing let initialSeerQuery = ''; const queryDetails = useMemo(() => { @@ -120,20 +135,24 @@ export function SpansTabSeerComboBox() { }, }); - // Convert single query response to the expected format + // Convert responses array to the expected format return { - status: data.status, - unsupported_reason: null, - queries: [ - { - visualizations: [], - query: data.query, - sort: '', - groupBys: [], - statsPeriod: '', - mode: 'spans', - }, - ], + status: 'ok', + unsupported_reason: data.responses[0]?.unsupported_reason ?? null, + queries: data.responses.map(r => ({ + visualizations: + r?.visualization?.map(v => ({ + chartType: v?.chart_type, + yAxes: v?.y_axes, + })) ?? [], + query: r?.query ?? '', + sort: r?.sort ?? '', + groupBys: r?.group_by ?? [], + statsPeriod: r?.stats_period ?? '', + start: r?.start ?? null, + end: r?.end ?? null, + mode: r?.mode ?? 'spans', + })), }; } @@ -161,6 +180,8 @@ export function SpansTabSeerComboBox() { sort: q?.sort ?? '', groupBys: q?.group_by ?? [], statsPeriod: q?.stats_period ?? '', + start: null, + end: null, mode: q?.mode ?? 'spans', })), }; @@ -170,17 +191,43 @@ export function SpansTabSeerComboBox() { const applySeerSearchQuery = useCallback( (result: AskSeerSearchQuery) => { if (!result) return; - const {query: queryToUse, visualizations, groupBys, sort, statsPeriod} = result; + const { + query: queryToUse, + visualizations, + groupBys, + sort, + statsPeriod, + start: resultStart, + end: resultEnd, + } = result; + + // Use start/end from result if provided, otherwise fall back to page filters + let start: string | null | undefined; + let end: string | null | undefined; - const startFilter = pageFilters.selection.datetime.start?.valueOf(); - const start = startFilter - ? new Date(startFilter).toISOString() - : pageFilters.selection.datetime.start; + if (resultStart && resultEnd) { + // Treat UTC dates as local dates by removing the 'Z' suffix + // This ensures "2025-12-06T00:00:00Z" is interpreted as Dec 6th midnight local time + const startLocal = resultStart.endsWith('Z') + ? resultStart.slice(0, -1) + : resultStart; + const endLocal = resultEnd.endsWith('Z') ? resultEnd.slice(0, -1) : resultEnd; - const endFilter = pageFilters.selection.datetime.end?.valueOf(); - const end = endFilter - ? new Date(endFilter).toISOString() - : pageFilters.selection.datetime.end; + // Convert to ISO string format expected by the page filters + start = new Date(startLocal).toISOString(); + end = new Date(endLocal).toISOString(); + } else { + // Fall back to page filters + const startFilter = pageFilters.selection.datetime.start?.valueOf(); + start = startFilter + ? new Date(startFilter).toISOString() + : pageFilters.selection.datetime.start; + + const endFilter = pageFilters.selection.datetime.end?.valueOf(); + end = endFilter + ? new Date(endFilter).toISOString() + : pageFilters.selection.datetime.end; + } const selection = { ...pageFilters.selection, @@ -188,7 +235,11 @@ export function SpansTabSeerComboBox() { start, end, utc: pageFilters.selection.datetime.utc, - period: statsPeriod || pageFilters.selection.datetime.period, + // Only use statsPeriod if we don't have explicit start/end from result + period: + resultStart && resultEnd + ? null + : statsPeriod || pageFilters.selection.datetime.period, }, }; From de34a4e17e8910643cd308d1cc18ab501222b4d8 Mon Sep 17 00:00:00 2001 From: Aayush Seth Date: Fri, 2 Jan 2026 12:28:13 -0800 Subject: [PATCH 3/8] typing --- .../explore/spans/spansTabSeerComboBox.tsx | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/static/app/views/explore/spans/spansTabSeerComboBox.tsx b/static/app/views/explore/spans/spansTabSeerComboBox.tsx index 2a2e96d0a3a173..e3ee816c2823a9 100644 --- a/static/app/views/explore/spans/spansTabSeerComboBox.tsx +++ b/static/app/views/explore/spans/spansTabSeerComboBox.tsx @@ -5,6 +5,7 @@ import {useSearchQueryBuilder} from 'sentry/components/searchQueryBuilder/contex import {parseQueryBuilderValue} from 'sentry/components/searchQueryBuilder/utils'; import {Token} from 'sentry/components/searchSyntax/parser'; import {stringifyToken} from 'sentry/components/searchSyntax/utils'; +import type {DateString} from 'sentry/types/core'; import {trackAnalytics} from 'sentry/utils/analytics'; import {getFieldDefinition} from 'sentry/utils/fields'; import {fetchMutation, mutationOptions} from 'sentry/utils/queryClient'; @@ -202,8 +203,8 @@ export function SpansTabSeerComboBox() { } = result; // Use start/end from result if provided, otherwise fall back to page filters - let start: string | null | undefined; - let end: string | null | undefined; + let start: DateString = null; + let end: DateString = null; if (resultStart && resultEnd) { // Treat UTC dates as local dates by removing the 'Z' suffix @@ -217,16 +218,9 @@ export function SpansTabSeerComboBox() { start = new Date(startLocal).toISOString(); end = new Date(endLocal).toISOString(); } else { - // Fall back to page filters - const startFilter = pageFilters.selection.datetime.start?.valueOf(); - start = startFilter - ? new Date(startFilter).toISOString() - : pageFilters.selection.datetime.start; - - const endFilter = pageFilters.selection.datetime.end?.valueOf(); - end = endFilter - ? new Date(endFilter).toISOString() - : pageFilters.selection.datetime.end; + // Fall back to page filters directly - DateString type is compatible + start = pageFilters.selection.datetime.start; + end = pageFilters.selection.datetime.end; } const selection = { From 83078eeb8a4eec1ca8614d8877f1cec3433a9d82 Mon Sep 17 00:00:00 2001 From: Aayush Seth Date: Fri, 2 Jan 2026 13:29:40 -0800 Subject: [PATCH 4/8] clean up --- .../explore/spans/spansTabSeerComboBox.tsx | 20 ++++--------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/static/app/views/explore/spans/spansTabSeerComboBox.tsx b/static/app/views/explore/spans/spansTabSeerComboBox.tsx index e3ee816c2823a9..87b9cc726437c3 100644 --- a/static/app/views/explore/spans/spansTabSeerComboBox.tsx +++ b/static/app/views/explore/spans/spansTabSeerComboBox.tsx @@ -80,11 +80,9 @@ export function SpansTabSeerComboBox() { enableAISearch, } = useSearchQueryBuilder(); - // Check if the new translation endpoint should be used (internal testing) - // const useTranslateEndpoint = organization.features.includes( - // 'gen-ai-explore-traces-translate' - // ); - const useTranslateEndpoint = true; // TODO: Testing + const useTranslateEndpoint = organization.features.includes( + 'gen-ai-explore-traces-translate' + ); let initialSeerQuery = ''; const queryDetails = useMemo(() => { @@ -125,7 +123,6 @@ export function SpansTabSeerComboBox() { ? pageFilters.selection.projects : projects.filter(p => p.isMember).map(p => p.id); - // Use new translation endpoint if feature flag is enabled if (useTranslateEndpoint) { const data = await fetchMutation({ url: `/organizations/${organization.slug}/search-agent/translate/`, @@ -136,7 +133,6 @@ export function SpansTabSeerComboBox() { }, }); - // Convert responses array to the expected format return { status: 'ok', unsupported_reason: data.responses[0]?.unsupported_reason ?? null, @@ -157,7 +153,6 @@ export function SpansTabSeerComboBox() { }; } - // Use the original endpoint const data = await fetchMutation({ url: `/organizations/${organization.slug}/trace-explorer-ai/query/`, method: 'POST', @@ -202,23 +197,18 @@ export function SpansTabSeerComboBox() { end: resultEnd, } = result; - // Use start/end from result if provided, otherwise fall back to page filters let start: DateString = null; let end: DateString = null; if (resultStart && resultEnd) { - // Treat UTC dates as local dates by removing the 'Z' suffix - // This ensures "2025-12-06T00:00:00Z" is interpreted as Dec 6th midnight local time + // Strip 'Z' suffix to treat UTC dates as local time const startLocal = resultStart.endsWith('Z') ? resultStart.slice(0, -1) : resultStart; const endLocal = resultEnd.endsWith('Z') ? resultEnd.slice(0, -1) : resultEnd; - - // Convert to ISO string format expected by the page filters start = new Date(startLocal).toISOString(); end = new Date(endLocal).toISOString(); } else { - // Fall back to page filters directly - DateString type is compatible start = pageFilters.selection.datetime.start; end = pageFilters.selection.datetime.end; } @@ -229,7 +219,6 @@ export function SpansTabSeerComboBox() { start, end, utc: pageFilters.selection.datetime.utc, - // Only use statsPeriod if we don't have explicit start/end from result period: resultStart && resultEnd ? null @@ -284,7 +273,6 @@ export function SpansTabSeerComboBox() { !organization?.hideAiFeatures && organization.features.includes('gen-ai-features'); - // Skip setup call when using the translation endpoint (it doesn't require setup) useTraceExploreAiQuerySetup({ enableAISearch: areAiFeaturesAllowed && !useTranslateEndpoint, }); From 0cd9676c4e62143c398cbc0ca5903a37281b0323 Mon Sep 17 00:00:00 2001 From: Aayush Seth Date: Fri, 2 Jan 2026 13:34:38 -0800 Subject: [PATCH 5/8] update ff name --- static/app/views/explore/spans/spansTabSeerComboBox.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/app/views/explore/spans/spansTabSeerComboBox.tsx b/static/app/views/explore/spans/spansTabSeerComboBox.tsx index 87b9cc726437c3..aadab1e4927e12 100644 --- a/static/app/views/explore/spans/spansTabSeerComboBox.tsx +++ b/static/app/views/explore/spans/spansTabSeerComboBox.tsx @@ -81,7 +81,7 @@ export function SpansTabSeerComboBox() { } = useSearchQueryBuilder(); const useTranslateEndpoint = organization.features.includes( - 'gen-ai-explore-traces-translate' + 'gen-ai-search-agent-translate' ); let initialSeerQuery = ''; From 631e862f5bb3731e956559b514e06bb55d204352 Mon Sep 17 00:00:00 2001 From: Aayush Seth Date: Fri, 2 Jan 2026 17:00:20 -0800 Subject: [PATCH 6/8] fix unsupported reason --- static/app/views/explore/spans/spansTabSeerComboBox.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/static/app/views/explore/spans/spansTabSeerComboBox.tsx b/static/app/views/explore/spans/spansTabSeerComboBox.tsx index aadab1e4927e12..8fcfedfe6ac806 100644 --- a/static/app/views/explore/spans/spansTabSeerComboBox.tsx +++ b/static/app/views/explore/spans/spansTabSeerComboBox.tsx @@ -59,12 +59,12 @@ interface TraceAskSeerTranslateResponse { sort: string; start: string | null; stats_period: string; - unsupported_reason: string | null; visualization: Array<{ chart_type: number; y_axes: string[]; }>; }>; + unsupported_reason: string | null; } export function SpansTabSeerComboBox() { @@ -83,7 +83,6 @@ export function SpansTabSeerComboBox() { const useTranslateEndpoint = organization.features.includes( 'gen-ai-search-agent-translate' ); - let initialSeerQuery = ''; const queryDetails = useMemo(() => { const queryToUse = committedQuery.length > 0 ? committedQuery : query; @@ -135,7 +134,7 @@ export function SpansTabSeerComboBox() { return { status: 'ok', - unsupported_reason: data.responses[0]?.unsupported_reason ?? null, + unsupported_reason: data.unsupported_reason, queries: data.responses.map(r => ({ visualizations: r?.visualization?.map(v => ({ From 7e5efd3e5eaf36879f94a91549275600afc6714a Mon Sep 17 00:00:00 2001 From: Aayush Seth Date: Mon, 5 Jan 2026 11:02:27 -0800 Subject: [PATCH 7/8] use moment --- .../askSeerCombobox/queryTokens.tsx | 47 +++++++------------ 1 file changed, 17 insertions(+), 30 deletions(-) diff --git a/static/app/components/searchQueryBuilder/askSeerCombobox/queryTokens.tsx b/static/app/components/searchQueryBuilder/askSeerCombobox/queryTokens.tsx index a150b20f3ad92e..1af47193bb1f53 100644 --- a/static/app/components/searchQueryBuilder/askSeerCombobox/queryTokens.tsx +++ b/static/app/components/searchQueryBuilder/askSeerCombobox/queryTokens.tsx @@ -1,4 +1,5 @@ import styled from '@emotion/styled'; +import moment from 'moment-timezone'; import type {QueryTokensProps} from 'sentry/components/searchQueryBuilder/askSeerCombobox/types'; import {useSearchQueryBuilder} from 'sentry/components/searchQueryBuilder/context'; @@ -7,47 +8,33 @@ import {parseQueryBuilderValue} from 'sentry/components/searchQueryBuilder/utils import {t} from 'sentry/locale'; function formatDateRange(start: string, end: string): string { - // Treat UTC dates as local dates by removing the 'Z' suffix - // This ensures "2025-12-06T00:00:00Z" displays as Dec 6th in the user's timezone - const startLocal = start.endsWith('Z') ? start.slice(0, -1) : start; - const endLocal = end.endsWith('Z') ? end.slice(0, -1) : end; - - const startDate = new Date(startLocal); - const endDate = new Date(endLocal); + // Parse as UTC but display the UTC values directly (without timezone conversion) + // The endpoint returns times in UTC format, but the values represent what the user + // intended in their local context. E.g., if user asks for "9pm", endpoint returns + // "T21:00:00Z" - we want to display "9:00 PM", not convert to local timezone. + const startMoment = moment.utc(start); + const endMoment = moment.utc(end); // Check if times are at midnight (date-only range) const startIsMidnight = - startDate.getHours() === 0 && - startDate.getMinutes() === 0 && - startDate.getSeconds() === 0; + startMoment.hours() === 0 && + startMoment.minutes() === 0 && + startMoment.seconds() === 0; const endIsMidnight = - endDate.getHours() === 0 && endDate.getMinutes() === 0 && endDate.getSeconds() === 0; + endMoment.hours() === 0 && endMoment.minutes() === 0 && endMoment.seconds() === 0; const endIsEndOfDay = - endDate.getHours() === 23 && - endDate.getMinutes() === 59 && - endDate.getSeconds() === 59; + endMoment.hours() === 23 && endMoment.minutes() === 59 && endMoment.seconds() === 59; // Use date-only format if both are midnight or end of day const useDateOnly = startIsMidnight && (endIsMidnight || endIsEndOfDay); - const dateOptions: Intl.DateTimeFormatOptions = { - month: 'short', - day: 'numeric', - year: 'numeric', - }; - - const dateTimeOptions: Intl.DateTimeFormatOptions = { - month: 'short', - day: 'numeric', - year: 'numeric', - hour: 'numeric', - minute: '2-digit', - }; + const dateFormat = 'MMM D, YYYY'; + const dateTimeFormat = 'MMM D, YYYY h:mm A'; - const formatOptions = useDateOnly ? dateOptions : dateTimeOptions; + const formatStr = useDateOnly ? dateFormat : dateTimeFormat; - const startFormatted = startDate.toLocaleString('en-US', formatOptions); - const endFormatted = endDate.toLocaleString('en-US', formatOptions); + const startFormatted = startMoment.format(formatStr); + const endFormatted = endMoment.format(formatStr); return `${startFormatted} - ${endFormatted}`; } From fa24f275537cdf121bc83bf21088e798eafd9ea9 Mon Sep 17 00:00:00 2001 From: Aayush Seth Date: Mon, 5 Jan 2026 11:14:13 -0800 Subject: [PATCH 8/8] move to utils --- .../askSeerCombobox/queryTokens.tsx | 36 +----------- .../askSeerCombobox/utils.ts | 58 +++++++++---------- 2 files changed, 28 insertions(+), 66 deletions(-) diff --git a/static/app/components/searchQueryBuilder/askSeerCombobox/queryTokens.tsx b/static/app/components/searchQueryBuilder/askSeerCombobox/queryTokens.tsx index 1af47193bb1f53..5720f1a43307f9 100644 --- a/static/app/components/searchQueryBuilder/askSeerCombobox/queryTokens.tsx +++ b/static/app/components/searchQueryBuilder/askSeerCombobox/queryTokens.tsx @@ -1,44 +1,12 @@ import styled from '@emotion/styled'; -import moment from 'moment-timezone'; import type {QueryTokensProps} from 'sentry/components/searchQueryBuilder/askSeerCombobox/types'; +import {formatDateRange} from 'sentry/components/searchQueryBuilder/askSeerCombobox/utils'; import {useSearchQueryBuilder} from 'sentry/components/searchQueryBuilder/context'; import {ProvidedFormattedQuery} from 'sentry/components/searchQueryBuilder/formattedQuery'; import {parseQueryBuilderValue} from 'sentry/components/searchQueryBuilder/utils'; import {t} from 'sentry/locale'; -function formatDateRange(start: string, end: string): string { - // Parse as UTC but display the UTC values directly (without timezone conversion) - // The endpoint returns times in UTC format, but the values represent what the user - // intended in their local context. E.g., if user asks for "9pm", endpoint returns - // "T21:00:00Z" - we want to display "9:00 PM", not convert to local timezone. - const startMoment = moment.utc(start); - const endMoment = moment.utc(end); - - // Check if times are at midnight (date-only range) - const startIsMidnight = - startMoment.hours() === 0 && - startMoment.minutes() === 0 && - startMoment.seconds() === 0; - const endIsMidnight = - endMoment.hours() === 0 && endMoment.minutes() === 0 && endMoment.seconds() === 0; - const endIsEndOfDay = - endMoment.hours() === 23 && endMoment.minutes() === 59 && endMoment.seconds() === 59; - - // Use date-only format if both are midnight or end of day - const useDateOnly = startIsMidnight && (endIsMidnight || endIsEndOfDay); - - const dateFormat = 'MMM D, YYYY'; - const dateTimeFormat = 'MMM D, YYYY h:mm A'; - - const formatStr = useDateOnly ? dateFormat : dateTimeFormat; - - const startFormatted = startMoment.format(formatStr); - const endFormatted = endMoment.format(formatStr); - - return `${startFormatted} - ${endFormatted}`; -} - function QueryTokens({ groupBys, query, @@ -95,7 +63,7 @@ function QueryTokens({ tokens.push( {t('Time Range')} - {formatDateRange(start, end)} + {formatDateRange(start, end, ' - ')} ); } else if (statsPeriod && statsPeriod.length > 0) { diff --git a/static/app/components/searchQueryBuilder/askSeerCombobox/utils.ts b/static/app/components/searchQueryBuilder/askSeerCombobox/utils.ts index 06e0fe89d59e90..3cd07ca499288f 100644 --- a/static/app/components/searchQueryBuilder/askSeerCombobox/utils.ts +++ b/static/app/components/searchQueryBuilder/askSeerCombobox/utils.ts @@ -1,3 +1,5 @@ +import moment from 'moment-timezone'; + import type { AskSeerSearchItems, NoneOfTheseItem, @@ -79,48 +81,40 @@ export function formatQueryToNaturalLanguage(query: string): string { return `${formattedQuery} `; } -function formatDateRangeForText(start: string, end: string): string { - // Treat UTC dates as local dates by removing the 'Z' suffix - const startLocal = start.endsWith('Z') ? start.slice(0, -1) : start; - const endLocal = end.endsWith('Z') ? end.slice(0, -1) : end; - - const startDate = new Date(startLocal); - const endDate = new Date(endLocal); +/** + * Formats a date range for display. + * + * The endpoint returns times in UTC format, but the values represent what the user + * intended in their local context. E.g., if user asks for "9pm", endpoint returns + * "T21:00:00Z" - we want to display "9:00 PM", not convert to local timezone. + */ +export function formatDateRange(start: string, end: string, separator = ' to '): string { + // Parse as UTC but display the UTC values directly (without timezone conversion) + const startMoment = moment.utc(start); + const endMoment = moment.utc(end); // Check if times are at midnight (date-only range) const startIsMidnight = - startDate.getHours() === 0 && - startDate.getMinutes() === 0 && - startDate.getSeconds() === 0; + startMoment.hours() === 0 && + startMoment.minutes() === 0 && + startMoment.seconds() === 0; const endIsMidnight = - endDate.getHours() === 0 && endDate.getMinutes() === 0 && endDate.getSeconds() === 0; + endMoment.hours() === 0 && endMoment.minutes() === 0 && endMoment.seconds() === 0; const endIsEndOfDay = - endDate.getHours() === 23 && - endDate.getMinutes() === 59 && - endDate.getSeconds() === 59; + endMoment.hours() === 23 && endMoment.minutes() === 59 && endMoment.seconds() === 59; + // Use date-only format if both are midnight or end of day const useDateOnly = startIsMidnight && (endIsMidnight || endIsEndOfDay); - const dateOptions: Intl.DateTimeFormatOptions = { - month: 'short', - day: 'numeric', - year: 'numeric', - }; - - const dateTimeOptions: Intl.DateTimeFormatOptions = { - month: 'short', - day: 'numeric', - year: 'numeric', - hour: 'numeric', - minute: '2-digit', - }; + const dateFormat = 'MMM D, YYYY'; + const dateTimeFormat = 'MMM D, YYYY h:mm A'; - const formatOptions = useDateOnly ? dateOptions : dateTimeOptions; + const formatStr = useDateOnly ? dateFormat : dateTimeFormat; - const startFormatted = startDate.toLocaleString('en-US', formatOptions); - const endFormatted = endDate.toLocaleString('en-US', formatOptions); + const startFormatted = startMoment.format(formatStr); + const endFormatted = endMoment.format(formatStr); - return `${startFormatted} to ${endFormatted}`; + return `${startFormatted}${separator}${endFormatted}`; } export function generateQueryTokensString(args: QueryTokensProps): string { @@ -149,7 +143,7 @@ export function generateQueryTokensString(args: QueryTokensProps): string { // Prefer absolute date range over statsPeriod if (args?.start && args?.end) { - parts.push(`time range is '${formatDateRangeForText(args.start, args.end)}'`); + parts.push(`time range is '${formatDateRange(args.start, args.end)}'`); } else if (args?.statsPeriod && args.statsPeriod.length > 0) { parts.push(`time range is '${args?.statsPeriod}'`); }