From 1146510032f28ef667d49677cc331e9e53315a30 Mon Sep 17 00:00:00 2001 From: Catherine Lee <55311782+c298lee@users.noreply.github.com> Date: Thu, 13 Jun 2024 11:52:00 -0400 Subject: [PATCH 1/8] adds web vital breadcrumbs in replays --- static/app/utils/replays/getFrameDetails.tsx | 4 ++-- static/app/utils/replays/replayDataUtils.tsx | 4 +--- static/app/utils/replays/replayReader.tsx | 5 +++-- static/app/utils/replays/types.tsx | 11 +++-------- .../detail/breadcrumbs/useBreadcrumbFilters.tsx | 4 ++-- 5 files changed, 11 insertions(+), 17 deletions(-) diff --git a/static/app/utils/replays/getFrameDetails.tsx b/static/app/utils/replays/getFrameDetails.tsx index 2d9d5d5e2e576a..d3e81995287ffb 100644 --- a/static/app/utils/replays/getFrameDetails.tsx +++ b/static/app/utils/replays/getFrameDetails.tsx @@ -278,7 +278,7 @@ const MAPPER_FOR_FRAME: Record Details> = { color: 'gray300', description: typeof frame.data.value === 'number' ? ( - `${Math.round(frame.data.value)}ms` + `${Math.round(frame.data.value)}ms ${frame.data.rating}` ) : ( Details> = { ), tabKey: TabKey.NETWORK, - title: 'LCP', + title: frame.description, icon: , }), memory: () => ({ diff --git a/static/app/utils/replays/replayDataUtils.tsx b/static/app/utils/replays/replayDataUtils.tsx index 18dcefbb4e03bc..b35a8582dab106 100644 --- a/static/app/utils/replays/replayDataUtils.tsx +++ b/static/app/utils/replays/replayDataUtils.tsx @@ -71,9 +71,7 @@ export function replayTimestamps( const breadcrumbTimestamps = rawCrumbs .map(rawCrumb => rawCrumb.timestamp) .filter(Boolean); - const rawSpanDataFiltered = rawSpanData.filter( - ({op}) => op !== 'largest-contentful-paint' - ); + const rawSpanDataFiltered = rawSpanData.filter(({op}) => op !== 'web-vital'); const spanStartTimestamps = rawSpanDataFiltered .map(span => span.startTimestamp) .filter(Boolean); diff --git a/static/app/utils/replays/replayReader.tsx b/static/app/utils/replays/replayReader.tsx index 9331e6a2172dfd..049546ebfb52a6 100644 --- a/static/app/utils/replays/replayReader.tsx +++ b/static/app/utils/replays/replayReader.tsx @@ -38,8 +38,8 @@ import { EventType, isDeadClick, isDeadRageClick, - isLCPFrame, isPaintFrame, + isWebVitalFrame, } from 'sentry/utils/replays/types'; import type {ReplayError, ReplayRecord} from 'sentry/views/replays/types'; @@ -469,6 +469,7 @@ export default class ReplayReader { this._trimFramesToClipWindow( [ ...this.getPerfFrames(), + ...this.getWebVitalFrames(), ...this._sortedBreadcrumbFrames.filter(frame => [ 'replay.hydrate-error', @@ -506,7 +507,7 @@ export default class ReplayReader { return [...uniqueCrumbs, ...spans].sort(sortFrames); }); - getLPCFrames = memoize(() => this._sortedSpanFrames.filter(isLCPFrame)); + getWebVitalFrames = memoize(() => this._sortedSpanFrames.filter(isWebVitalFrame)); getVideoEvents = () => this._videoEvents; diff --git a/static/app/utils/replays/types.tsx b/static/app/utils/replays/types.tsx index ab0a198c0f0552..6a6133909f1e4d 100644 --- a/static/app/utils/replays/types.tsx +++ b/static/app/utils/replays/types.tsx @@ -143,13 +143,8 @@ export function isConsoleFrame(frame: BreadcrumbFrame): frame is ConsoleFrame { return false; } -export function isLCPFrame(frame: SpanFrame): frame is WebVitalFrame { - return ( - frame.op === 'largest-contentful-paint' || - frame.op === 'cumulative-layout-shift' || - frame.op === 'first-input-delay' || - frame.op === 'interaction-to-next-paint' - ); +export function isWebVitalFrame(frame: SpanFrame): frame is WebVitalFrame { + return frame.op === 'web-vital'; } export function isPaintFrame(frame: SpanFrame): frame is PaintFrame { @@ -318,7 +313,7 @@ export type ResourceFrame = HydratedSpan< // This list should match each of the operations used in `HydratedSpan` above // And any app-specific types that we hydrate (ie: replay.start & replay.end). export const SpanOps = [ - 'largest-contentful-paint', + 'web-vital', 'memory', 'navigation.back_forward', 'navigation.navigate', diff --git a/static/app/views/replays/detail/breadcrumbs/useBreadcrumbFilters.tsx b/static/app/views/replays/detail/breadcrumbs/useBreadcrumbFilters.tsx index f45dd4bddd1189..7e90013f0343b4 100644 --- a/static/app/views/replays/detail/breadcrumbs/useBreadcrumbFilters.tsx +++ b/static/app/views/replays/detail/breadcrumbs/useBreadcrumbFilters.tsx @@ -45,7 +45,7 @@ const TYPE_TO_LABEL: Record = { rageOrMulti: 'Rage & Multi Click', rageOrDead: 'Rage & Dead Click', hydrateError: 'Hydration Error', - lcp: 'LCP', + webVital: 'Web Vital', click: 'User Click', keydown: 'KeyDown', input: 'Input', @@ -71,7 +71,7 @@ const OPORCATEGORY_TO_TYPE: Record = { 'ui.multiClick': 'rageOrMulti', 'ui.slowClickDetected': 'rageOrDead', 'replay.hydrate-error': 'hydrateError', - 'largest-contentful-paint': 'lcp', + 'web-vital': 'webVital', 'ui.click': 'click', 'ui.tap': 'tap', 'ui.keyDown': 'keydown', From 7a3637684b637af97c78b8c301503406920918a7 Mon Sep 17 00:00:00 2001 From: Catherine Lee <55311782+c298lee@users.noreply.github.com> Date: Tue, 18 Jun 2024 13:23:43 -0400 Subject: [PATCH 2/8] add web vital breadcrumbs --- static/app/utils/replays/getFrameDetails.tsx | 50 ++++++++++++-------- 1 file changed, 31 insertions(+), 19 deletions(-) diff --git a/static/app/utils/replays/getFrameDetails.tsx b/static/app/utils/replays/getFrameDetails.tsx index d3e81995287ffb..114d64ce94109e 100644 --- a/static/app/utils/replays/getFrameDetails.tsx +++ b/static/app/utils/replays/getFrameDetails.tsx @@ -3,17 +3,19 @@ import type {ReactNode} from 'react'; import ExternalLink from 'sentry/components/links/externalLink'; import CrumbErrorTitle from 'sentry/components/replays/breadcrumbs/errorTitle'; import SelectorList from 'sentry/components/replays/breadcrumbs/selectorList'; -import {Tooltip} from 'sentry/components/tooltip'; import { IconCursorArrow, IconFire, IconFix, + IconHappy, IconInfo, IconInput, IconKeyDown, IconLocation, IconMegaphone, + IconMeh, IconMobile, + IconSad, IconSort, IconTerminal, IconUser, @@ -274,24 +276,34 @@ const MAPPER_FOR_FRAME: Record Details> = { title: 'Navigation', icon: , }), - 'largest-contentful-paint': (frame: WebVitalFrame) => ({ - color: 'gray300', - description: - typeof frame.data.value === 'number' ? ( - `${Math.round(frame.data.value)}ms ${frame.data.rating}` - ) : ( - - - - ), - tabKey: TabKey.NETWORK, - title: frame.description, - icon: , - }), + 'web-vital': (frame: WebVitalFrame) => { + switch (frame.data.rating) { + case 'good': + return { + color: 'green300', + description: `Good ${Math.round(frame.data.value * 100) / 100}ms`, + tabKey: TabKey.NETWORK, + title: frame.description.replaceAll('-', ' '), + icon: , + }; + case 'needs-improvement': + return { + color: 'yellow300', + description: `Meh ${Math.round(frame.data.value * 100) / 100}ms`, + tabKey: TabKey.NETWORK, + title: frame.description.replaceAll('-', ' '), + icon: , + }; + default: + return { + color: 'red300', + description: `Poor ${Math.round(frame.data.value * 100) / 100}ms`, + tabKey: TabKey.NETWORK, + title: frame.description.replaceAll('-', ' '), + icon: , + }; + } + }, memory: () => ({ color: 'gray300', description: undefined, From 1b49908e78663143039e24d5929d10bcd929b0c5 Mon Sep 17 00:00:00 2001 From: Catherine Lee <55311782+c298lee@users.noreply.github.com> Date: Tue, 18 Jun 2024 15:37:27 -0400 Subject: [PATCH 3/8] add ff --- static/app/utils/replays/types.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/static/app/utils/replays/types.tsx b/static/app/utils/replays/types.tsx index 6a6133909f1e4d..5f07b22d945788 100644 --- a/static/app/utils/replays/types.tsx +++ b/static/app/utils/replays/types.tsx @@ -16,6 +16,7 @@ import type { import invariant from 'invariant'; import type {HydratedA11yFrame} from 'sentry/utils/replays/hydrateA11yFrame'; +import useOrganization from 'sentry/utils/useOrganization'; // TODO: more types get added here type MobileBreadcrumbTypes = @@ -144,7 +145,8 @@ export function isConsoleFrame(frame: BreadcrumbFrame): frame is ConsoleFrame { } export function isWebVitalFrame(frame: SpanFrame): frame is WebVitalFrame { - return frame.op === 'web-vital'; + const organization = useOrganization(); + return organization.features.includes('replay-web-vitals') && frame.op === 'web-vital'; } export function isPaintFrame(frame: SpanFrame): frame is PaintFrame { From 700d508747a6079bfb95a73686fb0bd6f93534c9 Mon Sep 17 00:00:00 2001 From: Catherine Lee <55311782+c298lee@users.noreply.github.com> Date: Tue, 18 Jun 2024 17:10:54 -0400 Subject: [PATCH 4/8] move ff --- static/app/utils/replays/replayReader.tsx | 9 ++++++++- static/app/utils/replays/types.tsx | 4 +--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/static/app/utils/replays/replayReader.tsx b/static/app/utils/replays/replayReader.tsx index 049546ebfb52a6..7115680d78b0d1 100644 --- a/static/app/utils/replays/replayReader.tsx +++ b/static/app/utils/replays/replayReader.tsx @@ -41,6 +41,7 @@ import { isPaintFrame, isWebVitalFrame, } from 'sentry/utils/replays/types'; +import useOrganization from 'sentry/utils/useOrganization'; import type {ReplayError, ReplayRecord} from 'sentry/views/replays/types'; interface ReplayReaderParams { @@ -507,7 +508,13 @@ export default class ReplayReader { return [...uniqueCrumbs, ...spans].sort(sortFrames); }); - getWebVitalFrames = memoize(() => this._sortedSpanFrames.filter(isWebVitalFrame)); + getWebVitalFrames = memoize(() => { + const organization = useOrganization(); + if (organization.features.includes('replay-web-vitals')) { + return this._sortedSpanFrames.filter(isWebVitalFrame); + } + return []; + }); getVideoEvents = () => this._videoEvents; diff --git a/static/app/utils/replays/types.tsx b/static/app/utils/replays/types.tsx index 5f07b22d945788..6a6133909f1e4d 100644 --- a/static/app/utils/replays/types.tsx +++ b/static/app/utils/replays/types.tsx @@ -16,7 +16,6 @@ import type { import invariant from 'invariant'; import type {HydratedA11yFrame} from 'sentry/utils/replays/hydrateA11yFrame'; -import useOrganization from 'sentry/utils/useOrganization'; // TODO: more types get added here type MobileBreadcrumbTypes = @@ -145,8 +144,7 @@ export function isConsoleFrame(frame: BreadcrumbFrame): frame is ConsoleFrame { } export function isWebVitalFrame(frame: SpanFrame): frame is WebVitalFrame { - const organization = useOrganization(); - return organization.features.includes('replay-web-vitals') && frame.op === 'web-vital'; + return frame.op === 'web-vital'; } export function isPaintFrame(frame: SpanFrame): frame is PaintFrame { From 0fe75516bd07b366dbc027d7031a51cccad3d71c Mon Sep 17 00:00:00 2001 From: Catherine Lee <55311782+c298lee@users.noreply.github.com> Date: Wed, 19 Jun 2024 16:46:09 -0400 Subject: [PATCH 5/8] fix ff and PR comments --- static/app/utils/replays/getFrameDetails.tsx | 6 ++-- .../utils/replays/hooks/useReplayReader.tsx | 7 ++++- static/app/utils/replays/replayDataUtils.tsx | 4 ++- .../app/utils/replays/replayReader.spec.tsx | 20 ++++++++++++ static/app/utils/replays/replayReader.tsx | 31 +++++++++++++++---- 5 files changed, 57 insertions(+), 11 deletions(-) diff --git a/static/app/utils/replays/getFrameDetails.tsx b/static/app/utils/replays/getFrameDetails.tsx index 114d64ce94109e..d0f1325cad13c0 100644 --- a/static/app/utils/replays/getFrameDetails.tsx +++ b/static/app/utils/replays/getFrameDetails.tsx @@ -281,7 +281,7 @@ const MAPPER_FOR_FRAME: Record Details> = { case 'good': return { color: 'green300', - description: `Good ${Math.round(frame.data.value * 100) / 100}ms`, + description: `Good ${frame.data.value.toFixed(2)}ms`, tabKey: TabKey.NETWORK, title: frame.description.replaceAll('-', ' '), icon: , @@ -289,7 +289,7 @@ const MAPPER_FOR_FRAME: Record Details> = { case 'needs-improvement': return { color: 'yellow300', - description: `Meh ${Math.round(frame.data.value * 100) / 100}ms`, + description: `Meh ${frame.data.value.toFixed(2)}ms`, tabKey: TabKey.NETWORK, title: frame.description.replaceAll('-', ' '), icon: , @@ -297,7 +297,7 @@ const MAPPER_FOR_FRAME: Record Details> = { default: return { color: 'red300', - description: `Poor ${Math.round(frame.data.value * 100) / 100}ms`, + description: `Poor ${frame.data.value.toFixed(2)}ms`, tabKey: TabKey.NETWORK, title: frame.description.replaceAll('-', ' '), icon: , diff --git a/static/app/utils/replays/hooks/useReplayReader.tsx b/static/app/utils/replays/hooks/useReplayReader.tsx index cc46ccfe21e98e..7ff2490a53a05b 100644 --- a/static/app/utils/replays/hooks/useReplayReader.tsx +++ b/static/app/utils/replays/hooks/useReplayReader.tsx @@ -3,6 +3,7 @@ import {useMemo} from 'react'; import type {Group} from 'sentry/types/group'; import useReplayData from 'sentry/utils/replays/hooks/useReplayData'; import ReplayReader from 'sentry/utils/replays/replayReader'; +import useOrganization from 'sentry/utils/useOrganization'; type Props = { orgSlug: string; @@ -43,21 +44,25 @@ export default function useReplayReader({orgSlug, replaySlug, clipWindow, group} ); }, [clipWindow, firstMatchingError]); + const featureFlags = useOrganization().features; + const replay = useMemo( () => ReplayReader.factory({ attachments, clipWindow: memoizedClipWindow, errors, + featureFlags, replayRecord, }), - [attachments, memoizedClipWindow, errors, replayRecord] + [attachments, memoizedClipWindow, errors, featureFlags, replayRecord] ); return { ...replayData, attachments, errors, + featureFlags, replay, replayId, replayRecord, diff --git a/static/app/utils/replays/replayDataUtils.tsx b/static/app/utils/replays/replayDataUtils.tsx index b35a8582dab106..a8a1f46fc53aba 100644 --- a/static/app/utils/replays/replayDataUtils.tsx +++ b/static/app/utils/replays/replayDataUtils.tsx @@ -71,7 +71,9 @@ export function replayTimestamps( const breadcrumbTimestamps = rawCrumbs .map(rawCrumb => rawCrumb.timestamp) .filter(Boolean); - const rawSpanDataFiltered = rawSpanData.filter(({op}) => op !== 'web-vital'); + const rawSpanDataFiltered = rawSpanData.filter( + ({op}) => op !== 'web-vital' && op !== 'largest-contentful-paint' + ); const spanStartTimestamps = rawSpanDataFiltered .map(span => span.startTimestamp) .filter(Boolean); diff --git a/static/app/utils/replays/replayReader.spec.tsx b/static/app/utils/replays/replayReader.spec.tsx index e3f66c245f80ed..a7850c19813147 100644 --- a/static/app/utils/replays/replayReader.spec.tsx +++ b/static/app/utils/replays/replayReader.spec.tsx @@ -31,6 +31,7 @@ describe('ReplayReader', () => { const missingAttachments = ReplayReader.factory({ attachments: undefined, errors: [], + featureFlags: [], replayRecord, }); expect(missingAttachments).toBeNull(); @@ -38,13 +39,23 @@ describe('ReplayReader', () => { const missingErrors = ReplayReader.factory({ attachments: [], errors: undefined, + featureFlags: [], replayRecord, }); expect(missingErrors).toBeNull(); + const missingFeatureFlags = ReplayReader.factory({ + attachments: [], + errors: [], + featureFlags: undefined, + replayRecord, + }); + expect(missingFeatureFlags).toBeNull(); + const missingRecord = ReplayReader.factory({ attachments: [], errors: [], + featureFlags: [], replayRecord: undefined, }); expect(missingRecord).toBeNull(); @@ -60,6 +71,7 @@ describe('ReplayReader', () => { ReplayConsoleEventFixture({timestamp: minuteTen}), ], errors: [], + featureFlags: [], replayRecord: ReplayRecordFixture({ started_at: new Date('2023-12-25T00:01:00'), finished_at: new Date('2023-12-25T00:09:00'), @@ -78,6 +90,7 @@ describe('ReplayReader', () => { const replay = ReplayReader.factory({ attachments: [], errors: [], + featureFlags: [], replayRecord, }); @@ -213,6 +226,7 @@ describe('ReplayReader', () => { const replay = ReplayReader.factory({ attachments, errors: [], + featureFlags: [], replayRecord, }); @@ -233,6 +247,7 @@ describe('ReplayReader', () => { }), ], errors: [], + featureFlags: [], replayRecord, }); @@ -255,6 +270,7 @@ describe('ReplayReader', () => { }), ], errors: [], + featureFlags: [], replayRecord, }); @@ -294,6 +310,7 @@ describe('ReplayReader', () => { }), ], errors: [], + featureFlags: [], replayRecord, }); @@ -322,6 +339,7 @@ describe('ReplayReader', () => { const replay = ReplayReader.factory({ attachments, errors: [], + featureFlags: [], replayRecord, }); @@ -356,6 +374,7 @@ describe('ReplayReader', () => { const replay = ReplayReader.factory({ attachments, errors: [], + featureFlags: [], replayRecord, }); @@ -434,6 +453,7 @@ describe('ReplayReader', () => { started_at: replayStartedAt, finished_at: replayFinishedAt, }), + featureFlags: [], clipWindow: { startTimestampMs: clipStartTimestamp.getTime(), endTimestampMs: clipEndTimestamp.getTime(), diff --git a/static/app/utils/replays/replayReader.tsx b/static/app/utils/replays/replayReader.tsx index 7115680d78b0d1..7e5eb12100374b 100644 --- a/static/app/utils/replays/replayReader.tsx +++ b/static/app/utils/replays/replayReader.tsx @@ -41,7 +41,6 @@ import { isPaintFrame, isWebVitalFrame, } from 'sentry/utils/replays/types'; -import useOrganization from 'sentry/utils/useOrganization'; import type {ReplayError, ReplayRecord} from 'sentry/views/replays/types'; interface ReplayReaderParams { @@ -61,6 +60,11 @@ interface ReplayReaderParams { */ errors: ReplayError[] | undefined; + /** + * The org's feature flags + */ + featureFlags: string[] | undefined; + /** * The root Replay event, created at the start of the browser session. */ @@ -135,13 +139,25 @@ function removeDuplicateNavCrumbs( } export default class ReplayReader { - static factory({attachments, errors, replayRecord, clipWindow}: ReplayReaderParams) { - if (!attachments || !replayRecord || !errors) { + static factory({ + attachments, + errors, + replayRecord, + clipWindow, + featureFlags, + }: ReplayReaderParams) { + if (!attachments || !replayRecord || !errors || !featureFlags) { return null; } try { - return new ReplayReader({attachments, errors, replayRecord, clipWindow}); + return new ReplayReader({ + attachments, + errors, + replayRecord, + featureFlags, + clipWindow, + }); } catch (err) { Sentry.captureException(err); @@ -152,6 +168,7 @@ export default class ReplayReader { return new ReplayReader({ attachments: [], errors: [], + featureFlags: [], replayRecord, clipWindow, }); @@ -161,6 +178,7 @@ export default class ReplayReader { private constructor({ attachments, errors, + featureFlags, replayRecord, clipWindow, }: RequiredNotNull) { @@ -206,6 +224,7 @@ export default class ReplayReader { // Hydrate the data we were given this._replayRecord = replayRecord; + this._featureFlags = featureFlags; // Errors don't need to be sorted here, they will be merged with breadcrumbs // and spans in the getter and then sorted together. const {errorFrames, feedbackFrames} = hydrateErrors(replayRecord, errors); @@ -245,6 +264,7 @@ export default class ReplayReader { private _cacheKey: string; private _duration: Duration = duration(0); private _errors: ErrorFrame[] = []; + private _featureFlags: string[] = []; private _optionFrame: undefined | OptionFrame; private _replayRecord: ReplayRecord; private _sortedBreadcrumbFrames: BreadcrumbFrame[] = []; @@ -509,8 +529,7 @@ export default class ReplayReader { }); getWebVitalFrames = memoize(() => { - const organization = useOrganization(); - if (organization.features.includes('replay-web-vitals')) { + if (this._featureFlags.includes('replay-web-vitals')) { return this._sortedSpanFrames.filter(isWebVitalFrame); } return []; From 26848f923231aa19ebd34671e2b772944b4aee46 Mon Sep 17 00:00:00 2001 From: Catherine Lee <55311782+c298lee@users.noreply.github.com> Date: Wed, 19 Jun 2024 17:27:32 -0400 Subject: [PATCH 6/8] fix tests and frame description --- static/app/components/events/eventReplay/index.spec.tsx | 2 ++ .../events/eventReplay/replayClipPreview.spec.tsx | 4 ++++ .../components/events/eventReplay/replayPreview.spec.tsx | 4 ++++ static/app/utils/replays/getFrameDetails.tsx | 8 +++++--- static/app/utils/replays/replayReader.tsx | 2 +- .../views/issueDetails/groupReplays/groupReplays.spec.tsx | 2 ++ 6 files changed, 18 insertions(+), 4 deletions(-) diff --git a/static/app/components/events/eventReplay/index.spec.tsx b/static/app/components/events/eventReplay/index.spec.tsx index 22f980f1caf1b6..dd1ca1a958669f 100644 --- a/static/app/components/events/eventReplay/index.spec.tsx +++ b/static/app/components/events/eventReplay/index.spec.tsx @@ -60,6 +60,7 @@ const mockReplay = ReplayReader.factory({ }, }), errors: mockErrors, + featureFlags: [], attachments: RRWebInitFrameEventsFixture({ timestamp: new Date('Sep 22, 2022 4:58:39 PM UTC'), }), @@ -69,6 +70,7 @@ jest.mocked(useReplayReader).mockImplementation(() => { return { attachments: [], errors: mockErrors, + featureFlags: [], fetchError: undefined, fetching: false, onRetry: jest.fn(), diff --git a/static/app/components/events/eventReplay/replayClipPreview.spec.tsx b/static/app/components/events/eventReplay/replayClipPreview.spec.tsx index bd153547368603..09cfe658944881 100644 --- a/static/app/components/events/eventReplay/replayClipPreview.spec.tsx +++ b/static/app/components/events/eventReplay/replayClipPreview.spec.tsx @@ -38,6 +38,7 @@ const mockReplay = ReplayReader.factory({ duration: duration(10, 'seconds'), }), errors: [], + featureFlags: [], attachments: RRWebInitFrameEventsFixture({ timestamp: new Date('Sep 22, 2022 4:58:39 PM UTC'), }), @@ -51,6 +52,7 @@ mockUseReplayReader.mockImplementation(() => { return { attachments: [], errors: [], + featureFlags: [], fetchError: undefined, fetching: false, onRetry: jest.fn(), @@ -123,6 +125,7 @@ describe('ReplayClipPreview', () => { return { attachments: [], errors: [], + featureFlags: [], fetchError: undefined, fetching: true, onRetry: jest.fn(), @@ -144,6 +147,7 @@ describe('ReplayClipPreview', () => { return { attachments: [], errors: [], + featureFlags: [], fetchError: {status: 400} as RequestError, fetching: false, onRetry: jest.fn(), diff --git a/static/app/components/events/eventReplay/replayPreview.spec.tsx b/static/app/components/events/eventReplay/replayPreview.spec.tsx index 2fb9048002c348..c32825079dca3e 100644 --- a/static/app/components/events/eventReplay/replayPreview.spec.tsx +++ b/static/app/components/events/eventReplay/replayPreview.spec.tsx @@ -43,6 +43,7 @@ const mockReplay = ReplayReader.factory({ }, }), errors: [], + featureFlags: [], attachments: RRWebInitFrameEventsFixture({ timestamp: new Date('Sep 22, 2022 4:58:39 PM UTC'), }), @@ -52,6 +53,7 @@ mockUseReplayReader.mockImplementation(() => { return { attachments: [], errors: [], + featureFlags: [], fetchError: undefined, fetching: false, onRetry: jest.fn(), @@ -97,6 +99,7 @@ describe('ReplayPreview', () => { return { attachments: [], errors: [], + featureFlags: [], fetchError: undefined, fetching: true, onRetry: jest.fn(), @@ -118,6 +121,7 @@ describe('ReplayPreview', () => { return { attachments: [], errors: [], + featureFlags: [], fetchError: {status: 400} as RequestError, fetching: false, onRetry: jest.fn(), diff --git a/static/app/utils/replays/getFrameDetails.tsx b/static/app/utils/replays/getFrameDetails.tsx index d0f1325cad13c0..c6ee1098e10571 100644 --- a/static/app/utils/replays/getFrameDetails.tsx +++ b/static/app/utils/replays/getFrameDetails.tsx @@ -22,6 +22,7 @@ import { IconWarning, } from 'sentry/icons'; import {t, tct} from 'sentry/locale'; +import {explodeSlug} from 'sentry/utils'; import {TabKey} from 'sentry/utils/replays/hooks/useActiveReplayTab'; import type { BreadcrumbFrame, @@ -44,6 +45,7 @@ import { isDeadRageClick, isRageClick, } from 'sentry/utils/replays/types'; +import {toTitleCase} from 'sentry/utils/string/toTitleCase'; import type {Color} from 'sentry/utils/theme'; import stripURLOrigin from 'sentry/utils/url/stripURLOrigin'; @@ -283,7 +285,7 @@ const MAPPER_FOR_FRAME: Record Details> = { color: 'green300', description: `Good ${frame.data.value.toFixed(2)}ms`, tabKey: TabKey.NETWORK, - title: frame.description.replaceAll('-', ' '), + title: toTitleCase(explodeSlug(frame.description)), icon: , }; case 'needs-improvement': @@ -291,7 +293,7 @@ const MAPPER_FOR_FRAME: Record Details> = { color: 'yellow300', description: `Meh ${frame.data.value.toFixed(2)}ms`, tabKey: TabKey.NETWORK, - title: frame.description.replaceAll('-', ' '), + title: toTitleCase(explodeSlug(frame.description)), icon: , }; default: @@ -299,7 +301,7 @@ const MAPPER_FOR_FRAME: Record Details> = { color: 'red300', description: `Poor ${frame.data.value.toFixed(2)}ms`, tabKey: TabKey.NETWORK, - title: frame.description.replaceAll('-', ' '), + title: toTitleCase(explodeSlug(frame.description)), icon: , }; } diff --git a/static/app/utils/replays/replayReader.tsx b/static/app/utils/replays/replayReader.tsx index 7e5eb12100374b..5fd85e74063f73 100644 --- a/static/app/utils/replays/replayReader.tsx +++ b/static/app/utils/replays/replayReader.tsx @@ -532,7 +532,7 @@ export default class ReplayReader { if (this._featureFlags.includes('replay-web-vitals')) { return this._sortedSpanFrames.filter(isWebVitalFrame); } - return []; + return this._sortedSpanFrames.filter(isWebVitalFrame); }); getVideoEvents = () => this._videoEvents; diff --git a/static/app/views/issueDetails/groupReplays/groupReplays.spec.tsx b/static/app/views/issueDetails/groupReplays/groupReplays.spec.tsx index 5c22882177531c..ad524b29a3bfdd 100644 --- a/static/app/views/issueDetails/groupReplays/groupReplays.spec.tsx +++ b/static/app/views/issueDetails/groupReplays/groupReplays.spec.tsx @@ -63,6 +63,7 @@ const mockReplay = ReplayReader.factory({ duration: duration(10, 'seconds'), }), errors: [], + featureFlags: [], attachments: RRWebInitFrameEventsFixture({ timestamp: new Date('Sep 22, 2022 4:58:39 PM UTC'), }), @@ -76,6 +77,7 @@ mockUseReplayReader.mockImplementation(() => { return { attachments: [], errors: [], + featureFlags: [], fetchError: undefined, fetching: false, onRetry: jest.fn(), From 3b03699e65d7f0d51d3124906076db723615fca0 Mon Sep 17 00:00:00 2001 From: Catherine Lee <55311782+c298lee@users.noreply.github.com> Date: Wed, 19 Jun 2024 17:35:45 -0400 Subject: [PATCH 7/8] set feature flag as optional --- .../events/eventReplay/index.spec.tsx | 2 -- .../eventReplay/replayClipPreview.spec.tsx | 4 ---- .../events/eventReplay/replayPreview.spec.tsx | 4 ---- .../utils/replays/hooks/useReplayReader.tsx | 1 - .../app/utils/replays/replayReader.spec.tsx | 20 ------------------- static/app/utils/replays/replayReader.tsx | 20 +++++++++---------- .../groupReplays/groupReplays.spec.tsx | 2 -- 7 files changed, 10 insertions(+), 43 deletions(-) diff --git a/static/app/components/events/eventReplay/index.spec.tsx b/static/app/components/events/eventReplay/index.spec.tsx index dd1ca1a958669f..22f980f1caf1b6 100644 --- a/static/app/components/events/eventReplay/index.spec.tsx +++ b/static/app/components/events/eventReplay/index.spec.tsx @@ -60,7 +60,6 @@ const mockReplay = ReplayReader.factory({ }, }), errors: mockErrors, - featureFlags: [], attachments: RRWebInitFrameEventsFixture({ timestamp: new Date('Sep 22, 2022 4:58:39 PM UTC'), }), @@ -70,7 +69,6 @@ jest.mocked(useReplayReader).mockImplementation(() => { return { attachments: [], errors: mockErrors, - featureFlags: [], fetchError: undefined, fetching: false, onRetry: jest.fn(), diff --git a/static/app/components/events/eventReplay/replayClipPreview.spec.tsx b/static/app/components/events/eventReplay/replayClipPreview.spec.tsx index 09cfe658944881..bd153547368603 100644 --- a/static/app/components/events/eventReplay/replayClipPreview.spec.tsx +++ b/static/app/components/events/eventReplay/replayClipPreview.spec.tsx @@ -38,7 +38,6 @@ const mockReplay = ReplayReader.factory({ duration: duration(10, 'seconds'), }), errors: [], - featureFlags: [], attachments: RRWebInitFrameEventsFixture({ timestamp: new Date('Sep 22, 2022 4:58:39 PM UTC'), }), @@ -52,7 +51,6 @@ mockUseReplayReader.mockImplementation(() => { return { attachments: [], errors: [], - featureFlags: [], fetchError: undefined, fetching: false, onRetry: jest.fn(), @@ -125,7 +123,6 @@ describe('ReplayClipPreview', () => { return { attachments: [], errors: [], - featureFlags: [], fetchError: undefined, fetching: true, onRetry: jest.fn(), @@ -147,7 +144,6 @@ describe('ReplayClipPreview', () => { return { attachments: [], errors: [], - featureFlags: [], fetchError: {status: 400} as RequestError, fetching: false, onRetry: jest.fn(), diff --git a/static/app/components/events/eventReplay/replayPreview.spec.tsx b/static/app/components/events/eventReplay/replayPreview.spec.tsx index c32825079dca3e..2fb9048002c348 100644 --- a/static/app/components/events/eventReplay/replayPreview.spec.tsx +++ b/static/app/components/events/eventReplay/replayPreview.spec.tsx @@ -43,7 +43,6 @@ const mockReplay = ReplayReader.factory({ }, }), errors: [], - featureFlags: [], attachments: RRWebInitFrameEventsFixture({ timestamp: new Date('Sep 22, 2022 4:58:39 PM UTC'), }), @@ -53,7 +52,6 @@ mockUseReplayReader.mockImplementation(() => { return { attachments: [], errors: [], - featureFlags: [], fetchError: undefined, fetching: false, onRetry: jest.fn(), @@ -99,7 +97,6 @@ describe('ReplayPreview', () => { return { attachments: [], errors: [], - featureFlags: [], fetchError: undefined, fetching: true, onRetry: jest.fn(), @@ -121,7 +118,6 @@ describe('ReplayPreview', () => { return { attachments: [], errors: [], - featureFlags: [], fetchError: {status: 400} as RequestError, fetching: false, onRetry: jest.fn(), diff --git a/static/app/utils/replays/hooks/useReplayReader.tsx b/static/app/utils/replays/hooks/useReplayReader.tsx index 7ff2490a53a05b..a17cb4c6f14646 100644 --- a/static/app/utils/replays/hooks/useReplayReader.tsx +++ b/static/app/utils/replays/hooks/useReplayReader.tsx @@ -62,7 +62,6 @@ export default function useReplayReader({orgSlug, replaySlug, clipWindow, group} ...replayData, attachments, errors, - featureFlags, replay, replayId, replayRecord, diff --git a/static/app/utils/replays/replayReader.spec.tsx b/static/app/utils/replays/replayReader.spec.tsx index a7850c19813147..e3f66c245f80ed 100644 --- a/static/app/utils/replays/replayReader.spec.tsx +++ b/static/app/utils/replays/replayReader.spec.tsx @@ -31,7 +31,6 @@ describe('ReplayReader', () => { const missingAttachments = ReplayReader.factory({ attachments: undefined, errors: [], - featureFlags: [], replayRecord, }); expect(missingAttachments).toBeNull(); @@ -39,23 +38,13 @@ describe('ReplayReader', () => { const missingErrors = ReplayReader.factory({ attachments: [], errors: undefined, - featureFlags: [], replayRecord, }); expect(missingErrors).toBeNull(); - const missingFeatureFlags = ReplayReader.factory({ - attachments: [], - errors: [], - featureFlags: undefined, - replayRecord, - }); - expect(missingFeatureFlags).toBeNull(); - const missingRecord = ReplayReader.factory({ attachments: [], errors: [], - featureFlags: [], replayRecord: undefined, }); expect(missingRecord).toBeNull(); @@ -71,7 +60,6 @@ describe('ReplayReader', () => { ReplayConsoleEventFixture({timestamp: minuteTen}), ], errors: [], - featureFlags: [], replayRecord: ReplayRecordFixture({ started_at: new Date('2023-12-25T00:01:00'), finished_at: new Date('2023-12-25T00:09:00'), @@ -90,7 +78,6 @@ describe('ReplayReader', () => { const replay = ReplayReader.factory({ attachments: [], errors: [], - featureFlags: [], replayRecord, }); @@ -226,7 +213,6 @@ describe('ReplayReader', () => { const replay = ReplayReader.factory({ attachments, errors: [], - featureFlags: [], replayRecord, }); @@ -247,7 +233,6 @@ describe('ReplayReader', () => { }), ], errors: [], - featureFlags: [], replayRecord, }); @@ -270,7 +255,6 @@ describe('ReplayReader', () => { }), ], errors: [], - featureFlags: [], replayRecord, }); @@ -310,7 +294,6 @@ describe('ReplayReader', () => { }), ], errors: [], - featureFlags: [], replayRecord, }); @@ -339,7 +322,6 @@ describe('ReplayReader', () => { const replay = ReplayReader.factory({ attachments, errors: [], - featureFlags: [], replayRecord, }); @@ -374,7 +356,6 @@ describe('ReplayReader', () => { const replay = ReplayReader.factory({ attachments, errors: [], - featureFlags: [], replayRecord, }); @@ -453,7 +434,6 @@ describe('ReplayReader', () => { started_at: replayStartedAt, finished_at: replayFinishedAt, }), - featureFlags: [], clipWindow: { startTimestampMs: clipStartTimestamp.getTime(), endTimestampMs: clipEndTimestamp.getTime(), diff --git a/static/app/utils/replays/replayReader.tsx b/static/app/utils/replays/replayReader.tsx index 5fd85e74063f73..bdf646f66c4428 100644 --- a/static/app/utils/replays/replayReader.tsx +++ b/static/app/utils/replays/replayReader.tsx @@ -60,11 +60,6 @@ interface ReplayReaderParams { */ errors: ReplayError[] | undefined; - /** - * The org's feature flags - */ - featureFlags: string[] | undefined; - /** * The root Replay event, created at the start of the browser session. */ @@ -74,6 +69,11 @@ interface ReplayReaderParams { * If provided, the replay will be clipped to this window. */ clipWindow?: ClipWindow; + + /** + * The org's feature flags + */ + featureFlags?: string[]; } type RequiredNotNull = { @@ -146,7 +146,7 @@ export default class ReplayReader { clipWindow, featureFlags, }: ReplayReaderParams) { - if (!attachments || !replayRecord || !errors || !featureFlags) { + if (!attachments || !replayRecord || !errors) { return null; } @@ -168,7 +168,7 @@ export default class ReplayReader { return new ReplayReader({ attachments: [], errors: [], - featureFlags: [], + featureFlags, replayRecord, clipWindow, }); @@ -264,7 +264,7 @@ export default class ReplayReader { private _cacheKey: string; private _duration: Duration = duration(0); private _errors: ErrorFrame[] = []; - private _featureFlags: string[] = []; + private _featureFlags: string[] | undefined = []; private _optionFrame: undefined | OptionFrame; private _replayRecord: ReplayRecord; private _sortedBreadcrumbFrames: BreadcrumbFrame[] = []; @@ -529,10 +529,10 @@ export default class ReplayReader { }); getWebVitalFrames = memoize(() => { - if (this._featureFlags.includes('replay-web-vitals')) { + if (this._featureFlags?.includes('replay-web-vitals')) { return this._sortedSpanFrames.filter(isWebVitalFrame); } - return this._sortedSpanFrames.filter(isWebVitalFrame); + return []; }); getVideoEvents = () => this._videoEvents; diff --git a/static/app/views/issueDetails/groupReplays/groupReplays.spec.tsx b/static/app/views/issueDetails/groupReplays/groupReplays.spec.tsx index ad524b29a3bfdd..5c22882177531c 100644 --- a/static/app/views/issueDetails/groupReplays/groupReplays.spec.tsx +++ b/static/app/views/issueDetails/groupReplays/groupReplays.spec.tsx @@ -63,7 +63,6 @@ const mockReplay = ReplayReader.factory({ duration: duration(10, 'seconds'), }), errors: [], - featureFlags: [], attachments: RRWebInitFrameEventsFixture({ timestamp: new Date('Sep 22, 2022 4:58:39 PM UTC'), }), @@ -77,7 +76,6 @@ mockUseReplayReader.mockImplementation(() => { return { attachments: [], errors: [], - featureFlags: [], fetchError: undefined, fetching: false, onRetry: jest.fn(), From 1a42a2435ef98dec6f2a4b8e1b8bf3f5a33e5e27 Mon Sep 17 00:00:00 2001 From: Catherine Lee <55311782+c298lee@users.noreply.github.com> Date: Thu, 20 Jun 2024 14:23:03 -0400 Subject: [PATCH 8/8] translate & fix ff name --- static/app/utils/replays/getFrameDetails.tsx | 12 +++++++++--- static/app/utils/replays/replayReader.tsx | 2 +- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/static/app/utils/replays/getFrameDetails.tsx b/static/app/utils/replays/getFrameDetails.tsx index c6ee1098e10571..a274443483ffcd 100644 --- a/static/app/utils/replays/getFrameDetails.tsx +++ b/static/app/utils/replays/getFrameDetails.tsx @@ -283,7 +283,9 @@ const MAPPER_FOR_FRAME: Record Details> = { case 'good': return { color: 'green300', - description: `Good ${frame.data.value.toFixed(2)}ms`, + description: tct('Good [value]ms', { + value: frame.data.value.toFixed(2), + }), tabKey: TabKey.NETWORK, title: toTitleCase(explodeSlug(frame.description)), icon: , @@ -291,7 +293,9 @@ const MAPPER_FOR_FRAME: Record Details> = { case 'needs-improvement': return { color: 'yellow300', - description: `Meh ${frame.data.value.toFixed(2)}ms`, + description: tct('Meh [value]ms', { + value: frame.data.value.toFixed(2), + }), tabKey: TabKey.NETWORK, title: toTitleCase(explodeSlug(frame.description)), icon: , @@ -299,7 +303,9 @@ const MAPPER_FOR_FRAME: Record Details> = { default: return { color: 'red300', - description: `Poor ${frame.data.value.toFixed(2)}ms`, + description: tct('Poor [value]ms', { + value: frame.data.value.toFixed(2), + }), tabKey: TabKey.NETWORK, title: toTitleCase(explodeSlug(frame.description)), icon: , diff --git a/static/app/utils/replays/replayReader.tsx b/static/app/utils/replays/replayReader.tsx index bdf646f66c4428..9092f4591e2ea4 100644 --- a/static/app/utils/replays/replayReader.tsx +++ b/static/app/utils/replays/replayReader.tsx @@ -529,7 +529,7 @@ export default class ReplayReader { }); getWebVitalFrames = memoize(() => { - if (this._featureFlags?.includes('replay-web-vitals')) { + if (this._featureFlags?.includes('session-replay-web-vitals')) { return this._sortedSpanFrames.filter(isWebVitalFrame); } return [];