Skip to content

Commit 463ab74

Browse files
authored
feat(linkedTraces): Add link to next trace (#89016)
Showing "Next Trace" in the trace view. This is a temporary solution as it is not super performant. It's getting all root spans within a specified timeframe (e.g. 1h) and searches for the trace that has the current trace as linked "previous trace". ![image](https://github.com/user-attachments/assets/12854fe9-f2e2-4b77-b44c-9908d8370237)
1 parent a77f457 commit 463ab74

File tree

4 files changed

+173
-21
lines changed

4 files changed

+173
-21
lines changed

static/app/views/insights/types.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,7 @@ export type SpanIndexedResponse = {
363363
[SpanIndexedField.USER_USERNAME]: string;
364364
[SpanIndexedField.USER_IP]: string;
365365
[SpanIndexedField.USER_DISPLAY]: string;
366+
[SpanIndexedField.IS_TRANSACTION]: number;
366367
[SpanIndexedField.INP]: number;
367368
[SpanIndexedField.INP_SCORE]: number;
368369
[SpanIndexedField.INP_SCORE_WEIGHT]: number;

static/app/views/performance/newTraceDetails/traceContextPanel.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,16 @@ export function TraceContextPanel({tree, rootEvent}: Props) {
5454
end: rootEvent.data?.endTimestamp,
5555
}}
5656
/>
57+
<TraceLinkNavigationButton
58+
direction={'next'}
59+
isLoading={rootEvent.isLoading}
60+
projectID={rootEvent.data?.projectID ?? ''}
61+
traceContext={rootEvent.data?.contexts.trace}
62+
currentTraceTimestamps={{
63+
start: rootEvent.data?.startTimestamp,
64+
end: rootEvent.data?.endTimestamp,
65+
}}
66+
/>
5767
</TraceLinksNavigationContainer>
5868
)}
5969

static/app/views/performance/newTraceDetails/traceLinksNavigation/traceLinkNavigationButton.tsx

Lines changed: 50 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -15,32 +15,32 @@ import {useLocation} from 'sentry/utils/useLocation';
1515
import useOrganization from 'sentry/utils/useOrganization';
1616
import {useTrace} from 'sentry/views/performance/newTraceDetails/traceApi/useTrace';
1717
import {isEmptyTrace} from 'sentry/views/performance/newTraceDetails/traceApi/utils';
18+
import {useFindNextTrace} from 'sentry/views/performance/newTraceDetails/traceLinksNavigation/useFindNextTrace';
1819
import {getTraceDetailsUrl} from 'sentry/views/performance/traceDetails/utils';
1920

20-
// Currently, we only support previous but component can be used for 'next trace' in the future
21-
type ConnectedTraceConnection = 'previous'; // | 'next';
21+
export type ConnectedTraceConnection = 'previous' | 'next';
2222

2323
const LINKED_TRACE_MAX_DURATION = 3600; // 1h in seconds
2424

2525
function useIsTraceAvailable(
26-
traceLink?: SpanLink,
27-
previousTraceTimestamp?: number
26+
traceID?: SpanLink['trace_id'],
27+
linkedTraceTimestamp?: number
2828
): {
2929
isAvailable: boolean;
3030
isLoading: boolean;
3131
} {
3232
const trace = useTrace({
33-
traceSlug: traceLink?.trace_id,
34-
timestamp: previousTraceTimestamp,
33+
traceSlug: traceID,
34+
timestamp: linkedTraceTimestamp,
3535
});
3636

3737
const isAvailable = useMemo(() => {
38-
if (!traceLink) {
38+
if (!traceID) {
3939
return false;
4040
}
4141

4242
return Boolean(trace.data && !isEmptyTrace(trace.data));
43-
}, [traceLink, trace]);
43+
}, [traceID, trace]);
4444

4545
return {
4646
isAvailable,
@@ -52,37 +52,47 @@ type TraceLinkNavigationButtonProps = {
5252
currentTraceTimestamps: {end?: number; start?: number};
5353
direction: ConnectedTraceConnection;
5454
isLoading?: boolean;
55+
projectID?: string;
5556
traceContext?: TraceContextType;
5657
};
5758

5859
export function TraceLinkNavigationButton({
5960
direction,
6061
traceContext,
6162
isLoading,
63+
projectID,
6264
currentTraceTimestamps,
6365
}: TraceLinkNavigationButtonProps) {
6466
const organization = useOrganization();
6567
const location = useLocation();
6668

67-
const traceLink = traceContext?.links?.find(
68-
link => link.attributes?.['sentry.link.type'] === `${direction}_trace`
69-
);
70-
7169
// We connect traces over a 1h period - As we don't have timestamps of the linked trace, it is calculated based on this timeframe
7270
const linkedTraceTimestamp =
7371
direction === 'previous' && currentTraceTimestamps.start
74-
? currentTraceTimestamps.start - LINKED_TRACE_MAX_DURATION // Earliest start times of previous trace
75-
: // : direction === 'next' && currentTraceTimestamps.end
76-
// ? currentTraceTimestamps.end + LINKED_TRACE_MAX_DURATION
77-
undefined;
72+
? currentTraceTimestamps.start - LINKED_TRACE_MAX_DURATION // Earliest start time of previous trace (- 1h)
73+
: direction === 'next' && currentTraceTimestamps.end
74+
? currentTraceTimestamps.end + LINKED_TRACE_MAX_DURATION // Latest end time of next trace (+ 1h)
75+
: undefined;
76+
77+
const previousTraceLink = traceContext?.links?.find(
78+
link => link.attributes?.['sentry.link.type'] === `${direction}_trace`
79+
);
80+
81+
const nextTraceData = useFindNextTrace({
82+
direction,
83+
currentTraceID: traceContext?.trace_id,
84+
linkedTraceStartTimestamp: currentTraceTimestamps.end,
85+
linkedTraceEndTimestamp: linkedTraceTimestamp,
86+
projectID,
87+
});
7888

7989
const dateSelection = useMemo(
8090
() => normalizeDateTimeParams(location.query),
8191
[location.query]
8292
);
8393

8494
const {isAvailable: isLinkedTraceAvailable} = useIsTraceAvailable(
85-
traceLink,
95+
direction === 'previous' ? previousTraceLink?.trace_id : nextTraceData?.trace_id,
8696
linkedTraceTimestamp
8797
);
8898

@@ -92,13 +102,13 @@ export function TraceLinkNavigationButton({
92102
return null;
93103
}
94104

95-
if (traceLink && isLinkedTraceAvailable) {
105+
if (previousTraceLink && isLinkedTraceAvailable) {
96106
return (
97107
<TraceLink
98108
color="gray500"
99109
to={getTraceDetailsUrl({
100-
traceSlug: traceLink.trace_id,
101-
spanId: traceLink.span_id,
110+
traceSlug: previousTraceLink.trace_id,
111+
spanId: previousTraceLink.span_id,
102112
dateSelection,
103113
timestamp: linkedTraceTimestamp,
104114
location,
@@ -111,7 +121,26 @@ export function TraceLinkNavigationButton({
111121
);
112122
}
113123

114-
if (traceLink?.sampled === false) {
124+
if (nextTraceData?.trace_id && nextTraceData.span_id && isLinkedTraceAvailable) {
125+
return (
126+
<TraceLink
127+
color="gray500"
128+
to={getTraceDetailsUrl({
129+
traceSlug: nextTraceData.trace_id,
130+
spanId: nextTraceData.span_id,
131+
dateSelection,
132+
timestamp: linkedTraceTimestamp,
133+
location,
134+
organization,
135+
})}
136+
>
137+
<TraceLinkText>{t('Go to Next Trace')}</TraceLinkText>
138+
<IconChevron direction="right" />
139+
</TraceLink>
140+
);
141+
}
142+
143+
if (previousTraceLink?.sampled === false) {
115144
return (
116145
<StyledTooltip
117146
position="right"
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import {useMemo} from 'react';
2+
3+
import type {TraceContextType} from 'sentry/components/events/interfaces/spans/types';
4+
import type {EventTransaction} from 'sentry/types/event';
5+
import {useApiQueries} from 'sentry/utils/queryClient';
6+
import {MutableSearch} from 'sentry/utils/tokenizeSearch';
7+
import useOrganization from 'sentry/utils/useOrganization';
8+
import {useSyncedLocalStorageState} from 'sentry/utils/useSyncedLocalStorageState';
9+
import {useSpansIndexed} from 'sentry/views/insights/common/queries/useDiscover';
10+
import {SpanIndexedField} from 'sentry/views/insights/types';
11+
import {TRACE_FORMAT_PREFERENCE_KEY} from 'sentry/views/performance/newTraceDetails/traceHeader/styles';
12+
13+
import type {ConnectedTraceConnection} from './traceLinkNavigationButton';
14+
15+
/**
16+
* A temporary solution for getting "next trace" data.
17+
* As traces currently only include information about the previous trace, the next trace is obtained by
18+
* getting all root spans in a certain timespan (e.g. 1h) and searching the root span which has the
19+
* current trace as it's "previous trace".
20+
*/
21+
export function useFindNextTrace({
22+
direction,
23+
currentTraceID,
24+
linkedTraceStartTimestamp,
25+
linkedTraceEndTimestamp,
26+
projectID,
27+
}: {
28+
direction: ConnectedTraceConnection;
29+
currentTraceID?: string;
30+
linkedTraceEndTimestamp?: number;
31+
linkedTraceStartTimestamp?: number;
32+
projectID?: string;
33+
}): TraceContextType | undefined {
34+
const {data: indexedSpans} = useSpansIndexed(
35+
{
36+
limit: direction === 'next' && projectID ? 100 : 1,
37+
noPagination: true,
38+
pageFilters: {
39+
projects: projectID ? [Number(projectID)] : [],
40+
environments: [],
41+
datetime: {
42+
period: null,
43+
utc: null,
44+
start: linkedTraceStartTimestamp
45+
? new Date(linkedTraceStartTimestamp * 1000).toISOString()
46+
: null,
47+
end: linkedTraceEndTimestamp
48+
? new Date(linkedTraceEndTimestamp * 1000).toISOString()
49+
: null,
50+
},
51+
},
52+
search: MutableSearch.fromQueryObject({is_transaction: 1}),
53+
fields: [
54+
SpanIndexedField.TRANSACTION_ID,
55+
SpanIndexedField.PROJECT_ID,
56+
SpanIndexedField.PROJECT,
57+
],
58+
},
59+
'api.trace-view.linked-traces'
60+
);
61+
62+
const traceData = indexedSpans.map(span => ({
63+
projectSlug: span.project,
64+
eventId: span['transaction.id'],
65+
}));
66+
67+
const rootEvents = useTraceRootEvents(traceData);
68+
69+
const nextTrace = rootEvents.find(rootEvent => {
70+
const traceContext = rootEvent.data?.contexts?.trace;
71+
const hasMatchingLink = traceContext?.links?.some(
72+
link =>
73+
link.attributes?.['sentry.link.type'] === `previous_trace` &&
74+
link.trace_id === currentTraceID
75+
);
76+
77+
return hasMatchingLink;
78+
});
79+
80+
return nextTrace?.data?.contexts.trace;
81+
}
82+
83+
// Similar to `useTraceRootEvent` but allows fetching data for "more than one" trace data
84+
function useTraceRootEvents(
85+
traceData: Array<{eventId?: string; projectSlug?: string}> | null
86+
) {
87+
const organization = useOrganization();
88+
const [storedTraceFormat] = useSyncedLocalStorageState(
89+
TRACE_FORMAT_PREFERENCE_KEY,
90+
'non-eap'
91+
);
92+
93+
const queryKeys = useMemo(() => {
94+
if (!traceData) {
95+
return [];
96+
}
97+
98+
return traceData.map(
99+
trace =>
100+
[
101+
`/organizations/${organization.slug}/events/${trace?.projectSlug}:${trace.eventId}/`,
102+
{query: {referrer: 'trace-details-summary'}},
103+
] as const
104+
);
105+
}, [traceData, organization.slug]);
106+
107+
return useApiQueries<EventTransaction>(queryKeys, {
108+
staleTime: 0,
109+
enabled:
110+
Array.isArray(traceData) && traceData.length > 0 && storedTraceFormat === 'non-eap',
111+
});
112+
}

0 commit comments

Comments
 (0)