Skip to content

Commit 2bcbd18

Browse files
authored
feat(trace-waterfall): Add previous and next trace buttons (EAP) (#96510)
Reworks the previous implementation of showing previous/next trace buttons: - Linked Trace buttons are now compatible with EAP (only)! - For now, we solely rely on the `sentry.previous_trace` span attribute. Meaning, we don't use span links at the moment, because span links in EAP are WIP. In the near future they'll be stored as a JSON blob. For us to rely on them, we need them to be queryable again though. Therefore, the SDK currently sends the `sentry.previous_trace` span attribute, which we can use to search for specific spans on the EAP dataset. - the buttons are still feature-flagged for the `sentry` and `sentry-sdks` orgs. We can remove the flags once we dogfooded for a bit and things seem to work as expected. - Use the `useSpans` query to find the next trace linking to the current trace (reverse span link lookup) - Use the `useSpans` instead of `useTrace` query to check if the previous trace is actually available (vs. dropped server-side/not ingested) - Rework the logic around showing "previous trace not available|sampled" errors - Use two dedicated hooks `findPreviousTrace` and `findNextTrace` instead of once combined - Add placeholders to avoid the waterfall panel's lower end shifting upwards once links are loaded (see video) - This has the inherent disadvantage that the vertical space for the waterfall becomes smaller. We can explore alternative positions for the previous/next buttons to get rid of this limitation again. I'd argue that for now, as long as things are feature flagged, having the placeholders is better than causing layout shift and repaints.
1 parent 7c5c6db commit 2bcbd18

File tree

7 files changed

+362
-247
lines changed

7 files changed

+362
-247
lines changed

static/app/components/events/interfaces/spans/types.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,7 @@ export enum TickAlignment {
202202

203203
type AttributeValue = string | number | boolean | string[] | number[] | boolean[];
204204

205-
export type SpanLink = {
205+
type SpanLink = {
206206
span_id: string;
207207
trace_id: string;
208208
attributes?: Record<string, AttributeValue> & {'sentry.link.type'?: AttributeValue};

static/app/views/insights/common/queries/useDiscover.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@ import {DiscoverDatasets} from 'sentry/utils/discover/types';
55
import type {MutableSearch} from 'sentry/utils/tokenizeSearch';
66
import usePageFilters from 'sentry/utils/usePageFilters';
77
import type {SamplingMode} from 'sentry/views/explore/hooks/useProgressiveQuery';
8-
import {useWrappedDiscoverQuery} from 'sentry/views/insights/common/queries/useSpansQuery';
8+
import {
9+
useWrappedDiscoverQuery,
10+
useWrappedDiscoverQueryWithoutPageFilters,
11+
} from 'sentry/views/insights/common/queries/useSpansQuery';
912
import type {SpanProperty, SpanResponse} from 'sentry/views/insights/types';
1013

1114
interface UseDiscoverQueryOptions {
@@ -22,6 +25,11 @@ interface UseDiscoverOptions<Fields> {
2225
orderby?: string | string[];
2326
pageFilters?: PageFilters;
2427
projectIds?: number[];
28+
/**
29+
* If true, the query will be executed without the page filters.
30+
* {@link pageFilters} can still be passed and will be used to build the event view on top of the query.
31+
*/
32+
queryWithoutPageFilters?: boolean;
2533
samplingMode?: SamplingMode;
2634
/**
2735
* TODO - ideally this probably would be only `Mutable Search`, but it doesn't handle some situations well
@@ -72,7 +80,11 @@ const useDiscover = <T extends Array<Extract<keyof ResponseType, string>>, Respo
7280
orderby
7381
);
7482

75-
const result = useWrappedDiscoverQuery({
83+
const queryFn = options.queryWithoutPageFilters
84+
? useWrappedDiscoverQueryWithoutPageFilters
85+
: useWrappedDiscoverQuery;
86+
87+
const result = queryFn({
7688
eventView,
7789
initialData: [],
7890
limit,

static/app/views/insights/common/queries/useSpansQuery.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -303,7 +303,7 @@ export function useWrappedDiscoverQuery<T>(props: WrappedDiscoverQueryProps<T>)
303303
return useWrappedDiscoverQueryBase({...props, pageFiltersReady});
304304
}
305305

306-
function useWrappedDiscoverQueryWithoutPageFilters<T>(
306+
export function useWrappedDiscoverQueryWithoutPageFilters<T>(
307307
props: WrappedDiscoverQueryProps<T>
308308
) {
309309
return useWrappedDiscoverQueryBase({...props, pageFiltersReady: true});

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

Lines changed: 122 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -3,143 +3,149 @@ import styled from '@emotion/styled';
33

44
import {ExternalLink, Link} from 'sentry/components/core/link';
55
import {Tooltip} from 'sentry/components/core/tooltip';
6-
import type {
7-
SpanLink,
8-
TraceContextType,
9-
} from 'sentry/components/events/interfaces/spans/types';
106
import {normalizeDateTimeParams} from 'sentry/components/organizations/pageFilters/parse';
117
import {IconChevron} from 'sentry/icons';
128
import {t, tct} from 'sentry/locale';
139
import {space} from 'sentry/styles/space';
1410
import {useLocation} from 'sentry/utils/useLocation';
1511
import useOrganization from 'sentry/utils/useOrganization';
16-
import {useTrace} from 'sentry/views/performance/newTraceDetails/traceApi/useTrace';
17-
import {isEmptyTrace} from 'sentry/views/performance/newTraceDetails/traceApi/utils';
18-
import {useFindNextTrace} from 'sentry/views/performance/newTraceDetails/traceLinksNavigation/useFindNextTrace';
12+
import type {TraceItemResponseAttribute} from 'sentry/views/explore/hooks/useTraceItemDetails';
13+
import {
14+
useFindNextTrace,
15+
useFindPreviousTrace,
16+
} from 'sentry/views/performance/newTraceDetails/traceLinksNavigation/useFindLinkedTraces';
17+
import {useTraceStateDispatch} from 'sentry/views/performance/newTraceDetails/traceState/traceStateProvider';
1918
import {getTraceDetailsUrl} from 'sentry/views/performance/traceDetails/utils';
2019

2120
export type ConnectedTraceConnection = 'previous' | 'next';
2221

2322
const LINKED_TRACE_MAX_DURATION = 3600; // 1h in seconds
2423

25-
function useIsTraceAvailable(
26-
traceID?: SpanLink['trace_id'],
27-
linkedTraceTimestamp?: number
28-
): {
29-
isAvailable: boolean;
30-
isLoading: boolean;
31-
} {
32-
const trace = useTrace({
33-
traceSlug: traceID,
34-
timestamp: linkedTraceTimestamp,
35-
});
36-
37-
const isAvailable = useMemo(() => {
38-
if (!traceID) {
39-
return false;
40-
}
41-
42-
return Boolean(trace.data && !isEmptyTrace(trace.data));
43-
}, [traceID, trace]);
44-
45-
return {
46-
isAvailable,
47-
isLoading: trace.isLoading,
48-
};
49-
}
50-
5124
type TraceLinkNavigationButtonProps = {
52-
currentTraceTimestamps: {end?: number; start?: number};
25+
attributes: TraceItemResponseAttribute[];
26+
currentTraceStartTimestamp: number;
5327
direction: ConnectedTraceConnection;
54-
isLoading?: boolean;
55-
projectID?: string;
56-
traceContext?: TraceContextType;
5728
};
5829

5930
export function TraceLinkNavigationButton({
6031
direction,
61-
traceContext,
62-
isLoading,
63-
projectID,
64-
currentTraceTimestamps,
32+
attributes,
33+
currentTraceStartTimestamp,
6534
}: TraceLinkNavigationButtonProps) {
6635
const organization = useOrganization();
6736
const location = useLocation();
6837

6938
// We connect traces over a 1h period - As we don't have timestamps of the linked trace, it is calculated based on this timeframe
70-
const linkedTraceTimestamp =
71-
direction === 'previous' && currentTraceTimestamps.start
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-
);
39+
const linkedTraceWindowTimestamp =
40+
direction === 'previous'
41+
? currentTraceStartTimestamp - LINKED_TRACE_MAX_DURATION // Earliest start time of previous trace (- 1h)
42+
: currentTraceStartTimestamp + LINKED_TRACE_MAX_DURATION; // Latest end time of next trace (+ 1h)
43+
44+
const {
45+
available: isPreviousTraceAvailable,
46+
id: previousTraceSpanId,
47+
trace: previousTraceId,
48+
sampled: previousTraceSampled,
49+
isLoading: isPreviousTraceLoading,
50+
} = useFindPreviousTrace({
51+
direction,
52+
previousTraceEndTimestamp: currentTraceStartTimestamp,
53+
previousTraceStartTimestamp: linkedTraceWindowTimestamp,
54+
attributes,
55+
});
8056

81-
const nextTraceData = useFindNextTrace({
57+
const {
58+
id: nextTraceSpanId,
59+
trace: nextTraceId,
60+
isLoading: isNextTraceLoading,
61+
} = useFindNextTrace({
8262
direction,
83-
currentTraceID: traceContext?.trace_id,
84-
linkedTraceStartTimestamp: currentTraceTimestamps.end,
85-
linkedTraceEndTimestamp: linkedTraceTimestamp,
86-
projectID,
63+
nextTraceEndTimestamp: linkedTraceWindowTimestamp,
64+
nextTraceStartTimestamp: currentTraceStartTimestamp,
65+
attributes,
8766
});
8867

8968
const dateSelection = useMemo(
9069
() => normalizeDateTimeParams(location.query),
9170
[location.query]
9271
);
9372

94-
const {isAvailable: isLinkedTraceAvailable} = useIsTraceAvailable(
95-
direction === 'previous' ? previousTraceLink?.trace_id : nextTraceData?.trace_id,
96-
linkedTraceTimestamp
97-
);
73+
const traceDispatch = useTraceStateDispatch();
9874

99-
if (isLoading) {
100-
// We don't show a placeholder/skeleton here as it would cause layout shifts most of the time.
101-
// Most traces don't have a next/previous trace and the hard to avoid layout shift should only occur if the actual button can be shown.
102-
return null;
75+
function closeSpanDetailsDrawer() {
76+
traceDispatch({
77+
type: 'minimize drawer',
78+
payload: true,
79+
});
10380
}
10481

105-
if (previousTraceLink && isLinkedTraceAvailable) {
106-
return (
107-
<StyledTooltip
108-
position="right"
109-
delay={400}
110-
isHoverable
111-
title={tct(
112-
`This links to the previous trace within the same session. To learn more, [link:read the docs].`,
113-
{
114-
link: (
115-
<ExternalLink
116-
href={
117-
'https://docs.sentry.io/concepts/key-terms/tracing/trace-view/#previous-and-next-traces'
118-
}
119-
/>
120-
),
121-
}
122-
)}
123-
>
124-
<TraceLink
125-
color="gray500"
126-
to={getTraceDetailsUrl({
127-
traceSlug: previousTraceLink.trace_id,
128-
spanId: previousTraceLink.span_id,
129-
dateSelection,
130-
timestamp: linkedTraceTimestamp,
131-
location,
132-
organization,
133-
})}
82+
if (direction === 'previous' && previousTraceId && !isPreviousTraceLoading) {
83+
if (isPreviousTraceAvailable) {
84+
return (
85+
<StyledTooltip
86+
position="right"
87+
delay={400}
88+
isHoverable
89+
title={tct(
90+
`This links to the previous trace within the same session. To learn more, [link:read the docs].`,
91+
{
92+
link: (
93+
<ExternalLink
94+
href={
95+
'https://docs.sentry.io/concepts/key-terms/tracing/trace-view/#previous-and-next-traces'
96+
}
97+
/>
98+
),
99+
}
100+
)}
134101
>
135-
<IconChevron direction="left" />
136-
<TraceLinkText>{t('Go to Previous Trace')}</TraceLinkText>
137-
</TraceLink>
138-
</StyledTooltip>
139-
);
102+
<TraceLink
103+
color="gray500"
104+
onClick={() => closeSpanDetailsDrawer()}
105+
to={getTraceDetailsUrl({
106+
traceSlug: previousTraceId,
107+
spanId: previousTraceSpanId,
108+
dateSelection,
109+
timestamp: linkedTraceWindowTimestamp,
110+
location,
111+
organization,
112+
})}
113+
>
114+
<IconChevron direction="left" />
115+
<TraceLinkText>{t('Go to Previous Trace')}</TraceLinkText>
116+
</TraceLink>
117+
</StyledTooltip>
118+
);
119+
}
120+
121+
if (!previousTraceSampled) {
122+
return (
123+
<StyledTooltip
124+
position="right"
125+
title={t(
126+
'Trace contains a link to an unsampled trace. Increase traces sample rate in SDK settings to see more connected traces'
127+
)}
128+
>
129+
<TraceLinkText>{t('Previous trace not sampled')}</TraceLinkText>
130+
</StyledTooltip>
131+
);
132+
}
133+
134+
if (!isPreviousTraceAvailable) {
135+
return (
136+
<StyledTooltip
137+
position="right"
138+
title={t(
139+
'Trace contains a link to a trace that is not available. This means that the trace was not stored.'
140+
)}
141+
>
142+
<TraceLinkText>{t('Previous trace not available')}</TraceLinkText>
143+
</StyledTooltip>
144+
);
145+
}
140146
}
141147

142-
if (nextTraceData?.trace_id && nextTraceData.span_id && isLinkedTraceAvailable) {
148+
if (direction === 'next' && !isNextTraceLoading && nextTraceId && nextTraceSpanId) {
143149
return (
144150
<StyledTooltip
145151
position="left"
@@ -160,11 +166,12 @@ export function TraceLinkNavigationButton({
160166
>
161167
<TraceLink
162168
color="gray500"
169+
onClick={closeSpanDetailsDrawer}
163170
to={getTraceDetailsUrl({
164-
traceSlug: nextTraceData.trace_id,
165-
spanId: nextTraceData.span_id,
171+
traceSlug: nextTraceId,
172+
spanId: nextTraceSpanId,
166173
dateSelection,
167-
timestamp: linkedTraceTimestamp,
174+
timestamp: linkedTraceWindowTimestamp,
168175
location,
169176
organization,
170177
})}
@@ -176,25 +183,23 @@ export function TraceLinkNavigationButton({
176183
);
177184
}
178185

179-
if (previousTraceLink?.sampled === false) {
180-
return (
181-
<StyledTooltip
182-
position="right"
183-
title={t(
184-
'Trace contains a link to unsampled trace. Increase traces sample rate in SDK settings to see more connected traces'
185-
)}
186-
>
187-
<TraceLinkText>{t('Previous trace not available')}</TraceLinkText>
188-
</StyledTooltip>
189-
);
190-
}
186+
// If there's no linked trace, let's render a placeholder for now to avoid layout shifts
187+
// We should reconsider the place where we render these buttons, to avoid reducing the
188+
// waterfall height permanently
189+
return <TraceLinkNavigationButtonPlaceHolder />;
190+
}
191191

192-
// If there is no linked trace or an undefined sampling decision
193-
return null;
192+
export function TraceLinkNavigationButtonPlaceHolder() {
193+
return <PlaceHolderText>&nbsp;</PlaceHolderText>;
194194
}
195195

196+
const PlaceHolderText = styled('span')`
197+
padding: ${space(0.5)} ${space(0.5)};
198+
visibility: hidden;
199+
`;
200+
196201
const StyledTooltip = styled(Tooltip)`
197-
padding: ${space(0.5)} ${space(1)};
202+
padding: ${space(0.5)} ${space(0.5)};
198203
text-decoration: underline dotted
199204
${p => (p.disabled ? p.theme.gray300 : p.theme.gray300)};
200205
`;

0 commit comments

Comments
 (0)