UNPKG

react-ga

Version:
592 lines (516 loc) 15.2 kB
/** * React Google Analytics Module * * @package react-ga * @author Adam Lofting <adam@mozillafoundation.org> * Atul Varma <atul@mozillafoundation.org> */ /** * Utilities */ import format from './utils/format'; import removeLeadingSlash from './utils/removeLeadingSlash'; import trim from './utils/trim'; import loadGA from './utils/loadGA'; import warn from './utils/console/warn'; import log from './utils/console/log'; import TestModeAPI from './utils/testModeAPI'; const _isNotBrowser = typeof window === 'undefined' || typeof document === 'undefined'; let _debug = false; let _titleCase = true; let _testMode = false; let _alwaysSendToDefaultTracker = true; let _redactEmail = true; const internalGa = (...args) => { if (_testMode) return TestModeAPI.ga(...args); if (_isNotBrowser) return false; if (!window.ga) return warn( 'ReactGA.initialize must be called first or GoogleAnalytics should be loaded manually' ); return window.ga(...args); }; function _format(s) { return format(s, _titleCase, _redactEmail); } function _gaCommand(trackerNames, ...args) { const command = args[0]; if (typeof internalGa === 'function') { if (typeof command !== 'string') { warn('ga command must be a string'); return; } if (_alwaysSendToDefaultTracker || !Array.isArray(trackerNames)) internalGa(...args); if (Array.isArray(trackerNames)) { trackerNames.forEach((name) => { internalGa(...[`${name}.${command}`].concat(args.slice(1))); }); } } } function _initialize(gaTrackingID, options) { if (!gaTrackingID) { warn('gaTrackingID is required in initialize()'); return; } if (options) { if (options.debug && options.debug === true) { _debug = true; } if (options.titleCase === false) { _titleCase = false; } if (options.redactEmail === false) { _redactEmail = false; } if (options.useExistingGa) { return; } } if (options && options.gaOptions) { internalGa('create', gaTrackingID, options.gaOptions); } else { internalGa('create', gaTrackingID, 'auto'); } } export function addTrackers(configsOrTrackingId, options) { if (Array.isArray(configsOrTrackingId)) { configsOrTrackingId.forEach((config) => { if (typeof config !== 'object') { warn('All configs must be an object'); return; } _initialize(config.trackingId, config); }); } else { _initialize(configsOrTrackingId, options); } return true; } export function initialize(configsOrTrackingId, options) { if (options && options.testMode === true) { _testMode = true; } else { if (_isNotBrowser) { return; } if (!options || options.standardImplementation !== true) loadGA(options); } _alwaysSendToDefaultTracker = options && typeof options.alwaysSendToDefaultTracker === 'boolean' ? options.alwaysSendToDefaultTracker : true; addTrackers(configsOrTrackingId, options); } /** * ga: * Returns the original GA object. */ export function ga(...args) { if (args.length > 0) { internalGa(...args); if (_debug) { log("called ga('arguments');"); log(`with arguments: ${JSON.stringify(args)}`); } } return window.ga; } /** * set: * GA tracker set method * @param {Object} fieldsObject - a field/value pair or a group of field/value pairs on the tracker * @param {Array} trackerNames - (optional) a list of extra trackers to run the command on */ export function set(fieldsObject, trackerNames) { if (!fieldsObject) { warn('`fieldsObject` is required in .set()'); return; } if (typeof fieldsObject !== 'object') { warn('Expected `fieldsObject` arg to be an Object'); return; } if (Object.keys(fieldsObject).length === 0) { warn('empty `fieldsObject` given to .set()'); } _gaCommand(trackerNames, 'set', fieldsObject); if (_debug) { log("called ga('set', fieldsObject);"); log(`with fieldsObject: ${JSON.stringify(fieldsObject)}`); } } /** * send: * Clone of the low level `ga.send` method * WARNING: No validations will be applied to this * @param {Object} fieldObject - field object for tracking different analytics * @param {Array} trackerNames - trackers to send the command to * @param {Array} trackerNames - (optional) a list of extra trackers to run the command on */ export function send(fieldObject, trackerNames) { _gaCommand(trackerNames, 'send', fieldObject); if (_debug) { log("called ga('send', fieldObject);"); log(`with fieldObject: ${JSON.stringify(fieldObject)}`); log(`with trackers: ${JSON.stringify(trackerNames)}`); } } /** * pageview: * Basic GA pageview tracking * @param {String} path - the current page page e.g. '/about' * @param {Array} trackerNames - (optional) a list of extra trackers to run the command on * @param {String} title - (optional) the page title e. g. 'My Website' */ export function pageview(rawPath, trackerNames, title) { if (!rawPath) { warn('path is required in .pageview()'); return; } const path = trim(rawPath); if (path === '') { warn('path cannot be an empty string in .pageview()'); return; } const extraFields = {}; if (title) { extraFields.title = title; } if (typeof ga === 'function') { _gaCommand(trackerNames, 'send', { hitType: 'pageview', page: path, ...extraFields }); if (_debug) { log("called ga('send', 'pageview', path);"); let extraLog = ''; if (title) { extraLog = ` and title: ${title}`; } log(`with path: ${path}${extraLog}`); } } } /** * modalview: * a proxy to basic GA pageview tracking to consistently track * modal views that are an equivalent UX to a traditional pageview * @param {String} modalName e.g. 'add-or-edit-club' * @param {Array} trackerNames - (optional) a list of extra trackers to run the command on */ export function modalview(rawModalName, trackerNames) { if (!rawModalName) { warn('modalName is required in .modalview(modalName)'); return; } const modalName = removeLeadingSlash(trim(rawModalName)); if (modalName === '') { warn('modalName cannot be an empty string or a single / in .modalview()'); return; } if (typeof ga === 'function') { const path = `/modal/${modalName}`; _gaCommand(trackerNames, 'send', 'pageview', path); if (_debug) { log("called ga('send', 'pageview', path);"); log(`with path: ${path}`); } } } /** * timing: * GA timing * @param args.category {String} required * @param args.variable {String} required * @param args.value {Int} required * @param args.label {String} required * @param {Array} trackerNames - (optional) a list of extra trackers to run the command on */ export function timing( { category, variable, value, label } = {}, trackerNames = undefined ) { if (typeof ga === 'function') { if (!category || !variable || typeof value !== 'number') { warn( 'args.category, args.variable ' + 'AND args.value are required in timing() ' + 'AND args.value has to be a number' ); return; } // Required Fields const fieldObject = { hitType: 'timing', timingCategory: _format(category), timingVar: _format(variable), timingValue: value }; if (label) { fieldObject.timingLabel = _format(label); } send(fieldObject, trackerNames); } } /** * event: * GA event tracking * @param args.category {String} required * @param args.action {String} required * @param args.label {String} optional * @param args.value {Int} optional * @param args.nonInteraction {boolean} optional * @param args.transport {string} optional * @param {{action: string, category: string}} trackerNames - (optional) a list of extra trackers to run the command on */ export function event( { category, action, label, value, nonInteraction, transport, ...args } = {}, trackerNames = undefined ) { if (typeof ga === 'function') { // Simple Validation if (!category || !action) { 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.eventLabel = _format(label); } if (typeof value !== 'undefined') { if (typeof value !== 'number') { warn('Expected `args.value` arg to be a Number.'); } else { fieldObject.eventValue = value; } } if (typeof nonInteraction !== 'undefined') { if (typeof nonInteraction !== 'boolean') { warn('`args.nonInteraction` must be a boolean.'); } else { fieldObject.nonInteraction = nonInteraction; } } if (typeof transport !== 'undefined') { if (typeof transport !== 'string') { warn('`args.transport` must be a string.'); } else { if (['beacon', 'xhr', 'image'].indexOf(transport) === -1) { warn( '`args.transport` must be either one of these values: `beacon`, `xhr` or `image`' ); } fieldObject.transport = transport; } } Object.keys(args) .filter((key) => key.substr(0, 'dimension'.length) === 'dimension') .forEach((key) => { fieldObject[key] = args[key]; }); Object.keys(args) .filter((key) => key.substr(0, 'metric'.length) === 'metric') .forEach((key) => { fieldObject[key] = args[key]; }); // Send to GA send(fieldObject, trackerNames); } } /** * exception: * GA exception tracking * @param args.description {String} optional * @param args.fatal {boolean} optional * @param {Array} trackerNames - (optional) a list of extra trackers to run the command on */ export function exception({ description, fatal }, trackerNames) { if (typeof ga === 'function') { // Required Fields const fieldObject = { hitType: 'exception' }; // Optional Fields if (description) { fieldObject.exDescription = _format(description); } if (typeof fatal !== 'undefined') { if (typeof fatal !== 'boolean') { warn('`args.fatal` must be a boolean.'); } else { fieldObject.exFatal = fatal; } } // Send to GA send(fieldObject, trackerNames); } } export const plugin = { /** * require: * GA requires a plugin * @param name {String} e.g. 'ecommerce' or 'myplugin' * @param options {Object} optional e.g {path: '/log', debug: true} * @param trackerName {String} optional e.g 'trackerName' */ require: (rawName, options, trackerName) => { if (typeof ga === 'function') { // Required Fields if (!rawName) { warn('`name` is required in .require()'); return; } const name = trim(rawName); if (name === '') { warn('`name` cannot be an empty string in .require()'); return; } const requireString = trackerName ? `${trackerName}.require` : 'require'; // Optional Fields if (options) { if (typeof options !== 'object') { warn('Expected `options` arg to be an Object'); return; } if (Object.keys(options).length === 0) { warn('Empty `options` given to .require()'); } ga(requireString, name, options); if (_debug) { log(`called ga('require', '${name}', ${JSON.stringify(options)}`); } } else { ga(requireString, name); if (_debug) { log(`called ga('require', '${name}');`); } } } }, /** * execute: * GA execute action for plugin * Takes variable number of arguments * @param pluginName {String} e.g. 'ecommerce' or 'myplugin' * @param action {String} e.g. 'addItem' or 'myCustomAction' * @param actionType {String} optional e.g. 'detail' * @param payload {Object} optional e.g { id: '1x5e', name : 'My product to track' } */ execute: (pluginName, action, ...args) => { let payload; let actionType; if (args.length === 1) { [payload] = args; } else { [actionType, payload] = args; } if (typeof ga === 'function') { if (typeof pluginName !== 'string') { warn('Expected `pluginName` arg to be a String.'); } else if (typeof action !== 'string') { warn('Expected `action` arg to be a String.'); } else { const command = `${pluginName}:${action}`; payload = payload || null; if (actionType && payload) { ga(command, actionType, payload); if (_debug) { log(`called ga('${command}');`); log( `actionType: "${actionType}" with payload: ${JSON.stringify( payload )}` ); } } else if (payload) { ga(command, payload); if (_debug) { log(`called ga('${command}');`); log(`with payload: ${JSON.stringify(payload)}`); } } else { ga(command); if (_debug) { log(`called ga('${command}');`); } } } } } }; /** * outboundLink: * GA outboundLink tracking * @param args.label {String} e.g. url, or 'Create an Account' * @param {function} hitCallback - Called after processing a hit. */ export function outboundLink(args, hitCallback, trackerNames) { if (typeof hitCallback !== 'function') { warn('hitCallback function is required'); return; } if (typeof ga === 'function') { // Simple Validation if (!args || !args.label) { warn('args.label is required in outboundLink()'); return; } // Required Fields const fieldObject = { hitType: 'event', eventCategory: 'Outbound', eventAction: 'Click', eventLabel: _format(args.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; // Send to GA send(fieldObject, trackerNames); } else { // if ga is not defined, return the callback so the application // continues to work as expected setTimeout(hitCallback, 0); } } export const testModeAPI = TestModeAPI; export default { initialize, ga, set, send, pageview, modalview, timing, event, exception, plugin, outboundLink, testModeAPI: TestModeAPI };