Skip to content

feat(replay): Link from Web Vitals breadcrumbs in Replay Details to Insights #97442

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Aug 8, 2025
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 50 additions & 15 deletions static/app/components/replays/breadcrumbs/breadcrumbWebVital.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,20 @@ import type {ReactNode} from 'react';
import styled from '@emotion/styled';

import {Button} from 'sentry/components/core/button';
import {LinkButton} from 'sentry/components/core/button/linkButton';
import {Flex} from 'sentry/components/core/layout/flex';
import StructuredEventData from 'sentry/components/structuredEventData';
import {t} from 'sentry/locale';
import {space} from 'sentry/styles/space';
import type {Extraction} from 'sentry/utils/replays/extractDomNodes';
import {useReplayReader} from 'sentry/utils/replays/playback/providers/replayReaderProvider';
import type {ReplayFrame} from 'sentry/utils/replays/types';
import {isCLSFrame, isWebVitalFrame} from 'sentry/utils/replays/types';
import useOrganization from 'sentry/utils/useOrganization';
import type {OnExpandCallback} from 'sentry/views/replays/detail/useVirtualizedInspector';

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

interface Props {
frame: ReplayFrame;
Expand All @@ -29,15 +34,20 @@ export function BreadcrumbWebVital({
onMouseEnter,
onMouseLeave,
}: Props) {
const organization = useOrganization();
const replayReader = useReplayReader();

if (!isWebVitalFrame(frame)) {
return null;
}

const webVitalData = {value: frame.data.value};
const selectors = extraction?.selectors;
const webVitalData: Record<string, number | ReactNode | LayoutShift[]> = {
value: frame.data.value,
};

if (isCLSFrame(frame) && frame.data.attributions && selectors) {
const layoutShifts: Array<Record<string, ReactNode[]>> = [];
const layoutShifts: LayoutShift[] = [];
for (const attr of frame.data.attributions) {
const elements: ReactNode[] = [];
if ('nodeIds' in attr && Array.isArray(attr.nodeIds)) {
Expand Down Expand Up @@ -72,12 +82,10 @@ export function BreadcrumbWebVital({
layoutShifts.push({[`score ${attr.value}`]: elements});
}
if (layoutShifts.length) {
// @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
webVitalData['Layout shifts'] = layoutShifts;
}
} else if (selectors) {
selectors.forEach((key, value) => {
// @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
webVitalData[key] = (
<span
key={key}
Expand All @@ -95,17 +103,33 @@ export function BreadcrumbWebVital({
}

return (
<StructuredEventData
initialExpandedPaths={expandPaths ?? []}
onToggleExpand={(expandedPaths, path) => {
onInspectorExpanded(
path,
Object.fromEntries(expandedPaths.map(item => [item, true]))
);
}}
data={webVitalData}
withAnnotatedText
/>
<Flex gap="lg" justify="between" align="start">
<NoMarginWrapper flex="1">
<StructuredEventData
initialExpandedPaths={expandPaths ?? []}
onToggleExpand={(expandedPaths, path) => {
onInspectorExpanded(
path,
Object.fromEntries(expandedPaths.map(item => [item, true]))
);
}}
data={webVitalData}
withAnnotatedText
/>
</NoMarginWrapper>
<NoWrapButton
priority="link"
size="xs"
to={{
pathname: `/organizations/${organization.slug}/insights/frontend/pageloads/overview/`,
query: {
projectId: replayReader?.getReplay().project_id,
},
}}
>
{t('All Web Vitals')}
</NoWrapButton>
</Flex>
);
}

Expand All @@ -131,3 +155,14 @@ const SelectorButton = styled(Button)`
height: auto;
min-height: auto;
`;

const NoMarginWrapper = styled(Flex)`
pre {
margin: 0;
flex: 1;
}
`;

const NoWrapButton = styled(LinkButton)`
white-space: nowrap;
`;
Loading