Skip to content

Commit 7dd0f96

Browse files
Abdkhan14Abdullah Khan
and
Abdullah Khan
authored
feat(trace-eap-waterfall): Rendering vitals in eap trace waterfall (#90879)
We still don't have scores for eap measurements loading, but that should work once the backend changes are merged: <img width="1244" alt="Screenshot 2025-05-05 at 11 04 38 AM" src="https://github.com/user-attachments/assets/02c97b55-32dd-46db-804f-292fa54424ba" /> --------- Co-authored-by: Abdullah Khan <abdullahkhan@PG9Y57YDXQ.local>
1 parent b411ba9 commit 7dd0f96

File tree

9 files changed

+199
-67
lines changed

9 files changed

+199
-67
lines changed

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

+20-6
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import type {Organization} from 'sentry/types/organization';
1919
import type {PlatformKey, Project} from 'sentry/types/project';
2020
import {trackAnalytics} from 'sentry/utils/analytics';
2121
import {formatTraceDuration} from 'sentry/utils/duration/formatTraceDuration';
22-
import {WEB_VITAL_DETAILS} from 'sentry/utils/performance/vitals/constants';
22+
import {VITAL_DETAILS} from 'sentry/utils/performance/vitals/constants';
2323
import {replayPlayerTimestampEmitter} from 'sentry/utils/replays/replayPlayerTimestampEmitter';
2424
import useApi from 'sentry/utils/useApi';
2525
import useOrganization from 'sentry/utils/useOrganization';
@@ -426,14 +426,14 @@ export function Trace({
426426
? trace.indicators.map((indicator, i) => {
427427
const status =
428428
indicator.score === undefined
429-
? 'none'
429+
? 'None'
430430
: STATUS_TEXT[scoreToStatus(indicator.score)];
431-
const webvital = indicator.label.toLowerCase() as WebVitals;
431+
const vital = indicator.type as WebVitals;
432432

433433
const defaultFormatter = (value: number) =>
434434
getFormattedDuration(value / 1000);
435435
const formatter =
436-
WEB_VITALS_METERS_CONFIG[webvital]?.formatter ?? defaultFormatter;
436+
WEB_VITALS_METERS_CONFIG[vital]?.formatter ?? defaultFormatter;
437437

438438
return (
439439
<Fragment key={i}>
@@ -445,9 +445,10 @@ export function Trace({
445445
<Tooltip
446446
title={
447447
<div>
448-
{WEB_VITAL_DETAILS[`measurements.${webvital}`]?.name}
448+
{VITAL_DETAILS[`measurements.${vital}`]?.name}
449449
<br />
450-
{formatter(indicator.measurement.value)} - {status}
450+
{formatter(indicator.measurement.value)}
451+
{status !== 'None' && ` - ${status}`}
451452
</div>
452453
}
453454
>
@@ -951,6 +952,19 @@ const TraceStylingWrapper = styled('div')`
951952
}
952953
}
953954
955+
&.None {
956+
color: ${p => p.theme.subText};
957+
border: 1px solid ${p => p.theme.border};
958+
959+
&.light {
960+
background-color: rgb(245 245 245);
961+
}
962+
963+
&.dark {
964+
background-color: rgb(60 59 59);
965+
}
966+
}
967+
954968
&.Meh {
955969
color: ${p => p.theme.yellow400};
956970
border: 1px solid ${p => p.theme.yellow300};

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ export function TraceContextPanel({
7575
</TraceLinksNavigationContainer>
7676
)}
7777

78-
<VitalMetersContainer id={TraceContextSectionKeys.WEB_VITALS}>
78+
<VitalMetersContainer id={TraceContextSectionKeys.VITALS}>
7979
<TraceContextVitals rootEventResults={rootEventResults} tree={tree} logs={logs} />
8080
</VitalMetersContainer>
8181
{hasTags && rootEventResults.data && (

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

+51-6
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import styled from '@emotion/styled';
22

33
import {Tooltip} from 'sentry/components/core/tooltip';
44
import {space} from 'sentry/styles/space';
5+
import {defined} from 'sentry/utils';
56
import getDuration from 'sentry/utils/duration/getDuration';
67
import {VITAL_DETAILS} from 'sentry/utils/performance/vitals/constants';
78
import type {Vital} from 'sentry/utils/performance/vitals/types';
@@ -18,7 +19,13 @@ import {
1819
STATUS_TEXT,
1920
} from 'sentry/views/insights/browser/webVitals/utils/scoreToStatus';
2021
import type {TraceRootEventQueryResults} from 'sentry/views/performance/newTraceDetails/traceApi/useTraceRootEvent';
22+
import {isTraceItemDetailsResponse} from 'sentry/views/performance/newTraceDetails/traceApi/utils';
23+
import {isEAPTraceNode} from 'sentry/views/performance/newTraceDetails/traceGuards';
2124
import type {TraceTree} from 'sentry/views/performance/newTraceDetails/traceModels/traceTree';
25+
import {
26+
TRACE_VIEW_MOBILE_VITALS,
27+
TRACE_VIEW_WEB_VITALS,
28+
} from 'sentry/views/performance/newTraceDetails/traceModels/traceTree.measurements';
2229
import {useTraceContextSections} from 'sentry/views/performance/newTraceDetails/useTraceContextSections';
2330

2431
type Props = {
@@ -29,18 +36,31 @@ type Props = {
2936

3037
export function TraceContextVitals({rootEventResults, tree, logs}: Props) {
3138
const {hasVitals} = useTraceContextSections({tree, rootEventResults, logs});
39+
const traceNode = tree.root.children[0];
3240

33-
if (!hasVitals) {
41+
// TODO Abdullah Khan: Ignoring loading/error states for now
42+
if (!hasVitals || !rootEventResults.data || !traceNode) {
3443
return null;
3544
}
3645

37-
const allVitals = Array.from(tree.vitals.values()).flat();
38-
return allVitals.map(vital => {
46+
const vitalsToDisplay = tree.vital_types.has('web')
47+
? TRACE_VIEW_WEB_VITALS
48+
: TRACE_VIEW_MOBILE_VITALS;
49+
50+
const isEAPTrace = isEAPTraceNode(traceNode);
51+
const collectedVitals =
52+
isEAPTrace && tree.vital_types.has('mobile')
53+
? getMobileVitalsFromRootEventResults(rootEventResults.data)
54+
: Array.from(tree.vitals.values()).flat();
55+
56+
return vitalsToDisplay.map(vitalKey => {
3957
const vitalDetails =
40-
VITAL_DETAILS[`measurements.${vital.key}` as keyof typeof VITAL_DETAILS];
58+
VITAL_DETAILS[`measurements.${vitalKey}` as keyof typeof VITAL_DETAILS];
59+
const vital = collectedVitals.find(v => v.key === vitalKey);
60+
4161
return (
4262
<VitalPill
43-
key={vital?.key}
63+
key={vitalKey}
4464
vital={vitalDetails}
4565
score={vital?.score}
4666
meterValue={vital?.measurement.value}
@@ -49,6 +69,31 @@ export function TraceContextVitals({rootEventResults, tree, logs}: Props) {
4969
});
5070
}
5171

72+
function getMobileVitalsFromRootEventResults(
73+
data: TraceRootEventQueryResults['data']
74+
): TraceTree.CollectedVital[] {
75+
if (!data || !isTraceItemDetailsResponse(data)) {
76+
return [];
77+
}
78+
79+
return data.attributes
80+
.map(attribute => {
81+
const vitalKey = attribute.name.replace('measurements.', '');
82+
if (
83+
TRACE_VIEW_MOBILE_VITALS.includes(vitalKey) &&
84+
typeof attribute.value === 'number'
85+
) {
86+
return {
87+
key: vitalKey,
88+
measurement: {value: attribute.value},
89+
score: undefined,
90+
};
91+
}
92+
return undefined;
93+
})
94+
.filter(defined);
95+
}
96+
5297
function defaultVitalValueFormatter(vital: Vital, value: number) {
5398
if (vital?.type === 'duration') {
5499
return getDuration(value / 1000, 2, true);
@@ -106,7 +151,7 @@ const VitalPillContainer = styled('div')`
106151
flex-grow: 1;
107152
max-width: 20%;
108153
height: 30px;
109-
margin-bottom: ${space(1)};
154+
margin: ${space(1)} 0;
110155
`;
111156

112157
const VitalPillName = styled('div')<{status: PerformanceScore}>`

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

+17
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type {Measurement} from 'sentry/types/event';
12
import type {TraceSplitResults} from 'sentry/utils/performance/quickTrace/types';
23

34
import {MissingInstrumentationNode} from './traceModels/missingInstrumentationNode';
@@ -212,3 +213,19 @@ export function isTraceOccurence(
212213
): issue is TraceTree.TraceOccurrence {
213214
return 'issue_id' in issue && issue.event_type !== 'error';
214215
}
216+
217+
export function isEAPMeasurementValue(
218+
value: number | Measurement | undefined
219+
): value is number {
220+
return value !== undefined && typeof value === 'number';
221+
}
222+
223+
export function isEAPMeasurements(
224+
value: Record<string, Measurement> | Record<string, number> | undefined
225+
): value is Record<string, number> {
226+
if (value === undefined) {
227+
return false;
228+
}
229+
230+
return Object.values(value).every(isEAPMeasurementValue);
231+
}

static/app/views/performance/newTraceDetails/traceHeader/scrollToSectionLinks.tsx

+3-6
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,15 @@ import {useTraceContextSections} from 'sentry/views/performance/newTraceDetails/
1313

1414
export const enum TraceContextSectionKeys {
1515
TAGS = 'trace-context-tags',
16-
WEB_VITALS = 'trace-context-web-vitals',
16+
VITALS = 'trace-context-web-vitals',
1717
LOGS = 'trace-context-logs',
1818
PROFILES = 'trace-context-profiles',
1919
SUMMARY = 'trace-context-summary',
2020
}
2121

2222
const sectionLabels: Partial<Record<TraceContextSectionKeys, string>> = {
2323
[TraceContextSectionKeys.TAGS]: t('Tags'),
24-
[TraceContextSectionKeys.WEB_VITALS]: t('Web Vitals'),
24+
[TraceContextSectionKeys.VITALS]: t('Vitals'),
2525
[TraceContextSectionKeys.LOGS]: t('Logs'),
2626
[TraceContextSectionKeys.PROFILES]: t('Profiles'),
2727
[TraceContextSectionKeys.SUMMARY]: t('Summary'),
@@ -78,10 +78,7 @@ function ScrollToSectionLinks({
7878
<StyledScrollCarousel gap={1} aria-label={t('Jump to:')}>
7979
<div aria-hidden>{t('Jump to:')}</div>
8080
{hasVitals && (
81-
<SectionLink
82-
sectionKey={TraceContextSectionKeys.WEB_VITALS}
83-
location={location}
84-
/>
81+
<SectionLink sectionKey={TraceContextSectionKeys.VITALS} location={location} />
8582
)}
8683
{hasTags && (
8784
<SectionLink sectionKey={TraceContextSectionKeys.TAGS} location={location} />

0 commit comments

Comments
 (0)