react-ga4
Version:
React Google Analytics 4
476 lines (418 loc) • 12.9 kB
text/typescript
import gtag from "./gtag";
import format from "./format";
/*
Links
https://developers.google.com/gtagjs/reference/api
https://developers.google.com/tag-platform/gtagjs/reference
*/
export interface GaOptions {
cookieUpdate?: boolean; // true
cookieExpires?: number; // Default two years 63072000
cookieDomain?: string; // auto
cookieFlags?: string;
userId?: string;
clientId?: string;
anonymizeIp?: boolean;
contentGroup1?: string;
contentGroup2?: string;
contentGroup3?: string;
contentGroup4?: string;
contentGroup5?: string;
allowAdFeatures?: boolean;
allowAdPersonalizationSignals?: boolean;
nonInteraction?: boolean;
page?: string;
}
export interface UaEventOptions {
action: string;
category: string;
label?: string;
value?: number;
nonInteraction?: boolean;
transport?: 'beacon' | 'xhr' | 'image';
}
export interface InitOptions {
trackingId: string;
gaOptions?: GaOptions | any;
gtagOptions?: any; // New parameter
}
export class GA4 {
isInitialized!: boolean;
_testMode!: boolean;
_currentMeasurementId!: string;
_hasLoadedGA!: boolean;
_isQueuing!: boolean;
_queueGtag!: any[];
constructor() {
this.reset();
}
reset = (): void => {
this.isInitialized = false;
this._testMode = false;
this._currentMeasurementId = "";
this._hasLoadedGA = false;
this._isQueuing = false;
this._queueGtag = [];
};
_gtag = (...args: any[]): void => {
if (!this._testMode) {
if (this._isQueuing) {
this._queueGtag.push(args);
} else {
gtag(...args);
}
} else {
this._queueGtag.push(args);
}
};
gtag(...args: any[]): void {
this._gtag(...args);
}
_loadGA = (
GA_MEASUREMENT_ID: string,
nonce?: string,
gtagUrl: string = "https://www.googletagmanager.com/gtag/js"
): void => {
if (typeof window === "undefined" || typeof document === "undefined") {
return;
}
if (!this._hasLoadedGA) {
// Global Site Tag (gtag.js) - Google Analytics
const script = document.createElement("script");
script.async = true;
script.src = `${gtagUrl}?id=${GA_MEASUREMENT_ID}`;
if (nonce) {
script.setAttribute("nonce", nonce);
}
document.body.appendChild(script);
window.dataLayer = window.dataLayer || [];
window.gtag = function gtag() {
window.dataLayer.push(arguments);
};
this._hasLoadedGA = true;
}
};
_toGtagOptions = (gaOptions?: GaOptions): Record<string, any> | undefined => {
if (!gaOptions) {
return;
}
const mapFields: Record<string, string> = {
// Old https://developers.google.com/analytics/devguides/collection/analyticsjs/field-reference#cookieUpdate
// New https://developers.google.com/analytics/devguides/collection/gtagjs/cookies-user-id#cookie_update
cookieUpdate: "cookie_update",
cookieExpires: "cookie_expires",
cookieDomain: "cookie_domain",
cookieFlags: "cookie_flags", // must be in set method?
userId: "user_id",
clientId: "client_id",
anonymizeIp: "anonymize_ip",
// https://support.google.com/analytics/answer/2853546?hl=en#zippy=%2Cin-this-article
contentGroup1: "content_group1",
contentGroup2: "content_group2",
contentGroup3: "content_group3",
contentGroup4: "content_group4",
contentGroup5: "content_group5",
// https://support.google.com/analytics/answer/9050852?hl=en
allowAdFeatures: "allow_google_signals",
allowAdPersonalizationSignals: "allow_ad_personalization_signals",
nonInteraction: "non_interaction",
page: "page_path",
hitCallback: "event_callback",
};
const gtagOptions = Object.entries(gaOptions).reduce(
(prev, [key, value]) => {
if (mapFields[key]) {
prev[mapFields[key]] = value;
} else {
prev[key] = value;
}
return prev;
},
{} as Record<string, any>
);
return gtagOptions;
};
/**
*
* @param {InitOptions[]|string} GA_MEASUREMENT_ID
* @param {Object} [options]
* @param {string} [options.nonce]
* @param {boolean} [options.testMode=false]
* @param {string} [options.gtagUrl=https://www.googletagmanager.com/gtag/js]
* @param {GaOptions|any} [options.gaOptions]
* @param {Object} [options.gtagOptions] New parameter
*/
initialize = (GA_MEASUREMENT_ID: InitOptions[] | string, options: {
nonce?: string;
testMode?: boolean;
gtagUrl?: string;
gaOptions?: GaOptions | any;
gtagOptions?: any;
} = {}): void => {
if (!GA_MEASUREMENT_ID) {
throw new Error("Require GA_MEASUREMENT_ID");
}
const initConfigs: InitOptions[] =
typeof GA_MEASUREMENT_ID === "string"
? [{ trackingId: GA_MEASUREMENT_ID }]
: GA_MEASUREMENT_ID;
this._currentMeasurementId = initConfigs[0].trackingId;
const {
gaOptions,
gtagOptions,
nonce,
testMode = false,
gtagUrl,
} = options;
this._testMode = testMode;
if (!testMode) {
this._loadGA(this._currentMeasurementId, nonce, gtagUrl);
}
if (!this.isInitialized) {
this._gtag("js", new Date());
initConfigs.forEach((config) => {
const mergedGtagOptions = {
...this._toGtagOptions({ ...gaOptions, ...config.gaOptions }),
...gtagOptions,
...config.gtagOptions,
};
if (Object.keys(mergedGtagOptions).length) {
this._gtag("config", config.trackingId, mergedGtagOptions);
} else {
this._gtag("config", config.trackingId);
}
});
}
this.isInitialized = true;
if (!testMode) {
const queues = [...this._queueGtag];
this._queueGtag = [];
this._isQueuing = false;
while (queues.length) {
const queue = queues.shift();
this._gtag(...queue);
if (queue[0] === "get") {
this._isQueuing = true;
}
}
}
};
set = (fieldsObject: any): void => {
if (!fieldsObject) {
console.warn("`fieldsObject` is required in .set()");
return;
}
if (typeof fieldsObject !== "object") {
console.warn("Expected `fieldsObject` arg to be an Object");
return;
}
if (Object.keys(fieldsObject).length === 0) {
console.warn("empty `fieldsObject` given to .set()");
}
this._gaCommand("set", fieldsObject);
};
_gaCommandSendEvent = (
eventCategory: string,
eventAction: string,
eventLabel?: string,
eventValue?: number,
fieldsObject?: any
): void => {
this._gtag("event", eventAction, {
event_category: eventCategory,
event_label: eventLabel,
value: eventValue,
...(fieldsObject && { non_interaction: fieldsObject.nonInteraction }),
...this._toGtagOptions(fieldsObject),
});
};
_gaCommandSendEventParameters = (...args: any[]): void => {
if (typeof args[0] === "string") {
this._gaCommandSendEvent(...(args.slice(1) as [string, string, string?, number?, any?]));
} else {
const {
eventCategory,
eventAction,
eventLabel,
eventValue,
// eslint-disable-next-line no-unused-vars
hitType,
...rest
} = args[0];
this._gaCommandSendEvent(
eventCategory,
eventAction,
eventLabel,
eventValue,
rest
);
}
};
_gaCommandSendTiming = (
timingCategory: string,
timingVar: string,
timingValue: number,
timingLabel?: string
): void => {
this._gtag("event", "timing_complete", {
name: timingVar,
value: timingValue,
event_category: timingCategory,
event_label: timingLabel,
});
};
_gaCommandSendPageview = (page?: string, fieldsObject?: any): void => {
if (fieldsObject && Object.keys(fieldsObject).length) {
const { title, location, ...rest } = this._toGtagOptions(fieldsObject) || {};
this._gtag("event", "page_view", {
...(page && { page_path: page }),
...(title && { page_title: title }),
...(location && { page_location: location }),
...rest,
});
} else if (page) {
this._gtag("event", "page_view", { page_path: page });
} else {
this._gtag("event", "page_view");
}
};
_gaCommandSendPageviewParameters = (...args: any[]): void => {
if (typeof args[0] === "string") {
this._gaCommandSendPageview(...args.slice(1));
} else {
const {
page,
// eslint-disable-next-line no-unused-vars
hitType,
...rest
} = args[0];
this._gaCommandSendPageview(page, rest);
}
};
// https://developers.google.com/analytics/devguides/collection/analyticsjs/command-queue-reference#send
_gaCommandSend = (...args: any[]): void => {
const hitType = typeof args[0] === "string" ? args[0] : args[0].hitType;
switch (hitType) {
case "event":
this._gaCommandSendEventParameters(...args);
break;
case "pageview":
this._gaCommandSendPageviewParameters(...args);
break;
case "timing":
this._gaCommandSendTiming(...(args.slice(1) as [string, string, number, string?]));
break;
case "screenview":
case "transaction":
case "item":
case "social":
case "exception":
console.warn(`Unsupported send command: ${hitType}`);
break;
default:
console.warn(`Send command doesn't exist: ${hitType}`);
}
};
_gaCommandSet = (...args: any[]): void => {
if (typeof args[0] === "string") {
args[0] = { [args[0]]: args[1] };
}
this._gtag("set", this._toGtagOptions(args[0]));
};
_gaCommand = (command: string, ...args: any[]): void => {
switch (command) {
case "send":
this._gaCommandSend(...args);
break;
case "set":
this._gaCommandSet(...args);
break;
default:
console.warn(`Command doesn't exist: ${command}`);
}
};
ga = (...args: any[]): any => {
if (typeof args[0] === "string") {
this._gaCommand(...(args as [string, ...any[]]));
} else {
const [readyCallback] = args;
this._gtag("get", this._currentMeasurementId, "client_id", (clientId: string) => {
this._isQueuing = false;
const queues = this._queueGtag;
readyCallback({
get: (property: string) =>
property === "clientId"
? clientId
: property === "trackingId"
? this._currentMeasurementId
: property === "apiVersion"
? "1"
: undefined,
});
while (queues.length) {
const queue = queues.shift();
this._gtag(...queue);
}
});
this._isQueuing = true;
}
return this.ga;
};
/**
* @param {UaEventOptions|string} optionsOrName
* @param {Object} [params]
*/
event = (optionsOrName: UaEventOptions | string, params?: any): void => {
if (typeof optionsOrName === "string") {
this._gtag("event", optionsOrName, this._toGtagOptions(params));
} else {
const { action, category, label, value, nonInteraction, transport } =
optionsOrName;
if (!category || !action) {
console.warn("args.category AND args.action are required in event()");
return;
}
// Required Fields
const fieldObject = {
hitType: "event",
eventCategory: format(category),
eventAction: format(action),
};
// Optional Fields
if (label) {
(fieldObject as any).eventLabel = format(label);
}
if (typeof value !== "undefined") {
if (typeof value !== "number") {
console.warn("Expected `args.value` arg to be a Number.");
} else {
(fieldObject as any).eventValue = value;
}
}
if (typeof nonInteraction !== "undefined") {
if (typeof nonInteraction !== "boolean") {
console.warn("`args.nonInteraction` must be a boolean.");
} else {
(fieldObject as any).nonInteraction = nonInteraction;
}
}
if (typeof transport !== "undefined") {
if (typeof transport !== "string") {
console.warn("`args.transport` must be a string.");
} else {
if (["beacon", "xhr", "image"].indexOf(transport) === -1) {
console.warn(
"`args.transport` must be either one of these values: `beacon`, `xhr` or `image`"
);
}
(fieldObject as any).transport = transport;
}
}
this._gaCommand("send", fieldObject);
}
};
send = (fieldObject: any): void => {
this._gaCommand("send", fieldObject);
};
}
export default new GA4();