Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions packages/session-replay-browser/src/session-replay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -510,6 +510,20 @@ export class SessionReplay implements AmplitudeSessionReplay {
}

const shouldRecord = this.getShouldRecord();

// Self-heal: if this session should be recording but there is no active recording
// (e.g. an external re-init/teardown called stopRecordingEvents and left us stopped)
// and a start isn't already in flight, resume recording. This runs on every event via
// the plugin's execute() path, so — unlike the targeting-config restart path — it
// restores recording even when no targeting config is set. Mirrors focusListener's
// reconcile, and is a no-op once recording is active (recordCancelCallback is set).
if (shouldRecord && !this.recordCancelCallback && !this.recordEventsInFlight) {
this.loggerProvider.log(
'Resuming Session Replay recording: session should be recorded but no ongoing recording.',
);
void this.recordEvents();
}

let eventProperties: { [key: string]: string | null } = {};

if (shouldRecord) {
Expand Down
59 changes: 59 additions & 0 deletions packages/session-replay-browser/test/session-replay.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1842,6 +1842,65 @@ describe('SessionReplay', () => {
});
});

test('self-heal: resumes recording when session should record but no ongoing recording (no targeting config)', async () => {
await sessionReplay.init(apiKey, mockOptions).promise;
// Simulate an external re-init/teardown having stopped recording.
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
(sessionReplay as any).recordCancelCallback = null;
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
(sessionReplay as any).recordEventsInFlight = false;
sessionReplay.getShouldRecord = () => true;
// No targeting config on this config — self-heal must not depend on one.
if (sessionReplay.config) {
sessionReplay.config.targetingConfig = undefined;
}
const recordEventsSpy = jest.spyOn(sessionReplay, 'recordEvents').mockResolvedValue(undefined);

sessionReplay.getSessionReplayProperties();

expect(recordEventsSpy).toHaveBeenCalledTimes(1);
});

test('self-heal: does not resume recording when recording is already active', async () => {
await sessionReplay.init(apiKey, mockOptions).promise;
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
(sessionReplay as any).recordCancelCallback = jest.fn();
sessionReplay.getShouldRecord = () => true;
const recordEventsSpy = jest.spyOn(sessionReplay, 'recordEvents').mockResolvedValue(undefined);

sessionReplay.getSessionReplayProperties();

expect(recordEventsSpy).not.toHaveBeenCalled();
});

test('self-heal: does not resume recording when a start is already in flight', async () => {
await sessionReplay.init(apiKey, mockOptions).promise;
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
(sessionReplay as any).recordCancelCallback = null;
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
(sessionReplay as any).recordEventsInFlight = true;
sessionReplay.getShouldRecord = () => true;
const recordEventsSpy = jest.spyOn(sessionReplay, 'recordEvents').mockResolvedValue(undefined);

sessionReplay.getSessionReplayProperties();

expect(recordEventsSpy).not.toHaveBeenCalled();
});

test('self-heal: does not resume recording when session should not record', async () => {
await sessionReplay.init(apiKey, mockOptions).promise;
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
(sessionReplay as any).recordCancelCallback = null;
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
(sessionReplay as any).recordEventsInFlight = false;
sessionReplay.getShouldRecord = () => false;
const recordEventsSpy = jest.spyOn(sessionReplay, 'recordEvents').mockResolvedValue(undefined);

sessionReplay.getSessionReplayProperties();

expect(recordEventsSpy).not.toHaveBeenCalled();
});

test('should ignore focus handler when debug mode is on.', async () => {
jest.spyOn(AnalyticsCore, 'getGlobalScope').mockReturnValue({
...mockGlobalScope,
Expand Down
Loading