123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494 |
- import {EventType, IncrementalSource} from '@sentry-internal/rrweb';
- import {
- ReplayClickEventFixture,
- ReplayConsoleEventFixture,
- ReplayDeadClickEventFixture,
- ReplayMemoryEventFixture,
- ReplayNavigateEventFixture,
- } from 'sentry-fixture/replay/helpers';
- import {ReplayNavFrameFixture} from 'sentry-fixture/replay/replayBreadcrumbFrameData';
- import {
- ReplayBreadcrumbFrameEventFixture,
- ReplayOptionFrameEventFixture,
- ReplayOptionFrameFixture,
- ReplaySpanFrameEventFixture,
- } from 'sentry-fixture/replay/replayFrameEvents';
- import {ReplayRequestFrameFixture} from 'sentry-fixture/replay/replaySpanFrameData';
- import {
- RRWebDOMFrameFixture,
- RRWebFullSnapshotFrameEventFixture,
- } 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';
- describe('ReplayReader', () => {
- const replayRecord = ReplayRecordFixture({});
- it('Should return null if there are missing arguments', () => {
- const missingAttachments = ReplayReader.factory({
- attachments: undefined,
- errors: [],
- replayRecord,
- });
- expect(missingAttachments).toBeNull();
- const missingErrors = ReplayReader.factory({
- attachments: [],
- errors: undefined,
- replayRecord,
- });
- expect(missingErrors).toBeNull();
- const missingRecord = ReplayReader.factory({
- attachments: [],
- errors: [],
- replayRecord: undefined,
- });
- expect(missingRecord).toBeNull();
- });
- it('should calculate started_at/finished_at/duration based on first/last events', () => {
- const minuteZero = new Date('2023-12-25T00:00:00');
- const minuteTen = new Date('2023-12-25T00:10:00');
- const replay = ReplayReader.factory({
- attachments: [
- ReplayConsoleEventFixture({timestamp: minuteZero}),
- ReplayConsoleEventFixture({timestamp: minuteTen}),
- ],
- errors: [],
- replayRecord: ReplayRecordFixture({
- started_at: new Date('2023-12-25T00:01:00'),
- finished_at: new Date('2023-12-25T00:09:00'),
- duration: undefined, // will be calculated
- }),
- });
- const expectedDuration = 10 * 60 * 1000; // 10 minutes, in ms
- expect(replay?.getReplay().started_at).toEqual(minuteZero);
- expect(replay?.getReplay().finished_at).toEqual(minuteTen);
- expect(replay?.getReplay().duration.asMilliseconds()).toEqual(expectedDuration);
- expect(replay?.getDurationMs()).toEqual(expectedDuration);
- });
- it('should make the replayRecord available through a getter method', () => {
- const replay = ReplayReader.factory({
- attachments: [],
- errors: [],
- replayRecord,
- });
- expect(replay?.getReplay()).toEqual(replayRecord);
- });
- describe('attachment splitting', () => {
- const timestamp = new Date('2023-12-25T00:02:00');
- const secondTimestamp = new Date('2023-12-25T00:04:00');
- const thirdTimestamp = new Date('2023-12-25T00:05:00');
- const optionsFrame = ReplayOptionFrameFixture({});
- const optionsEvent = ReplayOptionFrameEventFixture({
- timestamp,
- data: {payload: optionsFrame},
- });
- const firstDiv = RRWebFullSnapshotFrameEventFixture({timestamp});
- const secondDiv = RRWebFullSnapshotFrameEventFixture({timestamp});
- const clickEvent = ReplayClickEventFixture({timestamp});
- const secondClickEvent = ReplayClickEventFixture({timestamp: secondTimestamp});
- const thirdClickEvent = ReplayClickEventFixture({timestamp: thirdTimestamp});
- const deadClickEvent = ReplayDeadClickEventFixture({timestamp});
- const firstMemory = ReplayMemoryEventFixture({
- startTimestamp: timestamp,
- endTimestamp: timestamp,
- });
- const secondMemory = ReplayMemoryEventFixture({
- startTimestamp: timestamp,
- endTimestamp: timestamp,
- });
- const navigationEvent = ReplayNavigateEventFixture({
- startTimestamp: new Date('2023-12-25T00:03:00'),
- endTimestamp: new Date('2023-12-25T00:03:30'),
- });
- const navCrumb = ReplayBreadcrumbFrameEventFixture({
- timestamp: new Date('2023-12-25T00:03:00'),
- data: {
- payload: ReplayNavFrameFixture({
- timestamp: new Date('2023-12-25T00:03:00'),
- }),
- },
- });
- const consoleEvent = ReplayConsoleEventFixture({timestamp});
- const customEvent = ReplayBreadcrumbFrameEventFixture({
- timestamp: new Date('2023-12-25T00:02:30'),
- data: {
- payload: {
- category: 'redux.action',
- data: {
- action: 'save.click',
- },
- message: '',
- timestamp: new Date('2023-12-25T00:02:30').getTime() / 1000,
- type: BreadcrumbType.DEFAULT,
- },
- },
- });
- const attachments = [
- clickEvent,
- secondClickEvent,
- thirdClickEvent,
- consoleEvent,
- firstDiv,
- firstMemory,
- navigationEvent,
- navCrumb,
- optionsEvent,
- secondDiv,
- secondMemory,
- customEvent,
- deadClickEvent,
- ];
- it.each([
- {
- method: 'getRRWebFrames',
- expected: [
- {
- type: EventType.Custom,
- timestamp: expect.any(Number),
- data: {tag: 'replay.start', payload: {}},
- },
- firstDiv,
- secondDiv,
- {
- type: EventType.Custom,
- timestamp: expect.any(Number),
- data: {tag: 'replay.end', payload: {}},
- },
- ],
- },
- {
- method: 'getConsoleFrames',
- expected: [
- expect.objectContaining({category: 'console'}),
- expect.objectContaining({category: 'redux.action'}),
- ],
- },
- {
- method: 'getNetworkFrames',
- expected: [expect.objectContaining({op: 'navigation.navigate'})],
- },
- {
- method: 'getDOMFrames',
- expected: [
- expect.objectContaining({category: 'ui.slowClickDetected'}),
- expect.objectContaining({category: 'ui.click'}),
- expect.objectContaining({category: 'ui.click'}),
- ],
- },
- {
- method: 'getMemoryFrames',
- expected: [
- expect.objectContaining({op: 'memory'}),
- expect.objectContaining({op: 'memory'}),
- ],
- },
- {
- method: 'getChapterFrames',
- expected: [
- expect.objectContaining({category: 'replay.init'}),
- expect.objectContaining({category: 'ui.slowClickDetected'}),
- expect.objectContaining({category: 'navigation'}),
- expect.objectContaining({op: 'navigation.navigate'}),
- expect.objectContaining({category: 'ui.click'}),
- expect.objectContaining({category: 'ui.click'}),
- ],
- },
- {
- method: 'getSDKOptions',
- expected: optionsFrame,
- },
- ])('Calling $method will filter frames', ({method, expected}) => {
- const replay = ReplayReader.factory({
- attachments,
- errors: [],
- replayRecord,
- });
- const exec = replay?.[method];
- expect(exec()).toStrictEqual(expected);
- });
- });
- it('shoud return the SDK config if there is a RecordingOptions event found', () => {
- const timestamp = new Date();
- const optionsFrame = ReplayOptionFrameFixture({});
- const replay = ReplayReader.factory({
- attachments: [
- ReplayOptionFrameEventFixture({
- timestamp,
- data: {payload: optionsFrame},
- }),
- ],
- errors: [],
- replayRecord,
- });
- expect(replay?.getSDKOptions()).toBe(optionsFrame);
- });
- describe('isNetworkDetailsSetup', () => {
- it('should have isNetworkDetailsSetup=true if sdkConfig says so', () => {
- const timestamp = new Date();
- const replay = ReplayReader.factory({
- attachments: [
- ReplayOptionFrameEventFixture({
- timestamp,
- data: {
- payload: ReplayOptionFrameFixture({
- networkDetailHasUrls: true,
- }),
- },
- }),
- ],
- errors: [],
- replayRecord,
- });
- expect(replay?.isNetworkDetailsSetup()).toBeTruthy();
- });
- it.each([
- {
- data: {
- method: 'GET',
- request: {headers: {accept: 'application/json'}},
- },
- expected: true,
- },
- {
- data: {
- method: 'GET',
- },
- expected: false,
- },
- ])('should have isNetworkDetailsSetup=$expected', ({data, expected}) => {
- const startTimestamp = new Date();
- const endTimestamp = new Date();
- const replay = ReplayReader.factory({
- attachments: [
- ReplaySpanFrameEventFixture({
- timestamp: startTimestamp,
- data: {
- payload: ReplayRequestFrameFixture({
- op: 'resource.fetch',
- startTimestamp,
- endTimestamp,
- description: '/api/0/issues/',
- data,
- }),
- },
- }),
- ],
- errors: [],
- replayRecord,
- });
- expect(replay?.isNetworkDetailsSetup()).toBe(expected);
- });
- });
- it('detects canvas element from full snapshot', () => {
- const timestamp = new Date('2023-12-25T00:02:00');
- const firstDiv = RRWebFullSnapshotFrameEventFixture({
- timestamp,
- childNodes: [
- RRWebDOMFrameFixture({
- tagName: 'div',
- childNodes: [
- RRWebDOMFrameFixture({
- tagName: 'canvas',
- }),
- ],
- }),
- ],
- });
- const attachments = [firstDiv];
- const replay = ReplayReader.factory({
- attachments,
- errors: [],
- replayRecord,
- });
- expect(replay?.hasCanvasElementInReplay()).toBe(true);
- });
- it('detects canvas element from dom mutations', () => {
- 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 replay = ReplayReader.factory({
- attachments,
- errors: [],
- replayRecord,
- });
- expect(replay?.hasCanvasElementInReplay()).toBe(true);
- });
- describe('clip window', () => {
- const replayStartedAt = new Date('2024-01-01T00:02:00');
- const replayFinishedAt = new Date('2024-01-01T00:04:00');
- const clipStartTimestamp = new Date('2024-01-01T00:03:00');
- const clipEndTimestamp = new Date('2024-01-01T00:03:10');
- const rrwebFrame1 = RRWebFullSnapshotFrameEventFixture({
- timestamp: new Date('2024-01-01T00:02:30'),
- });
- const rrwebFrame2 = RRWebFullSnapshotFrameEventFixture({
- timestamp: new Date('2024-01-01T00:03:09'),
- });
- const rrwebFrame3 = RRWebFullSnapshotFrameEventFixture({
- timestamp: new Date('2024-01-01T00:03:30'),
- });
- const breadcrumbAttachment1 = ReplayBreadcrumbFrameEventFixture({
- timestamp: new Date('2024-01-01T00:02:30'),
- data: {
- payload: ReplayNavFrameFixture({
- timestamp: new Date('2024-01-01T00:02:30'),
- }),
- },
- });
- const breadcrumbAttachment2 = ReplayBreadcrumbFrameEventFixture({
- timestamp: new Date('2024-01-01T00:03:05'),
- data: {
- payload: ReplayNavFrameFixture({
- timestamp: new Date('2024-01-01T00:03:05'),
- }),
- },
- });
- const breadcrumbAttachment3 = ReplayBreadcrumbFrameEventFixture({
- timestamp: new Date('2024-01-01T00:03:30'),
- data: {
- payload: ReplayNavFrameFixture({
- timestamp: new Date('2024-01-01T00:03:30'),
- }),
- },
- });
- const error1 = ReplayErrorFixture({
- id: '1',
- issue: '100',
- timestamp: '2024-01-01T00:02:30',
- });
- const error2 = ReplayErrorFixture({
- id: '2',
- issue: '200',
- timestamp: '2024-01-01T00:03:06',
- });
- const error3 = ReplayErrorFixture({
- id: '1',
- issue: '100',
- timestamp: '2024-01-01T00:03:30',
- });
- const replay = ReplayReader.factory({
- attachments: [
- rrwebFrame1,
- rrwebFrame2,
- rrwebFrame3,
- breadcrumbAttachment1,
- breadcrumbAttachment2,
- breadcrumbAttachment3,
- ],
- errors: [error1, error2, error3],
- replayRecord: ReplayRecordFixture({
- started_at: replayStartedAt,
- finished_at: replayFinishedAt,
- }),
- clipWindow: {
- startTimestampMs: clipStartTimestamp.getTime(),
- endTimestampMs: clipEndTimestamp.getTime(),
- },
- });
- it('should adjust the end time and duration for the clip window', () => {
- // Duration should be between the clip start time and end time
- expect(replay?.getDurationMs()).toEqual(10_000);
- // Start offset should be set
- expect(replay?.getStartOffsetMs()).toEqual(
- clipStartTimestamp.getTime() - replayStartedAt.getTime()
- );
- expect(replay?.getStartTimestampMs()).toEqual(clipStartTimestamp.getTime());
- });
- it('should trim rrweb frames from the end but not the beginning', () => {
- expect(replay?.getRRWebFrames()).toEqual([
- expect.objectContaining({
- type: EventType.Custom,
- data: {tag: 'replay.start', payload: {}},
- }),
- expect.objectContaining({
- type: EventType.FullSnapshot,
- timestamp: rrwebFrame1.timestamp,
- }),
- expect.objectContaining({
- type: EventType.FullSnapshot,
- timestamp: rrwebFrame2.timestamp,
- }),
- expect.objectContaining({
- type: EventType.Custom,
- data: {tag: 'replay.clip_end', payload: {}},
- timestamp: clipEndTimestamp.getTime(),
- }),
- // rrwebFrame3 should not be returned
- ]);
- });
- it('should only return chapter frames within window and shift their clipOffsets', () => {
- expect(replay?.getChapterFrames()).toEqual([
- // Only breadcrumb2 and error2 should be included
- expect.objectContaining({
- category: 'navigation',
- timestampMs: breadcrumbAttachment2.timestamp,
- // offset is relative to the start of the clip window
- offsetMs: 5_000,
- }),
- expect.objectContaining({
- category: 'issue',
- timestampMs: new Date(error2.timestamp).getTime(),
- offsetMs: 6_000,
- }),
- ]);
- });
- });
- });
|