Skip to content

Instantly share code, notes, and snippets.

@renatoaraujoc
Last active June 16, 2024 12:25
Show Gist options
  • 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;
}
}
@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