Skip to content

Commit 2a6e603

Browse files
authored
feat(replay): Link from Web Vitals breadcrumbs in Replay Details to Insights (#97442)
**Before** <img width="681" height="135" alt="SCR-20250807-ndjd" src="https://github.com/user-attachments/assets/4ae7d3f3-a3b4-4012-be75-29db7822867c" /> **After** <img width="676" height="99" alt="SCR-20250807-ndqn" src="https://github.com/user-attachments/assets/f41eea5c-20cf-4070-863e-2ab607646e2e" />
1 parent 6ff0fb7 commit 2a6e603

File tree

1 file changed

+50
-15
lines changed

1 file changed

+50
-15
lines changed

static/app/components/replays/breadcrumbs/breadcrumbWebVital.tsx

Lines changed: 50 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,20 @@ import type {ReactNode} from 'react';
22
import styled from '@emotion/styled';
33

44
import {Button} from 'sentry/components/core/button';
5+
import {LinkButton} from 'sentry/components/core/button/linkButton';
6+
import {Flex} from 'sentry/components/core/layout/flex';
57
import StructuredEventData from 'sentry/components/structuredEventData';
68
import {t} from 'sentry/locale';
79
import {space} from 'sentry/styles/space';
810
import type {Extraction} from 'sentry/utils/replays/extractDomNodes';
11+
import {useReplayReader} from 'sentry/utils/replays/playback/providers/replayReaderProvider';
912
import type {ReplayFrame} from 'sentry/utils/replays/types';
1013
import {isCLSFrame, isWebVitalFrame} from 'sentry/utils/replays/types';
14+
import useOrganization from 'sentry/utils/useOrganization';
1115
import type {OnExpandCallback} from 'sentry/views/replays/detail/useVirtualizedInspector';
1216

1317
type MouseCallback = (frame: ReplayFrame, nodeId?: number) => void;
18+
type LayoutShift = Record<string, ReactNode[]>;
1419

1520
interface Props {
1621
frame: ReplayFrame;
@@ -29,15 +34,20 @@ export function BreadcrumbWebVital({
2934
onMouseEnter,
3035
onMouseLeave,
3136
}: Props) {
37+
const organization = useOrganization();
38+
const replayReader = useReplayReader();
39+
3240
if (!isWebVitalFrame(frame)) {
3341
return null;
3442
}
3543

36-
const webVitalData = {value: frame.data.value};
3744
const selectors = extraction?.selectors;
45+
const webVitalData: Record<string, number | ReactNode | LayoutShift[]> = {
46+
value: frame.data.value,
47+
};
3848

3949
if (isCLSFrame(frame) && frame.data.attributions && selectors) {
40-
const layoutShifts: Array<Record<string, ReactNode[]>> = [];
50+
const layoutShifts: LayoutShift[] = [];
4151
for (const attr of frame.data.attributions) {
4252
const elements: ReactNode[] = [];
4353
if ('nodeIds' in attr && Array.isArray(attr.nodeIds)) {
@@ -72,12 +82,10 @@ export function BreadcrumbWebVital({
7282
layoutShifts.push({[`score ${attr.value}`]: elements});
7383
}
7484
if (layoutShifts.length) {
75-
// @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
7685
webVitalData['Layout shifts'] = layoutShifts;
7786
}
7887
} else if (selectors) {
7988
selectors.forEach((key, value) => {
80-
// @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
8189
webVitalData[key] = (
8290
<span
8391
key={key}
@@ -95,17 +103,33 @@ export function BreadcrumbWebVital({
95103
}
96104

97105
return (
98-
<StructuredEventData
99-
initialExpandedPaths={expandPaths ?? []}
100-
onToggleExpand={(expandedPaths, path) => {
101-
onInspectorExpanded(
102-
path,
103-
Object.fromEntries(expandedPaths.map(item => [item, true]))
104-
);
105-
}}
106-
data={webVitalData}
107-
withAnnotatedText
108-
/>
106+
<Flex gap="lg" justify="between" align="start">
107+
<NoMarginWrapper flex="1">
108+
<StructuredEventData
109+
initialExpandedPaths={expandPaths ?? []}
110+
onToggleExpand={(expandedPaths, path) => {
111+
onInspectorExpanded(
112+
path,
113+
Object.fromEntries(expandedPaths.map(item => [item, true]))
114+
);
115+
}}
116+
data={webVitalData}
117+
withAnnotatedText
118+
/>
119+
</NoMarginWrapper>
120+
<NoWrapButton
121+
priority="link"
122+
size="xs"
123+
to={{
124+
pathname: `/organizations/${organization.slug}/insights/frontend/pageloads/overview/`,
125+
query: {
126+
projectId: replayReader?.getReplay().project_id,
127+
},
128+
}}
129+
>
130+
{t('All Web Vitals')}
131+
</NoWrapButton>
132+
</Flex>
109133
);
110134
}
111135

@@ -131,3 +155,14 @@ const SelectorButton = styled(Button)`
131155
height: auto;
132156
min-height: auto;
133157
`;
158+
159+
const NoMarginWrapper = styled(Flex)`
160+
pre {
161+
margin: 0;
162+
flex: 1;
163+
}
164+
`;
165+
166+
const NoWrapButton = styled(LinkButton)`
167+
white-space: nowrap;
168+
`;

0 commit comments

Comments
 (0)