Skip to content

Instantly share code, notes, and snippets.

@renatoaraujoc
Last active April 25, 2023 10:48
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save renatoaraujoc/5491f54c3abe29913f9877c7e0d2ee0d to your computer and use it in GitHub Desktop.
Save renatoaraujoc/5491f54c3abe29913f9877c7e0d2ee0d to your computer and use it in GitHub Desktop.
Angular - GoogleTagManager Service that includes Partytown to be run when App is ready
/*
* Base implementation of this service, modify it to your needs.
* Pushing events to dataLayer has to be included yet, working on it (like page_view for router nav events)
*/
import { DOCUMENT, isPlatformServer } from '@angular/common';
import {
inject,
Injectable,
InjectionToken,
PLATFORM_ID,
ValueProvider
} from '@angular/core';
import type { PartytownConfig } from '@builder.io/partytown/integration';
import { WINDOW } from '@ng-web-apis/common';
// Make dataLayer and PartyTownConfig global
declare global {
interface Window {
dataLayer: DataLayer;
partyTown: PartytownConfig;
}
}
// Declare dataLayer types
type DataLayerProps =
// represents the logged user id
'user_id';
type DataLayerObject = Record<DataLayerProps | string, string | number | null>;
type DataLayer = DataLayerObject[];
// Partytown default config
const defaultPartyTownConfig: Partial<PartytownConfig> = {
forward: ['dataLayer.push']
};
type GTMConfig = {
id: string;
partyTown?: {
debug: boolean;
basePath: string;
config?: PartytownConfig;
};
};
const GTM_CONFIG = new InjectionToken<GTMConfig>('gtmConfig');
export const provideGoogleTagManagerConfig: (
config: GTMConfig
) => ValueProvider = (gtmConfig) => ({
provide: GTM_CONFIG,
useValue: {
...gtmConfig,
partyTown: {
...gtmConfig.partyTown,
config: {
...defaultPartyTownConfig,
...(gtmConfig.partyTown?.config ?? {})
}
}
}
});
@Injectable({
providedIn: 'root'
})
export class GoogleTagManagerService {
private _isInit = false;
private lastDataLayerProps: DataLayerObject = {
user_id: null
};
private readonly _isPlatformServer = isPlatformServer(inject(PLATFORM_ID));
private readonly _gtmConfig = inject(GTM_CONFIG, { optional: true });
private readonly _document = inject(DOCUMENT);
private readonly _window = inject(WINDOW);
private get dataLayer(): DataLayer {
this.__checkIfIsInit();
return this._window.dataLayer;
}
setUserId(userId: string | null) {
this.__checkIfIsInit();
this.dataLayer.push(
this.pushToDefaultDataLayer({
user_id: userId
})
);
}
injectScript(
initialDataLayerProps: Partial<DataLayerObject> = this
.lastDataLayerProps
) {
if (this._isPlatformServer) {
return;
}
if (!this._gtmConfig) {
throw new Error(
`Provide the GTM config with 'provideGoogleTagManagerConfig()' before calling this method.`
);
}
const isDebugMode = this._window
? !!new URL(this._window.location.href).searchParams.get(
'gtm_debug'
)
: false;
const headElem = this._document.head;
// create dataLayer
this._window.dataLayer = Array.isArray(this._window.dataLayer)
? this._window.dataLayer
: [];
// set initial values for the dataLayer to buffer initial events
this._window.dataLayer.push(
this.pushToDefaultDataLayer(initialDataLayerProps)
);
// PartyTown source + config
let partyTownLibraryScript: HTMLScriptElement | null = null;
if (this._gtmConfig.partyTown) {
const { debug: ptDebug, basePath: ptBasePath } =
this._gtmConfig.partyTown;
// declare partyTown config
this._window.partyTown = this._gtmConfig.partyTown.config ?? {};
// create partyTown library script
partyTownLibraryScript = document.createElement('script');
partyTownLibraryScript.type = 'text/javascript';
partyTownLibraryScript.src = `${ptBasePath}/${
ptDebug ? 'debug/' : ''
}partytown.js`;
}
// GTM Script
const gtmScript = document.createElement('script');
gtmScript.type =
isDebugMode || !this._gtmConfig.partyTown
? 'text/javascript'
: 'text/partyTown';
gtmScript.textContent = `
(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', '${this._gtmConfig.id}');
`;
// Attach partyTown if we're not in debug mode and we have a partyTown config
if (!isDebugMode && partyTownLibraryScript) {
headElem.appendChild(partyTownLibraryScript);
}
// attach gtm script
headElem.appendChild(gtmScript);
// Okay, all good.
this._isInit = true;
}
private __checkIfIsInit() {
if (!this._isInit) {
throw new Error(
'injectScript() not called, initialize the GTM first.'
);
}
}
private pushToDefaultDataLayer(props: Partial<DataLayerObject>) {
this.lastDataLayerProps = Object.keys(this.lastDataLayerProps).reduce(
(acc, next) => {
acc[next] =
next in props
? props[next] ?? null
: this.lastDataLayerProps[next];
return acc;
},
{} as DataLayerObject
);
return this.lastDataLayerProps;
}
}
@renatoaraujoc
Copy link
Author

renatoaraujoc commented Apr 20, 2023

Usage example:

// In your root ngModule (or similar if you're using standalone api)
providers: [
    provideGoogleTagManagerConfig({
        id: 'YOU_GTM_ID',
        partyTown: {
            debug: true,
            basePath: '/~partytown',
            config: {
                // example reverse-proxy, see how to set it up below
                resolveUrl: (url, location, type) => {
                    if (type === 'script') {
                        const proxyUrl = new URL('/gtm', location.origin);
                        proxyUrl.searchParams.append('url', url.href);

                        return proxyUrl;
                    }

                    return url;
                }
            }
        }
    })
]

When to call it, in your root.component.ts:

// When app is ready at browser side (PLATFORM === Browser), call this:
private __installGoogleTagManager() {
    const { jwtToken } = this._appStoreService.auth;

    // installs GTM
    this._gtmService.injectScript({
        // change this to your specific app implementation
        user_id: !jwtToken ? null : `${jwtToken.data.user.id}`
    });

    // listen to userId and set it, change this to your app specific implementation
    this._appStoreService.auth.jwtToken$
        .pipe(
            skip(1),
            distinctUntilChanged(),
            takeUntilDestroyed(this._destroyRef)
        )
        .subscribe((_jwtToken) => {
            this._gtmService.setUserId(
                !_jwtToken ? null : `${_jwtToken.data.user.id}`
            );
        });
}

In your app build props:

"targets": {
    "build": {
        // ...
        "assets": [
            // ... other assets
            {
                "glob": "**/*",
                "input": "node_modules/@builder.io/partytown/lib",
                "output": "/~partytown"
            }
        ]

Bonus, in case you need a reverse proxy, you can use your server.ts (universal) like this:
First install: http-proxy-middleware and then...

// Reverse proxy for googleTagManager
// This is a very simple implementation :)
server.use('/gtm', (req, res, next) => {
    const url = new URL(req.query.url as string).searchParams.get('url');

    if (!url) {
        res.status(400).send('Missing url parameter');
        return;
    }

    const gtmUrl = new URL(url);

    const proxy = createProxyMiddleware({
        target: gtmUrl.origin,
        changeOrigin: true,
        pathRewrite: () => gtmUrl.pathname + gtmUrl.search
    });

    proxy(req, res, next);
});

@peplocanto
Copy link

import { InjectionToken } from '@angular/core';
import type { PartytownConfig } from '@builder.io/partytown/integration';

export interface GtmData {
  event: string;
  category?: string;
  action?: string;
  label?: string;
  pageName?: string;
}

export interface ExtraWindowProps {
  dataLayer: GtmData[];
  partyTown: PartytownConfig;
}

export type GlobalObject = Window & typeof globalThis & ExtraWindowProps;

export const GLOBAL_OBJECT = new InjectionToken<GlobalObject>('GLOBAL_OBJECT_INJECTION_TOKEN');

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment