From 3c7bfe192dee65df2df1b4d0b50f629f2745447a Mon Sep 17 00:00:00 2001 From: Ogi <86684834+obostjancic@users.noreply.github.com> Date: Tue, 14 Apr 2026 14:00:34 +0200 Subject: [PATCH 1/5] fix(conversations): Show timing on conversation details Add a visible conversation timing summary to the detail page so it is easy to see when a conversation started and when the last message happened. This surfaces the existing span timestamps in the summary header instead of making people infer timing from the table or the message list. Refs TET-2210 Co-Authored-By: OpenAI Codex --- .../components/conversationSummary.spec.tsx | 59 +++++++++ .../components/conversationSummary.tsx | 121 +++++++++++++----- 2 files changed, 146 insertions(+), 34 deletions(-) create mode 100644 static/app/views/insights/pages/conversations/components/conversationSummary.spec.tsx diff --git a/static/app/views/insights/pages/conversations/components/conversationSummary.spec.tsx b/static/app/views/insights/pages/conversations/components/conversationSummary.spec.tsx new file mode 100644 index 00000000000000..debcc6e9e6880c --- /dev/null +++ b/static/app/views/insights/pages/conversations/components/conversationSummary.spec.tsx @@ -0,0 +1,59 @@ +import {render, screen} from 'sentry-test/reactTestingLibrary'; + +import {TimezoneProvider} from 'sentry/components/timezoneProvider'; +import {SpanFields} from 'sentry/views/insights/types'; + +import {ConversationSummary} from './conversationSummary'; + +function createMockNode(overrides: { + id: string; + endTimestamp?: number; + startTimestamp?: number; +}) { + const {id, startTimestamp = 1000, endTimestamp = startTimestamp + 120} = overrides; + + return { + id, + type: 'span' as const, + op: 'gen_ai.generate', + startTimestamp, + endTimestamp, + value: { + start_timestamp: startTimestamp, + end_timestamp: endTimestamp, + }, + attributes: { + [SpanFields.GEN_AI_OPERATION_TYPE]: 'ai_client', + }, + errors: new Set(), + }; +} + +describe('ConversationSummary', () => { + it('shows when the conversation started and when the last message happened', () => { + const firstNode = createMockNode({ + id: 'span-1', + startTimestamp: 1476662480, + endTimestamp: 1476662540, + }); + const secondNode = createMockNode({ + id: 'span-2', + startTimestamp: 1476662600, + endTimestamp: 1508208080, + }); + + render( + + + + ); + + expect(screen.getByText('Started')).toBeInTheDocument(); + expect(screen.getByText('Oct 16, 2016 7:41 PM PDT')).toBeInTheDocument(); + expect(screen.getByText('Last message')).toBeInTheDocument(); + expect(screen.getByText(/ago$/)).toBeInTheDocument(); + }); +}); diff --git a/static/app/views/insights/pages/conversations/components/conversationSummary.tsx b/static/app/views/insights/pages/conversations/components/conversationSummary.tsx index ba169269d2796f..6c9d6d6cbd3073 100644 --- a/static/app/views/insights/pages/conversations/components/conversationSummary.tsx +++ b/static/app/views/insights/pages/conversations/components/conversationSummary.tsx @@ -11,8 +11,10 @@ import {Heading, Text} from '@sentry/scraps/text'; import {Tooltip} from '@sentry/scraps/tooltip'; import {Count} from 'sentry/components/count'; +import {DateTime} from 'sentry/components/dateTime'; import {usePageFilters} from 'sentry/components/pageFilters/usePageFilters'; import {Placeholder} from 'sentry/components/placeholder'; +import {TimeSince} from 'sentry/components/timeSince'; import {IconCopy} from 'sentry/icons'; import {t} from 'sentry/locale'; import {normalizeUrl} from 'sentry/utils/url/normalizeUrl'; @@ -99,6 +101,32 @@ export function ConversationSummary({ const organization = useOrganization(); const {selection} = usePageFilters(); const aggregates = useMemo(() => calculateAggregates(nodes), [nodes]); + const conversationWindow = useMemo(() => { + if (nodes.length === 0) { + return null; + } + + let startTimestamp = Number.POSITIVE_INFINITY; + let endTimestamp = Number.NEGATIVE_INFINITY; + + for (const node of nodes) { + if (typeof node.startTimestamp === 'number') { + startTimestamp = Math.min(startTimestamp, node.startTimestamp); + } + if (typeof node.endTimestamp === 'number') { + endTimestamp = Math.max(endTimestamp, node.endTimestamp); + } + } + + if (!Number.isFinite(startTimestamp) || !Number.isFinite(endTimestamp)) { + return null; + } + + return { + startDate: new Date(startTimestamp * 1e3), + endDate: new Date(endTimestamp * 1e3), + }; + }, [nodes]); const handleCopyConversationId = useCallback(() => { copyToClipboard(conversationId, { @@ -127,41 +155,66 @@ export function ConversationSummary({ return ( - - {t('Conversation #%s', conversationId.slice(0, 8))} - -