From f1c0105febe66f509510bbc7d58f1a468c813be2 Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto Date: Sat, 9 May 2026 07:38:00 -0400 Subject: [PATCH] fix: handle invalid queued replay config JSON --- .changeset/quiet-badgers-parse.md | 5 ++++ .../replay/lazy-sessionrecording.test.ts | 20 ++++++++++++++ .../src/__tests__/posthog-core.test.ts | 17 ++++++++++++ .../external/lazy-loaded-session-recorder.ts | 10 ++++++- .../extensions/replay/session-recording.ts | 10 ++++++- packages/browser/src/posthog-core.ts | 26 ++++++++++++------- 6 files changed, 77 insertions(+), 11 deletions(-) create mode 100644 .changeset/quiet-badgers-parse.md diff --git a/.changeset/quiet-badgers-parse.md b/.changeset/quiet-badgers-parse.md new file mode 100644 index 0000000000..4669e5c7e7 --- /dev/null +++ b/.changeset/quiet-badgers-parse.md @@ -0,0 +1,5 @@ +--- +'posthog-js': patch +--- + +Handle invalid persisted session replay config JSON gracefully diff --git a/packages/browser/src/__tests__/extensions/replay/lazy-sessionrecording.test.ts b/packages/browser/src/__tests__/extensions/replay/lazy-sessionrecording.test.ts index 3f5db9f70f..688bf0d405 100644 --- a/packages/browser/src/__tests__/extensions/replay/lazy-sessionrecording.test.ts +++ b/packages/browser/src/__tests__/extensions/replay/lazy-sessionrecording.test.ts @@ -410,6 +410,26 @@ describe('Lazy SessionRecording', () => { expect(result?.enabled).toBe(true) }) + it('ignores invalid persisted JSON config when checking freshness', () => { + posthog.persistence?.register({ + [SESSION_RECORDING_REMOTE_CONFIG]: '{not json', + }) + + expect(sessionRecording['_isRemoteConfigFresh']()).toBe(false) + expect(posthog.get_property(SESSION_RECORDING_REMOTE_CONFIG)).toBe('{not json') + }) + + it('ignores invalid persisted JSON config when reading remote config', () => { + posthog.persistence?.register({ + [SESSION_RECORDING_REMOTE_CONFIG]: '{not json', + }) + + const result = sessionRecording['_lazyLoadedSessionRecording']['_remoteConfig'] + + expect(result).toBeUndefined() + expect(posthog.get_property(SESSION_RECORDING_REMOTE_CONFIG)).toBe('{not json') + }) + it('trusts stale config once recording has started (long-lived SPA)', () => { expect(sessionRecording['_lazyLoadedSessionRecording'].isStarted).toBe(true) diff --git a/packages/browser/src/__tests__/posthog-core.test.ts b/packages/browser/src/__tests__/posthog-core.test.ts index 8653b4dfa6..24c2f53d12 100644 --- a/packages/browser/src/__tests__/posthog-core.test.ts +++ b/packages/browser/src/__tests__/posthog-core.test.ts @@ -585,5 +585,22 @@ describe('posthog core', () => { registerSpy.mockRestore() captureSpy.mockRestore() }) + + it('should not abort queued calls when one call throws', () => { + const posthog = defaultPostHog() + const captureSpy = jest.spyOn(posthog, 'capture').mockImplementation() + ;(posthog as any).parseInvalidJson = (payload: string) => JSON.parse(payload) + + expect(() => { + posthog._execute_array([ + ['parseInvalidJson', '{not json'], + ['capture', 'test-event'], + ]) + }).not.toThrow() + + expect(captureSpy).toHaveBeenCalledWith('test-event') + captureSpy.mockRestore() + delete (posthog as any).parseInvalidJson + }) }) }) diff --git a/packages/browser/src/extensions/replay/external/lazy-loaded-session-recorder.ts b/packages/browser/src/extensions/replay/external/lazy-loaded-session-recorder.ts index 27995684ed..fbbd4f6c5b 100644 --- a/packages/browser/src/extensions/replay/external/lazy-loaded-session-recorder.ts +++ b/packages/browser/src/extensions/replay/external/lazy-loaded-session-recorder.ts @@ -743,7 +743,15 @@ export class LazyLoadedSessionRecording implements LazyLoadedSessionRecordingInt if (!persistedConfig) { return undefined } - const parsedConfig = isObject(persistedConfig) ? persistedConfig : JSON.parse(persistedConfig) + let parsedConfig: SessionRecordingPersistedConfig + try { + parsedConfig = isObject(persistedConfig) ? persistedConfig : JSON.parse(persistedConfig) + } catch (e) { + // Do not unregister here: the SDK only registers structured configs, and this read path should + // ignore corrupt legacy/external values without mutating persistence. + logger.warn('persisted remote config for session recording is invalid and will be ignored', e) + return undefined + } // Only check TTL if recording hasn't started yet // Once started, trust the config until a hard page load diff --git a/packages/browser/src/extensions/replay/session-recording.ts b/packages/browser/src/extensions/replay/session-recording.ts index a524368283..cae95da2b0 100644 --- a/packages/browser/src/extensions/replay/session-recording.ts +++ b/packages/browser/src/extensions/replay/session-recording.ts @@ -273,7 +273,15 @@ export class SessionRecording implements Extension { if (!persistedConfig) { return false } - const config = typeof persistedConfig === 'object' ? persistedConfig : JSON.parse(persistedConfig) + let config: SessionRecordingPersistedConfig + try { + config = typeof persistedConfig === 'object' ? persistedConfig : JSON.parse(persistedConfig) + } catch (e) { + // Do not unregister here: the SDK only registers structured configs, and this read path should + // ignore corrupt legacy/external values without mutating persistence. + logger.warn('persisted remote config for session recording is invalid and will be ignored', e) + return false + } // default to now so that configs persisted by older SDK versions // (which never set cache_timestamp) are treated as fresh const cacheTimestamp = config.cache_timestamp ?? Date.now() diff --git a/packages/browser/src/posthog-core.ts b/packages/browser/src/posthog-core.ts index 5cc74127f6..3a3f962588 100644 --- a/packages/browser/src/posthog-core.ts +++ b/packages/browser/src/posthog-core.ts @@ -1090,7 +1090,11 @@ export class PostHog implements PostHogInterface { if (isArray(fn_name)) { capturing_calls.push(item) // chained call e.g. posthog.get_group().set() } else if (isFunction(item)) { - ;(item as any).call(this) + try { + ;(item as any).call(this) + } catch (e) { + logger.error('Error executing queued PostHog call', item, e) + } } else if (isArray(item) && fn_name === 'alias') { alias_calls.push(item) } else if ( @@ -1107,14 +1111,18 @@ export class PostHog implements PostHogInterface { const execute = function (calls: SnippetArrayItem[], thisArg: any) { eachArray(calls, function (item) { - if (isArray(item[0])) { - // chained call - let caller = thisArg - each(item, function (call) { - caller = caller[call[0]].apply(caller, call.slice(1)) - }) - } else { - thisArg[item[0]].apply(thisArg, item.slice(1)) + try { + if (isArray(item[0])) { + // chained call + let caller = thisArg + each(item, function (call) { + caller = caller[call[0]].apply(caller, call.slice(1)) + }) + } else { + thisArg[item[0]].apply(thisArg, item.slice(1)) + } + } catch (e) { + logger.error('Error executing queued PostHog call', item, e) } }) }