Skip to content

feat: add GTM integration #814

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

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
8 changes: 4 additions & 4 deletions src/initialize.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ import {
import {
configure as configureAnalytics, SegmentAnalyticsService, identifyAnonymousUser, identifyAuthenticatedUser,
} from './analytics';
import { GoogleAnalyticsLoader } from './scripts';
import { GoogleAnalyticsLoader, GoogleTagManagerLoader } from './scripts';
import {
getAuthenticatedHttpClient,
configure as configureAuth,
Expand Down Expand Up @@ -269,8 +269,8 @@ function applyOverrideHandlers(overrides) {
* @param {*} [options.analyticsService=SegmentAnalyticsService] The `AnalyticsService`
* implementation to use.
* @param {*} [options.authMiddleware=[]] An array of middleware to apply to http clients in the auth service.
* @param {*} [options.externalScripts=[GoogleAnalyticsLoader]] An array of externalScripts.
* By default added GoogleAnalyticsLoader.
* @param {*} [options.externalScripts=[GoogleAnalyticsLoader, GoogleTagManagerLoader]] An array of externalScripts.
* By default adds GoogleAnalyticsLoader and GoogleTagManagerLoader.
* @param {*} [options.requireAuthenticatedUser=false] If true, turns on automatic login
* redirection for unauthenticated users. Defaults to false, meaning that by default the
* application will allow anonymous/unauthenticated sessions.
Expand All @@ -290,7 +290,7 @@ export async function initialize({
analyticsService = SegmentAnalyticsService,
authService = AxiosJwtAuthService,
authMiddleware = [],
externalScripts = [GoogleAnalyticsLoader],
externalScripts = [GoogleAnalyticsLoader, GoogleTagManagerLoader],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we have GoogleAnalyticsLoader defined here already, it looks like the right place to add support for something like GTM. However, adding support for arbitrary external scripts, however popular, is not really maintainable for the project.

Instead, it would be better if we made externalScripts configurable via env.config.jsx, removing Google Analytics support in the process but allowing operators to add any external script they need without requiring the platform to explicitly support it. Is this a change you're comfortable contributing?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hello @arbrandes, thank you for the idea of making externalScripts configurable!

We'll discuss this approach with our team and may consider creating a separate PR to explore that direction.

For now, I think to keep the current implementation as-is, since in our projects we frequently use GTM in this way. We’d also like to gather more feedback from the community, including real-world examples of GTM usage, to help refine and potentially improve our solution.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@arbrandes thanks for your feedback! Can we go ahead with the changes we have in this PR and create a separate issue to implement the functionality of adding externalScripts via env.config.jsx?

requireAuthenticatedUser: requireUser = false,
hydrateAuthenticatedUser: hydrateUser = false,
messages,
Expand Down
48 changes: 48 additions & 0 deletions src/scripts/GoogleTagManagerLoader.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/**
* @implements {GoogleTagManagerLoader}
* @memberof module:GoogleTagManagerLoader
*/
class GoogleTagManagerLoader {
constructor({ config }) {
this.gtmId = config.GOOGLE_TAG_MANAGER_ID;
}

loadScript() {
if (!this.gtmId) {
return;
}

global.google_tag_manager = global.google_tag_manager || [];
const { google_tag_manager: googleTagManager } = global;

// If the snippet was invoked do nothing.
if (googleTagManager.invoked) {
return;
}

// Invoked flag, to make sure the snippet
// is never invoked twice.
googleTagManager.invoked = true;

googleTagManager.load = (id) => {
const gtmScript = document.createElement('script');
gtmScript.type = 'text/javascript';
gtmScript.innerHTML = `
(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer', '${id}');
`;

// Insert our scripts next to the first script element.
const first = document.getElementsByTagName('script')[0];
first.parentNode.insertBefore(gtmScript, first);
};

// Load gtmAnalytics.
googleTagManager.load(this.gtmId);
}
}

export default GoogleTagManagerLoader;
49 changes: 49 additions & 0 deletions src/scripts/GoogleTagManagerLoader.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import GoogleTagManagerLoader from './GoogleTagManagerLoader';

describe('GoogleTagManagerLoader', () => {
const mockGTMId = 'GOOGLE_TAG_MANAGER_ID_test_id';
let insertBeforeMock;

beforeEach(() => {
global.google_tag_manager = undefined;
insertBeforeMock = jest.fn();

document.getElementsByTagName = jest.fn(() => [
{
parentNode: {
insertBefore: insertBeforeMock,
},
},
]);
});

it('should load GTM script', () => {
const loader = new GoogleTagManagerLoader({ config: { GOOGLE_TAG_MANAGER_ID: mockGTMId } });

loader.loadScript();

expect(global.google_tag_manager).toBeDefined();
expect(global.google_tag_manager.invoked).toBe(true);

const firstScript = document.getElementsByTagName()[0];
expect(firstScript.parentNode.insertBefore).toHaveBeenCalled();
});

it('should not load script if account is not defined', () => {
const loader = new GoogleTagManagerLoader({ config: { GOOGLE_TAG_MANAGER_ID: '' } });

loader.loadScript();

expect(global.google_tag_manager).toBeUndefined();
});

it('should not load script if google_tag_manager is already invoked', () => {
global.google_tag_manager = { invoked: true };
const loader = new GoogleTagManagerLoader({ config: { GOOGLE_TAG_MANAGER_ID: mockGTMId } });

loader.loadScript();

expect(global.google_tag_manager.invoked).toBe(true);
expect(document.getElementsByTagName).not.toHaveBeenCalled();
});
});
1 change: 1 addition & 0 deletions src/scripts/index.js
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
/* eslint-disable import/prefer-default-export */
export { default as GoogleAnalyticsLoader } from './GoogleAnalyticsLoader';
export { default as GoogleTagManagerLoader } from './GoogleTagManagerLoader';