diff --git a/packages/plugin-session-replay-browser/CHANGELOG.md b/packages/plugin-session-replay-browser/CHANGELOG.md index 89b49dc8a..8689ae26c 100644 --- a/packages/plugin-session-replay-browser/CHANGELOG.md +++ b/packages/plugin-session-replay-browser/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.22.18-fixsdk-init.0](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-session-replay-browser@1.22.17...@amplitude/plugin-session-replay-browser@1.22.18-fixsdk-init.0) (2025-10-01) + +**Note:** Version bump only for package @amplitude/plugin-session-replay-browser + + + + + ## [1.22.17](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-session-replay-browser@1.22.16...@amplitude/plugin-session-replay-browser@1.22.17) (2025-09-25) **Note:** Version bump only for package @amplitude/plugin-session-replay-browser diff --git a/packages/plugin-session-replay-browser/package.json b/packages/plugin-session-replay-browser/package.json index fb86dcd3a..33c6f231a 100644 --- a/packages/plugin-session-replay-browser/package.json +++ b/packages/plugin-session-replay-browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/plugin-session-replay-browser", - "version": "1.22.17", + "version": "1.22.18-fixsdk-init.0", "description": "", "author": "Amplitude Inc", "homepage": "https://github.com/amplitude/Amplitude-TypeScript", @@ -42,7 +42,7 @@ "@amplitude/analytics-client-common": "^2.4.1", "@amplitude/analytics-core": "^2.26.0", "@amplitude/analytics-types": "^2.10.0", - "@amplitude/session-replay-browser": "^1.28.14", + "@amplitude/session-replay-browser": "^1.29.0-fixsdk-init.0", "idb-keyval": "^6.2.1", "tslib": "^2.4.1" }, diff --git a/packages/plugin-session-replay-browser/src/version.ts b/packages/plugin-session-replay-browser/src/version.ts index 1f43c72be..50fc430a4 100644 --- a/packages/plugin-session-replay-browser/src/version.ts +++ b/packages/plugin-session-replay-browser/src/version.ts @@ -1,2 +1,2 @@ // Autogenerated by `yarn version-file`. DO NOT EDIT -export const VERSION = '1.22.17'; +export const VERSION = '1.22.18-fixsdk-init.0'; diff --git a/packages/segment-session-replay-plugin/CHANGELOG.md b/packages/segment-session-replay-plugin/CHANGELOG.md index 5c78d0b1f..25e51414a 100644 --- a/packages/segment-session-replay-plugin/CHANGELOG.md +++ b/packages/segment-session-replay-plugin/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [0.0.0-fixsdk-init.0](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/segment-session-replay-plugin@0.0.0-beta.33...@amplitude/segment-session-replay-plugin@0.0.0-fixsdk-init.0) (2025-10-01) + +**Note:** Version bump only for package @amplitude/segment-session-replay-plugin + + + + + # [0.0.0-beta.33](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/segment-session-replay-plugin@0.0.0-beta.32...@amplitude/segment-session-replay-plugin@0.0.0-beta.33) (2025-09-25) **Note:** Version bump only for package @amplitude/segment-session-replay-plugin diff --git a/packages/segment-session-replay-plugin/package.json b/packages/segment-session-replay-plugin/package.json index fb881a1c8..b113c2ac7 100644 --- a/packages/segment-session-replay-plugin/package.json +++ b/packages/segment-session-replay-plugin/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/segment-session-replay-plugin", - "version": "0.0.0-beta.33", + "version": "0.0.0-fixsdk-init.0", "description": "Plugin for Segment's analytics.js library to support Amplitude's Session Replay.", "keywords": [ "amplitude", @@ -47,7 +47,7 @@ "version-file": "echo '// Autogenerated by `yarn version-file`. DO NOT EDIT' > src/version.ts && node -p \"'export const VERSION = \\'' + require('./package.json').version + '\\';'\" >> src/version.ts" }, "dependencies": { - "@amplitude/session-replay-browser": "^1.28.14", + "@amplitude/session-replay-browser": "^1.29.0-fixsdk-init.0", "@segment/analytics-next": "^1.81.0", "js-cookie": "^3.0.5" }, diff --git a/packages/segment-session-replay-plugin/src/version.ts b/packages/segment-session-replay-plugin/src/version.ts index 0f1854737..10c50c475 100644 --- a/packages/segment-session-replay-plugin/src/version.ts +++ b/packages/segment-session-replay-plugin/src/version.ts @@ -1,2 +1,2 @@ // Autogenerated by `yarn version-file`. DO NOT EDIT -export const VERSION = '0.0.0-beta.33'; +export const VERSION = '0.0.0-fixsdk-init.0'; diff --git a/packages/session-replay-browser/CHANGELOG.md b/packages/session-replay-browser/CHANGELOG.md index 2018f77d3..ad7c254a6 100644 --- a/packages/session-replay-browser/CHANGELOG.md +++ b/packages/session-replay-browser/CHANGELOG.md @@ -3,6 +3,27 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [1.29.0-fixsdk-init.0](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/session-replay-browser@1.28.14...@amplitude/session-replay-browser@1.29.0-fixsdk-init.0) (2025-10-01) + + +### Bug Fixes + +* **session-replay-browser:** simplify event initialization logic ([7ac9f0a](https://github.com/amplitude/Amplitude-TypeScript/commit/7ac9f0a737bdbb346df87ccb7edc19ec8b029264)) +* **session-replay:** add forceRestart option to recordEvents method ([480f9be](https://github.com/amplitude/Amplitude-TypeScript/commit/480f9be93869e881f13958c693fae7fbfd6a4ee7)) +* **session-replay:** enhance debug message to include sessionId when starting a new capture ([c68312f](https://github.com/amplitude/Amplitude-TypeScript/commit/c68312f19611c7f5811d1fc1ee82146bc205a89b)) +* **session-replay:** require recordEvents() params ([f75a512](https://github.com/amplitude/Amplitude-TypeScript/commit/f75a51292a5cb894504d4f4a65f59b10d0e1ebbb)) +* **session-replay:** update debug message for existing capture with forceRestart option ([c81dab9](https://github.com/amplitude/Amplitude-TypeScript/commit/c81dab9a6990efbe52cd7fa4ddbe63a125547345)) + + +### Features + +* **session-replay:** add forceRestart param to initialize ([fda1c00](https://github.com/amplitude/Amplitude-TypeScript/commit/fda1c00d3e955423c8b2d1da47d59b161788df37)) +* **session-replay:** enable recordEvents method to accept configuration object ([743e4af](https://github.com/amplitude/Amplitude-TypeScript/commit/743e4af622a6093f6feecd3863285710e142d86f)) + + + + + ## [1.28.14](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/session-replay-browser@1.28.13...@amplitude/session-replay-browser@1.28.14) (2025-09-25) diff --git a/packages/session-replay-browser/package.json b/packages/session-replay-browser/package.json index 2593032d4..62503916f 100644 --- a/packages/session-replay-browser/package.json +++ b/packages/session-replay-browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/session-replay-browser", - "version": "1.28.14", + "version": "1.29.0-fixsdk-init.0", "description": "", "author": "Amplitude Inc", "homepage": "https://github.com/amplitude/Amplitude-TypeScript", diff --git a/packages/session-replay-browser/src/session-replay.ts b/packages/session-replay-browser/src/session-replay.ts index 344e9ebf6..d4a9acb5b 100644 --- a/packages/session-replay-browser/src/session-replay.ts +++ b/packages/session-replay-browser/src/session-replay.ts @@ -236,9 +236,14 @@ export class SessionReplay implements AmplitudeSessionReplay { } if (this.config?.targetingConfig) { - await this.evaluateTargetingAndCapture({ userProperties: options?.userProperties }); + // NOTE: By passing isInit=true here, it ensures that a new recording is started + const isInit = true; + await this.evaluateTargetingAndCapture({ userProperties: options?.userProperties }, isInit); } else { - await this.recordEvents(); + await this.recordEvents({ + forceRestart: true, + shouldLogMetadata: true, + }); } } @@ -287,7 +292,10 @@ export class SessionReplay implements AmplitudeSessionReplay { focusListener = () => { // Restart recording on focus to ensure that when user // switches tabs, we take a full snapshot - void this.recordEvents(false); + void this.recordEvents({ + shouldLogMetadata: false, + forceRestart: true, + }); }; /** @@ -362,11 +370,11 @@ export class SessionReplay implements AmplitudeSessionReplay { ); } - if (isInit) { - void this.initialize(true); - } else { - await this.recordEvents(); - } + // NOTE: Both of these values are equal to the value of isInit. + // Create constants for clarity. + const shouldSendStoredEvents = isInit; + const forceRestart = isInit; + void this.initialize(shouldSendStoredEvents, forceRestart); }; sendEvents(sessionId?: string | number) { @@ -378,7 +386,7 @@ export class SessionReplay implements AmplitudeSessionReplay { this.eventsManager.sendCurrentSequenceEvents({ sessionId: sessionIdToSend, deviceId }); } - async initialize(shouldSendStoredEvents = false) { + async initialize(shouldSendStoredEvents = false, forceRestart = true) { if (!this.identifiers?.sessionId) { this.loggerProvider.log(`Session is not being recorded due to lack of session id.`); return Promise.resolve(); @@ -391,7 +399,10 @@ export class SessionReplay implements AmplitudeSessionReplay { } this.eventsManager && shouldSendStoredEvents && void this.eventsManager.sendStoredEvents({ deviceId }); - return this.recordEvents(); + return this.recordEvents({ + forceRestart, + shouldLogMetadata: true, + }); } shouldOptOut() { @@ -546,13 +557,21 @@ export class SessionReplay implements AmplitudeSessionReplay { } } - async recordEvents(shouldLogMetadata = true) { + async recordEvents({ shouldLogMetadata, forceRestart }: { shouldLogMetadata: boolean; forceRestart: boolean }) { const config = this.config; const shouldRecord = this.getShouldRecord(); const sessionId = this.identifiers?.sessionId; if (!shouldRecord || !sessionId || !config) { return; } + + // NOTE: If there is already an existing active recording, exit early unless forceRestart is true + if (this.recordCancelCallback && !forceRestart) { + this.loggerProvider.debug(`A capture is already in progress and forceRestart is false. Not restarting.`); + return; + } + this.loggerProvider.debug(`Starting new capture for session with sessionId=${sessionId}.`); + this.stopRecordingEvents(); const recordFunction = await this.getRecordFunction(); diff --git a/packages/session-replay-browser/src/version.ts b/packages/session-replay-browser/src/version.ts index f2a81a79e..7b89533a9 100644 --- a/packages/session-replay-browser/src/version.ts +++ b/packages/session-replay-browser/src/version.ts @@ -1,2 +1,2 @@ // Autogenerated by `yarn version-file`. DO NOT EDIT -export const VERSION = '1.28.14'; +export const VERSION = '1.29.0-fixsdk-init.0'; diff --git a/packages/session-replay-browser/test/session-replay.test.ts b/packages/session-replay-browser/test/session-replay.test.ts index 659171a7e..11d881530 100644 --- a/packages/session-replay-browser/test/session-replay.test.ts +++ b/packages/session-replay-browser/test/session-replay.test.ts @@ -248,14 +248,14 @@ describe('SessionReplay', () => { await sessionReplay.init(apiKey, mockOptions).promise; const startSpy = jest.spyOn(NetworkObservers.prototype, 'start'); - await sessionReplay.recordEvents(); + await sessionReplay.recordEvents({ forceRestart: true, shouldLogMetadata: true }); expect(startSpy).toHaveBeenCalled(); }); test('should not start network observers when network logging is disabled in remote config', async () => { await sessionReplay.init(apiKey, mockOptions).promise; const startSpy = jest.spyOn(NetworkObservers.prototype, 'start'); - await sessionReplay.recordEvents(); + await sessionReplay.recordEvents({ forceRestart: true, shouldLogMetadata: true }); expect(startSpy).not.toHaveBeenCalled(); }); @@ -610,7 +610,7 @@ describe('SessionReplay', () => { expect(initialize).toHaveBeenCalledTimes(1); - expect(initialize.mock.calls[0]).toEqual([true]); + expect(initialize.mock.calls[0]).toEqual([true, true]); }); test('should set up blur and focus event listeners', async () => { const initialize = jest.spyOn(sessionReplay, 'initialize'); @@ -701,7 +701,7 @@ describe('SessionReplay', () => { ...mockOptions, ...options, }).promise; - await sessionReplay.recordEvents(); + await sessionReplay.recordEvents({ forceRestart: true, shouldLogMetadata: true }); const recordArg = mockRecordFunction.mock.calls[0][0]; expect(recordArg?.applyBackgroundColorToBlockedElements).toBe(expectedValue); @@ -820,17 +820,17 @@ describe('SessionReplay', () => { const userProperties = { age: 30, city: 'New York' }; await (sessionReplay as any).asyncSetSessionId(456, '9l8m7n', { userProperties }); - expect(evaluateTargetingAndCaptureSpy).toHaveBeenCalledWith({ userProperties }); + expect(evaluateTargetingAndCaptureSpy).toHaveBeenCalledWith({ userProperties }, true); // Test without userProperties (options is undefined) await (sessionReplay as any).asyncSetSessionId(789, '9l8m7n'); - expect(evaluateTargetingAndCaptureSpy).toHaveBeenCalledWith({ userProperties: undefined }); + expect(evaluateTargetingAndCaptureSpy).toHaveBeenCalledWith({ userProperties: undefined }, true); // Test with empty options await (sessionReplay as any).asyncSetSessionId(101, '9l8m7n', {}); - expect(evaluateTargetingAndCaptureSpy).toHaveBeenCalledWith({ userProperties: undefined }); + expect(evaluateTargetingAndCaptureSpy).toHaveBeenCalledWith({ userProperties: undefined }, true); }); test('should call recordEvents when no targetingConfig', async () => { @@ -1265,7 +1265,7 @@ describe('SessionReplay', () => { const existingRecordFunction = jest.spyOn(SessionReplay.prototype, 'getRecordFunction' as any); existingRecordFunction.mockResolvedValue(recordFunction); - await sessionReplay.recordEvents(); + await sessionReplay.recordEvents({ forceRestart: true, shouldLogMetadata: true }); // Verify recordEvents was called but mockRecordFunction was not expect(recordEventsSpy).toHaveBeenCalledTimes(1); @@ -1290,7 +1290,7 @@ describe('SessionReplay', () => { const existingRecordFunction = jest.spyOn(SessionReplay.prototype, 'getRecordFunction' as any); existingRecordFunction.mockResolvedValue(recordFunction); - await sessionReplay.recordEvents(); + await sessionReplay.recordEvents({ forceRestart: true, shouldLogMetadata: true }); expect(recordFunction).not.toHaveBeenCalled(); }); @@ -1298,7 +1298,7 @@ describe('SessionReplay', () => { test('should return early if user opts out', async () => { await sessionReplay.init(apiKey, { ...mockOptions, optOut: true, privacyConfig: { blockSelector: ['#class'] } }) .promise; - await sessionReplay.recordEvents(); + await sessionReplay.recordEvents({ forceRestart: true, shouldLogMetadata: true }); expect(mockRecordFunction).not.toHaveBeenCalled(); if (!sessionReplay.eventsManager) { throw new Error('Did not call init'); @@ -1311,13 +1311,13 @@ describe('SessionReplay', () => { await sessionReplay.init(apiKey, mockOptions).promise; const stopRecordingMock = jest.fn(); sessionReplay.recordCancelCallback = stopRecordingMock; - await sessionReplay.recordEvents(); + await sessionReplay.recordEvents({ forceRestart: true, shouldLogMetadata: true }); expect(stopRecordingMock).toHaveBeenCalled(); }); test('should stop recording and send events if user opts out during recording', async () => { await sessionReplay.init(apiKey, mockOptions).promise; - await sessionReplay.recordEvents(); + await sessionReplay.recordEvents({ forceRestart: true, shouldLogMetadata: true }); const stopRecordingMock = jest.fn(); sessionReplay.recordCancelCallback = stopRecordingMock; if (!sessionReplay.eventsManager) { @@ -1343,7 +1343,7 @@ describe('SessionReplay', () => { test('should add an error handler', async () => { await sessionReplay.init(apiKey, mockOptions).promise; - await sessionReplay.recordEvents(); + await sessionReplay.recordEvents({ forceRestart: true, shouldLogMetadata: true }); const recordArg = mockRecordFunction.mock.calls[0][0]; const errorHandlerReturn = recordArg?.errorHandler && recordArg?.errorHandler(new Error('test error')); // eslint-disable-next-line @typescript-eslint/unbound-method @@ -1353,7 +1353,7 @@ describe('SessionReplay', () => { test('should add slim dom options', async () => { await sessionReplay.init(apiKey, { ...mockOptions, omitElementTags: { script: true, comment: true } }).promise; - await sessionReplay.recordEvents(); + await sessionReplay.recordEvents({ forceRestart: true, shouldLogMetadata: true }); const recordArg = mockRecordFunction.mock.calls[0][0]; expect(recordArg?.slimDOMOptions).toEqual({ script: true, comment: true }); }); @@ -1361,7 +1361,7 @@ describe('SessionReplay', () => { test('should rethrow CSSStylesheet errors', async () => { const sessionReplay = new SessionReplay(); await sessionReplay.init(apiKey, mockOptions).promise; - await sessionReplay.recordEvents(); + await sessionReplay.recordEvents({ forceRestart: true, shouldLogMetadata: true }); const recordArg = mockRecordFunction.mock.calls[0][0]; const stylesheetErrorMessage = "Failed to execute 'insertRule' on 'CSSStyleSheet': Failed to parse the rule 'body::-ms-expand{display: none}"; @@ -1373,7 +1373,7 @@ describe('SessionReplay', () => { test('should rethrow external errors', async () => { const sessionReplay = new SessionReplay(); await sessionReplay.init(apiKey, mockOptions).promise; - await sessionReplay.recordEvents(); + await sessionReplay.recordEvents({ forceRestart: true, shouldLogMetadata: true }); const recordArg = mockRecordFunction.mock.calls[0][0]; const error = new Error('test') as Error & { _external_?: boolean }; error._external_ = true; @@ -1385,7 +1385,7 @@ describe('SessionReplay', () => { test('should not add hooks if interaction config is not enabled', async () => { const sessionReplay = new SessionReplay(); await sessionReplay.init(apiKey, mockOptions).promise; - await sessionReplay.recordEvents(); + await sessionReplay.recordEvents({ forceRestart: true, shouldLogMetadata: true }); const recordArg = mockRecordFunction.mock.calls[0][0]; const error = new Error('test') as Error & { _external_?: boolean }; error._external_ = true; @@ -1404,7 +1404,7 @@ describe('SessionReplay', () => { const sessionReplay = new SessionReplay(); await sessionReplay.init(apiKey, mockOptions).promise; - await sessionReplay.recordEvents(); + await sessionReplay.recordEvents({ forceRestart: true, shouldLogMetadata: true }); const recordArg = mockRecordFunction.mock.calls[0][0]; const error = new Error('test') as Error & { _external_?: boolean }; error._external_ = true; @@ -1418,7 +1418,7 @@ describe('SessionReplay', () => { throw new Error('record failed'); }); const warnSpy = jest.spyOn(sessionReplay.loggerProvider, 'warn'); - await sessionReplay.recordEvents(); + await sessionReplay.recordEvents({ forceRestart: true, shouldLogMetadata: true }); expect(warnSpy).toHaveBeenCalledWith('Failed to initialize session replay:', expect.any(Error)); }); @@ -1433,7 +1433,7 @@ describe('SessionReplay', () => { const sessionReplay = new SessionReplay(); await sessionReplay.init(apiKey, mockOptions).promise; - await sessionReplay.recordEvents(); + await sessionReplay.recordEvents({ forceRestart: true, shouldLogMetadata: true }); const recordArg = mockRecordFunction.mock.calls[0][0]; const mouseInteractionHook = recordArg?.hooks?.mouseInteraction; @@ -1455,7 +1455,7 @@ describe('SessionReplay', () => { const sessionReplay = new SessionReplay(); await sessionReplay.init(apiKey, mockOptions).promise; - await sessionReplay.recordEvents(); + await sessionReplay.recordEvents({ forceRestart: true, shouldLogMetadata: true }); const recordArg = mockRecordFunction.mock.calls[0][0]; const mouseInteractionHook = recordArg?.hooks?.mouseInteraction; @@ -1475,7 +1475,7 @@ describe('SessionReplay', () => { const sessionReplay = new SessionReplay(); await sessionReplay.init(apiKey, mockOptions).promise; - await sessionReplay.recordEvents(); + await sessionReplay.recordEvents({ forceRestart: true, shouldLogMetadata: true }); const recordArg = mockRecordFunction.mock.calls[0][0]; const mouseInteractionHook = recordArg?.hooks?.mouseInteraction; @@ -1499,7 +1499,7 @@ describe('SessionReplay', () => { const sessionReplay = new SessionReplay(); const getPageUrlSpy = jest.spyOn(Helpers, 'getPageUrl'); await sessionReplay.init(apiKey, mockOptions).promise; - await sessionReplay.recordEvents(); + await sessionReplay.recordEvents({ forceRestart: true, shouldLogMetadata: true }); const recordArg = mockRecordFunction.mock.calls[0][0]; const metaEvent = { @@ -1528,7 +1528,7 @@ describe('SessionReplay', () => { const sessionReplay = new SessionReplay(); const getPageUrlSpy = jest.spyOn(Helpers, 'getPageUrl').mockReturnValue('https://example.com/sensitive-page'); await sessionReplay.init(apiKey, mockOptions).promise; - await sessionReplay.recordEvents(); + await sessionReplay.recordEvents({ forceRestart: true, shouldLogMetadata: true }); const recordArg = mockRecordFunction.mock.calls[0][0]; const originalHref = 'https://example.com/sensitive-page'; @@ -1558,7 +1558,7 @@ describe('SessionReplay', () => { const sessionReplay = new SessionReplay(); const getPageUrlSpy = jest.spyOn(Helpers, 'getPageUrl').mockReturnValue('https://example.com/sensitive-page'); await sessionReplay.init(apiKey, mockOptions).promise; - await sessionReplay.recordEvents(); + await sessionReplay.recordEvents({ forceRestart: true, shouldLogMetadata: true }); const recordArg = mockRecordFunction.mock.calls[0][0]; const originalHref = 'https://example.com/sensitive-page'; @@ -1589,7 +1589,7 @@ describe('SessionReplay', () => { const sessionReplay = new SessionReplay(); const getPageUrlSpy = jest.spyOn(Helpers, 'getPageUrl').mockReturnValue('https://example.com/sensitive-page'); await sessionReplay.init(apiKey, mockOptions).promise; - await sessionReplay.recordEvents(); + await sessionReplay.recordEvents({ forceRestart: true, shouldLogMetadata: true }); const recordArg = mockRecordFunction.mock.calls[0][0]; const originalHref = 'https://example.com/sensitive-page'; @@ -1621,7 +1621,7 @@ describe('SessionReplay', () => { const sessionReplay = new SessionReplay(); const getPageUrlSpy = jest.spyOn(Helpers, 'getPageUrl'); await sessionReplay.init(apiKey, mockOptions).promise; - await sessionReplay.recordEvents(); + await sessionReplay.recordEvents({ forceRestart: true, shouldLogMetadata: true }); const recordArg = mockRecordFunction.mock.calls[0][0]; const originalHref = 'https://example.com/sensitive-page'; @@ -1652,7 +1652,7 @@ describe('SessionReplay', () => { const sessionReplay = new SessionReplay(); const getPageUrlSpy = jest.spyOn(Helpers, 'getPageUrl'); await sessionReplay.init(apiKey, mockOptions).promise; - await sessionReplay.recordEvents(); + await sessionReplay.recordEvents({ forceRestart: true, shouldLogMetadata: true }); const recordArg = mockRecordFunction.mock.calls[0][0]; const metaEvent = { @@ -1682,7 +1682,7 @@ describe('SessionReplay', () => { const sessionReplay = new SessionReplay(); const getPageUrlSpy = jest.spyOn(Helpers, 'getPageUrl').mockReturnValue('https://example.com/sensitive-page'); await sessionReplay.init(apiKey, mockOptions).promise; - await sessionReplay.recordEvents(); + await sessionReplay.recordEvents({ forceRestart: true, shouldLogMetadata: true }); // Set config to undefined to test optional chaining sessionReplay.config = undefined; @@ -1717,7 +1717,7 @@ describe('SessionReplay', () => { const sessionReplay = new SessionReplay(); const getPageUrlSpy = jest.spyOn(Helpers, 'getPageUrl').mockReturnValue('https://example.com/sensitive-page'); await sessionReplay.init(apiKey, mockOptions).promise; - await sessionReplay.recordEvents(); + await sessionReplay.recordEvents({ forceRestart: true, shouldLogMetadata: true }); const recordArg = mockRecordFunction.mock.calls[0][0]; const originalHref = 'https://example.com/sensitive-page'; @@ -1749,7 +1749,7 @@ describe('SessionReplay', () => { const sessionReplay = new SessionReplay(); const getPageUrlSpy = jest.spyOn(Helpers, 'getPageUrl').mockReturnValue('https://example.com/sensitive-page'); await sessionReplay.init(apiKey, mockOptions).promise; - await sessionReplay.recordEvents(); + await sessionReplay.recordEvents({ forceRestart: true, shouldLogMetadata: true }); // Manually set interactionConfig to null to test optional chaining if (sessionReplay.config) { @@ -1789,7 +1789,7 @@ describe('SessionReplay', () => { const sessionReplay = new SessionReplay(); const getPageUrlSpy = jest.spyOn(Helpers, 'getPageUrl').mockReturnValue('https://example.com/sensitive-page'); await sessionReplay.init(apiKey, mockOptions).promise; - await sessionReplay.recordEvents(); + await sessionReplay.recordEvents({ forceRestart: true, shouldLogMetadata: true }); const recordArg = mockRecordFunction.mock.calls[0][0]; const originalHref = 'https://example.com/sensitive-page'; @@ -1807,6 +1807,28 @@ describe('SessionReplay', () => { expect(getPageUrlSpy).toHaveBeenCalledWith(originalHref, []); expect(metaEvent.data.href).toBe(originalHref); }); + + test.each([ + { description: 'forceRestart true', forceRestart: true, expectedNumberOfRecordCalls: 1 }, + { description: 'forceRestart false', forceRestart: false, expectedNumberOfRecordCalls: 0 }, + ])( + 'should not call recordFunction() if there is an active recording and forceRestart is false', + async ({ forceRestart, expectedNumberOfRecordCalls }) => { + // Arrange + const shouldLogMetadata = true; + + await sessionReplay.init(apiKey, mockOptions).promise; + await sessionReplay.recordEvents({ forceRestart: true, shouldLogMetadata: true }); // Start initial recording to create the active recording state + + mockRecordFunction.mockClear(); // Clear previous calls + + // Act - Attempt to start recording again when forceRestart is false + await sessionReplay.recordEvents({ shouldLogMetadata, forceRestart }); + + // Assert + expect(mockRecordFunction).toHaveBeenCalledTimes(expectedNumberOfRecordCalls); + }, + ); }); }); @@ -2269,7 +2291,7 @@ describe('SessionReplay', () => { responseBody: '', }; - await sessionReplay.recordEvents(); + await sessionReplay.recordEvents({ forceRestart: true, shouldLogMetadata: true }); expect(mockStart).toHaveBeenCalled(); const startCallback = mockStart.mock.calls[0][0] as (event: NetworkRequestEvent) => void; @@ -2513,7 +2535,7 @@ describe('SessionReplay', () => { // Spy on the record function to ensure it's not called mockRecordFunction.mockClear(); - await sessionReplay.recordEvents(); + await sessionReplay.recordEvents({ forceRestart: true, shouldLogMetadata: true }); expect(mockRecordFunction).not.toHaveBeenCalled(); expect(getRecordFunctionSpy).toHaveBeenCalled(); @@ -2529,7 +2551,7 @@ describe('SessionReplay', () => { mockRecordFunction.mockClear(); - await sessionReplay.recordEvents(); + await sessionReplay.recordEvents({ forceRestart: true, shouldLogMetadata: true }); expect(mockRecordFunction).not.toHaveBeenCalled(); expect(getRecordFunctionSpy).toHaveBeenCalled(); @@ -2584,7 +2606,7 @@ describe('SessionReplay', () => { await sessionReplay.evaluateTargetingAndCapture({}, true); - expect(initializeSpy).toHaveBeenCalledWith(true); + expect(initializeSpy).toHaveBeenCalledWith(true, true); }); test('should call recordEvents when isInit is false', async () => { @@ -2743,4 +2765,87 @@ describe('SessionReplay', () => { ); }); }); + + describe('restart behavior', () => { + test('should restart when the session id changes', async () => { + // Arrange + const recordFunction = createMockRecordFunction(); + jest.spyOn(SessionReplay.prototype, 'getRecordFunction' as any).mockResolvedValue(recordFunction); + const sessionReplay = new SessionReplay(); + await sessionReplay.init(apiKey, mockOptions).promise; + + if (!sessionReplay.eventsManager || !sessionReplay.joinedConfigGenerator || !sessionReplay.config) { + throw new Error('Init not called'); + } + + const updatedConfig = { ...sessionReplay.config, sampleRate: 0.9 }; + const generateJoinedConfigPromise = Promise.resolve({ + joinedConfig: updatedConfig, + localConfig: updatedConfig, + remoteConfig: undefined, + }); + jest + .spyOn(sessionReplay.joinedConfigGenerator, 'generateJoinedConfig') + .mockReturnValue(generateJoinedConfigPromise); + + // Clear any calls from initialization + recordFunction.mockClear(); + expect(recordFunction).not.toHaveBeenCalled(); + + // Act + await sessionReplay.setSessionId(456).promise; + await generateJoinedConfigPromise; + + // Assert + expect(recordFunction).toHaveBeenCalled(); + }); + + test('should restart when the focus changes', async () => { + // Arrange + const recordFunction = createMockRecordFunction(); + jest.spyOn(SessionReplay.prototype, 'getRecordFunction' as any).mockResolvedValue(recordFunction); + const sessionReplay = new SessionReplay(); + await sessionReplay.init(apiKey, mockOptions).promise; + + // Clear any calls from initialization + recordFunction.mockClear(); + expect(recordFunction).not.toHaveBeenCalled(); + + // Act + const focusCallback = addEventListenerMock.mock.calls[1][1]; + await focusCallback(); + + // Assert + expect(recordFunction).toHaveBeenCalled(); + }); + + test('should not restart when an event is executed', async () => { + // Arrange + const event = { event_type: 'page_view' }; + const userProperties = { city: 'San Francisco' }; + const recordFunction = createMockRecordFunction(); + jest.spyOn(SessionReplay.prototype, 'getRecordFunction' as any).mockResolvedValue(recordFunction); + const sessionReplay = new SessionReplay(); + + // Spy on initialize to track when it's called + const initializeSpy = jest.spyOn(sessionReplay, 'initialize'); + + await sessionReplay.init(apiKey, mockOptions).promise; + + // Wait for the async initialize call to complete (it's called with void in evaluateTargetingAndCapture) + if (initializeSpy.mock.calls.length > 0) { + await initializeSpy.mock.results[0]?.value; + } + + // Clear any calls from initialization + recordFunction.mockClear(); + expect(recordFunction).not.toHaveBeenCalled(); + + // Act - call evaluateTargetingAndCapture with an event the same way the plugin's execute() method does + await sessionReplay.evaluateTargetingAndCapture({ event, userProperties }, false); + + // Assert - recordFunction should not have been called again after init + expect(recordFunction).not.toHaveBeenCalled(); + }); + }); });