tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

google-analytics-and-tag-manager.js (4693B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 "use strict";
      6 
      7 /**
      8 * Bug 1713687 - Shim Google Analytics and Tag Manager
      9 *
     10 * Sites often rely on the Google Analytics window object and will
     11 * break if it fails to load or is blocked. This shim works around
     12 * such breakage.
     13 *
     14 * Sites also often use the Google Optimizer (asynchide) code snippet,
     15 * only for it to cause multi-second delays if Google Analytics does
     16 * not load. This shim also avoids such delays.
     17 *
     18 * They also rely on Google Tag Manager, which often goes hand-in-
     19 * hand with Analytics, but is not always blocked by anti-tracking
     20 * lists. Handling both in the same shim handles both cases.
     21 */
     22 
     23 if (window[window.GoogleAnalyticsObject || "ga"]?.loaded === undefined) {
     24  const DEFAULT_TRACKER_NAME = "t0";
     25 
     26  const trackers = new Map();
     27 
     28  const run = function (fn, ...args) {
     29    if (typeof fn === "function") {
     30      try {
     31        fn(...args);
     32      } catch (e) {
     33        console.error(e);
     34      }
     35    }
     36  };
     37 
     38  const create = (id, cookie, name, opts) => {
     39    id = id || opts?.trackerId;
     40    if (!id) {
     41      return undefined;
     42    }
     43    cookie = cookie || opts?.cookieDomain || "_ga";
     44    name = name || opts?.name || DEFAULT_TRACKER_NAME;
     45    if (!trackers.has(name)) {
     46      let props;
     47      try {
     48        props = new Map(Object.entries(opts));
     49      } catch (_) {
     50        props = new Map();
     51      }
     52      trackers.set(name, {
     53        get(p) {
     54          if (p === "name") {
     55            return name;
     56          } else if (p === "trackingId") {
     57            return id;
     58          } else if (p === "cookieDomain") {
     59            return cookie;
     60          }
     61          return props.get(p);
     62        },
     63        ma() {},
     64        requireSync() {},
     65        send() {},
     66        set(p, v) {
     67          if (typeof p !== "object") {
     68            p = Object.fromEntries([[p, v]]);
     69          }
     70          for (const k in p) {
     71            props.set(k, p[k]);
     72            if (k === "hitCallback") {
     73              run(p[k]);
     74            }
     75          }
     76        },
     77      });
     78    }
     79    return trackers.get(name);
     80  };
     81 
     82  const cmdRE = /((?<name>.*?)\.)?((?<plugin>.*?):)?(?<method>.*)/;
     83 
     84  function ga(cmd, ...args) {
     85    if (arguments.length === 1 && typeof cmd === "function") {
     86      run(cmd, trackers.get(DEFAULT_TRACKER_NAME));
     87      return undefined;
     88    }
     89 
     90    if (typeof cmd !== "string") {
     91      return undefined;
     92    }
     93 
     94    const groups = cmdRE.exec(cmd)?.groups;
     95    if (!groups) {
     96      console.error("Could not parse GA command", cmd);
     97      return undefined;
     98    }
     99 
    100    let { name, plugin, method } = groups;
    101 
    102    if (plugin) {
    103      return undefined;
    104    }
    105 
    106    if (cmd === "set") {
    107      trackers.get(name)?.set(args[0], args[1]);
    108    }
    109 
    110    if (method === "remove") {
    111      trackers.delete(name);
    112      return undefined;
    113    }
    114 
    115    if (cmd === "send") {
    116      run(args.at(-1)?.hitCallback);
    117      return undefined;
    118    }
    119 
    120    if (method === "create") {
    121      let id, cookie, fields;
    122      for (const param of args.slice(0, 4)) {
    123        if (typeof param === "object") {
    124          fields = param;
    125          break;
    126        }
    127        if (id === undefined) {
    128          id = param;
    129        } else if (cookie === undefined) {
    130          cookie = param;
    131        } else {
    132          name = param;
    133        }
    134      }
    135      return create(id, cookie, name, fields);
    136    }
    137 
    138    return undefined;
    139  }
    140 
    141  Object.assign(ga, {
    142    create: (a, b, c, d) => ga("create", a, b, c, d),
    143    getAll: () => Array.from(trackers.values()),
    144    getByName: name => trackers.get(name),
    145    loaded: true,
    146    remove: t => ga("remove", t),
    147  });
    148 
    149  // Process any GA command queue the site pre-declares (bug 1736850)
    150  const q = window[window.GoogleAnalyticsObject || "ga"]?.q;
    151  window[window.GoogleAnalyticsObject || "ga"] = ga;
    152 
    153  if (Array.isArray(q)) {
    154    const push = o => {
    155      ga(...o);
    156      return true;
    157    };
    158    q.push = push;
    159    q.forEach(o => push(o));
    160  }
    161 
    162  // Also process the Google Tag Manager dataLayer (bug 1713688)
    163  const dl = window.dataLayer;
    164 
    165  if (Array.isArray(dl) && !dl.find(e => e["gtm.start"])) {
    166    const push = function (o) {
    167      setTimeout(() => run(o?.eventCallback), 1);
    168      return true;
    169    };
    170    dl.push = push;
    171    dl.forEach(o => push(o));
    172  }
    173 
    174  // Run dataLayer.hide.end to handle asynchide (bug 1628151)
    175  run(window.dataLayer?.hide?.end);
    176 }
    177 
    178 if (!window?.gaplugins?.Linker) {
    179  window.gaplugins = window.gaplugins || {};
    180  window.gaplugins.Linker = class {
    181    autoLink() {}
    182    decorate(url) {
    183      return url;
    184    }
    185    passthrough() {}
    186  };
    187 }