UNPKG

react-ga-neo

Version:
480 lines (419 loc) 13.5 kB
import gtag from "./gtag"; import format from "./format"; /* 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 { 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, gtagUrl = "https://www.googletagmanager.com/gtag/js") => { 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.head.appendChild(script); window.dataLayer = window.dataLayer || []; window.gtag = function gtag() { window.dataLayer.push(arguments); }; this._hasLoadedGA = true; } }; _toGtagOptions = (gaOptions) => { 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 {string} [options.nonce] * @param {boolean} [options.testMode=false] * @param {boolean} [options.titleCase=true] * @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, options = {}) => { if (!GA_MEASUREMENT_ID) { throw new Error("GA_MEASUREMENT_ID required"); } const initConfigs = typeof GA_MEASUREMENT_ID === "string" ? [{ trackingId: GA_MEASUREMENT_ID }] : GA_MEASUREMENT_ID; this._currentMeasurementId = initConfigs[0].trackingId; const { gaOptions, gtagOptions, nonce, testMode = false, titleCase = true, gtagUrl } = options; this._testMode = testMode; this._titleCase = titleCase; 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, args = null) => { if (!fieldsObject) { console.warn("`fieldsObject` is required in .set()"); return; } if (typeof fieldsObject !== "object" && !args) { 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, args); }; _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(...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); } }; /** * @param {string} timingCategory * @param {string} timingVar * @param {number} timingValue * @param {string} timingLabel */ _gaCommandSendTiming = (timingCategory, timingVar, timingValue, timingLabel) => { this._gtag("event", "timing_complete", { event_category: timingCategory, name: timingVar, value: timingValue, 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(...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 isObject = typeof args[0] === "object"; const hitType = isObject ? args[0].hitType : args[0]; switch (hitType) { case "event": this._gaCommandSendEventParameters(...args); break; case "pageview": this._gaCommandSendPageviewParameters(...args); break; case "timing": if (isObject) { const { timingValue, timingCategory, timingVar, timingLabel } = args[0]; this._gaCommandSendTiming(timingCategory, timingVar, timingValue, timingLabel); } else { this._gaCommandSendTiming(...args.slice(1)); } break; case "screenview": case "transaction": case "item": case "social": case "exception": this.exception({ description: args[1], fatal: args[2], }); break; default: console.warn(`Send command doesn't exist: ${hitType}`); } }; _gaCommandSet = (...args) => { const newArgs = []; if (typeof args[0] === "string" && typeof args[1] === "object") { newArgs.push(args[0], args[1]); } else if (typeof args[0] === "string") { newArgs.push(this._toGtagOptions({ [args[0]]: args[1] })); } else { newArgs.push(this._toGtagOptions(args[0])); } this._gtag("set", ...newArgs); }; _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(...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 } = 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, this._titleCase), eventAction: format(action, this._titleCase), }; // Optional Fields if (label) { fieldObject.eventLabel = format(label, this._titleCase); } 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; } } this._gaCommand("send", fieldObject); } }; /** * @param {Object} fieldObject */ send = (fieldObject) => { this._gaCommand("send", fieldObject); }; /** * @param {Object} [details] * @param {string} [details.description] * @param {boolean} [details.fatal] */ exception = (details = {}) => { this._gtag("event", "exception", details); }; /** * @param {Object} timingObject * @param {string} timingObject.category * @param {string} timingObject.variable * @param {number} timingObject.value * @param {string} [timingObject.label] */ timing = ({ category, variable, value, label }) => { this._gaCommandSendTiming(category, variable, value, label); }; } export default new GA4();