From bedb0fe693e1f0f1b9738737d9c0edcbe8d6724a Mon Sep 17 00:00:00 2001 From: Billy Vong Date: Tue, 2 May 2023 14:13:19 +0200 Subject: [PATCH 01/10] feat(replay): Add event to capture options on checkouts Add a custom event that captures configuration options on checkout + segment 0. --- .../src/eventBuffer/EventBufferArray.ts | 7 ++- .../EventBufferCompressionWorker.ts | 7 ++- .../src/eventBuffer/EventBufferProxy.ts | 7 ++- packages/replay/src/integration.ts | 2 + packages/replay/src/types.ts | 17 +++++++ packages/replay/src/types/rrweb.ts | 2 +- .../replay/src/util/handleRecordingEmit.ts | 44 +++++++++++++++++++ packages/replay/src/util/sendReplayRequest.ts | 15 +------ .../test/integration/errorSampleRate.test.ts | 6 --- .../replay/test/integration/session.test.ts | 3 +- .../unit/util/handleRecordingEmit.test.ts | 29 ++++++++---- .../replay/test/utils/setupReplayContainer.ts | 44 ++++++++++++++----- 12 files changed, 138 insertions(+), 45 deletions(-) diff --git a/packages/replay/src/eventBuffer/EventBufferArray.ts b/packages/replay/src/eventBuffer/EventBufferArray.ts index 42ce00aabbc6..fddd2cdaa4da 100644 --- a/packages/replay/src/eventBuffer/EventBufferArray.ts +++ b/packages/replay/src/eventBuffer/EventBufferArray.ts @@ -1,4 +1,4 @@ -import type { AddEventResult, EventBuffer, RecordingEvent } from '../types'; +import type { AddEventResult, EventBuffer, EventBufferType, RecordingEvent } from '../types'; import { timestampToMs } from '../util/timestampToMs'; /** @@ -18,6 +18,11 @@ export class EventBufferArray implements EventBuffer { return this.events.length > 0; } + /** @inheritdoc */ + public get type(): EventBufferType { + return 'default'; + } + /** @inheritdoc */ public destroy(): void { this.events = []; diff --git a/packages/replay/src/eventBuffer/EventBufferCompressionWorker.ts b/packages/replay/src/eventBuffer/EventBufferCompressionWorker.ts index 57ad449dac55..45696ea46bc9 100644 --- a/packages/replay/src/eventBuffer/EventBufferCompressionWorker.ts +++ b/packages/replay/src/eventBuffer/EventBufferCompressionWorker.ts @@ -1,6 +1,6 @@ import type { ReplayRecordingData } from '@sentry/types'; -import type { AddEventResult, EventBuffer, RecordingEvent } from '../types'; +import type { AddEventResult, EventBuffer, EventBufferType, RecordingEvent } from '../types'; import { timestampToMs } from '../util/timestampToMs'; import { WorkerHandler } from './WorkerHandler'; @@ -22,6 +22,11 @@ export class EventBufferCompressionWorker implements EventBuffer { return !!this._earliestTimestamp; } + /** @inheritdoc */ + public get type(): EventBufferType { + return 'worker'; + } + /** * Ensure the worker is ready (or not). * This will either resolve when the worker is ready, or reject if an error occured. diff --git a/packages/replay/src/eventBuffer/EventBufferProxy.ts b/packages/replay/src/eventBuffer/EventBufferProxy.ts index 972fc9d58911..df50634bf3eb 100644 --- a/packages/replay/src/eventBuffer/EventBufferProxy.ts +++ b/packages/replay/src/eventBuffer/EventBufferProxy.ts @@ -1,7 +1,7 @@ import type { ReplayRecordingData } from '@sentry/types'; import { logger } from '@sentry/utils'; -import type { AddEventResult, EventBuffer, RecordingEvent } from '../types'; +import type { AddEventResult, EventBuffer, EventBufferType, RecordingEvent } from '../types'; import { EventBufferArray } from './EventBufferArray'; import { EventBufferCompressionWorker } from './EventBufferCompressionWorker'; @@ -24,6 +24,11 @@ export class EventBufferProxy implements EventBuffer { this._ensureWorkerIsLoadedPromise = this._ensureWorkerIsLoaded(); } + /** @inheritdoc */ + public get type(): EventBufferType { + return this._used.type; + } + /** @inheritDoc */ public get hasEvents(): boolean { return this._used.hasEvents; diff --git a/packages/replay/src/integration.ts b/packages/replay/src/integration.ts index 81279947b969..d5d4115fffc7 100644 --- a/packages/replay/src/integration.ts +++ b/packages/replay/src/integration.ts @@ -123,6 +123,8 @@ export class Replay implements Integration { errorSampleRate, useCompression, blockAllMedia, + maskAllInputs, + maskAllText, networkDetailAllowUrls, networkCaptureBodies, networkRequestHeaders: _getMergedNetworkHeaders(networkRequestHeaders), diff --git a/packages/replay/src/types.ts b/packages/replay/src/types.ts index 0d653f3a7817..56091f6e2a26 100644 --- a/packages/replay/src/types.ts +++ b/packages/replay/src/types.ts @@ -257,6 +257,16 @@ export interface ReplayPluginOptions extends ReplayNetworkOptions { */ blockAllMedia: boolean; + /** + * Mask all inputs in recordings + */ + maskAllInputs: boolean; + + /** + * Mask all text in recordings + */ + maskAllText: boolean; + /** * _experiments allows users to enable experimental or internal features. * We don't consider such features as part of the public API and hence we don't guarantee semver for them. @@ -435,12 +445,19 @@ export interface Session { shouldRefresh: boolean; } +export type EventBufferType = 'default' | 'worker'; + export interface EventBuffer { /** * If any events have been added to the buffer. */ readonly hasEvents: boolean; + /** + * The buffer type + */ + readonly type: EventBufferType; + /** * Destroy the event buffer. */ diff --git a/packages/replay/src/types/rrweb.ts b/packages/replay/src/types/rrweb.ts index 7a794face4cb..7f2dfec78110 100644 --- a/packages/replay/src/types/rrweb.ts +++ b/packages/replay/src/types/rrweb.ts @@ -3,7 +3,7 @@ type blockClass = string | RegExp; type maskTextClass = string | RegExp; -enum EventType { +export enum EventType { DomContentLoaded = 0, Load = 1, FullSnapshot = 2, diff --git a/packages/replay/src/util/handleRecordingEmit.ts b/packages/replay/src/util/handleRecordingEmit.ts index edf9aa4946f1..c8c3b5f63aa2 100644 --- a/packages/replay/src/util/handleRecordingEmit.ts +++ b/packages/replay/src/util/handleRecordingEmit.ts @@ -2,6 +2,7 @@ import { logger } from '@sentry/utils'; import { saveSession } from '../session/saveSession'; import type { RecordingEvent, ReplayContainer } from '../types'; +import { EventType } from '../types/rrweb'; import { addEvent } from './addEvent'; type RecordingEmitCallback = (event: RecordingEvent, isCheckout?: boolean) => void; @@ -48,6 +49,14 @@ export function getHandleRecordingEmit(replay: ReplayContainer): RecordingEmitCa return false; } + // Additionally, create a meta event that will capture certain SDK settings. + // In order to handle buffer mode, this needs to either be done when we + // receive checkout events or at flush time. + // + // `isCheckout` is always true, but want to be explicit that it should + // only be added for checkouts + void addSettingsEvent(replay, isCheckout); + // If there is a previousSessionId after a full snapshot occurs, then // the replay session was started due to session expiration. The new session // is started before triggering a new checkout and contains the id @@ -84,3 +93,38 @@ export function getHandleRecordingEmit(replay: ReplayContainer): RecordingEmitCa }); }; } + +/** + * Add an event to the event buffer. + * `isCheckout` is true if this is either the very first event, or an event triggered by `checkoutEveryNms`. + */ +export async function addSettingsEvent(replay: ReplayContainer, isCheckout?: boolean): Promise { + // Only need to add this event when sending the first segment + if (!isCheckout || !replay.session || replay.session.segmentId !== 0) { + return null; + } + + const options = replay.getOptions(); + const event = { + type: EventType.Custom, + timestamp: new Date().getTime(), + data: { + tag: 'options', + payload: { + sessionSampleRate: options.sessionSampleRate, + errorSampleRate: options.errorSampleRate, + useCompressionOption: options.useCompression, + blockAllMedia: options.blockAllMedia, + maskAllText: options.maskAllText, + maskAllInputs: options.maskAllInputs, + useCompression: replay.eventBuffer && replay.eventBuffer.type === 'worker', + networkDetailHasUrls: options.networkDetailAllowUrls.length > 0, + networkCaptureBodies: options.networkCaptureBodies, + networkRequestHeaders: options.networkRequestHeaders.length > 0, + networkResponseHeaders: options.networkResponseHeaders.length > 0, + }, + }, + }; + + return addEvent(replay, event, true); +} diff --git a/packages/replay/src/util/sendReplayRequest.ts b/packages/replay/src/util/sendReplayRequest.ts index 009fdc2067bf..839df6ae0d39 100644 --- a/packages/replay/src/util/sendReplayRequest.ts +++ b/packages/replay/src/util/sendReplayRequest.ts @@ -18,7 +18,6 @@ export async function sendReplayRequest({ eventContext, timestamp, session, - options, }: SendReplayData): Promise { const preparedRecordingData = prepareRecordingData({ recordingData, @@ -60,15 +59,6 @@ export async function sendReplayRequest({ return; } - replayEvent.contexts = { - ...replayEvent.contexts, - replay: { - ...(replayEvent.contexts && replayEvent.contexts.replay), - session_sample_rate: options.sessionSampleRate, - error_sample_rate: options.errorSampleRate, - }, - }; - /* For reference, the fully built event looks something like this: { @@ -99,10 +89,7 @@ export async function sendReplayRequest({ }, "sdkProcessingMetadata": {}, "contexts": { - "replay": { - "session_sample_rate": 1, - "error_sample_rate": 0, - }, + "replay": { }, }, } */ diff --git a/packages/replay/test/integration/errorSampleRate.test.ts b/packages/replay/test/integration/errorSampleRate.test.ts index 16962bf5b2f8..c588d603ff4f 100644 --- a/packages/replay/test/integration/errorSampleRate.test.ts +++ b/packages/replay/test/integration/errorSampleRate.test.ts @@ -183,12 +183,6 @@ describe('Integration | errorSampleRate', () => { recordingPayloadHeader: { segment_id: 0 }, replayEventPayload: expect.objectContaining({ replay_type: 'buffer', - contexts: { - replay: { - error_sample_rate: 1, - session_sample_rate: 0, - }, - }, }), recordingData: JSON.stringify([ { data: { isCheckout: true }, timestamp: BASE_TIMESTAMP, type: 2 }, diff --git a/packages/replay/test/integration/session.test.ts b/packages/replay/test/integration/session.test.ts index 6b0f942af616..d22e286f391a 100644 --- a/packages/replay/test/integration/session.test.ts +++ b/packages/replay/test/integration/session.test.ts @@ -17,6 +17,7 @@ import { createPerformanceSpans } from '../../src/util/createPerformanceSpans'; import { BASE_TIMESTAMP } from '../index'; import type { RecordMock } from '../mocks/mockRrweb'; import { resetSdkMock } from '../mocks/resetSdkMock'; +import { DEFAULT_OPTIONS_EVENT_PAYLOAD } from '../utils/setupReplayContainer'; import { useFakeTimers } from '../utils/use-fake-timers'; useFakeTimers(); @@ -388,7 +389,7 @@ describe('Integration | session', () => { expect(replay).toHaveLastSentReplay({ recordingPayloadHeader: { segment_id: 0 }, recordingData: JSON.stringify([ - { data: { isCheckout: true }, timestamp: newTimestamp, type: 2 }, + { type: 5, timestamp: newTimestamp, data: { tag: 'options', payload: DEFAULT_OPTIONS_EVENT_PAYLOAD } }, { type: 5, timestamp: newTimestamp, diff --git a/packages/replay/test/unit/util/handleRecordingEmit.test.ts b/packages/replay/test/unit/util/handleRecordingEmit.test.ts index 4762b875ce5b..a0760863d91a 100644 --- a/packages/replay/test/unit/util/handleRecordingEmit.test.ts +++ b/packages/replay/test/unit/util/handleRecordingEmit.test.ts @@ -3,11 +3,20 @@ import { EventType } from '@sentry-internal/rrweb'; import { BASE_TIMESTAMP } from '../..'; import * as SentryAddEvent from '../../../src/util/addEvent'; import { getHandleRecordingEmit } from '../../../src/util/handleRecordingEmit'; -import { setupReplayContainer } from '../../utils/setupReplayContainer'; +import { DEFAULT_OPTIONS_EVENT_PAYLOAD, setupReplayContainer } from '../../utils/setupReplayContainer'; import { useFakeTimers } from '../../utils/use-fake-timers'; useFakeTimers(); +const optionsEvent = { + type: 5, + data: { + tag: 'options', + payload: DEFAULT_OPTIONS_EVENT_PAYLOAD, + }, + timestamp: BASE_TIMESTAMP, +}; + describe('Unit | util | handleRecordingEmit', () => { let addEventMock: jest.SpyInstance; @@ -43,13 +52,14 @@ describe('Unit | util | handleRecordingEmit', () => { handler(event); await new Promise(process.nextTick); - expect(addEventMock).toBeCalledTimes(1); - expect(addEventMock).toHaveBeenLastCalledWith(replay, event, true); + expect(addEventMock).toBeCalledTimes(2); + expect(addEventMock).toHaveBeenNthCalledWith(1, replay, event, true); + expect(addEventMock).toHaveBeenLastCalledWith(replay, optionsEvent, true); handler(event); await new Promise(process.nextTick); - expect(addEventMock).toBeCalledTimes(2); + expect(addEventMock).toBeCalledTimes(3); expect(addEventMock).toHaveBeenLastCalledWith(replay, event, false); }); @@ -74,13 +84,16 @@ describe('Unit | util | handleRecordingEmit', () => { handler(event, true); await new Promise(process.nextTick); - expect(addEventMock).toBeCalledTimes(1); - expect(addEventMock).toHaveBeenLastCalledWith(replay, event, true); + // Called twice, once for event and once for settings on checkout only + expect(addEventMock).toBeCalledTimes(2); + expect(addEventMock).toHaveBeenNthCalledWith(1, replay, event, true); + expect(addEventMock).toHaveBeenLastCalledWith(replay, optionsEvent, true); handler(event, true); await new Promise(process.nextTick); - expect(addEventMock).toBeCalledTimes(2); - expect(addEventMock).toHaveBeenLastCalledWith(replay, event, true); + expect(addEventMock).toBeCalledTimes(4); + expect(addEventMock).toHaveBeenNthCalledWith(3, replay, event, true); + expect(addEventMock).toHaveBeenLastCalledWith(replay, { ...optionsEvent, timestamp: BASE_TIMESTAMP + 20 }, true); }); }); diff --git a/packages/replay/test/utils/setupReplayContainer.ts b/packages/replay/test/utils/setupReplayContainer.ts index cf4812da5f2f..83ced117c464 100644 --- a/packages/replay/test/utils/setupReplayContainer.ts +++ b/packages/replay/test/utils/setupReplayContainer.ts @@ -3,24 +3,30 @@ import { ReplayContainer } from '../../src/replay'; import { clearSession } from '../../src/session/clearSession'; import type { RecordingOptions, ReplayPluginOptions } from '../../src/types'; +const DEFAULT_OPTIONS = { + flushMinDelay: 100, + flushMaxDelay: 100, + stickySession: false, + sessionSampleRate: 0, + errorSampleRate: 1, + useCompression: false, + blockAllMedia: true, + networkDetailAllowUrls: [], + networkCaptureBodies: true, + networkRequestHeaders: [], + networkResponseHeaders: [], + _experiments: {}, +}; + export function setupReplayContainer({ options, recordingOptions, }: { options?: Partial; recordingOptions?: Partial } = {}): ReplayContainer { const replay = new ReplayContainer({ options: { - flushMinDelay: 100, - flushMaxDelay: 100, - stickySession: false, - sessionSampleRate: 0, - errorSampleRate: 1, - useCompression: false, - blockAllMedia: true, - networkDetailAllowUrls: [], - networkCaptureBodies: true, - networkRequestHeaders: [], - networkResponseHeaders: [], - _experiments: {}, + ...DEFAULT_OPTIONS, + maskAllInputs: !!recordingOptions?.maskAllInputs, + maskAllText: !!recordingOptions?.maskAllText, ...options, }, recordingOptions: { @@ -39,3 +45,17 @@ export function setupReplayContainer({ return replay; } + +export const DEFAULT_OPTIONS_EVENT_PAYLOAD = { + sessionSampleRate: DEFAULT_OPTIONS.sessionSampleRate, + errorSampleRate: DEFAULT_OPTIONS.errorSampleRate, + useCompressionOption: false, + blockAllMedia: DEFAULT_OPTIONS.blockAllMedia, + maskAllText: false, + maskAllInputs: false, + useCompression: DEFAULT_OPTIONS.useCompression, + networkDetailHasUrls: DEFAULT_OPTIONS.networkDetailAllowUrls.length > 0, + networkCaptureBodies: DEFAULT_OPTIONS.networkCaptureBodies, + networkRequestHeaders: DEFAULT_OPTIONS.networkRequestHeaders.length > 0, + networkResponseHeaders: DEFAULT_OPTIONS.networkResponseHeaders.length > 0, +}; From e2b692c1b223a791824558f136860272c52db8d9 Mon Sep 17 00:00:00 2001 From: Billy Vong Date: Tue, 2 May 2023 22:47:02 +0200 Subject: [PATCH 02/10] default -> sync --- packages/replay/src/eventBuffer/EventBufferArray.ts | 2 +- packages/replay/src/types.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/replay/src/eventBuffer/EventBufferArray.ts b/packages/replay/src/eventBuffer/EventBufferArray.ts index fddd2cdaa4da..eaebd1b174e7 100644 --- a/packages/replay/src/eventBuffer/EventBufferArray.ts +++ b/packages/replay/src/eventBuffer/EventBufferArray.ts @@ -20,7 +20,7 @@ export class EventBufferArray implements EventBuffer { /** @inheritdoc */ public get type(): EventBufferType { - return 'default'; + return 'sync'; } /** @inheritdoc */ diff --git a/packages/replay/src/types.ts b/packages/replay/src/types.ts index 56091f6e2a26..aef61f57a07a 100644 --- a/packages/replay/src/types.ts +++ b/packages/replay/src/types.ts @@ -445,7 +445,7 @@ export interface Session { shouldRefresh: boolean; } -export type EventBufferType = 'default' | 'worker'; +export type EventBufferType = 'sync' | 'worker'; export interface EventBuffer { /** From fc9a8decd1f05c156b3a347980da9f2456f87580 Mon Sep 17 00:00:00 2001 From: Billy Vong Date: Thu, 4 May 2023 20:03:30 -0400 Subject: [PATCH 03/10] update tests --- .../replay/src/util/handleRecordingEmit.ts | 30 +++++++++------ .../test/integration/errorSampleRate.test.ts | 38 ++++++++----------- .../replay/test/integration/events.test.ts | 6 --- .../replay/test/integration/session.test.ts | 8 +++- packages/replay/test/integration/stop.test.ts | 5 +++ .../unit/util/handleRecordingEmit.test.ts | 20 ++++------ 6 files changed, 54 insertions(+), 53 deletions(-) diff --git a/packages/replay/src/util/handleRecordingEmit.ts b/packages/replay/src/util/handleRecordingEmit.ts index c8c3b5f63aa2..545203c7940c 100644 --- a/packages/replay/src/util/handleRecordingEmit.ts +++ b/packages/replay/src/util/handleRecordingEmit.ts @@ -41,7 +41,7 @@ export function getHandleRecordingEmit(replay: ReplayContainer): RecordingEmitCa // We need to clear existing events on a checkout, otherwise they are // incremental event updates and should be appended - void addEvent(replay, event, isCheckout); + addEvent(replay, event, isCheckout); // Different behavior for full snapshots (type=2), ignore other event types // See https://github.com/rrweb-io/rrweb/blob/d8f9290ca496712aa1e7d472549480c4e7876594/packages/rrweb/src/types.ts#L16 @@ -94,18 +94,13 @@ export function getHandleRecordingEmit(replay: ReplayContainer): RecordingEmitCa }; } -/** - * Add an event to the event buffer. - * `isCheckout` is true if this is either the very first event, or an event triggered by `checkoutEveryNms`. - */ -export async function addSettingsEvent(replay: ReplayContainer, isCheckout?: boolean): Promise { - // Only need to add this event when sending the first segment - if (!isCheckout || !replay.session || replay.session.segmentId !== 0) { - return null; - } +/** +* Exported for tests +*/ +export function createOptionsEvent(replay: ReplayContainer): RecordingEvent { const options = replay.getOptions(); - const event = { + return { type: EventType.Custom, timestamp: new Date().getTime(), data: { @@ -125,6 +120,17 @@ export async function addSettingsEvent(replay: ReplayContainer, isCheckout?: boo }, }, }; +} + +/** + * Add a "meta" event that contains a simplified view on current configuration +* options. This should only be included on the first segment of a recording. + */ +function addSettingsEvent(replay: ReplayContainer, isCheckout?: boolean): Promise { + // Only need to add this event when sending the first segment + if (!isCheckout || !replay.session || replay.session.segmentId !== 0) { + return Promise.resolve(null); + } - return addEvent(replay, event, true); + return addEvent(replay, createOptionsEvent(replay), false); } diff --git a/packages/replay/test/integration/errorSampleRate.test.ts b/packages/replay/test/integration/errorSampleRate.test.ts index c588d603ff4f..6e768f5a6044 100644 --- a/packages/replay/test/integration/errorSampleRate.test.ts +++ b/packages/replay/test/integration/errorSampleRate.test.ts @@ -11,6 +11,7 @@ import { import type { ReplayContainer } from '../../src/replay'; import { clearSession } from '../../src/session/clearSession'; import { addEvent } from '../../src/util/addEvent'; +import { createOptionsEvent } from '../../src/util/handleRecordingEmit'; import { PerformanceEntryResource } from '../fixtures/performanceEntry/resource'; import type { RecordMock } from '../index'; import { BASE_TIMESTAMP } from '../index'; @@ -50,6 +51,7 @@ describe('Integration | errorSampleRate', () => { it('uploads a replay when `Sentry.captureException` is called and continues recording', async () => { const TEST_EVENT = { data: {}, timestamp: BASE_TIMESTAMP, type: 3 }; mockRecord._emitter(TEST_EVENT); + const optionsEvent = createOptionsEvent(replay) expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled(); expect(replay).not.toHaveLastSentReplay(); @@ -72,15 +74,10 @@ describe('Integration | errorSampleRate', () => { recordingPayloadHeader: { segment_id: 0 }, replayEventPayload: expect.objectContaining({ replay_type: 'buffer', - contexts: { - replay: { - error_sample_rate: 1, - session_sample_rate: 0, - }, - }, }), recordingData: JSON.stringify([ { data: { isCheckout: true }, timestamp: BASE_TIMESTAMP, type: 2 }, + optionsEvent, TEST_EVENT, { type: 5, @@ -104,12 +101,6 @@ describe('Integration | errorSampleRate', () => { recordingPayloadHeader: { segment_id: 1 }, replayEventPayload: expect.objectContaining({ replay_type: 'buffer', - contexts: { - replay: { - error_sample_rate: 1, - session_sample_rate: 0, - }, - }, }), recordingData: JSON.stringify([ { data: { isCheckout: true }, timestamp: BASE_TIMESTAMP + DEFAULT_FLUSH_MIN_DELAY + 40, type: 2 }, @@ -161,6 +152,7 @@ describe('Integration | errorSampleRate', () => { it('manually flushes replay and does not continue to record', async () => { const TEST_EVENT = { data: {}, timestamp: BASE_TIMESTAMP, type: 3 }; mockRecord._emitter(TEST_EVENT); + const optionsEvent = createOptionsEvent(replay); expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled(); expect(replay).not.toHaveLastSentReplay(); @@ -186,6 +178,7 @@ describe('Integration | errorSampleRate', () => { }), recordingData: JSON.stringify([ { data: { isCheckout: true }, timestamp: BASE_TIMESTAMP, type: 2 }, + optionsEvent, TEST_EVENT, { type: 5, @@ -218,15 +211,10 @@ describe('Integration | errorSampleRate', () => { recordingPayloadHeader: { segment_id: 0 }, replayEventPayload: expect.objectContaining({ replay_type: 'buffer', - contexts: { - replay: { - error_sample_rate: 1, - session_sample_rate: 0, - }, - }, }), recordingData: JSON.stringify([ { data: { isCheckout: true }, timestamp: BASE_TIMESTAMP, type: 2 }, + optionsEvent, TEST_EVENT, { type: 5, @@ -532,6 +520,7 @@ describe('Integration | errorSampleRate', () => { it('has the correct timestamps with deferred root event and last replay update', async () => { const TEST_EVENT = { data: {}, timestamp: BASE_TIMESTAMP, type: 3 }; mockRecord._emitter(TEST_EVENT); + const optionsEvent = createOptionsEvent(replay); expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled(); expect(replay).not.toHaveLastSentReplay(); @@ -548,7 +537,7 @@ describe('Integration | errorSampleRate', () => { await new Promise(process.nextTick); expect(replay).toHaveSentReplay({ - recordingData: JSON.stringify([{ data: { isCheckout: true }, timestamp: BASE_TIMESTAMP, type: 2 }, TEST_EVENT]), + recordingData: JSON.stringify([{ data: { isCheckout: true }, timestamp: BASE_TIMESTAMP, type: 2 }, optionsEvent, TEST_EVENT]), replayEventPayload: expect.objectContaining({ replay_start_timestamp: BASE_TIMESTAMP / 1000, // the exception happens roughly 10 seconds after BASE_TIMESTAMP @@ -584,6 +573,7 @@ describe('Integration | errorSampleRate', () => { // in production, this happens at a time interval // session started time should be updated to this current timestamp mockRecord.takeFullSnapshot(true); + const optionsEvent = createOptionsEvent(replay); jest.runAllTimers(); jest.advanceTimersByTime(20); @@ -611,6 +601,7 @@ describe('Integration | errorSampleRate', () => { timestamp: BASE_TIMESTAMP + ELAPSED + 20, type: 2, }, + optionsEvent, ]), }); }); @@ -726,8 +717,9 @@ it('sends a replay after loading the session multiple times', async () => { }, autoStart: false, }); - // @ts-ignore this is protected, but we want to call it for this test - integration._initialize(); + integration['_initialize'](); + + const optionsEvent = createOptionsEvent(replay) jest.runAllTimers(); @@ -744,12 +736,14 @@ it('sends a replay after loading the session multiple times', async () => { await new Promise(process.nextTick); expect(replay).toHaveSentReplay({ - recordingData: JSON.stringify([{ data: { isCheckout: true }, timestamp: BASE_TIMESTAMP, type: 2 }, TEST_EVENT]), + recordingPayloadHeader: { segment_id: 0 }, + recordingData: JSON.stringify([{ data: { isCheckout: true }, timestamp: BASE_TIMESTAMP, type: 2 }, optionsEvent, TEST_EVENT]), }); // Latest checkout when we call `startRecording` again after uploading segment // after an error occurs (e.g. when we switch to session replay recording) expect(replay).toHaveLastSentReplay({ + recordingPayloadHeader: { segment_id: 1 }, recordingData: JSON.stringify([{ data: { isCheckout: true }, timestamp: BASE_TIMESTAMP + 5040, type: 2 }]), }); }); diff --git a/packages/replay/test/integration/events.test.ts b/packages/replay/test/integration/events.test.ts index e0b229e1c82d..b95faffa59da 100644 --- a/packages/replay/test/integration/events.test.ts +++ b/packages/replay/test/integration/events.test.ts @@ -130,12 +130,6 @@ describe('Integration | events', () => { expect(replay).toHaveLastSentReplay({ replayEventPayload: expect.objectContaining({ replay_start_timestamp: (BASE_TIMESTAMP - 10000) / 1000, - contexts: { - replay: { - error_sample_rate: 0, - session_sample_rate: 1, - }, - }, urls: ['http://localhost/'], // this doesn't truly test if we are capturing the right URL as we don't change URLs, but good enough }), }); diff --git a/packages/replay/test/integration/session.test.ts b/packages/replay/test/integration/session.test.ts index d22e286f391a..3c3bf848cbe1 100644 --- a/packages/replay/test/integration/session.test.ts +++ b/packages/replay/test/integration/session.test.ts @@ -14,6 +14,7 @@ import { clearSession } from '../../src/session/clearSession'; import type { Session } from '../../src/types'; import { addEvent } from '../../src/util/addEvent'; import { createPerformanceSpans } from '../../src/util/createPerformanceSpans'; +import { createOptionsEvent } from '../../src/util/handleRecordingEmit'; import { BASE_TIMESTAMP } from '../index'; import type { RecordMock } from '../mocks/mockRrweb'; import { resetSdkMock } from '../mocks/resetSdkMock'; @@ -197,6 +198,8 @@ describe('Integration | session', () => { // Replay does not send immediately because checkout was due to expired session expect(replay).not.toHaveLastSentReplay(); + const optionsEvent = createOptionsEvent(replay); + await advanceTimers(DEFAULT_FLUSH_MIN_DELAY); const newTimestamp = BASE_TIMESTAMP + ELAPSED + 20; @@ -205,6 +208,7 @@ describe('Integration | session', () => { recordingPayloadHeader: { segment_id: 0 }, recordingData: JSON.stringify([ { data: { isCheckout: true }, timestamp: newTimestamp, type: 2 }, + optionsEvent, { type: 5, timestamp: newTimestamp, @@ -382,6 +386,7 @@ describe('Integration | session', () => { type: 3, }; mockRecord._emitter(NEW_TEST_EVENT); + const optionsEvent = createOptionsEvent(replay) jest.runAllTimers(); await advanceTimers(DEFAULT_FLUSH_MIN_DELAY); @@ -389,7 +394,8 @@ describe('Integration | session', () => { expect(replay).toHaveLastSentReplay({ recordingPayloadHeader: { segment_id: 0 }, recordingData: JSON.stringify([ - { type: 5, timestamp: newTimestamp, data: { tag: 'options', payload: DEFAULT_OPTIONS_EVENT_PAYLOAD } }, + {data: {isCheckout: true}, timestamp: BASE_TIMESTAMP + ELAPSED, type: 2}, + optionsEvent, { type: 5, timestamp: newTimestamp, diff --git a/packages/replay/test/integration/stop.test.ts b/packages/replay/test/integration/stop.test.ts index a477ee8e044f..cc0e28195244 100644 --- a/packages/replay/test/integration/stop.test.ts +++ b/packages/replay/test/integration/stop.test.ts @@ -5,6 +5,7 @@ import { WINDOW } from '../../src/constants'; import type { ReplayContainer } from '../../src/replay'; import { clearSession } from '../../src/session/clearSession'; import { addEvent } from '../../src/util/addEvent'; +import { createOptionsEvent } from '../../src/util/handleRecordingEmit'; // mock functions need to be imported first import { BASE_TIMESTAMP, mockRrweb, mockSdk } from '../index'; import { useFakeTimers } from '../utils/use-fake-timers'; @@ -95,6 +96,7 @@ describe('Integration | stop', () => { // re-enable replay integration.start(); + const optionsEvent = createOptionsEvent(replay); // will be different session expect(replay.session?.id).not.toEqual(previousSessionId); @@ -121,6 +123,7 @@ describe('Integration | stop', () => { jest.runAllTimers(); await new Promise(process.nextTick); expect(replay).toHaveLastSentReplay({ + recordingPayloadHeader: { segment_id: 0 }, recordingData: JSON.stringify([ // This event happens when we call `replay.start` { @@ -128,10 +131,12 @@ describe('Integration | stop', () => { timestamp: BASE_TIMESTAMP + ELAPSED + EXTRA_TICKS, type: 2, }, + optionsEvent, TEST_EVENT, hiddenBreadcrumb, ]), }); + // Session's last activity is last updated when we call `setup()` and *NOT* // when tab is blurred expect(replay.session?.lastActivity).toBe(BASE_TIMESTAMP + ELAPSED + 20); diff --git a/packages/replay/test/unit/util/handleRecordingEmit.test.ts b/packages/replay/test/unit/util/handleRecordingEmit.test.ts index a0760863d91a..c7de01b8380d 100644 --- a/packages/replay/test/unit/util/handleRecordingEmit.test.ts +++ b/packages/replay/test/unit/util/handleRecordingEmit.test.ts @@ -1,21 +1,15 @@ import { EventType } from '@sentry-internal/rrweb'; import { BASE_TIMESTAMP } from '../..'; +import type { RecordingEvent } from '../../../src/types'; import * as SentryAddEvent from '../../../src/util/addEvent'; -import { getHandleRecordingEmit } from '../../../src/util/handleRecordingEmit'; +import { createOptionsEvent,getHandleRecordingEmit } from '../../../src/util/handleRecordingEmit'; import { DEFAULT_OPTIONS_EVENT_PAYLOAD, setupReplayContainer } from '../../utils/setupReplayContainer'; import { useFakeTimers } from '../../utils/use-fake-timers'; useFakeTimers(); -const optionsEvent = { - type: 5, - data: { - tag: 'options', - payload: DEFAULT_OPTIONS_EVENT_PAYLOAD, - }, - timestamp: BASE_TIMESTAMP, -}; +let optionsEvent: RecordingEvent; describe('Unit | util | handleRecordingEmit', () => { let addEventMock: jest.SpyInstance; @@ -38,6 +32,7 @@ describe('Unit | util | handleRecordingEmit', () => { sessionSampleRate: 1, }, }); + optionsEvent = createOptionsEvent(replay) const handler = getHandleRecordingEmit(replay); @@ -54,7 +49,7 @@ describe('Unit | util | handleRecordingEmit', () => { expect(addEventMock).toBeCalledTimes(2); expect(addEventMock).toHaveBeenNthCalledWith(1, replay, event, true); - expect(addEventMock).toHaveBeenLastCalledWith(replay, optionsEvent, true); + expect(addEventMock).toHaveBeenLastCalledWith(replay, optionsEvent, false); handler(event); await new Promise(process.nextTick); @@ -70,6 +65,7 @@ describe('Unit | util | handleRecordingEmit', () => { sessionSampleRate: 1, }, }); + optionsEvent = createOptionsEvent(replay) const handler = getHandleRecordingEmit(replay); @@ -87,13 +83,13 @@ describe('Unit | util | handleRecordingEmit', () => { // Called twice, once for event and once for settings on checkout only expect(addEventMock).toBeCalledTimes(2); expect(addEventMock).toHaveBeenNthCalledWith(1, replay, event, true); - expect(addEventMock).toHaveBeenLastCalledWith(replay, optionsEvent, true); + expect(addEventMock).toHaveBeenLastCalledWith(replay, optionsEvent, false); handler(event, true); await new Promise(process.nextTick); expect(addEventMock).toBeCalledTimes(4); expect(addEventMock).toHaveBeenNthCalledWith(3, replay, event, true); - expect(addEventMock).toHaveBeenLastCalledWith(replay, { ...optionsEvent, timestamp: BASE_TIMESTAMP + 20 }, true); + expect(addEventMock).toHaveBeenLastCalledWith(replay, { ...optionsEvent, timestamp: BASE_TIMESTAMP + 20 }, false); }); }); From 16e575a923a19e218b453d7526bb7515ddde67cf Mon Sep 17 00:00:00 2001 From: Billy Vong Date: Thu, 4 May 2023 20:20:38 -0400 Subject: [PATCH 04/10] reivew fixes --- packages/replay/src/util/handleRecordingEmit.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/replay/src/util/handleRecordingEmit.ts b/packages/replay/src/util/handleRecordingEmit.ts index 545203c7940c..6ca577bd76c2 100644 --- a/packages/replay/src/util/handleRecordingEmit.ts +++ b/packages/replay/src/util/handleRecordingEmit.ts @@ -102,7 +102,7 @@ export function createOptionsEvent(replay: ReplayContainer): RecordingEvent { const options = replay.getOptions(); return { type: EventType.Custom, - timestamp: new Date().getTime(), + timestamp: Date.now(), data: { tag: 'options', payload: { @@ -112,11 +112,11 @@ export function createOptionsEvent(replay: ReplayContainer): RecordingEvent { blockAllMedia: options.blockAllMedia, maskAllText: options.maskAllText, maskAllInputs: options.maskAllInputs, - useCompression: replay.eventBuffer && replay.eventBuffer.type === 'worker', + useCompression: replay.eventBuffer ? replay.eventBuffer.type === 'worker' : false, networkDetailHasUrls: options.networkDetailAllowUrls.length > 0, networkCaptureBodies: options.networkCaptureBodies, - networkRequestHeaders: options.networkRequestHeaders.length > 0, - networkResponseHeaders: options.networkResponseHeaders.length > 0, + networkRequestHasHeaders: options.networkRequestHeaders.length > 0, + networkResponseHasHeaders: options.networkResponseHeaders.length > 0, }, }, }; From 94e61f3936cf8e61ccaa1a93cbedf11ce2708dd8 Mon Sep 17 00:00:00 2001 From: Billy Vong Date: Thu, 4 May 2023 20:26:53 -0400 Subject: [PATCH 05/10] Update packages/replay/src/util/handleRecordingEmit.ts --- packages/replay/src/util/handleRecordingEmit.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/replay/src/util/handleRecordingEmit.ts b/packages/replay/src/util/handleRecordingEmit.ts index 6ca577bd76c2..720273bc97af 100644 --- a/packages/replay/src/util/handleRecordingEmit.ts +++ b/packages/replay/src/util/handleRecordingEmit.ts @@ -41,7 +41,7 @@ export function getHandleRecordingEmit(replay: ReplayContainer): RecordingEmitCa // We need to clear existing events on a checkout, otherwise they are // incremental event updates and should be appended - addEvent(replay, event, isCheckout); + void addEvent(replay, event, isCheckout); // Different behavior for full snapshots (type=2), ignore other event types // See https://github.com/rrweb-io/rrweb/blob/d8f9290ca496712aa1e7d472549480c4e7876594/packages/rrweb/src/types.ts#L16 From a7fdcc2442f643d6c09bf8a94cd973827155497e Mon Sep 17 00:00:00 2001 From: Billy Vong Date: Fri, 5 May 2023 13:22:45 -0400 Subject: [PATCH 06/10] Update packages/replay/src/util/sendReplayRequest.ts Co-authored-by: Francesco Novy --- packages/replay/src/util/sendReplayRequest.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/replay/src/util/sendReplayRequest.ts b/packages/replay/src/util/sendReplayRequest.ts index 839df6ae0d39..65f217f857cd 100644 --- a/packages/replay/src/util/sendReplayRequest.ts +++ b/packages/replay/src/util/sendReplayRequest.ts @@ -89,7 +89,6 @@ export async function sendReplayRequest({ }, "sdkProcessingMetadata": {}, "contexts": { - "replay": { }, }, } */ From f9eab22d7ac49687a216880b1deab4123dcc2fc1 Mon Sep 17 00:00:00 2001 From: Billy Vong Date: Fri, 5 May 2023 13:26:08 -0400 Subject: [PATCH 07/10] update types --- packages/replay/src/util/handleRecordingEmit.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/replay/src/util/handleRecordingEmit.ts b/packages/replay/src/util/handleRecordingEmit.ts index 720273bc97af..f37f6ac48077 100644 --- a/packages/replay/src/util/handleRecordingEmit.ts +++ b/packages/replay/src/util/handleRecordingEmit.ts @@ -1,7 +1,7 @@ import { logger } from '@sentry/utils'; import { saveSession } from '../session/saveSession'; -import type { RecordingEvent, ReplayContainer } from '../types'; +import type { RecordingEvent, ReplayContainer, AddEventResult } from '../types'; import { EventType } from '../types/rrweb'; import { addEvent } from './addEvent'; @@ -126,7 +126,7 @@ export function createOptionsEvent(replay: ReplayContainer): RecordingEvent { * Add a "meta" event that contains a simplified view on current configuration * options. This should only be included on the first segment of a recording. */ -function addSettingsEvent(replay: ReplayContainer, isCheckout?: boolean): Promise { +function addSettingsEvent(replay: ReplayContainer, isCheckout?: boolean): Promise { // Only need to add this event when sending the first segment if (!isCheckout || !replay.session || replay.session.segmentId !== 0) { return Promise.resolve(null); From eed62a70e640e6a2748edaa626ef70643a1768d2 Mon Sep 17 00:00:00 2001 From: Billy Vong Date: Fri, 5 May 2023 15:01:16 -0400 Subject: [PATCH 08/10] fix tests + lint --- .../suites/replay/bufferMode/test.ts | 5 -- .../suites/replay/captureReplay/test.ts | 2 - .../captureReplayFromReplayPackage/test.ts | 2 - .../suites/replay/customEvents/test.ts | 57 +++++++++++++++++++ .../suites/replay/errors/errorMode/test.ts | 3 - .../utils/replayEventTemplates.ts | 1 - .../utils/replayHelpers.ts | 9 ++- .../replay/src/util/handleRecordingEmit.ts | 2 +- .../replay/test/integration/session.test.ts | 1 - .../unit/util/handleRecordingEmit.test.ts | 2 +- 10 files changed, 67 insertions(+), 17 deletions(-) diff --git a/packages/browser-integration-tests/suites/replay/bufferMode/test.ts b/packages/browser-integration-tests/suites/replay/bufferMode/test.ts index c5a4e9ae2526..2a835c444550 100644 --- a/packages/browser-integration-tests/suites/replay/bufferMode/test.ts +++ b/packages/browser-integration-tests/suites/replay/bufferMode/test.ts @@ -115,7 +115,6 @@ sentryTest( expect(event0).toEqual( getExpectedReplayEvent({ - contexts: { replay: { error_sample_rate: 0, session_sample_rate: 0 } }, error_ids: [errorEventId!], replay_type: 'buffer', }), @@ -150,7 +149,6 @@ sentryTest( expect(event1).toEqual( getExpectedReplayEvent({ - contexts: { replay: { error_sample_rate: 0, session_sample_rate: 0 } }, replay_type: 'buffer', // although we're in session mode, we still send 'buffer' as replay_type segment_id: 1, urls: [], @@ -162,7 +160,6 @@ sentryTest( expect(event2).toEqual( getExpectedReplayEvent({ - contexts: { replay: { error_sample_rate: 0, session_sample_rate: 0 } }, replay_type: 'buffer', // although we're in session mode, we still send 'buffer' as replay_type segment_id: 2, urls: [], @@ -266,7 +263,6 @@ sentryTest( expect(event0).toEqual( getExpectedReplayEvent({ - contexts: { replay: { error_sample_rate: 0, session_sample_rate: 0 } }, error_ids: [errorEventId!], replay_type: 'buffer', }), @@ -372,7 +368,6 @@ sentryTest('[buffer-mode] can sample on each error event', async ({ getLocalTest expect(event0).toEqual( getExpectedReplayEvent({ - contexts: { replay: { error_sample_rate: 1, session_sample_rate: 0 } }, error_ids: errorEventIds, replay_type: 'buffer', }), diff --git a/packages/browser-integration-tests/suites/replay/captureReplay/test.ts b/packages/browser-integration-tests/suites/replay/captureReplay/test.ts index 473d88ea53db..72cbc47efe99 100644 --- a/packages/browser-integration-tests/suites/replay/captureReplay/test.ts +++ b/packages/browser-integration-tests/suites/replay/captureReplay/test.ts @@ -64,7 +64,6 @@ sentryTest('should capture replays (@sentry/browser export)', async ({ getLocalT }, }, platform: 'javascript', - contexts: { replay: { session_sample_rate: 1, error_sample_rate: 0 } }, }); expect(replayEvent1).toBeDefined(); @@ -103,6 +102,5 @@ sentryTest('should capture replays (@sentry/browser export)', async ({ getLocalT }, }, platform: 'javascript', - contexts: { replay: { session_sample_rate: 1, error_sample_rate: 0 } }, }); }); diff --git a/packages/browser-integration-tests/suites/replay/captureReplayFromReplayPackage/test.ts b/packages/browser-integration-tests/suites/replay/captureReplayFromReplayPackage/test.ts index 03ee0f78e540..6caf1e4ea57c 100644 --- a/packages/browser-integration-tests/suites/replay/captureReplayFromReplayPackage/test.ts +++ b/packages/browser-integration-tests/suites/replay/captureReplayFromReplayPackage/test.ts @@ -64,7 +64,6 @@ sentryTest('should capture replays (@sentry/replay export)', async ({ getLocalTe }, }, platform: 'javascript', - contexts: { replay: { session_sample_rate: 1, error_sample_rate: 0 } }, }); expect(replayEvent1).toBeDefined(); @@ -103,6 +102,5 @@ sentryTest('should capture replays (@sentry/replay export)', async ({ getLocalTe }, }, platform: 'javascript', - contexts: { replay: { session_sample_rate: 1, error_sample_rate: 0 } }, }); }); diff --git a/packages/browser-integration-tests/suites/replay/customEvents/test.ts b/packages/browser-integration-tests/suites/replay/customEvents/test.ts index ce8f27bf4995..585266746365 100644 --- a/packages/browser-integration-tests/suites/replay/customEvents/test.ts +++ b/packages/browser-integration-tests/suites/replay/customEvents/test.ts @@ -174,3 +174,60 @@ sentryTest( ); }, ); + +sentryTest( + 'replay recording should contain an "options" breadcrumb for Replay SDK configuration', + async ({ forceFlushReplay, getLocalTestPath, page, browserName }) => { + // TODO(replay): This is flakey on firefox and webkit where clicks are flakey + if (shouldSkipReplayTest() || ['firefox', 'webkit'].includes(browserName)) { + sentryTest.skip(); + } + + const reqPromise0 = waitForReplayRequest(page, 0); + const reqPromise1 = waitForReplayRequest(page, 1); + + await page.route('https://dsn.ingest.sentry.io/**/*', route => { + return route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ id: 'test-id' }), + }); + }); + + const url = await getLocalTestPath({ testDir: __dirname }); + + await page.goto(url); + await forceFlushReplay(); + + await page.click('#error'); + await forceFlushReplay(); + + const req0 = await reqPromise0; + const content0 = getReplayRecordingContent(req0); + + expect(content0.optionsEvents).toEqual([ + { + tag: 'options', + payload: { + sessionSampleRate: 1, + errorSampleRate: 0, + useCompressionOption: false, + blockAllMedia: false, + maskAllText: true, + maskAllInputs: true, + useCompression: false, + networkDetailHasUrls: false, + networkCaptureBodies: true, + networkRequestHasHeaders: true, + networkResponseHasHeaders: true, + }, + }, + ]); + + const req1 = await reqPromise1; + const content1 = getReplayRecordingContent(req1); + + // Should only be on first segment + expect(content1.optionsEvents).toEqual([]); + }, +); diff --git a/packages/browser-integration-tests/suites/replay/errors/errorMode/test.ts b/packages/browser-integration-tests/suites/replay/errors/errorMode/test.ts index fee9e05d4a49..aa452cdd9307 100644 --- a/packages/browser-integration-tests/suites/replay/errors/errorMode/test.ts +++ b/packages/browser-integration-tests/suites/replay/errors/errorMode/test.ts @@ -84,7 +84,6 @@ sentryTest( expect(event0).toEqual( getExpectedReplayEvent({ - contexts: { replay: { error_sample_rate: 1, session_sample_rate: 0 } }, error_ids: [errorEventId!], replay_type: 'buffer', }), @@ -119,7 +118,6 @@ sentryTest( expect(event1).toEqual( getExpectedReplayEvent({ - contexts: { replay: { error_sample_rate: 1, session_sample_rate: 0 } }, replay_type: 'buffer', // although we're in session mode, we still send 'error' as replay_type segment_id: 1, urls: [], @@ -134,7 +132,6 @@ sentryTest( // we continue recording everything expect(event2).toEqual( getExpectedReplayEvent({ - contexts: { replay: { error_sample_rate: 1, session_sample_rate: 0 } }, replay_type: 'buffer', segment_id: 2, urls: [], diff --git a/packages/browser-integration-tests/utils/replayEventTemplates.ts b/packages/browser-integration-tests/utils/replayEventTemplates.ts index e6ee4bda18d2..d88fbd1bf0e5 100644 --- a/packages/browser-integration-tests/utils/replayEventTemplates.ts +++ b/packages/browser-integration-tests/utils/replayEventTemplates.ts @@ -38,7 +38,6 @@ const DEFAULT_REPLAY_EVENT = { }, }, platform: 'javascript', - contexts: { replay: { session_sample_rate: 1, error_sample_rate: 0 } }, }; /** diff --git a/packages/browser-integration-tests/utils/replayHelpers.ts b/packages/browser-integration-tests/utils/replayHelpers.ts index b1448dc97d85..415dcd667414 100644 --- a/packages/browser-integration-tests/utils/replayHelpers.ts +++ b/packages/browser-integration-tests/utils/replayHelpers.ts @@ -160,6 +160,7 @@ type CustomRecordingContent = { type RecordingContent = { fullSnapshots: RecordingSnapshot[]; incrementalSnapshots: RecordingSnapshot[]; + optionsEvents: CustomRecordingEvent[]; } & CustomRecordingContent; /** @@ -207,6 +208,11 @@ export function getIncrementalRecordingSnapshots(resOrReq: Request | Response): return events.filter(isIncrementalSnapshot); } +function getOptionsEvents(replayRequest: Request): CustomRecordingEvent[] { + const events = getDecompressedRecordingEvents(replayRequest); + return getAllCustomRrwebRecordingEvents(events).filter(data => data.tag === 'options'); +} + function getDecompressedRecordingEvents(resOrReq: Request | Response): RecordingSnapshot[] { const replayRequest = getRequest(resOrReq); return ( @@ -227,8 +233,9 @@ export function getReplayRecordingContent(resOrReq: Request | Response): Recordi const fullSnapshots = getFullRecordingSnapshots(replayRequest); const incrementalSnapshots = getIncrementalRecordingSnapshots(replayRequest); const customEvents = getCustomRecordingEvents(replayRequest); + const optionsEvents = getOptionsEvents(replayRequest); - return { fullSnapshots, incrementalSnapshots, ...customEvents }; + return { fullSnapshots, incrementalSnapshots, optionsEvents, ...customEvents }; } /** diff --git a/packages/replay/src/util/handleRecordingEmit.ts b/packages/replay/src/util/handleRecordingEmit.ts index f37f6ac48077..2d39e8f9bc9f 100644 --- a/packages/replay/src/util/handleRecordingEmit.ts +++ b/packages/replay/src/util/handleRecordingEmit.ts @@ -1,7 +1,7 @@ import { logger } from '@sentry/utils'; import { saveSession } from '../session/saveSession'; -import type { RecordingEvent, ReplayContainer, AddEventResult } from '../types'; +import type { AddEventResult,RecordingEvent, ReplayContainer } from '../types'; import { EventType } from '../types/rrweb'; import { addEvent } from './addEvent'; diff --git a/packages/replay/test/integration/session.test.ts b/packages/replay/test/integration/session.test.ts index 3c3bf848cbe1..1c9f97c5a261 100644 --- a/packages/replay/test/integration/session.test.ts +++ b/packages/replay/test/integration/session.test.ts @@ -18,7 +18,6 @@ import { createOptionsEvent } from '../../src/util/handleRecordingEmit'; import { BASE_TIMESTAMP } from '../index'; import type { RecordMock } from '../mocks/mockRrweb'; import { resetSdkMock } from '../mocks/resetSdkMock'; -import { DEFAULT_OPTIONS_EVENT_PAYLOAD } from '../utils/setupReplayContainer'; import { useFakeTimers } from '../utils/use-fake-timers'; useFakeTimers(); diff --git a/packages/replay/test/unit/util/handleRecordingEmit.test.ts b/packages/replay/test/unit/util/handleRecordingEmit.test.ts index c7de01b8380d..95911b587a22 100644 --- a/packages/replay/test/unit/util/handleRecordingEmit.test.ts +++ b/packages/replay/test/unit/util/handleRecordingEmit.test.ts @@ -4,7 +4,7 @@ import { BASE_TIMESTAMP } from '../..'; import type { RecordingEvent } from '../../../src/types'; import * as SentryAddEvent from '../../../src/util/addEvent'; import { createOptionsEvent,getHandleRecordingEmit } from '../../../src/util/handleRecordingEmit'; -import { DEFAULT_OPTIONS_EVENT_PAYLOAD, setupReplayContainer } from '../../utils/setupReplayContainer'; +import { setupReplayContainer } from '../../utils/setupReplayContainer'; import { useFakeTimers } from '../../utils/use-fake-timers'; useFakeTimers(); From dcd08abd26f808bef021b5971d0e4a89afc1532d Mon Sep 17 00:00:00 2001 From: Billy Vong Date: Fri, 5 May 2023 15:44:37 -0400 Subject: [PATCH 09/10] my linter never works --- .../tests/fixtures/ReplayRecordingData.ts | 20 +++++++++++++++++++ .../replay/src/util/handleRecordingEmit.ts | 9 ++++----- .../test/integration/errorSampleRate.test.ts | 16 +++++++++++---- .../replay/test/integration/session.test.ts | 4 ++-- .../unit/util/handleRecordingEmit.test.ts | 6 +++--- 5 files changed, 41 insertions(+), 14 deletions(-) diff --git a/packages/e2e-tests/test-applications/standard-frontend-react/tests/fixtures/ReplayRecordingData.ts b/packages/e2e-tests/test-applications/standard-frontend-react/tests/fixtures/ReplayRecordingData.ts index da5ba529edd7..5656e400bd1c 100644 --- a/packages/e2e-tests/test-applications/standard-frontend-react/tests/fixtures/ReplayRecordingData.ts +++ b/packages/e2e-tests/test-applications/standard-frontend-react/tests/fixtures/ReplayRecordingData.ts @@ -75,6 +75,26 @@ export const ReplayRecordingData = [ }, timestamp: expect.any(Number), }, + { + data: { + payload: { + blockAllMedia: true, + errorSampleRate: 0, + maskAllInputs: true, + maskAllText: true, + networkCaptureBodies: true, + networkDetailHasUrls: false, + networkRequestHasHeaders: true, + networkResponseHasHeaders: true, + sessionSampleRate: 1, + useCompression: false, + useCompressionOption: true, + }, + tag: 'options', + }, + timestamp: expect.any(Number), + type: 5, + }, { type: 5, timestamp: expect.any(Number), diff --git a/packages/replay/src/util/handleRecordingEmit.ts b/packages/replay/src/util/handleRecordingEmit.ts index 2d39e8f9bc9f..f72850f5536c 100644 --- a/packages/replay/src/util/handleRecordingEmit.ts +++ b/packages/replay/src/util/handleRecordingEmit.ts @@ -1,7 +1,7 @@ import { logger } from '@sentry/utils'; import { saveSession } from '../session/saveSession'; -import type { AddEventResult,RecordingEvent, ReplayContainer } from '../types'; +import type { AddEventResult, RecordingEvent, ReplayContainer } from '../types'; import { EventType } from '../types/rrweb'; import { addEvent } from './addEvent'; @@ -94,10 +94,9 @@ export function getHandleRecordingEmit(replay: ReplayContainer): RecordingEmitCa }; } - /** -* Exported for tests -*/ + * Exported for tests + */ export function createOptionsEvent(replay: ReplayContainer): RecordingEvent { const options = replay.getOptions(); return { @@ -124,7 +123,7 @@ export function createOptionsEvent(replay: ReplayContainer): RecordingEvent { /** * Add a "meta" event that contains a simplified view on current configuration -* options. This should only be included on the first segment of a recording. + * options. This should only be included on the first segment of a recording. */ function addSettingsEvent(replay: ReplayContainer, isCheckout?: boolean): Promise { // Only need to add this event when sending the first segment diff --git a/packages/replay/test/integration/errorSampleRate.test.ts b/packages/replay/test/integration/errorSampleRate.test.ts index 6e768f5a6044..74fde11f50f0 100644 --- a/packages/replay/test/integration/errorSampleRate.test.ts +++ b/packages/replay/test/integration/errorSampleRate.test.ts @@ -51,7 +51,7 @@ describe('Integration | errorSampleRate', () => { it('uploads a replay when `Sentry.captureException` is called and continues recording', async () => { const TEST_EVENT = { data: {}, timestamp: BASE_TIMESTAMP, type: 3 }; mockRecord._emitter(TEST_EVENT); - const optionsEvent = createOptionsEvent(replay) + const optionsEvent = createOptionsEvent(replay); expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled(); expect(replay).not.toHaveLastSentReplay(); @@ -537,7 +537,11 @@ describe('Integration | errorSampleRate', () => { await new Promise(process.nextTick); expect(replay).toHaveSentReplay({ - recordingData: JSON.stringify([{ data: { isCheckout: true }, timestamp: BASE_TIMESTAMP, type: 2 }, optionsEvent, TEST_EVENT]), + recordingData: JSON.stringify([ + { data: { isCheckout: true }, timestamp: BASE_TIMESTAMP, type: 2 }, + optionsEvent, + TEST_EVENT, + ]), replayEventPayload: expect.objectContaining({ replay_start_timestamp: BASE_TIMESTAMP / 1000, // the exception happens roughly 10 seconds after BASE_TIMESTAMP @@ -719,7 +723,7 @@ it('sends a replay after loading the session multiple times', async () => { }); integration['_initialize'](); - const optionsEvent = createOptionsEvent(replay) + const optionsEvent = createOptionsEvent(replay); jest.runAllTimers(); @@ -737,7 +741,11 @@ it('sends a replay after loading the session multiple times', async () => { expect(replay).toHaveSentReplay({ recordingPayloadHeader: { segment_id: 0 }, - recordingData: JSON.stringify([{ data: { isCheckout: true }, timestamp: BASE_TIMESTAMP, type: 2 }, optionsEvent, TEST_EVENT]), + recordingData: JSON.stringify([ + { data: { isCheckout: true }, timestamp: BASE_TIMESTAMP, type: 2 }, + optionsEvent, + TEST_EVENT, + ]), }); // Latest checkout when we call `startRecording` again after uploading segment diff --git a/packages/replay/test/integration/session.test.ts b/packages/replay/test/integration/session.test.ts index 1c9f97c5a261..304059659078 100644 --- a/packages/replay/test/integration/session.test.ts +++ b/packages/replay/test/integration/session.test.ts @@ -385,7 +385,7 @@ describe('Integration | session', () => { type: 3, }; mockRecord._emitter(NEW_TEST_EVENT); - const optionsEvent = createOptionsEvent(replay) + const optionsEvent = createOptionsEvent(replay); jest.runAllTimers(); await advanceTimers(DEFAULT_FLUSH_MIN_DELAY); @@ -393,7 +393,7 @@ describe('Integration | session', () => { expect(replay).toHaveLastSentReplay({ recordingPayloadHeader: { segment_id: 0 }, recordingData: JSON.stringify([ - {data: {isCheckout: true}, timestamp: BASE_TIMESTAMP + ELAPSED, type: 2}, + { data: { isCheckout: true }, timestamp: BASE_TIMESTAMP + ELAPSED, type: 2 }, optionsEvent, { type: 5, diff --git a/packages/replay/test/unit/util/handleRecordingEmit.test.ts b/packages/replay/test/unit/util/handleRecordingEmit.test.ts index 95911b587a22..a4c7f82c425d 100644 --- a/packages/replay/test/unit/util/handleRecordingEmit.test.ts +++ b/packages/replay/test/unit/util/handleRecordingEmit.test.ts @@ -3,7 +3,7 @@ import { EventType } from '@sentry-internal/rrweb'; import { BASE_TIMESTAMP } from '../..'; import type { RecordingEvent } from '../../../src/types'; import * as SentryAddEvent from '../../../src/util/addEvent'; -import { createOptionsEvent,getHandleRecordingEmit } from '../../../src/util/handleRecordingEmit'; +import { createOptionsEvent, getHandleRecordingEmit } from '../../../src/util/handleRecordingEmit'; import { setupReplayContainer } from '../../utils/setupReplayContainer'; import { useFakeTimers } from '../../utils/use-fake-timers'; @@ -32,7 +32,7 @@ describe('Unit | util | handleRecordingEmit', () => { sessionSampleRate: 1, }, }); - optionsEvent = createOptionsEvent(replay) + optionsEvent = createOptionsEvent(replay); const handler = getHandleRecordingEmit(replay); @@ -65,7 +65,7 @@ describe('Unit | util | handleRecordingEmit', () => { sessionSampleRate: 1, }, }); - optionsEvent = createOptionsEvent(replay) + optionsEvent = createOptionsEvent(replay); const handler = getHandleRecordingEmit(replay); From 4c9987c667e42e07e06a821e45b41e17519509ec Mon Sep 17 00:00:00 2001 From: Billy Vong Date: Fri, 5 May 2023 16:18:54 -0400 Subject: [PATCH 10/10] move event --- .../tests/fixtures/ReplayRecordingData.ts | 40 +++++++++---------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/packages/e2e-tests/test-applications/standard-frontend-react/tests/fixtures/ReplayRecordingData.ts b/packages/e2e-tests/test-applications/standard-frontend-react/tests/fixtures/ReplayRecordingData.ts index 5656e400bd1c..a22694a64304 100644 --- a/packages/e2e-tests/test-applications/standard-frontend-react/tests/fixtures/ReplayRecordingData.ts +++ b/packages/e2e-tests/test-applications/standard-frontend-react/tests/fixtures/ReplayRecordingData.ts @@ -7,6 +7,26 @@ export const ReplayRecordingData = [ data: { href: expect.stringMatching(/http:\/\/localhost:\d+\//), width: 1280, height: 720 }, timestamp: expect.any(Number), }, + { + data: { + payload: { + blockAllMedia: true, + errorSampleRate: 0, + maskAllInputs: true, + maskAllText: true, + networkCaptureBodies: true, + networkDetailHasUrls: false, + networkRequestHasHeaders: true, + networkResponseHasHeaders: true, + sessionSampleRate: 1, + useCompression: false, + useCompressionOption: true, + }, + tag: 'options', + }, + timestamp: expect.any(Number), + type: 5, + }, { type: 2, data: { @@ -75,26 +95,6 @@ export const ReplayRecordingData = [ }, timestamp: expect.any(Number), }, - { - data: { - payload: { - blockAllMedia: true, - errorSampleRate: 0, - maskAllInputs: true, - maskAllText: true, - networkCaptureBodies: true, - networkDetailHasUrls: false, - networkRequestHasHeaders: true, - networkResponseHasHeaders: true, - sessionSampleRate: 1, - useCompression: false, - useCompressionOption: true, - }, - tag: 'options', - }, - timestamp: expect.any(Number), - type: 5, - }, { type: 5, timestamp: expect.any(Number),