diff --git a/static/app/views/explore/conversations/components/conversationsTable.tsx b/static/app/views/explore/conversations/components/conversationsTable.tsx index 3aad437cbe91d6..e05cf05c90727f 100644 --- a/static/app/views/explore/conversations/components/conversationsTable.tsx +++ b/static/app/views/explore/conversations/components/conversationsTable.tsx @@ -8,6 +8,7 @@ import {Text} from '@sentry/scraps/text'; import {Tooltip} from '@sentry/scraps/tooltip'; import {Count} from 'sentry/components/count'; +import {usePageFilters} from 'sentry/components/pageFilters/usePageFilters'; import { COL_WIDTH_UNDEFINED, GridEditable, @@ -35,14 +36,26 @@ import {hasGenAiConversationsFeature} from 'sentry/views/explore/conversations/u import {LLMCosts} from 'sentry/views/insights/pages/agents/components/llmCosts'; import {AIContentRenderer} from 'sentry/views/performance/newTraceDetails/traceDrawer/details/span/eapSections/aiContentRenderer'; -function getConversationDetailUrl(orgSlug: string, conversation: Conversation): string { +const ONE_HOUR_MS = 60 * 60 * 1000; + +function getConversationDetailUrl( + orgSlug: string, + conversation: Conversation, + projects: number[] +): string { const basePath = `/organizations/${orgSlug}/explore/${CONVERSATIONS_LANDING_SUB_PATH}/${encodeURIComponent(conversation.conversationId)}/`; const params = new URLSearchParams(); if (conversation.startTimestamp) { - params.set('start', new Date(conversation.startTimestamp).toISOString()); + params.set( + 'start', + new Date(conversation.startTimestamp - ONE_HOUR_MS).toISOString() + ); } if (conversation.endTimestamp) { - params.set('end', new Date(conversation.endTimestamp).toISOString()); + params.set('end', new Date(conversation.endTimestamp + ONE_HOUR_MS).toISOString()); + } + for (const project of projects) { + params.append('project', String(project)); } const qs = params.toString(); return normalizeUrl(qs ? `${basePath}?${qs}` : basePath); @@ -205,15 +218,18 @@ const BodyCell = memo(function BodyCell({ }) { const organization = useOrganization(); const navigate = useNavigate(); + const {selection} = usePageFilters(); const navigateToDetail = useCallback(() => { - navigate(getConversationDetailUrl(organization.slug, dataRow)); - }, [navigate, organization.slug, dataRow]); + navigate(getConversationDetailUrl(organization.slug, dataRow, selection.projects)); + }, [navigate, organization.slug, dataRow, selection.projects]); switch (column.key) { case 'conversationId': return ( - + {isUUID(dataRow.conversationId) ? ( dataRow.conversationId.slice(0, 8) ) : ( diff --git a/static/app/views/explore/conversations/hooks/useConversation.spec.tsx b/static/app/views/explore/conversations/hooks/useConversation.spec.tsx index 5997f49a63e124..870c56b184aaf2 100644 --- a/static/app/views/explore/conversations/hooks/useConversation.spec.tsx +++ b/static/app/views/explore/conversations/hooks/useConversation.spec.tsx @@ -243,14 +243,14 @@ describe('useConversation', () => { }); // Verify the API was called with correct timestamps (with 1-hour padding) - // and that project comes from page filters (empty array = my projects), not hardcoded -1 + // and ALL_ACCESS_PROJECTS (-1) when no project is selected in page filters expect(mockRequest).toHaveBeenCalledWith( expect.stringContaining('/ai-conversations/conv-timestamps/'), expect.objectContaining({ query: expect.objectContaining({ start: new Date(startTimestamp - 60 * 60 * 1000).toISOString(), end: new Date(endTimestamp + 60 * 60 * 1000).toISOString(), - project: [], + project: [-1], }), }) ); @@ -403,6 +403,65 @@ describe('useConversation', () => { expect(queryArg).not.toHaveProperty('statsPeriod'); }); + it('falls back to ALL_ACCESS_PROJECTS and 30d when no filters are set', async () => { + const mockRequest = MockApiClient.addMockResponse({ + url: `/organizations/${organization.slug}/ai-conversations/conv-123/`, + body: [BASE_SPAN], + }); + + const {result} = renderHookWithProviders( + () => useConversation({conversationId: 'conv-123'}), + {organization} + ); + + await waitFor(() => expect(result.current.isLoading).toBe(false)); + + expect(mockRequest).toHaveBeenCalledWith( + expect.stringContaining('/ai-conversations/conv-123/'), + expect.objectContaining({ + query: expect.objectContaining({ + project: [-1], + statsPeriod: '30d', + }), + }) + ); + }); + + it('uses relative period from page filters when explicitly set', async () => { + act(() => + PageFiltersStore.updateDateTime({ + period: '7d', + start: null, + end: null, + utc: null, + }) + ); + + const mockRequest = MockApiClient.addMockResponse({ + url: `/organizations/${organization.slug}/ai-conversations/conv-123/`, + body: [BASE_SPAN], + }); + + const {result} = renderHookWithProviders( + () => useConversation({conversationId: 'conv-123'}), + {organization} + ); + + await waitFor(() => expect(result.current.isLoading).toBe(false)); + + expect(mockRequest).toHaveBeenCalledWith( + expect.stringContaining('/ai-conversations/conv-123/'), + expect.objectContaining({ + query: expect.objectContaining({ + statsPeriod: '7d', + }), + }) + ); + const queryArg = mockRequest.mock.calls[0]![1]!.query; + expect(queryArg).not.toHaveProperty('start'); + expect(queryArg).not.toHaveProperty('end'); + }); + it('filters to only gen_ai spans', async () => { MockApiClient.addMockResponse({ url: `/organizations/${organization.slug}/ai-conversations/conv-filter/`, diff --git a/static/app/views/explore/conversations/hooks/useConversation.tsx b/static/app/views/explore/conversations/hooks/useConversation.tsx index 8c66f4ba90a6ae..5c52692559e26c 100644 --- a/static/app/views/explore/conversations/hooks/useConversation.tsx +++ b/static/app/views/explore/conversations/hooks/useConversation.tsx @@ -1,6 +1,10 @@ import {useEffect, useMemo} from 'react'; import {skipToken, useInfiniteQuery} from '@tanstack/react-query'; +import { + ALL_ACCESS_PROJECTS, + getDefaultPageFilterSelection, +} from 'sentry/components/pageFilters/constants'; import {normalizeDateTimeParams} from 'sentry/components/pageFilters/parse'; import {usePageFilters} from 'sentry/components/pageFilters/usePageFilters'; import {apiOptions} from 'sentry/utils/api/apiOptions'; @@ -186,15 +190,25 @@ export function useConversation( const hasConversationTimestamps = conversation.startTimestamp !== undefined && conversation.endTimestamp !== undefined; + const defaultPeriod = getDefaultPageFilterSelection().datetime.period; + const hasExplicitDatetime = + selection.datetime.start !== null || + (selection.datetime.period !== null && selection.datetime.period !== defaultPeriod); + const datetimeParams = hasConversationTimestamps ? { start: new Date(conversation.startTimestamp! - ONE_HOUR_MS).toISOString(), end: new Date(conversation.endTimestamp! + ONE_HOUR_MS).toISOString(), } - : normalizeDateTimeParams(selection.datetime); + : hasExplicitDatetime + ? normalizeDateTimeParams(selection.datetime) + : {statsPeriod: '30d'}; + + const project = + selection.projects.length > 0 ? selection.projects : [ALL_ACCESS_PROJECTS]; const queryParams = { - project: selection.projects, + project, per_page: 1000, ...datetimeParams, }; diff --git a/static/app/views/explore/conversations/layout.tsx b/static/app/views/explore/conversations/layout.tsx index 7a8e50453d008d..6310d6cf364409 100644 --- a/static/app/views/explore/conversations/layout.tsx +++ b/static/app/views/explore/conversations/layout.tsx @@ -45,6 +45,8 @@ function ConversationsLayout() { function ConversationsLayoutContent() { const organization = useOrganization(); + const {conversationId} = useParams<{conversationId?: string}>(); + const isDetailPage = !!conversationId; return ( @@ -55,6 +57,7 @@ function ConversationsLayoutContent() {