From d9eda6d2bcf4c2230f59ae12ca4487d21166b7d0 Mon Sep 17 00:00:00 2001 From: Ryan Albrecht Date: Thu, 7 Aug 2025 14:47:08 -0700 Subject: [PATCH] feat(replay): Link from Web Vitals breadcrumbs in Replay Details to Insights --- .../breadcrumbs/breadcrumbWebVital.tsx | 65 ++++++++++++++----- 1 file changed, 50 insertions(+), 15 deletions(-) diff --git a/static/app/components/replays/breadcrumbs/breadcrumbWebVital.tsx b/static/app/components/replays/breadcrumbs/breadcrumbWebVital.tsx index a962d04ad4074d..9919523a05dd3b 100644 --- a/static/app/components/replays/breadcrumbs/breadcrumbWebVital.tsx +++ b/static/app/components/replays/breadcrumbs/breadcrumbWebVital.tsx @@ -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; interface Props { frame: ReplayFrame; @@ -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 = { + value: frame.data.value, + }; if (isCLSFrame(frame) && frame.data.attributions && selectors) { - const layoutShifts: Array> = []; + const layoutShifts: LayoutShift[] = []; for (const attr of frame.data.attributions) { const elements: ReactNode[] = []; if ('nodeIds' in attr && Array.isArray(attr.nodeIds)) { @@ -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] = ( { - onInspectorExpanded( - path, - Object.fromEntries(expandedPaths.map(item => [item, true])) - ); - }} - data={webVitalData} - withAnnotatedText - /> + + + { + onInspectorExpanded( + path, + Object.fromEntries(expandedPaths.map(item => [item, true])) + ); + }} + data={webVitalData} + withAnnotatedText + /> + + + {t('All Web Vitals')} + + ); } @@ -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; +`;