From dc11e22e7b49a8c848356d99bd0bc22dd3090bb7 Mon Sep 17 00:00:00 2001 From: Xinyi Ye Date: Fri, 15 May 2026 22:23:35 -0700 Subject: [PATCH 1/2] fix(analytics-browser): guard snippet bootstrap against undefined _iq and _q When window.amplitude (or window.amplitudeGTM) is pre-existing with invoked=true but missing _iq/_q, Object.keys(undefined) and queue.length both throw uncaught TypeErrors during SDK init. Together these account for ~70% of uncaught SDK errors in the planning section of the diagnostics dashboard. Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/analytics-browser/src/gtm-snippet-index.ts | 2 +- packages/analytics-browser/src/snippet-index.ts | 2 +- .../analytics-browser/src/utils/snippet-helper.ts | 3 +++ .../test/utils/snippet-helper.test.ts | 11 +++++++++++ 4 files changed, 16 insertions(+), 2 deletions(-) diff --git a/packages/analytics-browser/src/gtm-snippet-index.ts b/packages/analytics-browser/src/gtm-snippet-index.ts index 3e47b409b..b6a61facc 100644 --- a/packages/analytics-browser/src/gtm-snippet-index.ts +++ b/packages/analytics-browser/src/gtm-snippet-index.ts @@ -32,7 +32,7 @@ import { runQueuedFunctions } from './utils/snippet-helper'; GlobalScope.amplitudeGTM._q = []; runQueuedFunctions(amplitudeGTM, queue); - const instanceNames = Object.keys(GlobalScope.amplitudeGTM._iq) || []; + const instanceNames = Object.keys(GlobalScope.amplitudeGTM._iq || {}); for (let i = 0; i < instanceNames.length; i++) { const instanceName = instanceNames[i]; const instance = Object.assign(GlobalScope.amplitudeGTM._iq[instanceName], createNamedInstance(instanceName)); diff --git a/packages/analytics-browser/src/snippet-index.ts b/packages/analytics-browser/src/snippet-index.ts index d85b2481d..b0ce78564 100644 --- a/packages/analytics-browser/src/snippet-index.ts +++ b/packages/analytics-browser/src/snippet-index.ts @@ -49,7 +49,7 @@ function resolveCurrentScriptUrl(): string | undefined { GlobalScope.amplitude._q = []; runQueuedFunctions(amplitude, queue); - const instanceNames = Object.keys(GlobalScope.amplitude._iq) || []; + const instanceNames = Object.keys(GlobalScope.amplitude._iq || {}); for (let i = 0; i < instanceNames.length; i++) { const instanceName = instanceNames[i]; const instance = Object.assign(GlobalScope.amplitude._iq[instanceName], createNamedInstance(instanceName)); diff --git a/packages/analytics-browser/src/utils/snippet-helper.ts b/packages/analytics-browser/src/utils/snippet-helper.ts index 9a0b67363..ca9be7f2f 100644 --- a/packages/analytics-browser/src/utils/snippet-helper.ts +++ b/packages/analytics-browser/src/utils/snippet-helper.ts @@ -26,6 +26,9 @@ export const runQueuedFunctions = (instance: object, queue: QueueProxy) => { * Used to convert proxied Identify and Revenue objects. */ export const convertProxyObjectToRealObject = (instance: T, queue: QueueProxy): T => { + if (!queue) { + return instance; + } for (let i = 0; i < queue.length; i++) { const { name, args, resolve } = queue[i]; const fn = instance && instance[name as keyof T]; diff --git a/packages/analytics-browser/test/utils/snippet-helper.test.ts b/packages/analytics-browser/test/utils/snippet-helper.test.ts index c6f6cbe0b..97ea4586d 100644 --- a/packages/analytics-browser/test/utils/snippet-helper.test.ts +++ b/packages/analytics-browser/test/utils/snippet-helper.test.ts @@ -47,5 +47,16 @@ describe('snippet-helper', () => { expect(SnippetHelper.convertProxyObjectToRealObject(instance, queue)).toBe(instance); expect(resolve).toHaveBeenCalledTimes(1); }); + + test('should return instance when queue is undefined', () => { + const instance = { init: jest.fn() }; + expect( + SnippetHelper.convertProxyObjectToRealObject( + instance, + undefined as unknown as Parameters[1], + ), + ).toBe(instance); + expect(instance.init).not.toHaveBeenCalled(); + }); }); }); From c7f45fe1c11d43843e6ee56263e1ae134122004f Mon Sep 17 00:00:00 2001 From: Xinyi Ye Date: Fri, 15 May 2026 23:13:42 -0700 Subject: [PATCH 2/2] fix(analytics-browser): address review feedback on snippet bootstrap guards - Normalize `_q` and `_iq` on the pre-existing global so downstream code (e.g. the GTM wrapper that reads `amplitude._iq[name]` directly) doesn't crash after this bootstrap recovers. Per Codex review on PR #1763. - Widen `runQueuedFunctions` and `convertProxyObjectToRealObject` to accept `QueueProxy | undefined`, matching the runtime behavior added in this PR, and drop the unsafe cast in the regression test. Per Copilot review. Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/analytics-browser/src/gtm-snippet-index.ts | 10 +++++++++- packages/analytics-browser/src/snippet-index.ts | 10 +++++++++- packages/analytics-browser/src/utils/snippet-helper.ts | 4 ++-- .../test/utils/snippet-helper.test.ts | 7 +------ 4 files changed, 21 insertions(+), 10 deletions(-) diff --git a/packages/analytics-browser/src/gtm-snippet-index.ts b/packages/analytics-browser/src/gtm-snippet-index.ts index b6a61facc..a5ad62d7d 100644 --- a/packages/analytics-browser/src/gtm-snippet-index.ts +++ b/packages/analytics-browser/src/gtm-snippet-index.ts @@ -27,12 +27,20 @@ import { runQueuedFunctions } from './utils/snippet-helper'; createInstance: createNamedInstance, }); + // Normalize proxy queues so a pre-existing global with `invoked = true` but + // missing `_q` / `_iq` doesn't crash either this bootstrap or downstream + // GTM wrapper code that reads `amplitudeGTM._iq[name]` directly. + GlobalScope.amplitudeGTM._q = GlobalScope.amplitudeGTM._q || []; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + GlobalScope.amplitudeGTM._iq = GlobalScope.amplitudeGTM._iq || {}; + if (GlobalScope.amplitudeGTM.invoked) { const queue = GlobalScope.amplitudeGTM._q; GlobalScope.amplitudeGTM._q = []; runQueuedFunctions(amplitudeGTM, queue); - const instanceNames = Object.keys(GlobalScope.amplitudeGTM._iq || {}); + const instanceNames = Object.keys(GlobalScope.amplitudeGTM._iq); for (let i = 0; i < instanceNames.length; i++) { const instanceName = instanceNames[i]; const instance = Object.assign(GlobalScope.amplitudeGTM._iq[instanceName], createNamedInstance(instanceName)); diff --git a/packages/analytics-browser/src/snippet-index.ts b/packages/analytics-browser/src/snippet-index.ts index b0ce78564..58c3b90bf 100644 --- a/packages/analytics-browser/src/snippet-index.ts +++ b/packages/analytics-browser/src/snippet-index.ts @@ -44,12 +44,20 @@ function resolveCurrentScriptUrl(): string | undefined { createInstance: createNamedInstance, }); + // Normalize proxy queues so a pre-existing global with `invoked = true` but + // missing `_q` / `_iq` doesn't crash either this bootstrap or downstream + // code (e.g. GTM wrappers that read `amplitude._iq[name]` directly). + GlobalScope.amplitude._q = GlobalScope.amplitude._q || []; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + GlobalScope.amplitude._iq = GlobalScope.amplitude._iq || {}; + if (GlobalScope.amplitude.invoked) { const queue = GlobalScope.amplitude._q; GlobalScope.amplitude._q = []; runQueuedFunctions(amplitude, queue); - const instanceNames = Object.keys(GlobalScope.amplitude._iq || {}); + const instanceNames = Object.keys(GlobalScope.amplitude._iq); for (let i = 0; i < instanceNames.length; i++) { const instanceName = instanceNames[i]; const instance = Object.assign(GlobalScope.amplitude._iq[instanceName], createNamedInstance(instanceName)); diff --git a/packages/analytics-browser/src/utils/snippet-helper.ts b/packages/analytics-browser/src/utils/snippet-helper.ts index ca9be7f2f..ed3fd4920 100644 --- a/packages/analytics-browser/src/utils/snippet-helper.ts +++ b/packages/analytics-browser/src/utils/snippet-helper.ts @@ -17,7 +17,7 @@ interface InstanceProxy { * Applies the proxied functions on the proxied amplitude snippet to an instance of the real object. * @ignore */ -export const runQueuedFunctions = (instance: object, queue: QueueProxy) => { +export const runQueuedFunctions = (instance: object, queue: QueueProxy | undefined) => { convertProxyObjectToRealObject(instance, queue); }; @@ -25,7 +25,7 @@ export const runQueuedFunctions = (instance: object, queue: QueueProxy) => { * Applies the proxied functions on the proxied object to an instance of the real object. * Used to convert proxied Identify and Revenue objects. */ -export const convertProxyObjectToRealObject = (instance: T, queue: QueueProxy): T => { +export const convertProxyObjectToRealObject = (instance: T, queue: QueueProxy | undefined): T => { if (!queue) { return instance; } diff --git a/packages/analytics-browser/test/utils/snippet-helper.test.ts b/packages/analytics-browser/test/utils/snippet-helper.test.ts index 97ea4586d..e435fa102 100644 --- a/packages/analytics-browser/test/utils/snippet-helper.test.ts +++ b/packages/analytics-browser/test/utils/snippet-helper.test.ts @@ -50,12 +50,7 @@ describe('snippet-helper', () => { test('should return instance when queue is undefined', () => { const instance = { init: jest.fn() }; - expect( - SnippetHelper.convertProxyObjectToRealObject( - instance, - undefined as unknown as Parameters[1], - ), - ).toBe(instance); + expect(SnippetHelper.convertProxyObjectToRealObject(instance, undefined)).toBe(instance); expect(instance.init).not.toHaveBeenCalled(); }); });