Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,8 @@ export function AskSeerComboBox<T extends QueryTokensProps>({
query={item?.query}
groupBys={item?.groupBys}
statsPeriod={item?.statsPeriod}
start={item?.start}
end={item?.end}
visualizations={item?.visualizations}
/>
</Item>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import styled from '@emotion/styled';

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';
Expand All @@ -11,6 +12,8 @@ function QueryTokens({
query,
sort,
statsPeriod,
start,
end,
visualizations,
}: QueryTokensProps) {
const tokens = [];
Expand Down Expand Up @@ -55,7 +58,15 @@ function QueryTokens({
);
}

if (statsPeriod && statsPeriod.length > 0) {
// Display absolute date range if start and end are provided
if (start && end) {
tokens.push(
<Token key="timeRange">
<ExploreParamTitle>{t('Time Range')}</ExploreParamTitle>
<ExploreGroupBys>{formatDateRange(start, end, ' - ')}</ExploreGroupBys>
</Token>
);
} else if (statsPeriod && statsPeriod.length > 0) {
tokens.push(
<Token key="timeRange">
<ExploreParamTitle>{t('Time Range')}</ExploreParamTitle>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@ interface AskSeerSearchItem<S extends string> {
export type AskSeerSearchItems<T> = (AskSeerSearchItem<string> & 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[]}>;
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import moment from 'moment-timezone';

import type {
AskSeerSearchItems,
NoneOfTheseItem,
Expand Down Expand Up @@ -79,6 +81,42 @@ export function formatQueryToNaturalLanguage(query: string): string {
return `${formattedQuery} `;
}

/**
* 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 =
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}${separator}${endFormatted}`;
}

export function generateQueryTokensString(args: QueryTokensProps): string {
const parts = [];

Expand All @@ -103,7 +141,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 '${formatDateRange(args.start, args.end)}'`);
} else if (args?.statsPeriod && args.statsPeriod.length > 0) {
parts.push(`time range is '${args?.statsPeriod}'`);
}

Expand Down
96 changes: 85 additions & 11 deletions static/app/views/explore/spans/spansTabSeerComboBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -23,10 +24,12 @@ interface Visualization {
}

interface AskSeerSearchQuery {
end: string | null;
groupBys: string[];
mode: string;
query: string;
sort: string;
start: string | null;
statsPeriod: string;
visualizations: Visualization[];
}
Expand All @@ -47,6 +50,23 @@ interface TraceAskSeerSearchResponse {
unsupported_reason: string | null;
}

interface TraceAskSeerTranslateResponse {
responses: Array<{
end: string | null;
group_by: string[];
mode: string;
query: string;
sort: string;
start: string | null;
stats_period: string;
visualization: Array<{
chart_type: number;
y_axes: string[];
}>;
}>;
unsupported_reason: string | null;
}

export function SpansTabSeerComboBox() {
const navigate = useNavigate();
const {projects} = useProjects();
Expand All @@ -60,6 +80,9 @@ export function SpansTabSeerComboBox() {
enableAISearch,
} = useSearchQueryBuilder();

const useTranslateEndpoint = organization.features.includes(
'gen-ai-search-agent-translate'
);
let initialSeerQuery = '';
const queryDetails = useMemo(() => {
const queryToUse = committedQuery.length > 0 ? committedQuery : query;
Expand Down Expand Up @@ -99,6 +122,36 @@ export function SpansTabSeerComboBox() {
? pageFilters.selection.projects
: projects.filter(p => p.isMember).map(p => p.id);

if (useTranslateEndpoint) {
const data = await fetchMutation<TraceAskSeerTranslateResponse>({
url: `/organizations/${organization.slug}/search-agent/translate/`,
method: 'POST',
data: {
natural_language_query: queryToSubmit,
project_ids: selectedProjects,
},
});

return {
status: 'ok',
unsupported_reason: data.unsupported_reason,
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',
})),
};
}

const data = await fetchMutation<TraceAskSeerSearchResponse>({
url: `/organizations/${organization.slug}/trace-explorer-ai/query/`,
method: 'POST',
Expand All @@ -122,6 +175,8 @@ export function SpansTabSeerComboBox() {
sort: q?.sort ?? '',
groupBys: q?.group_by ?? [],
statsPeriod: q?.stats_period ?? '',
start: null,
end: null,
mode: q?.mode ?? 'spans',
})),
};
Expand All @@ -131,25 +186,42 @@ 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;

const startFilter = pageFilters.selection.datetime.start?.valueOf();
const start = startFilter
? new Date(startFilter).toISOString()
: pageFilters.selection.datetime.start;
let start: DateString = null;
let end: DateString = null;

const endFilter = pageFilters.selection.datetime.end?.valueOf();
const end = endFilter
? new Date(endFilter).toISOString()
: pageFilters.selection.datetime.end;
if (resultStart && resultEnd) {
// 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;
start = new Date(startLocal).toISOString();
end = new Date(endLocal).toISOString();
Comment on lines +202 to +209

This comment was marked as outdated.

} else {
start = pageFilters.selection.datetime.start;
end = pageFilters.selection.datetime.end;
}

const selection = {
...pageFilters.selection,
datetime: {
start,
end,
utc: pageFilters.selection.datetime.utc,
period: statsPeriod || pageFilters.selection.datetime.period,
period:
resultStart && resultEnd
? null
: statsPeriod || pageFilters.selection.datetime.period,
},
};

Expand Down Expand Up @@ -200,7 +272,9 @@ export function SpansTabSeerComboBox() {
!organization?.hideAiFeatures &&
organization.features.includes('gen-ai-features');

useTraceExploreAiQuerySetup({enableAISearch: areAiFeaturesAllowed});
useTraceExploreAiQuerySetup({
enableAISearch: areAiFeaturesAllowed && !useTranslateEndpoint,
});

return (
<AskSeerComboBox
Expand Down
Loading