Skip to content
Open
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
42 changes: 42 additions & 0 deletions static/app/views/explore/hooks/useTraceItemDetails.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
61 changes: 22 additions & 39 deletions static/app/views/explore/hooks/useTraceItemDetails.tsx
Original file line number Diff line number Diff line change
@@ -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, useQuery} from '@tanstack/react-query';

import {normalizeDateTimeParams} from 'sentry/components/pageFilters/parse';
import {usePageFilters} from 'sentry/components/pageFilters/usePageFilters';
Expand Down Expand Up @@ -207,50 +207,32 @@ 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<TraceItemDetailsMeta | undefined>();
const [traceItemAttributes, setTraceItemAttributes] = useState<
TraceItemResponseAttribute[] | undefined
>();
const [shouldFetch, setShouldFetch] = useState(false);

const prefetch = useCallback(() => {
const currentProject = projectRef.current;
if (!currentProject?.slug) {
return;
}
const timeQueryParams = defined(timestamp)
? {timestamp: normalizeTimestampToSeconds(timestamp)}
: normalizeDateTimeParams(selection.datetime);
const options = traceItemDetailsApiOptions({
const {data, isFetching} = useQuery({
...traceItemDetailsApiOptions({
organizationSlug: organization.slug,
projectSlug: currentProject.slug,
projectSlug: project?.slug ?? '',
traceItemId,
traceItemType,
referrer,
traceId,
...timeQueryParams,
});
queryClient.fetchQuery(options).then(
response => {
setTraceItemMeta(response?.json?.meta);
setTraceItemAttributes(response?.json?.attributes);
},
() => {}
);
}, [
organization.slug,
queryClient,
referrer,
selection.datetime,
timestamp,
traceId,
traceItemId,
traceItemType,
]);
...(timestamp
? {timestamp: normalizeTimestampToSeconds(timestamp)}
: normalizeDateTimeParams(selection.datetime)),
Comment thread
JoshuaKGoldberg marked this conversation as resolved.
}),
enabled: shouldFetch && !!project?.slug,
});

const prefetch = useCallback(() => setShouldFetch(true), []);

return {prefetch, project, traceItemMeta, traceItemAttributes};
return {
prefetch,
project,
traceItemMeta: data?.meta,
traceItemAttributes: data?.attributes,
isPending: isFetching,
};
}

export function usePrefetchTraceItemDetailsOnHover({
Expand Down Expand Up @@ -278,7 +260,7 @@ export function usePrefetchTraceItemDetailsOnHover({
*/
hoverPrefetchDisabled?: boolean;
}) {
const {prefetch, project, traceItemMeta, traceItemAttributes} =
const {prefetch, project, traceItemMeta, traceItemAttributes, isPending} =
useTraceItemDetailsPrefetch({
traceItemId,
projectId,
Expand Down Expand Up @@ -328,6 +310,7 @@ export function usePrefetchTraceItemDetailsOnHover({
isProjectReady: Boolean(project?.slug),
traceItemMeta,
traceItemAttributes,
isTraceItemDetailsPending: isPending,
};
}

Expand Down
2 changes: 1 addition & 1 deletion static/app/views/explore/logs/constants.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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]',
];

Expand Down
3 changes: 3 additions & 0 deletions static/app/views/explore/logs/fieldRenderers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ export interface RendererExtra extends RenderFunctionBaggage {
logColors: ReturnType<typeof getLogColors>;
align?: 'left' | 'center' | 'right';
canAppendTemplateToBody?: boolean;
isTraceItemDetailsPending?: boolean;
logEnd?: string;
logStart?: string;
meta?: EventsMetaType;
Expand Down Expand Up @@ -166,6 +167,7 @@ function TimestampRenderer(props: LogFieldRendererProps) {
<LogsTimestampTooltip
timestamp={props.item.value!}
attributes={props.extra.attributes}
isTraceItemDetailsPending={props.extra.isTraceItemDetailsPending}
shouldRender={props.extra.shouldRenderHoverElements}
>
<DateTime seconds milliseconds date={timestampToUse} />
Expand Down Expand Up @@ -213,6 +215,7 @@ function RelativeTimestampRenderer(props: LogFieldRendererProps) {
<LogsTimestampTooltip
timestamp={props.item.value!}
attributes={props.extra.attributes}
isTraceItemDetailsPending={props.extra.isTraceItemDetailsPending}
shouldRender={props.extra.shouldRenderHoverElements}
relativeTimeToReplay={relativeTimestampMs}
>
Expand Down
45 changes: 44 additions & 1 deletion static/app/views/explore/logs/logsTimeTooltip.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,10 +91,53 @@ describe('TimestampTooltipBody', () => {
</TimezoneProvider>
);

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(
<TimezoneProvider timezone="America/New_York">
<TimestampTooltipBody timestamp={timestamp} attributes={attributes} />
</TimezoneProvider>
);

expect(screen.getByText('Received')).toBeInTheDocument();
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(
<TimezoneProvider timezone="America/New_York">
<TimestampTooltipBody
timestamp={timestamp}
attributes={attributes}
isTraceItemDetailsPending
/>
</TimezoneProvider>
);

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';
Expand Down
41 changes: 25 additions & 16 deletions static/app/views/explore/logs/logsTimeTooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,20 @@ type Props = {
attributes: Record<string, string | number | boolean>;
children: React.ReactNode;
timestamp: string | number;
isTraceItemDetailsPending?: boolean;
relativeTimeToReplay?: number;
shouldRender?: boolean;
};

function TimestampTooltipBody({
timestamp,
attributes,
isTraceItemDetailsPending,
relativeTime,
}: {
attributes: Record<string, string | number | boolean>;
timestamp: string | number;
isTraceItemDetailsPending?: boolean;
relativeTime?: number;
}) {
const currentTimezone = useTimezone();
Expand All @@ -39,7 +42,9 @@ function TimestampTooltipBody({
: null;
const timestampToUse = preciseTimestampMs ? new Date(preciseTimestampMs) : timestamp;

const observedTimeNanos = attributes[OurLogKnownFieldKey.OBSERVED_TIMESTAMP_PRECISE];
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;
Expand Down Expand Up @@ -94,21 +99,23 @@ function TimestampTooltipBody({
</Fragment>
)}

<Fragment>
<HorizontalRule />
<dt>{t('Received')}</dt>
<dd>
{observedTime ? (
<TimestampValues>
<AutoSelectText>
<DateTime date={observedTime} seconds timeZone />
</AutoSelectText>
</TimestampValues>
) : (
<LoadingIndicator size={16} style={{margin: 0}} />
)}
</dd>
</Fragment>
{(observedTime || isTraceItemDetailsPending) && (
<Fragment>
<HorizontalRule />
<dt>{t('Received')}</dt>
<dd>
{observedTime ? (
<TimestampValues>
<AutoSelectText>
<DateTime date={observedTime} seconds timeZone />
</AutoSelectText>
</TimestampValues>
) : (
<LoadingIndicator size={16} style={{margin: 0}} />
)}
</dd>
</Fragment>
)}
</DescriptionList>
);
}
Expand All @@ -119,6 +126,7 @@ export function LogsTimestampTooltip({
timestamp,
attributes,
children,
isTraceItemDetailsPending,
shouldRender = true,
relativeTimeToReplay: relativeTime,
}: Props) {
Expand All @@ -137,6 +145,7 @@ export function LogsTimestampTooltip({
<TimestampTooltipBody
timestamp={timestamp}
attributes={attributes}
isTraceItemDetailsPending={isTraceItemDetailsPending}
relativeTime={relativeTime}
/>
</div>
Expand Down
58 changes: 58 additions & 0 deletions static/app/views/explore/logs/tables/logsTableRow.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<LogRowContent
dataRow={rowDataWithInternalObservedTimestamp}
highlightTerms={[]}
meta={LogFixtureMeta(rowDataWithInternalObservedTimestamp)}
sharedHoverTimeoutRef={{current: null}}
/>,
{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,
Expand Down
Loading
Loading