diff --git a/static/app/components/events/eventHydrationDiff/replayDiffContent.tsx b/static/app/components/events/eventHydrationDiff/replayDiffContent.tsx index b6e490aa0d3c9..cd3eae13e1785 100644 --- a/static/app/components/events/eventHydrationDiff/replayDiffContent.tsx +++ b/static/app/components/events/eventHydrationDiff/replayDiffContent.tsx @@ -7,6 +7,7 @@ import {ReplayGroupContextProvider} from 'sentry/components/replays/replayGroupC import {t} from 'sentry/locale'; import type {Event} from 'sentry/types/event'; import type {Group} from 'sentry/types/group'; +import {getReplayDiffOffsetsFromEvent} from 'sentry/utils/replays/getDiffTimestamps'; import useReplayReader from 'sentry/utils/replays/hooks/useReplayReader'; interface Props { @@ -31,11 +32,7 @@ export default function ReplayDiffContent({event, group, orgSlug, replaySlug}: P return null; } - // TODO: base the event timestamp off the replay data itself. - const startTimestampMS = - 'startTimestamp' in event ? event.startTimestamp * 1000 : undefined; - const timeOfEvent = event.dateCreated ?? startTimestampMS ?? event.dateReceived; - const eventTimestampMs = timeOfEvent ? Math.floor(new Date(timeOfEvent).getTime()) : 0; + const {leftOffsetMs, rightOffsetMs} = getReplayDiffOffsetsFromEvent(replay, event); return ( {t('Open Diff Viewer')} @@ -57,9 +54,9 @@ export default function ReplayDiffContent({event, group, orgSlug, replaySlug}: P diff --git a/static/app/components/replays/breadcrumbs/breadcrumbItem.tsx b/static/app/components/replays/breadcrumbs/breadcrumbItem.tsx index 2fa8fcbe0f798..a3c994dc1b31a 100644 --- a/static/app/components/replays/breadcrumbs/breadcrumbItem.tsx +++ b/static/app/components/replays/breadcrumbs/breadcrumbItem.tsx @@ -20,9 +20,21 @@ import {Tooltip} from 'sentry/components/tooltip'; import {t} from 'sentry/locale'; import {space} from 'sentry/styles/space'; import type {Extraction} from 'sentry/utils/replays/extractDomNodes'; +import {getReplayDiffOffsetsFromFrame} from 'sentry/utils/replays/getDiffTimestamps'; import getFrameDetails from 'sentry/utils/replays/getFrameDetails'; -import type {ErrorFrame, FeedbackFrame, ReplayFrame} from 'sentry/utils/replays/types'; -import {isErrorFrame, isFeedbackFrame} from 'sentry/utils/replays/types'; +import type ReplayReader from 'sentry/utils/replays/replayReader'; +import type { + ErrorFrame, + FeedbackFrame, + HydrationErrorFrame, + ReplayFrame, +} from 'sentry/utils/replays/types'; +import { + isBreadcrumbFrame, + isErrorFrame, + isFeedbackFrame, + isHydrationErrorFrame, +} from 'sentry/utils/replays/types'; import useOrganization from 'sentry/utils/useOrganization'; import useProjectFromSlug from 'sentry/utils/useProjectFromSlug'; import IconWrapper from 'sentry/views/replays/detail/iconWrapper'; @@ -88,22 +100,10 @@ function BreadcrumbItem({ }, [description, expandPaths, onInspectorExpanded]); const renderComparisonButton = useCallback(() => { - return frame?.data && 'mutations' in frame.data ? ( -
- - {t('Open Hydration Diff')} - -
+ return isBreadcrumbFrame(frame) && isHydrationErrorFrame(frame) ? ( + ) : null; - }, [frame?.data, frame.offsetMs, replay]); + }, [frame, replay]); const renderCodeSnippet = useCallback(() => { return extraction?.html ? ( @@ -187,6 +187,29 @@ function BreadcrumbItem({ ); } +function CrumbHydrationButton({ + replay, + frame, +}: { + frame: HydrationErrorFrame; + replay: ReplayReader | null; +}) { + const {leftOffsetMs, rightOffsetMs} = getReplayDiffOffsetsFromFrame(replay, frame); + + return ( +
+ + {t('Open Hydration Diff')} + +
+ ); +} + function CrumbErrorIssue({frame}: {frame: FeedbackFrame | ErrorFrame}) { const organization = useOrganization(); const project = useProjectFromSlug({organization, projectSlug: frame.data.projectSlug}); diff --git a/static/app/components/replays/breadcrumbs/openReplayComparisonButton.tsx b/static/app/components/replays/breadcrumbs/openReplayComparisonButton.tsx index 053c06fcabefe..283ee16dfb2f1 100644 --- a/static/app/components/replays/breadcrumbs/openReplayComparisonButton.tsx +++ b/static/app/components/replays/breadcrumbs/openReplayComparisonButton.tsx @@ -14,17 +14,17 @@ const LazyComparisonModal = lazy( interface Props { children: ReactNode; - leftTimestamp: number; + leftOffsetMs: number; replay: null | ReplayReader; - rightTimestamp: number; + rightOffsetMs: number; size?: ButtonProps['size']; } export function OpenReplayComparisonButton({ children, - leftTimestamp, + leftOffsetMs, replay, - rightTimestamp, + rightOffsetMs, size, }: Props) { const organization = useOrganization(); @@ -59,8 +59,8 @@ export function OpenReplayComparisonButton({ diff --git a/static/app/components/replays/breadcrumbs/replayComparisonModal.tsx b/static/app/components/replays/breadcrumbs/replayComparisonModal.tsx index e5e87b728ee9c..d300c5ff55833 100644 --- a/static/app/components/replays/breadcrumbs/replayComparisonModal.tsx +++ b/static/app/components/replays/breadcrumbs/replayComparisonModal.tsx @@ -11,19 +11,19 @@ import type ReplayReader from 'sentry/utils/replays/replayReader'; import {OrganizationContext} from 'sentry/views/organizationContext'; interface Props extends ModalRenderProps { - leftTimestamp: number; + leftOffsetMs: number; organization: Organization; replay: null | ReplayReader; - rightTimestamp: number; + rightOffsetMs: number; } export default function ReplayComparisonModal({ Body, Header, - leftTimestamp, + leftOffsetMs, organization, replay, - rightTimestamp, + rightOffsetMs, }: Props) { return ( @@ -55,8 +55,8 @@ export default function ReplayComparisonModal({ diff --git a/static/app/components/replays/replayDiff.tsx b/static/app/components/replays/replayDiff.tsx index 7a7e243ecd355..913679bd3df4c 100644 --- a/static/app/components/replays/replayDiff.tsx +++ b/static/app/components/replays/replayDiff.tsx @@ -20,9 +20,9 @@ import type ReplayReader from 'sentry/utils/replays/replayReader'; const MAX_CLAMP_TO_START = 2000; interface Props { - leftTimestamp: number; + leftOffsetMs: number; replay: null | ReplayReader; - rightTimestamp: number; + rightOffsetMs: number; defaultTab?: DiffType; } @@ -33,16 +33,16 @@ export enum DiffType { export default function ReplayDiff({ defaultTab = DiffType.VISUAL, - leftTimestamp, + leftOffsetMs, replay, - rightTimestamp, + rightOffsetMs, }: Props) { const fetching = false; const [leftBody, setLeftBody] = useState(null); const [rightBody, setRightBody] = useState(null); - let startOffset = leftTimestamp - 1; + let startOffset = leftOffsetMs - 1; // If the error occurs close to the start of the replay, clamp the start offset to 1 // to help compare with the html provided by the server, This helps with some errors on localhost. if (startOffset < MAX_CLAMP_TO_START) { @@ -96,16 +96,16 @@ export default function ReplayDiff({ - {rightTimestamp > 0 ? ( + {rightOffsetMs > 0 ? ( ) : ( diff --git a/static/app/utils/replays/getDiffTimestamps.spec.tsx b/static/app/utils/replays/getDiffTimestamps.spec.tsx new file mode 100644 index 0000000000000..4d43aa73f6a86 --- /dev/null +++ b/static/app/utils/replays/getDiffTimestamps.spec.tsx @@ -0,0 +1,169 @@ +import invariant from 'invariant'; +import {EventFixture} from 'sentry-fixture/event'; +import {ReplayHydrationErrorFrameFixture} from 'sentry-fixture/replay/replayBreadcrumbFrameData'; +import {ReplayBreadcrumbFrameEventFixture} from 'sentry-fixture/replay/replayFrameEvents'; +import { + RRWebDOMFrameFixture, + RRWebFullSnapshotFrameEventFixture, + RRWebIncrementalSnapshotFrameEventFixture, + RRWebInitFrameEventsFixture, +} from 'sentry-fixture/replay/rrweb'; +import {ReplayRecordFixture} from 'sentry-fixture/replayRecord'; + +import { + getReplayDiffOffsetsFromEvent, + getReplayDiffOffsetsFromFrame, +} from 'sentry/utils/replays/getDiffTimestamps'; +import hydrateBreadcrumbs from 'sentry/utils/replays/hydrateBreadcrumbs'; +import hydrateFrames from 'sentry/utils/replays/hydrateFrames'; +import ReplayReader from 'sentry/utils/replays/replayReader'; +import { + IncrementalSource, + isHydrationErrorFrame, + type RawBreadcrumbFrame, +} from 'sentry/utils/replays/types'; +import type {ReplayError} from 'sentry/views/replays/types'; + +const START_DATE = new Date('2022-06-15T00:40:00.000Z'); +const INIT_DATE = new Date('2022-06-15T00:40:00.100Z'); +const FULL_DATE = new Date('2022-06-15T00:40:00.200Z'); +const ERROR_DATE = new Date('2022-06-15T00:40:01.000Z'); // errors do not have ms precision +const CRUMB_1_DATE = new Date('2022-06-15T00:40:01.350Z'); +const INCR_DATE = new Date('2022-06-15T00:40:05.000Z'); +const CRUMB_2_DATE = new Date('2022-06-15T00:40:05.350Z'); +const END_DATE = new Date('2022-06-15T00:50:00.555Z'); + +const replayRecord = ReplayRecordFixture({ + started_at: START_DATE, + finished_at: END_DATE, +}); + +const RRWEB_EVENTS = [ + ...RRWebInitFrameEventsFixture({ + timestamp: INIT_DATE, + }), + RRWebFullSnapshotFrameEventFixture({timestamp: FULL_DATE}), + RRWebIncrementalSnapshotFrameEventFixture({ + timestamp: INCR_DATE, + data: { + source: IncrementalSource.Mutation, + adds: [ + { + node: RRWebDOMFrameFixture({ + tagName: 'canvas', + }), + parentId: 0, + nextId: null, + }, + ], + removes: [], + texts: [], + attributes: [], + }, + }), +]; + +function getMockReplay(rrwebEvents: any[], errors: ReplayError[]) { + const attachments = [...rrwebEvents]; + const replay = ReplayReader.factory({ + replayRecord, + errors, + attachments, + }); + + return {replay}; +} + +function getMockReplayWithCrumbFrame( + rrwebEvents: any[], + crumbFrame: RawBreadcrumbFrame, + errors: ReplayError[] +) { + const attachments = [...rrwebEvents]; + + attachments.push( + ReplayBreadcrumbFrameEventFixture({ + timestamp: new Date(crumbFrame.timestamp), + data: { + payload: crumbFrame, + }, + }) + ); + + const {rrwebFrames} = hydrateFrames(attachments); + const [hydrationErrorFrame] = hydrateBreadcrumbs( + replayRecord, + crumbFrame ? [crumbFrame] : [], + rrwebFrames + ); + + const replay = ReplayReader.factory({ + replayRecord, + errors, + attachments, + }); + + invariant(isHydrationErrorFrame(hydrationErrorFrame), ''); + return {hydrationErrorFrame, replay}; +} + +describe('getReplayDiffOffsetsFromFrame', () => { + it('should return the offset of the requested frame, and the next frame', () => { + const rawHydrationCrumbFrame = ReplayHydrationErrorFrameFixture({ + timestamp: CRUMB_1_DATE, + }); + const {replay, hydrationErrorFrame} = getMockReplayWithCrumbFrame( + RRWEB_EVENTS, + rawHydrationCrumbFrame, + [] + ); + + expect(getReplayDiffOffsetsFromFrame(replay, hydrationErrorFrame)).toEqual({ + leftOffsetMs: 1_350, // offset of CRUMB_1_DATE + rightOffsetMs: 5_000, // offset of the INCR_DATE + }); + }); + + it('should return the offset of the requested frame, and 0 if there is no next frame', () => { + const rawHydrationCrumbFrame = ReplayHydrationErrorFrameFixture({ + timestamp: CRUMB_2_DATE, + }); + const {replay, hydrationErrorFrame} = getMockReplayWithCrumbFrame( + RRWEB_EVENTS, + rawHydrationCrumbFrame, + [] + ); + + expect(getReplayDiffOffsetsFromFrame(replay, hydrationErrorFrame)).toEqual({ + leftOffsetMs: 5_350, // offset of CRUMB_2_DATE + rightOffsetMs: 0, // no next mutation date, so offset is 0 + }); + }); +}); + +describe('getReplayDiffOffsetsFromEvent', () => { + it('should get offsets based on a hydration breadcrumb that occurs within the same second of the error', () => { + const rawHydrationCrumbFrame = ReplayHydrationErrorFrameFixture({ + timestamp: CRUMB_1_DATE, + }); + const errorEvent = EventFixture({dateCreated: ERROR_DATE.toISOString()}); + const {replay} = getMockReplayWithCrumbFrame(RRWEB_EVENTS, rawHydrationCrumbFrame, [ + errorEvent as any as ReplayError, + ]); + + expect(getReplayDiffOffsetsFromEvent(replay!, errorEvent)).toEqual({ + leftOffsetMs: 1_350, // offset of CRUMB_1_DATE + rightOffsetMs: 5_000, // offset of the INCR_DATE + }); + }); + + it('should get offsets when no hydration breadcrumb exists', () => { + const errorEvent = EventFixture({dateCreated: ERROR_DATE.toISOString()}); + const {replay} = getMockReplay(RRWEB_EVENTS, [errorEvent as any as ReplayError]); + + expect(getReplayDiffOffsetsFromEvent(replay!, errorEvent)).toEqual({ + leftOffsetMs: 1_000, // offset of ERROR_DATE + rightOffsetMs: 5_000, // offset of the INCR_DATE + }); + }); +}); diff --git a/static/app/utils/replays/getDiffTimestamps.tsx b/static/app/utils/replays/getDiffTimestamps.tsx new file mode 100644 index 0000000000000..8fcd8fc76f82e --- /dev/null +++ b/static/app/utils/replays/getDiffTimestamps.tsx @@ -0,0 +1,60 @@ +import type {Event} from 'sentry/types/event'; +import type ReplayReader from 'sentry/utils/replays/replayReader'; +import { + type HydrationErrorFrame, + isHydrationErrorFrame, +} from 'sentry/utils/replays/types'; + +export function getReplayDiffOffsetsFromFrame( + replay: ReplayReader | null, + frame: HydrationErrorFrame +) { + return { + leftOffsetMs: frame.offsetMs, + rightOffsetMs: Math.max( + 0, + // `next.timestamp` is a timestamp since the unix epoch, so we remove the + // replay start timestamp to get an offset + (frame.data.mutations.next?.timestamp ?? 0) - + (replay?.getReplay().started_at.getTime() ?? 0) + ), + }; +} + +export function getReplayDiffOffsetsFromEvent(replay: ReplayReader, event: Event) { + const startTimestampMS = + 'startTimestamp' in event ? event.startTimestamp * 1000 : undefined; + const timeOfEvent = event.dateCreated ?? startTimestampMS ?? event.dateReceived; + const eventTimestampMs = timeOfEvent ? Math.floor(new Date(timeOfEvent).getTime()) : 0; + // `event.dateCreated` is the most common date to use, and it's in seconds not ms + + const hydrationFrame = replay + .getBreadcrumbFrames() + .find( + breadcrumb => + isHydrationErrorFrame(breadcrumb) && + breadcrumb.timestampMs > eventTimestampMs && + breadcrumb.timestampMs < eventTimestampMs + 1000 + ); + + if (hydrationFrame && isHydrationErrorFrame(hydrationFrame)) { + return getReplayDiffOffsetsFromFrame(replay, hydrationFrame); + } + + const replayStartTimestamp = replay?.getReplay().started_at.getTime() ?? 0; + + // Use the event timestamp for the left side. + // Event has only second precision, therefore the hydration error happened + // sometime after this timestamp. + const leftOffsetMs = Math.max(0, eventTimestampMs - replayStartTimestamp); + + // Use the timestamp of the first mutation to happen after the timestamp of + // the error event. + const rightOffsetMs = Math.max( + 0, + (replay.getRRWebMutations().find(frame => frame.timestamp > eventTimestampMs + 1000) + ?.timestamp ?? eventTimestampMs) - replayStartTimestamp + ); + + return {leftOffsetMs, rightOffsetMs}; +} diff --git a/static/app/utils/replays/replayReader.spec.tsx b/static/app/utils/replays/replayReader.spec.tsx index e3f66c245f80e..c4517fc1cb3d6 100644 --- a/static/app/utils/replays/replayReader.spec.tsx +++ b/static/app/utils/replays/replayReader.spec.tsx @@ -1,4 +1,3 @@ -import {EventType, IncrementalSource} from '@sentry-internal/rrweb'; import { ReplayClickEventFixture, ReplayConsoleEventFixture, @@ -17,12 +16,14 @@ import {ReplayRequestFrameFixture} from 'sentry-fixture/replay/replaySpanFrameDa import { RRWebDOMFrameFixture, RRWebFullSnapshotFrameEventFixture, + RRWebIncrementalSnapshotFrameEventFixture, } from 'sentry-fixture/replay/rrweb'; import {ReplayErrorFixture} from 'sentry-fixture/replayError'; import {ReplayRecordFixture} from 'sentry-fixture/replayRecord'; import {BreadcrumbType} from 'sentry/types/breadcrumbs'; import ReplayReader from 'sentry/utils/replays/replayReader'; +import {EventType, IncrementalSource} from 'sentry/utils/replays/types'; describe('ReplayReader', () => { const replayRecord = ReplayRecordFixture(); @@ -332,29 +333,27 @@ describe('ReplayReader', () => { const timestamp = new Date('2023-12-25T00:02:00'); const snapshot = RRWebFullSnapshotFrameEventFixture({timestamp}); - const attachments = [ - snapshot, - { - type: EventType.IncrementalSnapshot, - timestamp, - data: { - source: IncrementalSource.Mutation, - adds: [ - { - node: RRWebDOMFrameFixture({ - tagName: 'canvas', - }), - }, - ], - removes: [], - texts: [], - attributes: [], - }, + const increment = RRWebIncrementalSnapshotFrameEventFixture({ + timestamp, + data: { + source: IncrementalSource.Mutation, + adds: [ + { + node: RRWebDOMFrameFixture({ + tagName: 'canvas', + }), + parentId: 0, + nextId: null, + }, + ], + removes: [], + texts: [], + attributes: [], }, - ]; + }); const replay = ReplayReader.factory({ - attachments, + attachments: [snapshot, increment], errors: [], replayRecord, }); diff --git a/static/app/utils/replays/replayReader.tsx b/static/app/utils/replays/replayReader.tsx index 9092f4591e2ea..c45f6a8b31a56 100644 --- a/static/app/utils/replays/replayReader.tsx +++ b/static/app/utils/replays/replayReader.tsx @@ -1,6 +1,4 @@ import * as Sentry from '@sentry/react'; -import type {incrementalSnapshotEvent} from '@sentry-internal/rrweb'; -import {IncrementalSource} from '@sentry-internal/rrweb'; import memoize from 'lodash/memoize'; import {type Duration, duration} from 'moment'; @@ -25,6 +23,7 @@ import type { ClipWindow, ErrorFrame, fullSnapshotEvent, + incrementalSnapshotEvent, MemoryFrame, OptionFrame, RecordingFrame, @@ -36,6 +35,7 @@ import type { import { BreadcrumbCategories, EventType, + IncrementalSource, isDeadClick, isDeadRageClick, isPaintFrame, @@ -425,6 +425,8 @@ export default class ReplayReader { getRRWebFrames = () => this._sortedRRWebEvents; + getBreadcrumbFrames = () => this._sortedBreadcrumbFrames; + getRRWebMutations = () => this._sortedRRWebEvents.filter( event => diff --git a/static/app/utils/replays/types.tsx b/static/app/utils/replays/types.tsx index 6a6133909f1e4..ff67d3597f95f 100644 --- a/static/app/utils/replays/types.tsx +++ b/static/app/utils/replays/types.tsx @@ -1,10 +1,10 @@ import {EventType, type eventWithTime as TEventWithTime} from '@sentry-internal/rrweb'; export type {serializedNodeWithId} from '@sentry-internal/rrweb-snapshot'; -export type {fullSnapshotEvent} from '@sentry-internal/rrweb'; +export type {fullSnapshotEvent, incrementalSnapshotEvent} from '@sentry-internal/rrweb'; export {NodeType} from '@sentry-internal/rrweb-snapshot'; -export {EventType} from '@sentry-internal/rrweb'; +export {EventType, IncrementalSource} from '@sentry-internal/rrweb'; import type { ReplayBreadcrumbFrame as TRawBreadcrumbFrame, @@ -17,6 +17,24 @@ import invariant from 'invariant'; import type {HydratedA11yFrame} from 'sentry/utils/replays/hydrateA11yFrame'; +// These stub types should be coming from the sdk, but they're hard-coded until +// the SDK updates to the latest version... once that happens delete this! +// Needed for tests +// TODO[ryan953]: Remove this once the SDK is exporting the type as part of ReplayBreadcrumbFrame +export type RawHydrationErrorFrame = { + category: 'replay.hydrate-error'; + timestamp: number; + type: string; + data?: { + url?: string; + }; + message?: string; +}; + +// These stub types should be coming from the sdk, but they're hard-coded until +// the SDK updates to the latest version... once that happens delete this! +type StubBreadcrumbTypes = RawHydrationErrorFrame; + // TODO: more types get added here type MobileBreadcrumbTypes = | { @@ -55,6 +73,7 @@ type MobileBreadcrumbTypes = * because the mobile SDK does not send that property currently. */ type ExtraBreadcrumbTypes = + | StubBreadcrumbTypes | MobileBreadcrumbTypes | { category: 'navigation'; @@ -168,6 +187,12 @@ export function isRageClick(frame: MultiClickFrame) { return frame.data.clickCount >= 5; } +export function isHydrationErrorFrame( + frame: BreadcrumbFrame +): frame is HydrationErrorFrame { + return frame.category === 'replay.hydrate-error'; +} + type Overwrite = Pick> & U; type HydratedTimestamp = { @@ -255,6 +280,19 @@ export type InputFrame = HydratedBreadcrumb<'ui.input'>; export type KeyboardEventFrame = HydratedBreadcrumb<'ui.keyDown'>; export type MultiClickFrame = HydratedBreadcrumb<'ui.multiClick'>; export type MutationFrame = HydratedBreadcrumb<'replay.mutations'>; +export type HydrationErrorFrame = Overwrite< + HydratedBreadcrumb<'replay.hydrate-error'>, + { + data: { + description: string; + mutations: { + next: RecordingFrame | null; + prev: RecordingFrame | null; + }; + url?: string; + }; + } +>; export type NavFrame = HydratedBreadcrumb<'navigation'>; export type SlowClickFrame = HydratedBreadcrumb<'ui.slowClickDetected'>; export type DeviceBatteryFrame = HydratedBreadcrumb<'device.battery'>; diff --git a/tests/js/fixtures/replay/replayBreadcrumbFrameData.ts b/tests/js/fixtures/replay/replayBreadcrumbFrameData.ts index 210010c4db49e..b8186b618fc25 100644 --- a/tests/js/fixtures/replay/replayBreadcrumbFrameData.ts +++ b/tests/js/fixtures/replay/replayBreadcrumbFrameData.ts @@ -1,5 +1,5 @@ import {BreadcrumbType} from 'sentry/types/breadcrumbs'; -import {RawBreadcrumbFrame} from 'sentry/utils/replays/types'; +import {RawBreadcrumbFrame, RawHydrationErrorFrame} from 'sentry/utils/replays/types'; type Overwrite = Pick> & U; @@ -108,6 +108,18 @@ export function ReplaySlowClickFrameFixture( }; } +export function ReplayHydrationErrorFrameFixture( + fields: TestableFrame<'replay.hydrate-error'> +): RawHydrationErrorFrame { + return { + category: 'replay.hydrate-error', + message: '', + timestamp: fields.timestamp.getTime() / 1000, + data: fields.data ?? undefined, + type: BreadcrumbType.DEFAULT, + }; +} + export function ReplayMutationFrameFixture( fields: TestableFrame<'replay.mutations'> ): MockFrame<'replay.mutations'> { diff --git a/tests/js/fixtures/replay/replayFrameEvents.ts b/tests/js/fixtures/replay/replayFrameEvents.ts index 45c259610e4a1..2d97b13b03ac7 100644 --- a/tests/js/fixtures/replay/replayFrameEvents.ts +++ b/tests/js/fixtures/replay/replayFrameEvents.ts @@ -18,7 +18,7 @@ type TestableFrameEvent< >; /** - * `BreadcrumbFrameData` has factories to help construct the correct payloads. + * `replayBreadcrumbFrameData.tsx` has factories to help construct the correct payloads. * * ``` * ReplayBreadcrumbFrameEventFixture({ @@ -44,14 +44,13 @@ export function ReplayBreadcrumbFrameEventFixture( } /** - * `SpanFrame()` is a factories to help consturt valid payloads given an operation name. - * `ReplaySpanFrameData.*` contains more factories to build the required inner dataset. + * `replaySpanFrameData.tsx` has factories to help consturt valid payloads given an operation name. * * ``` - * SpanFrameEventFixture({ + * ReplaySpanFrameEventFixture({ * timestamp, * data: { - * payload: ReplaySpanFrameEventFixture({ + * payload: ReplayNavigationFrameFixture({ * data: {...} * }), * }, diff --git a/tests/js/fixtures/replay/rrweb.ts b/tests/js/fixtures/replay/rrweb.ts index a52425ceaac31..c7a0f33d5b765 100644 --- a/tests/js/fixtures/replay/rrweb.ts +++ b/tests/js/fixtures/replay/rrweb.ts @@ -1,9 +1,12 @@ -import type {fullSnapshotEvent, serializedNodeWithId} from 'sentry/utils/replays/types'; +import type {fullSnapshotEvent, incrementalSnapshotEvent, serializedNodeWithId} from 'sentry/utils/replays/types'; import {EventType, NodeType} from 'sentry/utils/replays/types'; interface FullSnapshotEvent extends fullSnapshotEvent { timestamp: number; } +interface IncrementalSnapshotEvent extends incrementalSnapshotEvent { + timestamp: number; +} const nextRRWebId = (function () { let __rrwebID = 0; @@ -68,6 +71,20 @@ export function RRWebFullSnapshotFrameEventFixture({ }; } +export function RRWebIncrementalSnapshotFrameEventFixture({ + timestamp, + data, +}: { + timestamp: Date; + data: incrementalSnapshotEvent['data']; +}): IncrementalSnapshotEvent { + return { + type: EventType.IncrementalSnapshot, + timestamp: timestamp.getTime(), + data, + } +} + export function RRWebDOMFrameFixture({ id, tagName,