Skip to content

Commit 83bba26

Browse files
Abdkhan14Abdullah Khangetsantry[bot]
authored
feat(trace-tree-node): Mitigating error node type guards usage (#104501)
Co-authored-by: Abdullah Khan <abdullahkhan@PG9Y57YDXQ.local> Co-authored-by: getsantry[bot] <66042841+getsantry[bot]@users.noreply.github.com>
1 parent 1eb2f73 commit 83bba26

File tree

28 files changed

+923
-446
lines changed

28 files changed

+923
-446
lines changed

static/app/components/events/interfaces/performance/anrRootCause.tsx

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ import {SectionKey} from 'sentry/views/issueDetails/streamline/context';
2525
import {InterimSection} from 'sentry/views/issueDetails/streamline/interimSection';
2626
import {useIssuesTraceTree} from 'sentry/views/performance/newTraceDetails/traceApi/useIssuesTraceTree';
2727
import {useTrace} from 'sentry/views/performance/newTraceDetails/traceApi/useTrace';
28-
import {isEAPTraceOccurrence} from 'sentry/views/performance/newTraceDetails/traceGuards';
2928
import useTraceStateAnalytics from 'sentry/views/performance/newTraceDetails/useTraceStateAnalytics';
3029

3130
enum AnrRootCauseAllowlist {
@@ -143,12 +142,9 @@ export function AnrRootCause({event, organization}: Props) {
143142
>
144143
{potentialAnrRootCause?.map(occurence => {
145144
const project = projects.find(p => p.id === occurence.project_id.toString());
146-
const title = isEAPTraceOccurrence(occurence)
147-
? occurence.description
148-
: occurence.title;
149-
const shortId = isEAPTraceOccurrence(occurence)
150-
? occurence.short_id
151-
: occurence.issue_short_id;
145+
const isEAPOccurence = 'description' in occurence;
146+
const title = isEAPOccurence ? occurence.description : occurence.title;
147+
const shortId = isEAPOccurence ? occurence.short_id : occurence.issue_short_id;
152148
return (
153149
<IssueSummary key={occurence.issue_id}>
154150
<Title>

static/app/views/performance/newTraceDetails/traceApi/useTrace.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import {decodeScalar} from 'sentry/utils/queryString';
1010
import type RequestError from 'sentry/utils/requestError/requestError';
1111
import useOrganization from 'sentry/utils/useOrganization';
1212
import usePageFilters from 'sentry/utils/usePageFilters';
13-
import {isValidEventUUID} from 'sentry/views/performance/newTraceDetails/traceApi/utils';
1413
import type {TraceTree} from 'sentry/views/performance/newTraceDetails/traceModels/traceTree';
1514
import {useIsEAPTraceEnabled} from 'sentry/views/performance/newTraceDetails/useIsEAPTraceEnabled';
1615

@@ -291,3 +290,9 @@ export function useTrace(
291290

292291
return isDemoMode ? demoTrace : isEAPEnabled ? eapTraceQuery : traceQuery;
293292
}
293+
294+
const isValidEventUUID = (id: string): boolean => {
295+
const uuidRegex =
296+
/^[0-9a-f]{8}[0-9a-f]{4}[1-5][0-9a-f]{3}[89ab][0-9a-f]{3}[0-9a-f]{12}$/i;
297+
return uuidRegex.test(id);
298+
};

static/app/views/performance/newTraceDetails/traceApi/useTraceRootEvent.tsx

Lines changed: 14 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,7 @@ import {
1111
type OurLogsResponseItem,
1212
} from 'sentry/views/explore/logs/types';
1313
import {TraceItemDataset} from 'sentry/views/explore/types';
14-
import {getRepresentativeTraceEvent} from 'sentry/views/performance/newTraceDetails/traceApi/utils';
15-
import {
16-
isEAPError,
17-
isTraceError,
18-
} from 'sentry/views/performance/newTraceDetails/traceGuards';
1914
import type {TraceTree} from 'sentry/views/performance/newTraceDetails/traceModels/traceTree';
20-
import {useIsEAPTraceEnabled} from 'sentry/views/performance/newTraceDetails/useIsEAPTraceEnabled';
2115

2216
type Params = {
2317
logs: OurLogsResponseItem[] | undefined;
@@ -34,28 +28,20 @@ export function useTraceRootEvent({
3428
logs,
3529
traceId,
3630
}: Params): TraceRootEventQueryResults {
37-
const rep = getRepresentativeTraceEvent(tree, logs);
31+
const rep = tree.findRepresentativeTraceNode({logs});
3832
const organization = useOrganization();
3933

40-
// TODO: This is a bit of a mess, we won't need all of this once we switch to EAP only
4134
const treeIsLoading = tree.type === 'loading';
42-
const hasOnlyLogs = !!(tree.type === 'empty' && logs && logs.length > 0);
43-
const enabledBase =
44-
!treeIsLoading && (tree.type === 'trace' || hasOnlyLogs) && !!rep?.event && !!traceId;
4535

46-
const isRepEventError =
47-
rep.event && OurLogKnownFieldKey.PROJECT_ID in rep.event
48-
? false
49-
: isTraceError(rep.event) || isEAPError(rep.event);
36+
const enabledBase = !treeIsLoading && !!rep?.event;
5037

51-
const isEAPTraceEnabled = useIsEAPTraceEnabled();
52-
const isEAPQueryEnabled =
53-
!isRepEventError && // Errors are not supported in EAP yet
54-
(isEAPTraceEnabled || (!treeIsLoading && hasOnlyLogs));
38+
const isRepLog = rep?.dataset === TraceItemDataset.LOGS;
39+
const isEAPQueryEnabled = !!(isRepLog || rep?.event?.isEAPEvent);
5540

41+
const projectSlug = rep?.event?.projectSlug;
5642
const legacyRootEvent = useApiQuery<EventTransaction>(
5743
[
58-
`/organizations/${organization.slug}/events/${rep?.event?.project_slug}:${rep?.event?.event_id}/`,
44+
`/organizations/${organization.slug}/events/${projectSlug}:${rep?.event?.id}/`,
5945
{
6046
query: {
6147
referrer: 'trace-details-summary',
@@ -65,32 +51,27 @@ export function useTraceRootEvent({
6551
{
6652
// 10 minutes
6753
staleTime: 1000 * 60 * 10,
68-
enabled: enabledBase && !isEAPQueryEnabled,
54+
enabled: enabledBase && !isEAPQueryEnabled && !!projectSlug && !!rep?.event?.id,
6955
}
7056
);
7157

72-
const projectId = rep.event
58+
const projectId = rep?.event
7359
? OurLogKnownFieldKey.PROJECT_ID in rep.event
7460
? rep.event[OurLogKnownFieldKey.PROJECT_ID]
75-
: rep.event.project_id
61+
: rep.event.projectId
7662
: '';
77-
const eventId = rep.event
78-
? OurLogKnownFieldKey.ID in rep.event
63+
const eventId = rep?.event
64+
? OurLogKnownFieldKey.PROJECT_ID in rep.event
7965
? rep.event[OurLogKnownFieldKey.ID]
80-
: rep.event.event_id
66+
: rep.event.id
8167
: '';
82-
83-
const itemTypes = {
84-
log: TraceItemDataset.LOGS,
85-
span: TraceItemDataset.SPANS,
86-
uptime_check: TraceItemDataset.UPTIME_RESULTS,
87-
};
68+
const dataset = rep?.dataset ?? TraceItemDataset.SPANS;
8869

8970
const rootEvent = useTraceItemDetails({
9071
traceItemId: String(eventId),
9172
projectId: String(projectId),
9273
traceId,
93-
traceItemType: itemTypes[rep.type],
74+
traceItemType: dataset,
9475
referrer: 'api.explore.log-item-details',
9576
enabled: enabledBase && isEAPQueryEnabled,
9677
});
Lines changed: 16 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,22 @@
11
import type {TraceItemDetailsResponse} from 'sentry/views/explore/hooks/useTraceItemDetails';
2-
import type {OurLogsResponseItem} from 'sentry/views/explore/logs/types';
2+
import type {TraceSplitResults} from 'sentry/views/performance/newTraceDetails/traceApi/types';
33
import type {TraceRootEventQueryResults} from 'sentry/views/performance/newTraceDetails/traceApi/useTraceRootEvent';
4-
import {
5-
isEAPTraceNode,
6-
isRootEvent,
7-
isTraceNode,
8-
isTraceSplitResult,
9-
isUptimeCheckNode,
10-
} from 'sentry/views/performance/newTraceDetails/traceGuards';
114
import type {TraceTree} from 'sentry/views/performance/newTraceDetails/traceModels/traceTree';
5+
import type {BaseNode} from 'sentry/views/performance/newTraceDetails/traceModels/traceTreeNode/baseNode';
6+
7+
export function isBrowserRequestNode(node: BaseNode): boolean {
8+
return (
9+
// Adjust for SDK changes in https://github.com/getsentry/sentry-javascript/pull/13527
10+
node.op === 'browser.request' ||
11+
(node.op === 'browser' && node.description === 'request')
12+
);
13+
}
14+
15+
export function isTraceSplitResult(
16+
result: TraceTree.Trace
17+
): result is TraceSplitResults<TraceTree.Transaction> {
18+
return 'transactions' in result && 'orphan_errors' in result;
19+
}
1220

1321
export function isEmptyTrace(trace: TraceTree.Trace): boolean {
1422
if (isTraceSplitResult(trace)) {
@@ -18,110 +26,8 @@ export function isEmptyTrace(trace: TraceTree.Trace): boolean {
1826
return trace.length === 0;
1927
}
2028

21-
const CANDIDATE_TRACE_TITLE_OPS = ['pageload', 'navigation', 'ui.load'];
22-
23-
export type RepresentativeTraceEvent = {
24-
event: TraceTree.TraceEvent | OurLogsResponseItem | null;
25-
type: 'span' | 'log' | 'uptime_check';
26-
};
27-
28-
export const getRepresentativeTraceEvent = (
29-
tree: TraceTree,
30-
logs: OurLogsResponseItem[] | undefined
31-
): RepresentativeTraceEvent => {
32-
const hasLogs = logs && logs.length > 0;
33-
if (tree.type === 'empty' && hasLogs) {
34-
return {
35-
event: logs[0]!,
36-
type: 'log',
37-
};
38-
}
39-
40-
const traceNode = tree.root.children[0];
41-
42-
if (!traceNode) {
43-
return {
44-
event: null,
45-
type: 'span',
46-
};
47-
}
48-
49-
if (!isTraceNode(traceNode) && !isEAPTraceNode(traceNode)) {
50-
throw new TypeError('Not trace node');
51-
}
52-
53-
const traceChild = traceNode.children[0];
54-
55-
if (traceChild && isUptimeCheckNode(traceChild)) {
56-
return {type: 'uptime_check', event: traceChild.value};
57-
}
58-
59-
let preferredRootEvent: TraceTree.TraceEvent | null = null;
60-
let firstRootEvent: TraceTree.TraceEvent | null = null;
61-
let candidateEvent: TraceTree.TraceEvent | null = null;
62-
let firstEvent: TraceTree.TraceEvent | null = null;
63-
64-
const isEAP = isEAPTraceNode(traceNode);
65-
const events = isEAP
66-
? traceNode.value
67-
: [...traceNode.value.transactions, ...traceNode.value.orphan_errors];
68-
for (const event of events) {
69-
if (isRootEvent(event)) {
70-
if (!firstRootEvent) {
71-
firstRootEvent = event;
72-
}
73-
74-
if (hasPreferredOp(event)) {
75-
preferredRootEvent = event;
76-
break;
77-
}
78-
// Otherwise we keep looking for a root eap transaction. If we don't find one, we use other roots, like standalone spans.
79-
continue;
80-
} else if (
81-
// If we haven't found a root transaction, but we found a candidate transaction
82-
// with an op that we care about, we can use it for the title. We keep looking for
83-
// a root.
84-
!candidateEvent &&
85-
hasPreferredOp(event)
86-
) {
87-
candidateEvent = event;
88-
continue;
89-
} else if (!firstEvent) {
90-
// If we haven't found a root or candidate transaction, we can use the first transaction
91-
// in the trace for the title.
92-
firstEvent = event;
93-
}
94-
}
95-
96-
return {
97-
event: preferredRootEvent ?? firstRootEvent ?? candidateEvent ?? firstEvent,
98-
type: 'span',
99-
};
100-
};
101-
10229
export const isTraceItemDetailsResponse = (
10330
data: TraceRootEventQueryResults['data']
10431
): data is TraceItemDetailsResponse => {
10532
return data !== undefined && 'attributes' in data;
10633
};
107-
108-
export const isValidEventUUID = (id: string): boolean => {
109-
const uuidRegex =
110-
/^[0-9a-f]{8}[0-9a-f]{4}[1-5][0-9a-f]{3}[89ab][0-9a-f]{3}[0-9a-f]{12}$/i;
111-
return uuidRegex.test(id);
112-
};
113-
114-
/**
115-
* Prefer "special" root events over generic root events when generating a title
116-
* for the waterfall view. Picking these improves contextual navigation for linked
117-
* traces, resulting in more meaningful waterfall titles.
118-
*/
119-
function hasPreferredOp(event: TraceTree.TraceEvent): boolean {
120-
const op =
121-
'op' in event
122-
? event.op
123-
: 'transaction.op' in event
124-
? event['transaction.op']
125-
: undefined;
126-
return !!op && CANDIDATE_TRACE_TITLE_OPS.includes(op);
127-
}

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

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ import {
2424
import {SectionDivider} from 'sentry/views/issueDetails/streamline/foldSection';
2525
import type {TraceRootEventQueryResults} from 'sentry/views/performance/newTraceDetails/traceApi/useTraceRootEvent';
2626
import {isTraceItemDetailsResponse} from 'sentry/views/performance/newTraceDetails/traceApi/utils';
27-
import {isEAPTraceNode} from 'sentry/views/performance/newTraceDetails/traceGuards';
2827
import type {TraceTree} from 'sentry/views/performance/newTraceDetails/traceModels/traceTree';
2928
import {
3029
TRACE_VIEW_MOBILE_VITALS,
@@ -52,9 +51,8 @@ export function TraceContextVitals({rootEventResults, tree, containerWidth}: Pro
5251
? TRACE_VIEW_WEB_VITALS
5352
: TRACE_VIEW_MOBILE_VITALS;
5453

55-
const isEAPTrace = isEAPTraceNode(traceNode);
5654
const collectedVitals =
57-
isEAPTrace && tree.vital_types.has('mobile')
55+
traceNode.isEAPEvent && tree.vital_types.has('mobile')
5856
? getMobileVitalsFromRootEventResults(rootEventResults.data)
5957
: Array.from(tree.vitals.values()).flat();
6058

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import {OrganizationFixture} from 'sentry-fixture/organization';
2+
3+
import {render, screen} from 'sentry-test/reactTestingLibrary';
4+
5+
import type {TraceTreeNodeExtra} from 'sentry/views/performance/newTraceDetails/traceModels/traceTreeNode/baseNode';
6+
import {EapSpanNode} from 'sentry/views/performance/newTraceDetails/traceModels/traceTreeNode/eapSpanNode';
7+
import {SiblingAutogroupNode} from 'sentry/views/performance/newTraceDetails/traceModels/traceTreeNode/siblingAutogroupNode';
8+
import {
9+
makeEAPSpan,
10+
makeSiblingAutogroup,
11+
} from 'sentry/views/performance/newTraceDetails/traceModels/traceTreeTestUtils';
12+
import {DEFAULT_TRACE_VIEW_PREFERENCES} from 'sentry/views/performance/newTraceDetails/traceState/tracePreferences';
13+
import {TraceStateProvider} from 'sentry/views/performance/newTraceDetails/traceState/traceStateProvider';
14+
15+
import {AutogroupNodeDetails} from './index';
16+
17+
const createMockExtra = (
18+
overrides: Partial<TraceTreeNodeExtra> = {}
19+
): TraceTreeNodeExtra => ({
20+
organization: OrganizationFixture(),
21+
...overrides,
22+
});
23+
24+
describe('AutogroupNodeDetails', () => {
25+
it('renders autogroup details with title and description', () => {
26+
const organization = OrganizationFixture();
27+
const extra = createMockExtra({organization});
28+
const autogroupValue = makeSiblingAutogroup({
29+
span_id: 'test-span-id',
30+
autogrouped_by: {
31+
op: 'db.query',
32+
description: 'SELECT * FROM users',
33+
},
34+
});
35+
36+
const childSpanValue = makeEAPSpan({event_id: 'child-span-1'});
37+
const childNode = new EapSpanNode(null, childSpanValue, extra);
38+
const node = new SiblingAutogroupNode(null, autogroupValue, extra);
39+
node.children = [childNode];
40+
41+
render(
42+
<TraceStateProvider initialPreferences={DEFAULT_TRACE_VIEW_PREFERENCES}>
43+
<AutogroupNodeDetails
44+
node={node as any}
45+
organization={organization}
46+
onTabScrollToNode={jest.fn()}
47+
onParentClick={jest.fn()}
48+
manager={null}
49+
replay={null}
50+
traceId="test-trace-id"
51+
/>
52+
</TraceStateProvider>
53+
);
54+
55+
expect(screen.getByText('Autogroup')).toBeInTheDocument();
56+
57+
expect(screen.getByText(/ID: test-span-id/)).toBeInTheDocument();
58+
59+
expect(
60+
screen.getByText(/This block represents autogrouped spans/)
61+
).toBeInTheDocument();
62+
63+
expect(
64+
screen.getByText('5 or more siblings with the same operation and description')
65+
).toBeInTheDocument();
66+
expect(
67+
screen.getByText('2 or more descendants with the same operation')
68+
).toBeInTheDocument();
69+
70+
expect(
71+
screen.getByText(/You can either open this autogroup using the chevron/)
72+
).toBeInTheDocument();
73+
});
74+
});

0 commit comments

Comments
 (0)