Last active
June 16, 2024 12:25
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* 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; | |
} | |
} |
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
Usage example:
When to call it, in your root.component.ts:
In your app build props:
Bonus, in case you need a reverse proxy, you can use your server.ts (universal) like this:
First install:
http-proxy-middleware
and then...