Skip to content

Commit 9881bf3

Browse files
authored
feat(issues): Replace trace view on performance issues (#87143)
1 parent 9315e9c commit 9881bf3

File tree

3 files changed

+236
-30
lines changed

3 files changed

+236
-30
lines changed

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

+6-1
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,12 @@ export function EventTraceView({group, event, organization}: EventTraceViewProps
179179
const traceId = event.contexts.trace?.trace_id;
180180
const location = useLocation();
181181

182-
if (!traceId) {
182+
// Performance issues have a Span Evidence section that contains the trace view
183+
const isHiddenForPerformanceIssues =
184+
group.issueCategory === IssueCategory.PERFORMANCE &&
185+
organization.features.includes('issue-details-new-performance-trace-view');
186+
187+
if (!traceId || isHiddenForPerformanceIssues) {
183188
return null;
184189
}
185190

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

+69-29
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import styled from '@emotion/styled';
22

33
import {LinkButton} from 'sentry/components/core/button';
4+
import {SpanEvidenceTraceView} from 'sentry/components/events/interfaces/performance/spanEvidenceTraceView';
45
import {getProblemSpansForSpanTree} from 'sentry/components/events/interfaces/performance/utils';
56
import {IconSettings} from 'sentry/icons';
67
import {t} from 'sentry/locale';
@@ -33,17 +34,12 @@ export type TraceContextSpanProxy = Omit<TraceContextType, 'span_id'> & {
3334
span_id: string; // TODO: Remove this temporary type.
3435
};
3536

36-
export function SpanEvidenceSection({event, organization, projectSlug}: Props) {
37-
if (!event) {
38-
return null;
39-
}
40-
41-
const {affectedSpanIds, focusedSpanIds} = getProblemSpansForSpanTree(event);
42-
43-
const profileId = event.contexts?.profile?.profile_id ?? null;
44-
45-
const hasProfilingFeature = organization.features.includes('profiling');
46-
37+
function SpanEvidenceInteriumSection({
38+
children,
39+
event,
40+
organization,
41+
projectSlug,
42+
}: {children: React.ReactNode} & Props) {
4743
const typeId = event.occurrence?.type;
4844
const issueType = getIssueTypeFromOccurrenceType(typeId);
4945
const issueTitle = event.occurrence?.issueTitle;
@@ -74,6 +70,48 @@ export function SpanEvidenceSection({event, organization, projectSlug}: Props) {
7470
</LinkButton>
7571
)
7672
}
73+
>
74+
{children}
75+
</InterimSection>
76+
);
77+
}
78+
79+
export function SpanEvidenceSection({event, organization, projectSlug}: Props) {
80+
if (!event) {
81+
return null;
82+
}
83+
84+
const hasNewTraceView = organization.features.includes(
85+
'issue-details-new-performance-trace-view'
86+
);
87+
const traceId = event.contexts.trace?.trace_id;
88+
89+
if (hasNewTraceView && traceId) {
90+
return (
91+
<SpanEvidenceInteriumSection
92+
event={event}
93+
organization={organization}
94+
projectSlug={projectSlug}
95+
>
96+
<SpanEvidenceKeyValueList event={event} projectSlug={projectSlug} />
97+
<SpanEvidenceTraceView
98+
event={event}
99+
organization={organization}
100+
traceId={traceId}
101+
/>
102+
</SpanEvidenceInteriumSection>
103+
);
104+
}
105+
106+
const {affectedSpanIds, focusedSpanIds} = getProblemSpansForSpanTree(event);
107+
const profileId = event.contexts?.profile?.profile_id ?? null;
108+
const hasProfilingFeature = organization.features.includes('profiling');
109+
110+
return (
111+
<SpanEvidenceInteriumSection
112+
event={event}
113+
organization={organization}
114+
projectSlug={projectSlug}
77115
>
78116
<SpanEvidenceKeyValueList event={event} projectSlug={projectSlug} />
79117
{hasProfilingFeature ? (
@@ -83,23 +121,25 @@ export function SpanEvidenceSection({event, organization, projectSlug}: Props) {
83121
profileMeta={profileId || ''}
84122
>
85123
<ProfileContext.Consumer>
86-
{profiles => (
87-
<ProfileGroupProvider
88-
type="flamechart"
89-
input={profiles?.type === 'resolved' ? profiles.data : null}
90-
traceID={profileId || ''}
91-
>
92-
<TraceViewWrapper>
93-
<TraceView
94-
organization={organization}
95-
waterfallModel={
96-
new WaterfallModel(event, affectedSpanIds, focusedSpanIds)
97-
}
98-
isEmbedded
99-
/>
100-
</TraceViewWrapper>
101-
</ProfileGroupProvider>
102-
)}
124+
{profiles => {
125+
return (
126+
<ProfileGroupProvider
127+
type="flamechart"
128+
input={profiles?.type === 'resolved' ? profiles.data : null}
129+
traceID={profileId || ''}
130+
>
131+
<TraceViewWrapper>
132+
<TraceView
133+
organization={organization}
134+
waterfallModel={
135+
new WaterfallModel(event, affectedSpanIds, focusedSpanIds)
136+
}
137+
isEmbedded
138+
/>
139+
</TraceViewWrapper>
140+
</ProfileGroupProvider>
141+
);
142+
}}
103143
</ProfileContext.Consumer>
104144
</ProfilesProvider>
105145
) : (
@@ -111,7 +151,7 @@ export function SpanEvidenceSection({event, organization, projectSlug}: Props) {
111151
/>
112152
</TraceViewWrapper>
113153
)}
114-
</InterimSection>
154+
</SpanEvidenceInteriumSection>
115155
);
116156
}
117157

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
import {lazy, Suspense, useMemo} from 'react';
2+
import styled from '@emotion/styled';
3+
import type {LocationDescriptor} from 'history';
4+
5+
import Link from 'sentry/components/links/link';
6+
import {generateTraceTarget} from 'sentry/components/quickTrace/utils';
7+
import type {Event} from 'sentry/types/event';
8+
import type {Organization} from 'sentry/types/organization';
9+
import {defined} from 'sentry/utils';
10+
import {trackAnalytics} from 'sentry/utils/analytics';
11+
import {useLocation} from 'sentry/utils/useLocation';
12+
import {useIssuesTraceTree} from 'sentry/views/performance/newTraceDetails/traceApi/useIssuesTraceTree';
13+
import {useTrace} from 'sentry/views/performance/newTraceDetails/traceApi/useTrace';
14+
import {useTraceMeta} from 'sentry/views/performance/newTraceDetails/traceApi/useTraceMeta';
15+
import {useTraceRootEvent} from 'sentry/views/performance/newTraceDetails/traceApi/useTraceRootEvent';
16+
import {TraceViewSources} from 'sentry/views/performance/newTraceDetails/traceHeader/breadcrumbs';
17+
import {
18+
loadTraceViewPreferences,
19+
type TracePreferencesState,
20+
} from 'sentry/views/performance/newTraceDetails/traceState/tracePreferences';
21+
import {TraceStateProvider} from 'sentry/views/performance/newTraceDetails/traceState/traceStateProvider';
22+
import {useTraceEventView} from 'sentry/views/performance/newTraceDetails/useTraceEventView';
23+
import {useTraceQueryParams} from 'sentry/views/performance/newTraceDetails/useTraceQueryParams';
24+
25+
const LazyIssuesTraceWaterfall = lazy(() =>
26+
import('sentry/views/performance/newTraceDetails/issuesTraceWaterfall').then(
27+
module => ({default: module.IssuesTraceWaterfall})
28+
)
29+
);
30+
31+
function getHrefFromTraceTarget(traceTarget: LocationDescriptor) {
32+
if (typeof traceTarget === 'string') {
33+
return traceTarget;
34+
}
35+
36+
const searchParams = new URLSearchParams();
37+
for (const key in traceTarget.query) {
38+
if (defined(traceTarget.query[key])) {
39+
searchParams.append(key, traceTarget.query[key]);
40+
}
41+
}
42+
43+
return `${traceTarget.pathname}?${searchParams.toString()}`;
44+
}
45+
46+
const DEFAULT_ISSUE_DETAILS_TRACE_VIEW_PREFERENCES: TracePreferencesState = {
47+
drawer: {
48+
minimized: true,
49+
sizes: {
50+
'drawer left': 0.33,
51+
'drawer right': 0.33,
52+
'drawer bottom': 0.4,
53+
'trace context height': 150,
54+
},
55+
layoutOptions: [],
56+
},
57+
missing_instrumentation: true,
58+
autogroup: {
59+
parent: true,
60+
sibling: true,
61+
},
62+
layout: 'drawer bottom',
63+
list: {
64+
width: 0.5,
65+
},
66+
};
67+
68+
interface SpanEvidenceTraceViewProps {
69+
event: Event;
70+
organization: Organization;
71+
traceId: string;
72+
}
73+
74+
export function SpanEvidenceTraceView({
75+
event,
76+
organization,
77+
traceId,
78+
}: SpanEvidenceTraceViewProps) {
79+
const timestamp = new Date(event.dateReceived).getTime() / 1e3;
80+
81+
const location = useLocation();
82+
const trace = useTrace({
83+
timestamp,
84+
traceSlug: traceId,
85+
limit: 10000,
86+
});
87+
const meta = useTraceMeta([{traceSlug: traceId, timestamp}]);
88+
const tree = useIssuesTraceTree({trace, meta, replay: null});
89+
90+
const shouldLoadTraceRoot = !trace.isPending && trace.data;
91+
92+
const rootEvent = useTraceRootEvent(shouldLoadTraceRoot ? trace.data : null);
93+
const preferences = useMemo(
94+
() =>
95+
loadTraceViewPreferences('issue-details-trace-view-preferences') ||
96+
DEFAULT_ISSUE_DETAILS_TRACE_VIEW_PREFERENCES,
97+
[]
98+
);
99+
100+
const params = useTraceQueryParams({timestamp});
101+
const traceEventView = useTraceEventView(traceId, params);
102+
103+
if (!traceId) {
104+
return null;
105+
}
106+
107+
const traceTarget = generateTraceTarget(
108+
event,
109+
organization,
110+
{
111+
...location,
112+
query: {
113+
...location.query,
114+
groupId: event.groupID,
115+
},
116+
},
117+
TraceViewSources.ISSUE_DETAILS
118+
);
119+
120+
return (
121+
<TraceStateProvider
122+
initialPreferences={preferences}
123+
preferencesStorageKey="issue-details-view-preferences"
124+
>
125+
<IssuesTraceContainer>
126+
<Suspense fallback={null}>
127+
<LazyIssuesTraceWaterfall
128+
tree={tree}
129+
trace={trace}
130+
traceSlug={traceId}
131+
rootEvent={rootEvent}
132+
organization={organization}
133+
traceEventView={traceEventView}
134+
meta={meta}
135+
source="issues"
136+
replay={null}
137+
event={event}
138+
/>
139+
</Suspense>
140+
<IssuesTraceOverlayContainer
141+
to={getHrefFromTraceTarget(traceTarget)}
142+
onClick={() => {
143+
trackAnalytics('issue_details.view_full_trace_waterfall_clicked', {
144+
organization,
145+
});
146+
}}
147+
/>
148+
</IssuesTraceContainer>
149+
</TraceStateProvider>
150+
);
151+
}
152+
153+
const IssuesTraceContainer = styled('div')`
154+
position: relative;
155+
`;
156+
157+
const IssuesTraceOverlayContainer = styled(Link)`
158+
position: absolute;
159+
inset: 0;
160+
z-index: 10;
161+
`;

0 commit comments

Comments
 (0)