diff --git a/packages/plugin-session-replay-browser/src/session-replay.ts b/packages/plugin-session-replay-browser/src/session-replay.ts index be5a45918..5655f868b 100644 --- a/packages/plugin-session-replay-browser/src/session-replay.ts +++ b/packages/plugin-session-replay-browser/src/session-replay.ts @@ -63,6 +63,7 @@ export class SessionReplayPlugin implements EnrichmentPlugin { configEndpointUrl: this.options.configEndpointUrl, shouldInlineStylesheet: this.options.shouldInlineStylesheet, version: { type: 'plugin', version: VERSION }, + performanceConfig: this.options.performanceConfig, }).promise; } diff --git a/packages/plugin-session-replay-browser/src/typings/session-replay.ts b/packages/plugin-session-replay-browser/src/typings/session-replay.ts index dfdb0e2cc..87ccd1597 100644 --- a/packages/plugin-session-replay-browser/src/typings/session-replay.ts +++ b/packages/plugin-session-replay-browser/src/typings/session-replay.ts @@ -9,6 +9,11 @@ export interface SessionReplayPrivacyConfig { maskSelector?: string[]; unmaskSelector?: string[]; } + +export interface SessionReplayPerformanceConfig { + enabled: boolean; +} + export interface SessionReplayOptions { sampleRate?: number; privacyConfig?: SessionReplayPrivacyConfig; @@ -16,4 +21,5 @@ export interface SessionReplayOptions { forceSessionTracking?: boolean; configEndpointUrl?: string; shouldInlineStylesheet?: boolean; + performanceConfig?: SessionReplayPerformanceConfig; } diff --git a/packages/session-replay-browser/src/config/local-config.ts b/packages/session-replay-browser/src/config/local-config.ts index d2ae6b2b9..1237d609b 100644 --- a/packages/session-replay-browser/src/config/local-config.ts +++ b/packages/session-replay-browser/src/config/local-config.ts @@ -7,6 +7,7 @@ import { SessionReplayLocalConfig as ISessionReplayLocalConfig, InteractionConfig, PrivacyConfig, + SessionReplayPerformanceConfig, SessionReplayVersion, } from './types'; @@ -26,6 +27,7 @@ export class SessionReplayLocalConfig extends Config implements ISessionReplayLo configEndpointUrl?: string; shouldInlineStylesheet?: boolean; version?: SessionReplayVersion; + performanceConfig?: SessionReplayPerformanceConfig; constructor(apiKey: string, options: SessionReplayOptions) { const defaultConfig = getDefaultConfig(); @@ -45,6 +47,7 @@ export class SessionReplayLocalConfig extends Config implements ISessionReplayLo this.configEndpointUrl = options.configEndpointUrl; this.shouldInlineStylesheet = options.shouldInlineStylesheet; this.version = options.version; + this.performanceConfig = options.performanceConfig; if (options.privacyConfig) { this.privacyConfig = options.privacyConfig; diff --git a/packages/session-replay-browser/src/config/types.ts b/packages/session-replay-browser/src/config/types.ts index 8d731f442..a4bbdfe7e 100644 --- a/packages/session-replay-browser/src/config/types.ts +++ b/packages/session-replay-browser/src/config/types.ts @@ -49,6 +49,7 @@ export interface SessionReplayLocalConfig extends Config { configEndpointUrl?: string; shouldInlineStylesheet?: boolean; version?: SessionReplayVersion; + performanceConfig?: SessionReplayPerformanceConfig; } export interface SessionReplayJoinedConfig extends SessionReplayLocalConfig { @@ -72,4 +73,8 @@ export interface SessionReplayVersion { type: SessionReplayType; } +export interface SessionReplayPerformanceConfig { + enabled: boolean; +} + export type SessionReplayType = 'standalone' | 'plugin' | 'segment'; diff --git a/packages/session-replay-browser/src/session-replay.ts b/packages/session-replay-browser/src/session-replay.ts index fc5a0273b..a0022c9ef 100644 --- a/packages/session-replay-browser/src/session-replay.ts +++ b/packages/session-replay-browser/src/session-replay.ts @@ -1,7 +1,7 @@ import { getAnalyticsConnector, getGlobalScope } from '@amplitude/analytics-client-common'; import { Logger, returnWrapper } from '@amplitude/analytics-core'; import { Logger as ILogger, LogLevel } from '@amplitude/analytics-types'; -import { pack, record } from '@amplitude/rrweb'; +import { record } from '@amplitude/rrweb'; import { scrollCallback } from '@amplitude/rrweb-types'; import { createSessionReplayJoinedConfigGenerator } from './config/joined-config'; import { SessionReplayJoinedConfig, SessionReplayJoinedConfigGenerator } from './config/types'; @@ -30,7 +30,7 @@ import { SessionReplayOptions, } from './typings/session-replay'; import { VERSION } from './version'; -import type { eventWithTime } from '@amplitude/rrweb-types'; +import { EventCompressor } from './events/event-compressor'; type PageLeaveFn = (e: PageTransitionEvent | Event) => void; @@ -43,6 +43,7 @@ export class SessionReplay implements AmplitudeSessionReplay { loggerProvider: ILogger; recordCancelCallback: ReturnType | null = null; eventCount = 0; + eventCompressor: EventCompressor | undefined; // Visible for testing pageLeaveFns: PageLeaveFn[] = []; @@ -130,6 +131,7 @@ export class SessionReplay implements AmplitudeSessionReplay { } this.eventsManager = new MultiEventManager<'replay' | 'interaction', string>(...managers); + this.eventCompressor = new EventCompressor(this.eventsManager, this.config, this.getDeviceId()); this.loggerProvider.log('Installing @amplitude/session-replay-browser.'); @@ -310,40 +312,10 @@ export class SessionReplay implements AmplitudeSessionReplay { return maskSelector as unknown as string; } - compressEvents = (event: eventWithTime) => { - const packedEvent = pack(event); - return JSON.stringify(packedEvent); - }; - - addCompressedEvent = (event: eventWithTime, sessionId: number) => { - this.loggerProvider.debug('Compressing event for session replay: ', event); - const compressedEvent = this.compressEvents(event); - const deviceId = this.getDeviceId(); - this.eventsManager && - deviceId && - this.eventsManager.addEvent({ event: { type: 'replay', data: compressedEvent }, sessionId, deviceId }); - }; - - deferEventCompression = (canDelayCompression: boolean | undefined, event: eventWithTime, sessionId: number) => { - // In case the browser does not support requestIdleCallback, we will compress the event immediately - if (canDelayCompression) { - requestIdleCallback( - () => { - this.loggerProvider.debug('Adding event to idle callback queue: ', event); - this.addCompressedEvent(event, sessionId); - }, - { timeout: 2000 }, - ); // Timeout and run after 2 seconds - } else { - this.addCompressedEvent(event, sessionId); - } - }; - recordEvents() { - const globalScope = getGlobalScope(); const shouldRecord = this.getShouldRecord(); const sessionId = this.identifiers?.sessionId; - const canDelayCompression = globalScope && 'requestIdleCallback' in globalScope; + // const canDelayCompression = globalScope && 'requestIdleCallback' in globalScope; if (!shouldRecord || !sessionId || !this.config) { return; } @@ -359,7 +331,10 @@ export class SessionReplay implements AmplitudeSessionReplay { return; } - this.deferEventCompression(canDelayCompression, event, sessionId); + if (this.eventCompressor) { + // Schedule processing during idle time if the browser supports requestIdleCallback + this.eventCompressor.enqueueEvent(event, sessionId); + } }, inlineStylesheet: this.config.shouldInlineStylesheet, hooks: { diff --git a/packages/session-replay-browser/test/session-replay.test.ts b/packages/session-replay-browser/test/session-replay.test.ts index af1f3e75f..bffded17f 100644 --- a/packages/session-replay-browser/test/session-replay.test.ts +++ b/packages/session-replay-browser/test/session-replay.test.ts @@ -870,73 +870,6 @@ describe('SessionReplay', () => { expect(currentSequenceEvents).toEqual(undefined); }); - test('should defer event to when recording', async () => { - globalSpy = jest - .spyOn(AnalyticsClientCommon, 'getGlobalScope') - .mockReturnValue({ ...mockGlobalScope, requestIdleCallback: global.requestIdleCallback }); - await sessionReplay.init(apiKey, mockOptions).promise; - sessionReplay.recordEvents(); - if (!sessionReplay.eventsManager) { - throw new Error('Did not call init'); - } - const deferEventSpy = jest.spyOn(sessionReplay, 'deferEventCompression'); - - const recordArg = record.mock.calls[0][0]; - recordArg?.emit && recordArg?.emit(mockEvent); - - jest.advanceTimersByTime(2000); - - expect(deferEventSpy).toHaveBeenCalledTimes(1); - expect(deferEventSpy).toHaveBeenCalledWith(true, mockEvent, mockOptions.sessionId); - }); - - test('should call requestIdleCallback when deferring events', async () => { - globalSpy = jest - .spyOn(AnalyticsClientCommon, 'getGlobalScope') - .mockReturnValue({ ...mockGlobalScope, requestIdleCallback: global.requestIdleCallback }); - await sessionReplay.init(apiKey, mockOptions).promise; - const createEventsIDBStoreInstance = await (SessionReplayIDB.createEventsIDBStore as jest.Mock).mock.results[0] - .value; - if (!sessionReplay.eventsManager) { - throw new Error('Did not call init'); - } - const currentSequenceEvents = await createEventsIDBStoreInstance.db.get('sessionCurrentSequence', 123); - expect(currentSequenceEvents).toEqual(undefined); - const addCompressedEventSpy = jest.spyOn(sessionReplay, 'addCompressedEvent'); - const compressEventSpy = jest.spyOn(sessionReplay, 'compressEvents'); - - sessionReplay.deferEventCompression(true, mockEvent, 123); - jest.advanceTimersByTime(2000); - - expect(compressEventSpy).toHaveBeenCalledTimes(1); - expect(addCompressedEventSpy).toHaveBeenCalledTimes(1); - expect(addCompressedEventSpy).toHaveBeenCalledWith(mockEvent, mockOptions.sessionId); - expect(global.requestIdleCallback).toHaveBeenCalled(); - }); - - test('should add compressed events to event manger', async () => { - await sessionReplay.init(apiKey, mockOptions).promise; - const createEventsIDBStoreInstance = await (SessionReplayIDB.createEventsIDBStore as jest.Mock).mock.results[0] - .value; - if (!sessionReplay.eventsManager) { - throw new Error('Did not call init'); - } - const addEventSpy = jest.spyOn(sessionReplay.eventsManager, 'addEvent'); - - const currentSequenceEvents = await createEventsIDBStoreInstance.db.get('sessionCurrentSequence', 123); - expect(currentSequenceEvents).toEqual(undefined); - - sessionReplay.addCompressedEvent(mockEvent, 123); - const events = sessionReplay.compressEvents(mockEvent); - - expect(addEventSpy).toHaveBeenCalledTimes(1); - expect(addEventSpy).toHaveBeenCalledWith({ - event: { type: 'replay', data: events }, - sessionId: mockOptions.sessionId, - deviceId: mockOptions.deviceId, - }); - }); - test('should stop recording before starting anew', async () => { await sessionReplay.init(apiKey, mockOptions).promise; const stopRecordingMock = jest.fn();