import {EventType} from '@sentry-internal/rrweb'; import {BreadcrumbType} from 'sentry/types/breadcrumbs'; import ReplayReader from 'sentry/utils/replays/replayReader'; describe('ReplayReader', () => { const replayRecord = TestStubs.ReplayRecord({}); 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: [ TestStubs.Replay.ConsoleEvent({timestamp: minuteZero}), TestStubs.Replay.ConsoleEvent({timestamp: minuteTen}), ], errors: [], replayRecord: TestStubs.ReplayRecord({ 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 optionsFrame = TestStubs.Replay.OptionFrame({}); const optionsEvent = TestStubs.Replay.OptionFrameEvent({ timestamp, data: {payload: optionsFrame}, }); const firstDiv = TestStubs.Replay.RRWebFullSnapshotFrameEvent({timestamp}); const secondDiv = TestStubs.Replay.RRWebFullSnapshotFrameEvent({timestamp}); const clickEvent = TestStubs.Replay.ClickEvent({timestamp}); const firstMemory = TestStubs.Replay.MemoryEvent({ startTimestamp: timestamp, endTimestamp: timestamp, }); const secondMemory = TestStubs.Replay.MemoryEvent({ startTimestamp: timestamp, endTimestamp: timestamp, }); const navigationEvent = TestStubs.Replay.NavigateEvent({ startTimestamp: new Date('2023-12-25T00:03:00'), endTimestamp: new Date('2023-12-25T00:03:30'), }); const consoleEvent = TestStubs.Replay.ConsoleEvent({timestamp}); const customEvent = TestStubs.Replay.BreadcrumbFrameEvent({ 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, consoleEvent, firstDiv, firstMemory, navigationEvent, optionsEvent, secondDiv, secondMemory, customEvent, ]; 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'})], }, { method: 'getNetworkFrames', expected: [expect.objectContaining({op: 'navigation.navigate'})], }, { method: 'getDOMFrames', expected: [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.click'}), expect.objectContaining({category: 'redux.action'}), expect.objectContaining({op: 'navigation.navigate'}), ], }, { method: 'getTimelineFrames', expected: [ expect.objectContaining({category: 'replay.init'}), expect.objectContaining({category: 'ui.click'}), expect.objectContaining({op: 'navigation.navigate'}), ], }, { 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 = TestStubs.Replay.OptionFrame({}); const replay = ReplayReader.factory({ attachments: [ TestStubs.Replay.OptionFrameEvent({ timestamp, data: {payload: optionsFrame}, }), ], errors: [], replayRecord, }); expect(replay?.sdkConfig()).toBe(optionsFrame); }); describe('isNetworkDetailsSetup', () => { it('should have isNetworkDetailsSetup=true if sdkConfig says so', () => { const timestamp = new Date(); const replay = ReplayReader.factory({ attachments: [ TestStubs.Replay.OptionFrameEvent({ timestamp, data: { payload: TestStubs.Replay.OptionFrame({ 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: [ TestStubs.Replay.SpanFrameEvent({ timestamp: startTimestamp, data: { payload: TestStubs.Replay.RequestFrame({ op: 'resource.fetch', startTimestamp, endTimestamp, description: '/api/0/issues/', data, }), }, }), ], errors: [], replayRecord, }); expect(replay?.isNetworkDetailsSetup()).toBe(expected); }); }); });