diff --git a/packages/analytics-js-integrations/__tests__/integrations/FacebookPixel/browser.test.js b/packages/analytics-js-integrations/__tests__/integrations/FacebookPixel/browser.test.js index 48f9ebb04..9fc3ed16f 100644 --- a/packages/analytics-js-integrations/__tests__/integrations/FacebookPixel/browser.test.js +++ b/packages/analytics-js-integrations/__tests__/integrations/FacebookPixel/browser.test.js @@ -85,6 +85,74 @@ describe('FacebookPixel init tests', () => { }); }); +describe('FacebookPixel multiple destinations bootstrap guard', () => { + const mockAnalytics = {}; + let previousFbq; + let previousFbqShim; + + beforeEach(() => { + // Capture and clear the globals so this suite exercises a fresh bootstrap, + // then restore them in afterEach so sibling suites are not affected. + previousFbq = window.fbq; + previousFbqShim = window._fbq; + delete window.fbq; + delete window._fbq; + }); + + afterEach(() => { + window.fbq = previousFbq; + window._fbq = previousFbqShim; + }); + + test('does not re-bootstrap the global fbq shim when a second destination initializes', () => { + // First destination bootstraps the global fbq shim. + const firstPixel = new FacebookPixel({ pixelId: '111111111' }, mockAnalytics, destinationInfo); + firstPixel.init(); + + const shimAfterFirstInit = window.fbq; + expect(typeof shimAfterFirstInit).toBe('function'); + expect(window.fbq.version).toBe('2.0'); + + // Simulate the loaded SDK taking over: mark the shim and enqueue an event. + window.fbq.version = 'preserved-by-loaded-sdk'; + window.fbq.queue.push('event-from-first-destination'); + + // Second destination initializes. Its init() must NOT reset the global shim. + const secondPixel = new FacebookPixel({ pixelId: '222222222' }, mockAnalytics, destinationInfo); + secondPixel.init(); + + // The guard preserves the already established shim: version and queue are untouched. + expect(window.fbq).toBe(shimAfterFirstInit); + expect(window.fbq.version).toBe('preserved-by-loaded-sdk'); + expect(window.fbq.queue).toContain('event-from-first-destination'); + + // The second pixel still registers itself via its own init call. + expect(window.fbq.queue).toContainEqual( + expect.objectContaining({ 0: 'init', 1: '222222222' }), + ); + }); + + test('applies pushState flags on every init even when fbq is pre-existing', () => { + // Simulate the host page (or an earlier destination) having already bootstrapped + // Meta Pixel with automatic pageview tracking enabled. + window.fbq = jest.fn(); + window.fbq.queue = []; + window.fbq.version = 'pre-existing'; + window.fbq.disablePushState = false; + window.fbq.allowDuplicatePageViews = false; + + const pixel = new FacebookPixel({ pixelId: '333333333' }, mockAnalytics, destinationInfo); + pixel.init(); + + // The one-time shim state is left untouched (no version conflict)... + expect(window.fbq.version).toBe('pre-existing'); + // ...but the pageview flags are (re)applied on this init so we never fall back + // to Meta's automatic pageview/pushState tracking. + expect(window.fbq.disablePushState).toBe(true); + expect(window.fbq.allowDuplicatePageViews).toBe(true); + }); +}); + describe('FacebookPixel page', () => { let facebookPixel; const mockAnalytics = { diff --git a/packages/analytics-js-integrations/src/integrations/FacebookPixel/browser.js b/packages/analytics-js-integrations/src/integrations/FacebookPixel/browser.js index 94498024f..af7438afd 100644 --- a/packages/analytics-js-integrations/src/integrations/FacebookPixel/browser.js +++ b/packages/analytics-js-integrations/src/integrations/FacebookPixel/browser.js @@ -53,21 +53,31 @@ class FacebookPixel { } init() { - window._fbq = function () { - if (window.fbq.callMethod) { - window.fbq.callMethod.apply(window.fbq, arguments); - } else { - window.fbq.queue.push(arguments); - } - }; + // Bootstrap the global fbq shim only once per page. When multiple Meta Pixel + // destinations are configured, each one calls init(), and re-running this block + // mutates the already loaded fbevents.js SDK (resetting fbq.version, fbq.queue, etc.), + // which makes Meta log "Multiple pixels with conflicting versions were detected" + // and prevents the second pixel from initializing. Guard it like Meta's own snippet. + if (!window.fbq) { + window._fbq = function () { + if (window.fbq.callMethod) { + window.fbq.callMethod.apply(window.fbq, arguments); + } else { + window.fbq.queue.push(arguments); + } + }; - window.fbq = window.fbq || window._fbq; - window.fbq.push = window.fbq; - window.fbq.loaded = true; + window.fbq = window.fbq || window._fbq; + window.fbq.push = window.fbq; + window.fbq.loaded = true; + window.fbq.version = '2.0'; + window.fbq.queue = []; + } + // Apply these flags on every init() (even when fbq was bootstrapped by the host page + // or an earlier destination), so we never fall back to Meta's automatic pageview + // tracking. They don't cause the version conflict, so they stay outside the guard. window.fbq.disablePushState = true; // disables automatic pageview tracking window.fbq.allowDuplicatePageViews = true; // enables fb - window.fbq.version = '2.0'; - window.fbq.queue = []; window.fbq('set', 'autoConfig', this.autoConfig, this.pixelId); // toggle autoConfig : sends button click and page metadata if (this.advancedMapping) { if (this.useUpdatedMapping) {