From 368f560504ebf110b1768135af05d8f3f5ed0c21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josh=20Goldberg=20=E2=9C=A8?= Date: Thu, 6 Aug 2026 16:16:50 -0400 Subject: [PATCH 1/6] fix(logs): Resolve the received time in the timestamp hover tooltip The trace item details endpoint only aliases the observed timestamp to `observed_timestamp` when it comes back as an int. It is stored as a string, so it arrives as `sentry.observed_timestamp_nanos` instead, which the tooltip never looked up. In the expanded log details and the metrics sample details, whose attribute maps are keyed by the raw response names, "Received" therefore spun on a loading indicator forever. The tooltip now reads either name, and only renders the "Received" row once it has a value instead of falling back to an unbounded spinner. Fixes LOGS-937 --- static/app/views/explore/logs/constants.tsx | 2 +- .../explore/logs/logsTimeTooltip.spec.tsx | 22 ++++++- .../views/explore/logs/logsTimeTooltip.tsx | 25 ++++---- .../explore/logs/tables/logsTableRow.spec.tsx | 58 +++++++++++++++++++ static/app/views/explore/logs/types.tsx | 3 + 5 files changed, 96 insertions(+), 14 deletions(-) diff --git a/static/app/views/explore/logs/constants.tsx b/static/app/views/explore/logs/constants.tsx index 90399c1d79cb..9013af255d7e 100644 --- a/static/app/views/explore/logs/constants.tsx +++ b/static/app/views/explore/logs/constants.tsx @@ -72,7 +72,7 @@ export const HiddenLogDetailFields: OurLogFieldKey[] = [ // deprecated/otel fields that clutter the UI 'sentry.timestamp_nanos', - 'sentry.observed_timestamp_nanos', + OurLogKnownFieldKey.OBSERVED_TIMESTAMP_NANOS, 'tags[sentry.trace_flags,number]', ]; diff --git a/static/app/views/explore/logs/logsTimeTooltip.spec.tsx b/static/app/views/explore/logs/logsTimeTooltip.spec.tsx index 8959c721025f..131e31af8b34 100644 --- a/static/app/views/explore/logs/logsTimeTooltip.spec.tsx +++ b/static/app/views/explore/logs/logsTimeTooltip.spec.tsx @@ -91,10 +91,30 @@ describe('TimestampTooltipBody', () => { ); - expect(screen.getByText('Occurred')).toBeInTheDocument(); + expect(screen.queryByText('Received')).not.toBeInTheDocument(); expect(screen.queryAllByRole('time')).toHaveLength(2); }); + it('renders received time when the observed timestamp uses its internal name', () => { + const user = UserFixture(); + user.options.timezone = 'America/New_York'; + ConfigStore.set('user', user); + + const attributes = { + [OurLogKnownFieldKey.TIMESTAMP_PRECISE]: '1705333530456789012', + [OurLogKnownFieldKey.OBSERVED_TIMESTAMP_NANOS]: '1705333540456789012', + }; + + render( + + + + ); + + expect(screen.getByText('Received')).toBeInTheDocument(); + expect(screen.queryAllByRole('time')).toHaveLength(3); + }); + it('renders in 24h format when user preference is set', () => { const user = UserFixture(); user.options.timezone = 'America/New_York'; diff --git a/static/app/views/explore/logs/logsTimeTooltip.tsx b/static/app/views/explore/logs/logsTimeTooltip.tsx index 6d17ef8dc107..ae47da34b6cc 100644 --- a/static/app/views/explore/logs/logsTimeTooltip.tsx +++ b/static/app/views/explore/logs/logsTimeTooltip.tsx @@ -7,7 +7,6 @@ import {Tooltip} from '@sentry/scraps/tooltip'; import {AutoSelectText} from 'sentry/components/autoSelectText'; import {DateTime} from 'sentry/components/dateTime'; import {Duration} from 'sentry/components/duration/duration'; -import {LoadingIndicator} from 'sentry/components/loadingIndicator'; import {useTimezone} from 'sentry/components/timezoneProvider'; import {t} from 'sentry/locale'; import {trackAnalytics} from 'sentry/utils/analytics'; @@ -39,7 +38,11 @@ function TimestampTooltipBody({ : null; const timestampToUse = preciseTimestampMs ? new Date(preciseTimestampMs) : timestamp; - const observedTimeNanos = attributes[OurLogKnownFieldKey.OBSERVED_TIMESTAMP_PRECISE]; + // Trace item details only alias the observed timestamp when it comes back as an int, + // so string-stored values arrive under the internal name instead. + const observedTimeNanos = + attributes[OurLogKnownFieldKey.OBSERVED_TIMESTAMP_PRECISE] ?? + attributes[OurLogKnownFieldKey.OBSERVED_TIMESTAMP_NANOS]; const observedTime = observedTimeNanos ? new Date(Math.floor(Number(observedTimeNanos) / 1_000_000)) : null; @@ -94,21 +97,19 @@ function TimestampTooltipBody({ )} - - -
{t('Received')}
-
- {observedTime ? ( + {observedTime && ( + + +
{t('Received')}
+
- ) : ( - - )} -
-
+ + + )} ); } diff --git a/static/app/views/explore/logs/tables/logsTableRow.spec.tsx b/static/app/views/explore/logs/tables/logsTableRow.spec.tsx index ba86c76065c1..3456897c1848 100644 --- a/static/app/views/explore/logs/tables/logsTableRow.spec.tsx +++ b/static/app/views/explore/logs/tables/logsTableRow.spec.tsx @@ -411,6 +411,64 @@ describe('logsTableRow', () => { ); }); + it('renders a received time in the details timestamp tooltip when the observed timestamp uses its internal name', async () => { + const { + [OurLogKnownFieldKey.OBSERVED_TIMESTAMP_PRECISE]: observedTimestamp, + ...rowDataWithInternalObservedTimestamp + } = LogFixture({ + [OurLogKnownFieldKey.ID]: '4', + [OurLogKnownFieldKey.PROJECT_ID]: project.id, + [OurLogKnownFieldKey.ORGANIZATION_ID]: Number(organization.id), + }); + + MockApiClient.addMockResponse({ + url: `/projects/${organization.slug}/${project.slug}/trace-items/4/`, + method: 'GET', + body: { + itemId: '4', + links: null, + meta: {}, + timestamp: rowDataWithInternalObservedTimestamp[OurLogKnownFieldKey.TIMESTAMP], + attributes: [ + ...Object.entries(rowDataWithInternalObservedTimestamp).map( + ([k, v]) => + ({ + name: k, + value: v, + type: typeof v === 'string' ? 'str' : 'float', + }) as TraceItemResponseAttribute + ), + { + name: OurLogKnownFieldKey.OBSERVED_TIMESTAMP_NANOS, + value: String(observedTimestamp), + type: 'str', + }, + ], + }, + }); + + render( + , + {organization, initialRouterConfig, additionalWrapper: ProviderWrapper} + ); + + await userEvent.click(await screen.findByTestId('log-table-row')); + + const attributesTree = await screen.findByTestId('fields-tree'); + const [treeTimestamp] = within(attributesTree).getAllByText( + 'Apr 10, 2025 7:21:10.049 PM' + ); + await userEvent.hover(treeTimestamp!); + + expect(await screen.findByText('Received')).toBeInTheDocument(); + expect(screen.getByText('Apr 10, 2025 7:21:10 PM UTC')).toBeInTheDocument(); + }); + it('adds a similar spans action to the log message dropdown', async () => { const rowDataWithQuotedMessage = { ...rowData, diff --git a/static/app/views/explore/logs/types.tsx b/static/app/views/explore/logs/types.tsx index 0088f2fe0dd3..f929e0723a46 100644 --- a/static/app/views/explore/logs/types.tsx +++ b/static/app/views/explore/logs/types.tsx @@ -55,6 +55,9 @@ export enum OurLogKnownFieldKey { // From the EAP dataset directly not using a column alias, should be hidden. ITEM_TYPE = 'sentry.item_type', + // Trace item details fall back to this when OBSERVED_TIMESTAMP_PRECISE can't be aliased. + OBSERVED_TIMESTAMP_NANOS = 'sentry.observed_timestamp_nanos', + // Deprecated fields TIMESTAMP_NANOS = 'sentry.timestamp_nanos', From c2d0b0d608f3c21ae78c2c22a2c784945a17624c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josh=20Goldberg=20=E2=9C=A8?= Date: Fri, 7 Aug 2026 09:44:32 -0400 Subject: [PATCH 2/6] fix(logs): Show the received time spinner until the details request resolves The Received row only rendered once the observed timestamp was available, so the tooltip grew under the cursor when the trace item details arrived. It now renders the loading indicator while that request is in flight, and stops once it settles so a missing attribute does not spin forever. --- .../explore/hooks/useTraceItemDetails.tsx | 9 ++++-- .../app/views/explore/logs/fieldRenderers.tsx | 3 ++ .../explore/logs/logsTimeTooltip.spec.tsx | 23 +++++++++++++++ .../views/explore/logs/logsTimeTooltip.tsx | 24 ++++++++++----- .../explore/logs/tables/logsTableRow.tsx | 29 ++++++++++++------- 5 files changed, 66 insertions(+), 22 deletions(-) diff --git a/static/app/views/explore/hooks/useTraceItemDetails.tsx b/static/app/views/explore/hooks/useTraceItemDetails.tsx index 6ac14bf38315..2dee2280569a 100644 --- a/static/app/views/explore/hooks/useTraceItemDetails.tsx +++ b/static/app/views/explore/hooks/useTraceItemDetails.tsx @@ -214,6 +214,7 @@ function useTraceItemDetailsPrefetch({ const [traceItemAttributes, setTraceItemAttributes] = useState< TraceItemResponseAttribute[] | undefined >(); + const [isPending, setIsPending] = useState(true); const prefetch = useCallback(() => { const currentProject = projectRef.current; @@ -236,8 +237,9 @@ function useTraceItemDetailsPrefetch({ response => { setTraceItemMeta(response?.json?.meta); setTraceItemAttributes(response?.json?.attributes); + setIsPending(false); }, - () => {} + () => setIsPending(false) ); }, [ organization.slug, @@ -250,7 +252,7 @@ function useTraceItemDetailsPrefetch({ traceItemType, ]); - return {prefetch, project, traceItemMeta, traceItemAttributes}; + return {prefetch, project, traceItemMeta, traceItemAttributes, isPending}; } export function usePrefetchTraceItemDetailsOnHover({ @@ -278,7 +280,7 @@ export function usePrefetchTraceItemDetailsOnHover({ */ hoverPrefetchDisabled?: boolean; }) { - const {prefetch, project, traceItemMeta, traceItemAttributes} = + const {prefetch, project, traceItemMeta, traceItemAttributes, isPending} = useTraceItemDetailsPrefetch({ traceItemId, projectId, @@ -328,6 +330,7 @@ export function usePrefetchTraceItemDetailsOnHover({ isProjectReady: Boolean(project?.slug), traceItemMeta, traceItemAttributes, + isTraceItemDetailsPending: isPending, }; } diff --git a/static/app/views/explore/logs/fieldRenderers.tsx b/static/app/views/explore/logs/fieldRenderers.tsx index 3cb0ccbaa6bd..c45d78e39583 100644 --- a/static/app/views/explore/logs/fieldRenderers.tsx +++ b/static/app/views/explore/logs/fieldRenderers.tsx @@ -83,6 +83,7 @@ export interface RendererExtra extends RenderFunctionBaggage { logColors: ReturnType; align?: 'left' | 'center' | 'right'; canAppendTemplateToBody?: boolean; + isTraceItemDetailsPending?: boolean; logEnd?: string; logStart?: string; meta?: EventsMetaType; @@ -166,6 +167,7 @@ function TimestampRenderer(props: LogFieldRendererProps) { @@ -213,6 +215,7 @@ function RelativeTimestampRenderer(props: LogFieldRendererProps) { diff --git a/static/app/views/explore/logs/logsTimeTooltip.spec.tsx b/static/app/views/explore/logs/logsTimeTooltip.spec.tsx index 131e31af8b34..6ba5a656fac0 100644 --- a/static/app/views/explore/logs/logsTimeTooltip.spec.tsx +++ b/static/app/views/explore/logs/logsTimeTooltip.spec.tsx @@ -115,6 +115,29 @@ describe('TimestampTooltipBody', () => { expect(screen.queryAllByRole('time')).toHaveLength(3); }); + it('renders a loading received time when the trace item details are still pending', () => { + const user = UserFixture(); + user.options.timezone = 'America/New_York'; + ConfigStore.set('user', user); + + const attributes = { + [OurLogKnownFieldKey.TIMESTAMP_PRECISE]: '1705333530456789012', + }; + + render( + + + + ); + + expect(screen.getByText('Received')).toBeInTheDocument(); + expect(screen.getByTestId('loading-indicator')).toBeInTheDocument(); + }); + it('renders in 24h format when user preference is set', () => { const user = UserFixture(); user.options.timezone = 'America/New_York'; diff --git a/static/app/views/explore/logs/logsTimeTooltip.tsx b/static/app/views/explore/logs/logsTimeTooltip.tsx index ae47da34b6cc..b3114af7b713 100644 --- a/static/app/views/explore/logs/logsTimeTooltip.tsx +++ b/static/app/views/explore/logs/logsTimeTooltip.tsx @@ -7,6 +7,7 @@ import {Tooltip} from '@sentry/scraps/tooltip'; import {AutoSelectText} from 'sentry/components/autoSelectText'; import {DateTime} from 'sentry/components/dateTime'; import {Duration} from 'sentry/components/duration/duration'; +import {LoadingIndicator} from 'sentry/components/loadingIndicator'; import {useTimezone} from 'sentry/components/timezoneProvider'; import {t} from 'sentry/locale'; import {trackAnalytics} from 'sentry/utils/analytics'; @@ -17,6 +18,7 @@ type Props = { attributes: Record; children: React.ReactNode; timestamp: string | number; + isTraceItemDetailsPending?: boolean; relativeTimeToReplay?: number; shouldRender?: boolean; }; @@ -24,10 +26,12 @@ type Props = { function TimestampTooltipBody({ timestamp, attributes, + isTraceItemDetailsPending, relativeTime, }: { attributes: Record; timestamp: string | number; + isTraceItemDetailsPending?: boolean; relativeTime?: number; }) { const currentTimezone = useTimezone(); @@ -38,8 +42,6 @@ function TimestampTooltipBody({ : null; const timestampToUse = preciseTimestampMs ? new Date(preciseTimestampMs) : timestamp; - // Trace item details only alias the observed timestamp when it comes back as an int, - // so string-stored values arrive under the internal name instead. const observedTimeNanos = attributes[OurLogKnownFieldKey.OBSERVED_TIMESTAMP_PRECISE] ?? attributes[OurLogKnownFieldKey.OBSERVED_TIMESTAMP_NANOS]; @@ -97,16 +99,20 @@ function TimestampTooltipBody({ )} - {observedTime && ( + {(observedTime || isTraceItemDetailsPending) && (
{t('Received')}
- - - - - + {observedTime ? ( + + + + + + ) : ( + + )}
)} @@ -120,6 +126,7 @@ export function LogsTimestampTooltip({ timestamp, attributes, children, + isTraceItemDetailsPending, shouldRender = true, relativeTimeToReplay: relativeTime, }: Props) { @@ -138,6 +145,7 @@ export function LogsTimestampTooltip({ diff --git a/static/app/views/explore/logs/tables/logsTableRow.tsx b/static/app/views/explore/logs/tables/logsTableRow.tsx index 9692753c9f71..05a086330b6d 100644 --- a/static/app/views/explore/logs/tables/logsTableRow.tsx +++ b/static/app/views/explore/logs/tables/logsTableRow.tsx @@ -357,17 +357,23 @@ export const LogRowContent = memo(function LogRowContentImpl({ const logTimestampSeconds = isRegularLogResponseItem(dataRow) ? getLogRowTimestampMillis(dataRow) / 1000 : null; - const {hoverProps, prefetch, isProjectReady, traceItemMeta, traceItemAttributes} = - usePrefetchTraceItemDetailsOnHover({ - traceItemId: rowId, - projectId: String(dataRow[OurLogKnownFieldKey.PROJECT_ID]), - traceId: String(dataRow[OurLogKnownFieldKey.TRACE_ID]), - traceItemType: TraceItemDataset.LOGS, - referrer: 'api.explore.log-item-details', - timestamp: logTimestampSeconds, - sharedHoverTimeoutRef, - timeout: prefetchTimeout, - }); + const { + hoverProps, + prefetch, + isProjectReady, + isTraceItemDetailsPending, + traceItemMeta, + traceItemAttributes, + } = usePrefetchTraceItemDetailsOnHover({ + traceItemId: rowId, + projectId: String(dataRow[OurLogKnownFieldKey.PROJECT_ID]), + traceId: String(dataRow[OurLogKnownFieldKey.TRACE_ID]), + traceItemType: TraceItemDataset.LOGS, + referrer: 'api.explore.log-item-details', + timestamp: logTimestampSeconds, + sharedHoverTimeoutRef, + timeout: prefetchTimeout, + }); usePrefetchTraceItemDetailsOnMount({ prefetch, enabled: isHighlighted, @@ -383,6 +389,7 @@ export const LogRowContent = memo(function LogRowContentImpl({ highlightTerms, caseSensitiveHighlighting: !caseInsensitivity, datetime: selection.datetime, + isTraceItemDetailsPending, logColors, useFullSeverityText: false, location, From 73c5b8b1dde5ff2f99b2fe7dde6465112ff9b83b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josh=20Goldberg=20=E2=9C=A8?= Date: Fri, 7 Aug 2026 10:04:35 -0400 Subject: [PATCH 3/6] lil fix to only be pending upon preferch/request; and a string dedupe --- static/app/views/explore/hooks/useTraceItemDetails.tsx | 3 ++- static/app/views/explore/logs/tables/logsTableRow.tsx | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/static/app/views/explore/hooks/useTraceItemDetails.tsx b/static/app/views/explore/hooks/useTraceItemDetails.tsx index 2dee2280569a..2c6df93bc121 100644 --- a/static/app/views/explore/hooks/useTraceItemDetails.tsx +++ b/static/app/views/explore/hooks/useTraceItemDetails.tsx @@ -214,7 +214,7 @@ function useTraceItemDetailsPrefetch({ const [traceItemAttributes, setTraceItemAttributes] = useState< TraceItemResponseAttribute[] | undefined >(); - const [isPending, setIsPending] = useState(true); + const [isPending, setIsPending] = useState(false); const prefetch = useCallback(() => { const currentProject = projectRef.current; @@ -233,6 +233,7 @@ function useTraceItemDetailsPrefetch({ traceId, ...timeQueryParams, }); + setIsPending(true); queryClient.fetchQuery(options).then( response => { setTraceItemMeta(response?.json?.meta); diff --git a/static/app/views/explore/logs/tables/logsTableRow.tsx b/static/app/views/explore/logs/tables/logsTableRow.tsx index 05a086330b6d..e448e3914e98 100644 --- a/static/app/views/explore/logs/tables/logsTableRow.tsx +++ b/static/app/views/explore/logs/tables/logsTableRow.tsx @@ -382,7 +382,7 @@ export const LogRowContent = memo(function LogRowContentImpl({ const [caseInsensitivity] = useCaseInsensitivity(); const observedTimestamp = traceItemAttributes?.find( - a => a.name === 'sentry.observed_timestamp_nanos' + a => a.name === OurLogKnownFieldKey.OBSERVED_TIMESTAMP_NANOS ); const rendererExtra: RendererExtra = { From e0b0f5da96bd73d217afa0fd780310454409023d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josh=20Goldberg=20=E2=9C=A8?= Date: Fri, 7 Aug 2026 10:44:13 -0400 Subject: [PATCH 4/6] why use many state when few do trick --- .../hooks/useTraceItemDetails.spec.tsx | 42 ++++++++++++++++ .../explore/hooks/useTraceItemDetails.tsx | 49 +++++++------------ 2 files changed, 60 insertions(+), 31 deletions(-) diff --git a/static/app/views/explore/hooks/useTraceItemDetails.spec.tsx b/static/app/views/explore/hooks/useTraceItemDetails.spec.tsx index c7166d822bc9..d37c46cdf833 100644 --- a/static/app/views/explore/hooks/useTraceItemDetails.spec.tsx +++ b/static/app/views/explore/hooks/useTraceItemDetails.spec.tsx @@ -256,6 +256,48 @@ describe('useTraceItemDetails', () => { await waitFor(() => expect(traceItemDetailsMock).toHaveBeenCalledTimes(1)); }); + it('reports pending only while the prefetched details request is in flight', async () => { + initializePageFilters({ + period: '14d', + start: null, + end: null, + utc: false, + }); + MockApiClient.addMockResponse({ + method: 'GET', + url: `/projects/${organization.slug}/${project.slug}/trace-items/item-id/`, + asyncDelay: 100, + body: { + itemId: 'item-id', + links: null, + meta: {}, + timestamp: '2025-04-03T15:50:10.000Z', + attributes: [], + }, + }); + + const {result} = renderHookWithProviders(usePrefetchTraceItemDetailsOnHover, { + organization, + initialProps: { + projectId: project.id, + traceItemId: 'item-id', + traceId: '1234567890abcdef1234567890abcdef', + traceItemType: TraceItemDataset.LOGS, + referrer: 'api.explore.log-item-details', + timestamp: 123, + sharedHoverTimeoutRef: {current: null}, + timeout: 0, + }, + }); + + await waitFor(() => expect(ProjectsStore.getState().projects).toHaveLength(1)); + expect(result.current.isTraceItemDetailsPending).toBe(false); + + act(() => result.current.prefetch()); + await waitFor(() => expect(result.current.isTraceItemDetailsPending).toBe(true)); + await waitFor(() => expect(result.current.isTraceItemDetailsPending).toBe(false)); + }); + it('does not fetch details when the hovered element unmounts before the hover timeout elapses', async () => { jest.useFakeTimers(); initializePageFilters({ diff --git a/static/app/views/explore/hooks/useTraceItemDetails.tsx b/static/app/views/explore/hooks/useTraceItemDetails.tsx index 2c6df93bc121..4ecc878ebcd0 100644 --- a/static/app/views/explore/hooks/useTraceItemDetails.tsx +++ b/static/app/views/explore/hooks/useTraceItemDetails.tsx @@ -1,7 +1,7 @@ import {useCallback, useEffect, useRef, useState} from 'react'; import {useHover} from '@react-aria/interactions'; import {captureException} from '@sentry/react'; -import {skipToken, useQuery, useQueryClient} from '@tanstack/react-query'; +import {skipToken, useIsFetching, useQuery, useQueryClient} from '@tanstack/react-query'; import {normalizeDateTimeParams} from 'sentry/components/pageFilters/parse'; import {usePageFilters} from 'sentry/components/pageFilters/usePageFilters'; @@ -207,51 +207,38 @@ function useTraceItemDetailsPrefetch({ const organization = useOrganization(); const {selection} = usePageFilters(); const project = useProjectFromId({project_id: projectId}); - const projectRef = useRef(project); - projectRef.current = project; const queryClient = useQueryClient(); const [traceItemMeta, setTraceItemMeta] = useState(); const [traceItemAttributes, setTraceItemAttributes] = useState< TraceItemResponseAttribute[] | undefined >(); - const [isPending, setIsPending] = useState(false); + + const options = traceItemDetailsApiOptions({ + organizationSlug: organization.slug, + projectSlug: project?.slug ?? '', + traceItemId, + traceItemType, + referrer, + traceId, + ...(defined(timestamp) + ? {timestamp: normalizeTimestampToSeconds(timestamp)} + : normalizeDateTimeParams(selection.datetime)), + }); const prefetch = useCallback(() => { - const currentProject = projectRef.current; - if (!currentProject?.slug) { + if (!project?.slug) { return; } - const timeQueryParams = defined(timestamp) - ? {timestamp: normalizeTimestampToSeconds(timestamp)} - : normalizeDateTimeParams(selection.datetime); - const options = traceItemDetailsApiOptions({ - organizationSlug: organization.slug, - projectSlug: currentProject.slug, - traceItemId, - traceItemType, - referrer, - traceId, - ...timeQueryParams, - }); - setIsPending(true); queryClient.fetchQuery(options).then( response => { setTraceItemMeta(response?.json?.meta); setTraceItemAttributes(response?.json?.attributes); - setIsPending(false); }, - () => setIsPending(false) + () => {} ); - }, [ - organization.slug, - queryClient, - referrer, - selection.datetime, - timestamp, - traceId, - traceItemId, - traceItemType, - ]); + }, [options, project?.slug, queryClient]); + + const isPending = useIsFetching({queryKey: options.queryKey, exact: true}) > 0; return {prefetch, project, traceItemMeta, traceItemAttributes, isPending}; } From 2ab01b26c0e9dd1ad6b469f290723d1be492f15e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josh=20Goldberg=20=E2=9C=A8?= Date: Fri, 7 Aug 2026 13:12:11 -0400 Subject: [PATCH 5/6] further streamline with prefetch and useQuery --- .../explore/hooks/useTraceItemDetails.tsx | 54 ++++++++----------- 1 file changed, 23 insertions(+), 31 deletions(-) diff --git a/static/app/views/explore/hooks/useTraceItemDetails.tsx b/static/app/views/explore/hooks/useTraceItemDetails.tsx index 4ecc878ebcd0..618643687111 100644 --- a/static/app/views/explore/hooks/useTraceItemDetails.tsx +++ b/static/app/views/explore/hooks/useTraceItemDetails.tsx @@ -1,7 +1,7 @@ import {useCallback, useEffect, useRef, useState} from 'react'; import {useHover} from '@react-aria/interactions'; import {captureException} from '@sentry/react'; -import {skipToken, useIsFetching, useQuery, useQueryClient} from '@tanstack/react-query'; +import {skipToken, useQuery} from '@tanstack/react-query'; import {normalizeDateTimeParams} from 'sentry/components/pageFilters/parse'; import {usePageFilters} from 'sentry/components/pageFilters/usePageFilters'; @@ -207,40 +207,32 @@ function useTraceItemDetailsPrefetch({ const organization = useOrganization(); const {selection} = usePageFilters(); const project = useProjectFromId({project_id: projectId}); - const queryClient = useQueryClient(); - const [traceItemMeta, setTraceItemMeta] = useState(); - const [traceItemAttributes, setTraceItemAttributes] = useState< - TraceItemResponseAttribute[] | undefined - >(); + const [shouldFetch, setShouldFetch] = useState(false); - const options = traceItemDetailsApiOptions({ - organizationSlug: organization.slug, - projectSlug: project?.slug ?? '', - traceItemId, - traceItemType, - referrer, - traceId, - ...(defined(timestamp) - ? {timestamp: normalizeTimestampToSeconds(timestamp)} - : normalizeDateTimeParams(selection.datetime)), + const {data, isFetching} = useQuery({ + ...traceItemDetailsApiOptions({ + organizationSlug: organization.slug, + projectSlug: project?.slug ?? '', + traceItemId, + traceItemType, + referrer, + traceId, + ...(defined(timestamp) + ? {timestamp: normalizeTimestampToSeconds(timestamp)} + : normalizeDateTimeParams(selection.datetime)), + }), + enabled: shouldFetch && !!project?.slug, }); - const prefetch = useCallback(() => { - if (!project?.slug) { - return; - } - queryClient.fetchQuery(options).then( - response => { - setTraceItemMeta(response?.json?.meta); - setTraceItemAttributes(response?.json?.attributes); - }, - () => {} - ); - }, [options, project?.slug, queryClient]); - - const isPending = useIsFetching({queryKey: options.queryKey, exact: true}) > 0; + const prefetch = useCallback(() => setShouldFetch(true), []); - return {prefetch, project, traceItemMeta, traceItemAttributes, isPending}; + return { + prefetch, + project, + traceItemMeta: data?.meta, + traceItemAttributes: data?.attributes, + isPending: isFetching, + }; } export function usePrefetchTraceItemDetailsOnHover({ From 32705b4bff0913ef0aaef2c1dad6b3c8b6fa36ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josh=20Goldberg=20=E2=9C=A8?= Date: Fri, 7 Aug 2026 13:15:05 -0400 Subject: [PATCH 6/6] defined() --- static/app/views/explore/hooks/useTraceItemDetails.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/app/views/explore/hooks/useTraceItemDetails.tsx b/static/app/views/explore/hooks/useTraceItemDetails.tsx index 618643687111..02f6be6dd85c 100644 --- a/static/app/views/explore/hooks/useTraceItemDetails.tsx +++ b/static/app/views/explore/hooks/useTraceItemDetails.tsx @@ -217,7 +217,7 @@ function useTraceItemDetailsPrefetch({ traceItemType, referrer, traceId, - ...(defined(timestamp) + ...(timestamp ? {timestamp: normalizeTimestampToSeconds(timestamp)} : normalizeDateTimeParams(selection.datetime)), }),