From ade9f8810a3cb734bc50aa21c053e682074ea018 Mon Sep 17 00:00:00 2001 From: sandeepdigumarty Date: Tue, 18 Feb 2025 10:57:46 +0530 Subject: [PATCH 1/5] feat: add support for Google Tag Manager environment config --- .../src/integrations/GoogleTagManager/browser.js | 4 +++- .../src/integrations/GoogleTagManager/nativeSdkLoader.js | 7 +++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/analytics-js-integrations/src/integrations/GoogleTagManager/browser.js b/packages/analytics-js-integrations/src/integrations/GoogleTagManager/browser.js index 07bc7c3cd0..b64cb6ff74 100644 --- a/packages/analytics-js-integrations/src/integrations/GoogleTagManager/browser.js +++ b/packages/analytics-js-integrations/src/integrations/GoogleTagManager/browser.js @@ -17,6 +17,8 @@ class GoogleTagManager { this.containerID = config.containerID; this.name = NAME; this.serverUrl = config.serverUrl; + this.environmentID = config.environmentID; + this.authorizationToken = config.authorizationToken; ({ shouldApplyDeviceModeTransformation: this.shouldApplyDeviceModeTransformation, propagateEventsUntransformedOnError: this.propagateEventsUntransformedOnError, @@ -25,7 +27,7 @@ class GoogleTagManager { } init() { - loadNativeSdk(this.containerID, this.serverUrl); + loadNativeSdk(this.containerID, this.serverUrl, this.environmentID, this.authorizationToken); } isLoaded() { diff --git a/packages/analytics-js-integrations/src/integrations/GoogleTagManager/nativeSdkLoader.js b/packages/analytics-js-integrations/src/integrations/GoogleTagManager/nativeSdkLoader.js index c5879bd151..8343d5aad7 100644 --- a/packages/analytics-js-integrations/src/integrations/GoogleTagManager/nativeSdkLoader.js +++ b/packages/analytics-js-integrations/src/integrations/GoogleTagManager/nativeSdkLoader.js @@ -1,6 +1,6 @@ import { LOAD_ORIGIN } from '@rudderstack/analytics-js-common/v1.1/utils/constants'; -function loadNativeSdk(containerID, serverUrl) { +function loadNativeSdk(containerID, serverUrl, environmentID, authorizationToken) { const defaultUrl = `https://www.googletagmanager.com`; // ref: https://developers.google.com/tag-platform/tag-manager/server-side/send-data#update_the_gtmjs_source_domain @@ -11,9 +11,12 @@ function loadNativeSdk(containerID, serverUrl) { const f = d.getElementsByTagName(s)[0]; const j = d.createElement(s); const dl = l !== 'dataLayer' ? `&l=${l}` : ''; + const gtmEnv = environmentID ? `>m_preview=env-${encodeURIComponent(environmentID)}` : ''; + const gtmAuth = authorizationToken ? `>m_auth=${encodeURIComponent(authorizationToken)}` : ''; + const gtmCookies = '>m_cookies_win=x'; j.setAttribute('data-loader', LOAD_ORIGIN); j.async = true; - j.src = `${window.finalUrl}/gtm.js?id=${i}${dl}`; + j.src = `${window.finalUrl}/gtm.js?id=${i}${dl}${gtmAuth}${gtmEnv}${gtmCookies}`; f.parentNode.insertBefore(j, f); })(window, document, 'script', 'dataLayer', containerID); } From c765390eb6d7c0d367a520edaad04c146fba8514 Mon Sep 17 00:00:00 2001 From: sandeepdigumarty Date: Fri, 21 Feb 2025 10:58:58 +0530 Subject: [PATCH 2/5] chore: updated tests --- .../integrations/GoogleTagManager/browser.test.js | 9 ++++++++- .../src/integrations/GoogleTagManager/nativeSdkLoader.js | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/analytics-js-integrations/__tests__/integrations/GoogleTagManager/browser.test.js b/packages/analytics-js-integrations/__tests__/integrations/GoogleTagManager/browser.test.js index 8ab15a6f85..e55d45f537 100644 --- a/packages/analytics-js-integrations/__tests__/integrations/GoogleTagManager/browser.test.js +++ b/packages/analytics-js-integrations/__tests__/integrations/GoogleTagManager/browser.test.js @@ -15,6 +15,8 @@ describe('GoogleTagManager', () => { const config = { containerID: 'DUMMY_CONTAINER_ID', serverUrl: 'DUMMY_SERVER_URL', + environmentID: 'env-2', + authorizationToken: 'random', }; const analytics = { logLevel: 'debug', @@ -44,7 +46,12 @@ describe('GoogleTagManager', () => { describe('init', () => { it('should call loadNativeSdk with containerID and serverUrl', () => { googleTagManager.init(); - expect(loadNativeSdk).toHaveBeenCalledWith(config.containerID, config.serverUrl); + expect(loadNativeSdk).toHaveBeenCalledWith( + config.containerID, + config.serverUrl, + config.environmentID, + config.authorizationToken, + ); }); }); diff --git a/packages/analytics-js-integrations/src/integrations/GoogleTagManager/nativeSdkLoader.js b/packages/analytics-js-integrations/src/integrations/GoogleTagManager/nativeSdkLoader.js index 8343d5aad7..62de4075aa 100644 --- a/packages/analytics-js-integrations/src/integrations/GoogleTagManager/nativeSdkLoader.js +++ b/packages/analytics-js-integrations/src/integrations/GoogleTagManager/nativeSdkLoader.js @@ -11,7 +11,7 @@ function loadNativeSdk(containerID, serverUrl, environmentID, authorizationToken const f = d.getElementsByTagName(s)[0]; const j = d.createElement(s); const dl = l !== 'dataLayer' ? `&l=${l}` : ''; - const gtmEnv = environmentID ? `>m_preview=env-${encodeURIComponent(environmentID)}` : ''; + const gtmEnv = environmentID ? `>m_preview=${encodeURIComponent(environmentID)}` : ''; const gtmAuth = authorizationToken ? `>m_auth=${encodeURIComponent(authorizationToken)}` : ''; const gtmCookies = '>m_cookies_win=x'; j.setAttribute('data-loader', LOAD_ORIGIN); From 06567313f44877cd51fd97258f3fe5b8209e2369 Mon Sep 17 00:00:00 2001 From: sandeepdigumarty Date: Fri, 21 Feb 2025 11:14:51 +0530 Subject: [PATCH 3/5] chore: updated tests --- .../__tests__/integrations/GoogleTagManager/browser.test.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/analytics-js-integrations/__tests__/integrations/GoogleTagManager/browser.test.js b/packages/analytics-js-integrations/__tests__/integrations/GoogleTagManager/browser.test.js index e55d45f537..b3ec6848ab 100644 --- a/packages/analytics-js-integrations/__tests__/integrations/GoogleTagManager/browser.test.js +++ b/packages/analytics-js-integrations/__tests__/integrations/GoogleTagManager/browser.test.js @@ -37,6 +37,8 @@ describe('GoogleTagManager', () => { expect(googleTagManager.analytics).toEqual(analytics); expect(googleTagManager.containerID).toEqual(config.containerID); expect(googleTagManager.serverUrl).toEqual(config.serverUrl); + expect(googleTagManager.environmentID).toEqual(config.environmentID); + expect(googleTagManager.authorizationToken).toEqual(config.authorizationToken); expect(googleTagManager.shouldApplyDeviceModeTransformation).toEqual(true); expect(googleTagManager.propagateEventsUntransformedOnError).toEqual(false); expect(googleTagManager.destinationId).toEqual(destinationInfo.destinationId); From 50c23e3730d7a17cf18892aa0ad3c896153728e9 Mon Sep 17 00:00:00 2001 From: sandeepdigumarty Date: Fri, 21 Feb 2025 12:02:20 +0530 Subject: [PATCH 4/5] chore: addressed review comments --- .../GoogleTagManager/nativeSdkLoader.test.js | 76 +++++++++++++++++++ .../GoogleTagManager/nativeSdkLoader.js | 40 +++++----- 2 files changed, 99 insertions(+), 17 deletions(-) create mode 100644 packages/analytics-js-integrations/__tests__/integrations/GoogleTagManager/nativeSdkLoader.test.js diff --git a/packages/analytics-js-integrations/__tests__/integrations/GoogleTagManager/nativeSdkLoader.test.js b/packages/analytics-js-integrations/__tests__/integrations/GoogleTagManager/nativeSdkLoader.test.js new file mode 100644 index 0000000000..0620359b98 --- /dev/null +++ b/packages/analytics-js-integrations/__tests__/integrations/GoogleTagManager/nativeSdkLoader.test.js @@ -0,0 +1,76 @@ +import { LOAD_ORIGIN } from '@rudderstack/analytics-js-common/v1.1/utils/constants'; +import { loadNativeSdk } from '../../../src/integrations/GoogleTagManager/nativeSdkLoader'; + +describe('loadNativeSdk', () => { + // Setup a dummy script element so that getElementsByTagName('script')[0] returns something. + beforeEach(() => { + // Clear any previous finalUrl and dataLayer + delete window.finalUrl; + window.dataLayer = []; + document.body.innerHTML = ''; + }); + + afterEach(() => { + document.body.innerHTML = ''; + delete window.finalUrl; + window.dataLayer = undefined; + }); + + test('should set window.finalUrl to provided serverUrl', () => { + loadNativeSdk('GTM-TEST', 'https://custom-server.com', null, null); + expect(window.finalUrl).toBe('https://custom-server.com'); + }); + + test('should set window.finalUrl to default when serverUrl is not provided', () => { + loadNativeSdk('GTM-TEST', null, null, null); + expect(window.finalUrl).toBe('https://www.googletagmanager.com'); + }); + + test('should push a gtm.js event into dataLayer', () => { + loadNativeSdk('GTM-TEST', null, null, null); + expect(window.dataLayer.length).toBeGreaterThan(0); + const eventObj = window.dataLayer[0]; + expect(eventObj.event).toBe('gtm.js'); + expect(typeof eventObj['gtm.start']).toBe('number'); + }); + + test('should insert a script element with correct attributes and src (without environment or auth)', () => { + loadNativeSdk('GTM-TEST', 'https://custom-server.com', null, null); + + const scripts = document.getElementsByTagName('script'); + // The function inserts the new script before the first existing script element. + // So the new script should be at index 0. + const insertedScript = scripts[0]; + + expect(insertedScript.getAttribute('data-loader')).toBe(LOAD_ORIGIN); + expect(insertedScript.async).toBe(true); + // Since l is "dataLayer", dl is an empty string. + // Expected src: serverUrl/gtm.js?id=containerID + (no env/auth) + '>m_cookies_win=x' + const expectedSrc = `https://custom-server.com/gtm.js?id=GTM-TEST>m_cookies_win=x`; + expect(insertedScript.src).toBe(expectedSrc); + }); + + test('should insert a script element with correct query parameters including environmentID and authorizationToken', () => { + const containerID = 'GTM-TEST'; + const serverUrl = 'https://custom-server.com'; + const environmentID = 'env123'; + const authorizationToken = 'auth456'; + + loadNativeSdk(containerID, serverUrl, environmentID, authorizationToken); + + const scripts = document.getElementsByTagName('script'); + const insertedScript = scripts[0]; + + // Expected src: serverUrl/gtm.js?id=containerID + // + '>m_auth=' + encodeURIComponent(authorizationToken) + // + '>m_preview=' + encodeURIComponent(environmentID) + // + '>m_cookies_win=x' + const expectedSrc = + `${serverUrl}/gtm.js?id=${containerID}` + + '>m_cookies_win=x' + + `>m_preview=${encodeURIComponent(environmentID)}` + + `>m_auth=${encodeURIComponent(authorizationToken)}`; + + expect(insertedScript.src).toBe(expectedSrc); + }); +}); diff --git a/packages/analytics-js-integrations/src/integrations/GoogleTagManager/nativeSdkLoader.js b/packages/analytics-js-integrations/src/integrations/GoogleTagManager/nativeSdkLoader.js index 62de4075aa..7d95db4be0 100644 --- a/packages/analytics-js-integrations/src/integrations/GoogleTagManager/nativeSdkLoader.js +++ b/packages/analytics-js-integrations/src/integrations/GoogleTagManager/nativeSdkLoader.js @@ -1,23 +1,29 @@ import { LOAD_ORIGIN } from '@rudderstack/analytics-js-common/v1.1/utils/constants'; function loadNativeSdk(containerID, serverUrl, environmentID, authorizationToken) { - const defaultUrl = `https://www.googletagmanager.com`; - // ref: https://developers.google.com/tag-platform/tag-manager/server-side/send-data#update_the_gtmjs_source_domain - - window.finalUrl = serverUrl ? serverUrl : defaultUrl; - (function (w, d, s, l, i) { - w[l] = w[l] || []; - w[l].push({ 'gtm.start': new Date().getTime(), event: 'gtm.js' }); - const f = d.getElementsByTagName(s)[0]; - const j = d.createElement(s); - const dl = l !== 'dataLayer' ? `&l=${l}` : ''; - const gtmEnv = environmentID ? `>m_preview=${encodeURIComponent(environmentID)}` : ''; - const gtmAuth = authorizationToken ? `>m_auth=${encodeURIComponent(authorizationToken)}` : ''; - const gtmCookies = '>m_cookies_win=x'; - j.setAttribute('data-loader', LOAD_ORIGIN); - j.async = true; - j.src = `${window.finalUrl}/gtm.js?id=${i}${dl}${gtmAuth}${gtmEnv}${gtmCookies}`; - f.parentNode.insertBefore(j, f); + const defaultUrl = 'https://www.googletagmanager.com'; + window.finalUrl = serverUrl || defaultUrl; + + // Reference: https://developers.google.com/tag-platform/tag-manager/server-side/send-data#update_the_gtmjs_source_domain + (function (window, document, tag, dataLayerName, containerID) { + window[dataLayerName] = window[dataLayerName] || []; + window[dataLayerName].push({ 'gtm.start': new Date().getTime(), event: 'gtm.js' }); + + const firstScript = document.getElementsByTagName(tag)[0]; + const gtmScript = document.createElement(tag); + + // Construct query parameters using URLSearchParams + const queryParams = new URLSearchParams({ id: containerID, gtm_cookies_win: 'x' }); + + if (dataLayerName !== 'dataLayer') queryParams.append('l', dataLayerName); + if (environmentID) queryParams.append('gtm_preview', environmentID); + if (authorizationToken) queryParams.append('gtm_auth', authorizationToken); + + gtmScript.setAttribute('data-loader', LOAD_ORIGIN); + gtmScript.async = true; + gtmScript.src = `${window.finalUrl}/gtm.js?${queryParams.toString()}`; + + firstScript.parentNode.insertBefore(gtmScript, firstScript); })(window, document, 'script', 'dataLayer', containerID); } From 2c31fc23dc1f5352311f309b2b1fef8a3cb0fa72 Mon Sep 17 00:00:00 2001 From: sandeepdigumarty Date: Fri, 21 Feb 2025 12:25:09 +0530 Subject: [PATCH 5/5] chore: added test --- .../GoogleTagManager/nativeSdkLoader.test.js | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/packages/analytics-js-integrations/__tests__/integrations/GoogleTagManager/nativeSdkLoader.test.js b/packages/analytics-js-integrations/__tests__/integrations/GoogleTagManager/nativeSdkLoader.test.js index 0620359b98..0db55a27ca 100644 --- a/packages/analytics-js-integrations/__tests__/integrations/GoogleTagManager/nativeSdkLoader.test.js +++ b/packages/analytics-js-integrations/__tests__/integrations/GoogleTagManager/nativeSdkLoader.test.js @@ -73,4 +73,26 @@ describe('loadNativeSdk', () => { expect(insertedScript.src).toBe(expectedSrc); }); + + test('should set window.finalUrl and insert a script without environmentId and authorizationToken parameters', () => { + const containerID = 'GTM-TEST'; + const serverUrl = 'https://custom-server.com'; + // Call the function with undefined environmentID and authorizationToken. + loadNativeSdk(containerID, serverUrl, undefined, undefined); + + // Verify that window.finalUrl is set to the provided serverUrl. + expect(window.finalUrl).toBe(serverUrl); + + // Retrieve the inserted script element. The function inserts the new script before the dummy script. + const insertedScript = document.getElementsByTagName('script')[0]; + + // Since dataLayer is 'dataLayer', dl is an empty string. + // environmentID and authorizationToken are undefined, so their query parts are empty. + // The final src should be: serverUrl/gtm.js?id=containerID + '>m_cookies_win=x' + const expectedSrc = `${serverUrl}/gtm.js?id=${containerID}>m_cookies_win=x`; + + expect(insertedScript.getAttribute('data-loader')).toBe(LOAD_ORIGIN); + expect(insertedScript.async).toBe(true); + expect(insertedScript.src).toBe(expectedSrc); + }); });