Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add support for Google Tag Manager environment config #2045

Merged
merged 5 commits into from
Feb 21, 2025
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -35,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);
Expand All @@ -44,7 +48,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,
);
});
});

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
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 = '<script id="dummy-script"></script>';
});

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) + '&gtm_cookies_win=x'
const expectedSrc = `https://custom-server.com/gtm.js?id=GTM-TEST&gtm_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
// + '&gtm_auth=' + encodeURIComponent(authorizationToken)
// + '&gtm_preview=' + encodeURIComponent(environmentID)
// + '&gtm_cookies_win=x'
const expectedSrc =
`${serverUrl}/gtm.js?id=${containerID}` +
'&gtm_cookies_win=x' +
`&gtm_preview=${encodeURIComponent(environmentID)}` +
`&gtm_auth=${encodeURIComponent(authorizationToken)}`;

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 + '&gtm_cookies_win=x'
const expectedSrc = `${serverUrl}/gtm.js?id=${containerID}&gtm_cookies_win=x`;

expect(insertedScript.getAttribute('data-loader')).toBe(LOAD_ORIGIN);
expect(insertedScript.async).toBe(true);
expect(insertedScript.src).toBe(expectedSrc);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -25,7 +27,7 @@ class GoogleTagManager {
}

init() {
loadNativeSdk(this.containerID, this.serverUrl);
loadNativeSdk(this.containerID, this.serverUrl, this.environmentID, this.authorizationToken);
}

isLoaded() {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,29 @@
import { LOAD_ORIGIN } from '@rudderstack/analytics-js-common/v1.1/utils/constants';

function loadNativeSdk(containerID, serverUrl) {
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}` : '';
j.setAttribute('data-loader', LOAD_ORIGIN);
j.async = true;
j.src = `${window.finalUrl}/gtm.js?id=${i}${dl}`;
f.parentNode.insertBefore(j, f);
function loadNativeSdk(containerID, serverUrl, environmentID, authorizationToken) {
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);
}

Expand Down