google-analytics-ga4
Version:
This is package for google analytics GA4 .You can import it to any node project and use it .React , Vue, Anguler etc
620 lines (544 loc) • 16.8 kB
text/typescript
import gtag from "./gtag";
import format from "./format";
declare global {
interface Window {
gtag:any;
dataLayer:any;
}
}
export interface IfieldObject {
hitType: string
eventCategory: string
eventAction: string
eventValue?: string | number
eventLabel?: string
nonInteraction?: boolean
transport?: string,
hitCallback?: () => void
}
/*
Links
https://developers.google.com/gtagjs/reference/api
https://developers.google.com/tag-platform/gtagjs/reference
*/
/**
* @typedef GaOptions
* @type {Object}
* @property {boolean} [cookieUpdate=true]
* @property {number} [cookieExpires=63072000] Default two years
* @property {string} [cookieDomain="auto"]
* @property {string} [cookieFlags]
* @property {string} [userId]
* @property {string} [clientId]
* @property {boolean} [anonymizeIp]
* @property {string} [contentGroup1]
* @property {string} [contentGroup2]
* @property {string} [contentGroup3]
* @property {string} [contentGroup4]
* @property {string} [contentGroup5]
* @property {boolean} [allowAdFeatures=true]
* @property {boolean} [allowAdPersonalizationSignals]
* @property {boolean} [nonInteraction]
* @property {string} [page]
*/
/**
* @typedef UaEventOptions
* @type {Object}
* @property {string} action
* @property {string} category
* @property {string} [label]
* @property {number} [value]
* @property {boolean} [nonInteraction]
* @property {('beacon'|'xhr'|'image')} [transport]
*/
/**
* @typedef InitOptions
* @type {Object}
* @property {string} trackingId
* @property {GaOptions|any} [gaOptions]
* @property {Object} [gtagOptions] New parameter
*/
export class GA4 {
public isInitialized: any;
public _testMode: any;
public _currentMeasurementId: any;
public _hasLoadedGA: any;
public _isQueuing: any;
public _queueGtag: any;
public gaOptions: any;
public gtagOptions: any;
public legacyDimensionMetric: any;
public nonce: any;
public testMode: any;
public eventCategory: any;
public eventAction: any;
public eventLabel: any;
public eventValue: any;
public hitType: any;
public rest: any;
public title: any;
public location: any;
public page: any;
public action: any;
public category: any;
public label: any;
public value: any;
public nonInteraction: any;
public transport: any;
constructor() {
this.reset();
}
reset = () => {
this.isInitialized = false;
this._testMode = false;
this._currentMeasurementId;
this._hasLoadedGA = false;
this._isQueuing = false;
this._queueGtag = [];
};
_gtag = (...args) => {
if (!this._testMode) {
if (this._isQueuing) {
this._queueGtag.push(args);
} else {
gtag(...args);
}
} else {
this._queueGtag.push(args);
}
};
gtag(...args) {
this._gtag(...args);
}
_loadGA = (GA_MEASUREMENT_ID, nonce) => {
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 = `https://www.googletagmanager.com/gtag/js?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): any => {
if (!gaOptions) {
return;
}
const mapFields = {
// 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;
},
{}
);
return gtagOptions;
};
/**
*
* @param {InitOptions[]|string} GA_MEASUREMENT_ID
* @param {Object} [options]
* @param {boolean} [options.legacyDimensionMetric=true]
* @param {string} [options.nonce]
* @param {boolean} [options.testMode=false]
* @param {GaOptions|any} [options.gaOptions]
* @param {Object} [options.gtagOptions] New parameter
*/
initialize = (GA_MEASUREMENT_ID, options: any = {}) => {
if (!GA_MEASUREMENT_ID) {
throw new Error("Require GA_MEASUREMENT_ID");
}
const initConfigs =
typeof GA_MEASUREMENT_ID === "string"
? [{ trackingId: GA_MEASUREMENT_ID }]
: GA_MEASUREMENT_ID;
this._currentMeasurementId = initConfigs[0].trackingId;
const {
gaOptions,
gtagOptions,
legacyDimensionMetric = true,
nonce,
testMode = false,
} = options;
this._testMode = testMode;
if (!testMode) {
this._loadGA(this._currentMeasurementId, nonce);
}
if (!this.isInitialized) {
this._gtag("js", new Date());
initConfigs.forEach((config) => {
const mergedGtagOptions = this._appendCustomMap(
{
// https://developers.google.com/analytics/devguides/collection/gtagjs/pages#disable_pageview_measurement
send_page_view: false, // default true, but React GA had false before.
...this._toGtagOptions({ ...gaOptions, ...config.gaOptions }),
...gtagOptions,
...config.gtagOptions,
},
legacyDimensionMetric
);
this._gtag("config", config.trackingId, mergedGtagOptions);
});
}
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) => {
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,
eventAction,
eventLabel,
eventValue,
fieldsObject
) => {
this._gtag("event", eventAction, {
event_category: eventCategory,
event_label: eventLabel,
value: eventValue,
...(fieldsObject && { non_interaction: fieldsObject.nonInteraction }),
...this._toGtagOptions(fieldsObject),
});
};
_gaCommandSendEventParameters = (...args) => {
if (typeof args[0] === "string") {
this._gaCommandSendEvent.apply(null,args.slice(1));
} 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,
timingVar,
timingValue,
timingLabel
) => {
this._gtag("event", "timing_complete", {
name: timingVar,
value: timingValue,
event_category: timingCategory,
event_label: timingLabel,
});
};
_gaCommandSendPageview = (page, fieldsObject) => {
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) => {
if (typeof args[0] === "string") {
this._gaCommandSendPageview.apply(null,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) => {
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.apply(null,args.slice(1));
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) => {
if (typeof args[0] === "string") {
args[0] = { [args[0]]: args[1] };
}
this._gtag("set", this._toGtagOptions(args[0]));
};
_gaCommand = (command, ...args) => {
switch (command) {
case "send":
this._gaCommandSend(...args);
break;
case "set":
this._gaCommandSet(...args);
break;
default:
console.warn(`Command doesn't exist: ${command}`);
}
};
ga = (...args) => {
if (typeof args[0] === "string") {
this._gaCommand.apply(null,args);
} else {
const [readyCallback] = args;
this._gtag("get", this._currentMeasurementId, "client_id", (clientId) => {
this._isQueuing = false;
const queues = this._queueGtag;
readyCallback({
get: (property) =>
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, params) => {
if (typeof optionsOrName === "string") {
this._gtag("event", optionsOrName, this._toGtagOptions(params));
} else {
const {
action,
category,
label,
value,
nonInteraction,
transport,
...rest
} = optionsOrName;
if (!category || !action) {
console.warn("args.category AND args.action are required in event()");
return;
}
// Required Fields
const fieldObject: IfieldObject = {
hitType: "event",
eventCategory: format(category),
eventAction: format(action),
};
// Optional Fields
if (label) {
fieldObject.eventLabel = format(label);
}
if (typeof value !== "undefined") {
if (typeof value !== "number") {
console.warn("Expected `args.value` arg to be a Number.");
} else {
fieldObject.eventValue = value;
}
}
if (typeof nonInteraction !== "undefined") {
if (typeof nonInteraction !== "boolean") {
console.warn("`args.nonInteraction` must be a boolean.");
} else {
fieldObject.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.transport = transport;
}
}
Object.keys(rest)
.filter((key) => key.substr(0, "dimension".length) === "dimension")
.forEach((key) => {
fieldObject[key] = rest[key];
});
Object.keys(rest)
.filter((key) => key.substr(0, "metric".length) === "metric")
.forEach((key) => {
fieldObject[key] = rest[key];
});
this._gaCommand("send", fieldObject);
}
};
send = (fieldObject) => {
this._gaCommand("send", fieldObject);
};
_appendCustomMap(options, legacyDimensionMetric = true) {
if (!legacyDimensionMetric) {
return options;
}
if (!options.custom_map) {
options.custom_map = {};
}
for (let i = 1; i <= 200; i++) {
if (!options.custom_map[`dimension${i}`]) {
options.custom_map[`dimension${i}`] = `dimension${i}`;
}
if (!options.custom_map[`metric${i}`]) {
options.custom_map[`metric${i}`] = `metric${i}`;
}
}
return options;
}
/**
* @since v1.0.2
* @param {string} [path="location.href"]
* @param {string[]} [_] unsupported
* @param {string} [title="location.pathname"]
* @deprecated Use `.send("pageview")` instead
*/
pageview = (path, _, title) => {
const pathTrim = path?.trim();
if (pathTrim === "") {
console.warn("path cannot be an empty string in .pageview()");
return;
}
this._gaCommand("send", "pageview", pathTrim, { title });
};
/**
* @since v1.0.6
* @param {Object} options
* @param {string} options.label
* @param {function} hitCallback
* @deprecated Use `enhanced measurement` feature in Google Analytics.
*/
outboundLink({ label }, hitCallback) {
if (typeof hitCallback !== "function") {
console.warn("hitCallback function is required");
return;
}
if (!label) {
console.warn("args.label is required in outboundLink()");
return;
}
// Required Fields
const fieldObject: IfieldObject = {
hitType: "event",
eventCategory: "Outbound",
eventAction: "Click",
eventLabel: format(label),
};
let safetyCallbackCalled = false;
const safetyCallback = () => {
// This prevents a delayed response from GA
// causing hitCallback from being fired twice
safetyCallbackCalled = true;
hitCallback();
};
// Using a timeout to ensure the execution of critical application code
// in the case when the GA server might be down
// or an ad blocker prevents sending the data
// register safety net timeout:
const t = setTimeout(safetyCallback, 250);
const clearableCallbackForGA = () => {
clearTimeout(t);
if (!safetyCallbackCalled) {
hitCallback();
}
};
fieldObject.hitCallback = clearableCallbackForGA;
this._gaCommand("send", fieldObject);
}
}
export default new GA4();