summaryrefslogtreecommitdiffstats
path: root/browser/extensions/webcompat/shims/google-analytics-and-tag-manager.js
diff options
context:
space:
mode:
Diffstat (limited to 'browser/extensions/webcompat/shims/google-analytics-and-tag-manager.js')
-rw-r--r--browser/extensions/webcompat/shims/google-analytics-and-tag-manager.js187
1 files changed, 187 insertions, 0 deletions
diff --git a/browser/extensions/webcompat/shims/google-analytics-and-tag-manager.js b/browser/extensions/webcompat/shims/google-analytics-and-tag-manager.js
new file mode 100644
index 0000000000..8809fca8ec
--- /dev/null
+++ b/browser/extensions/webcompat/shims/google-analytics-and-tag-manager.js
@@ -0,0 +1,187 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/**
+ * Bug 1713687 - Shim Google Analytics and Tag Manager
+ *
+ * Sites often rely on the Google Analytics window object and will
+ * break if it fails to load or is blocked. This shim works around
+ * such breakage.
+ *
+ * Sites also often use the Google Optimizer (asynchide) code snippet,
+ * only for it to cause multi-second delays if Google Analytics does
+ * not load. This shim also avoids such delays.
+ *
+ * They also rely on Google Tag Manager, which often goes hand-in-
+ * hand with Analytics, but is not always blocked by anti-tracking
+ * lists. Handling both in the same shim handles both cases.
+ */
+
+if (window[window.GoogleAnalyticsObject || "ga"]?.loaded === undefined) {
+ const DEFAULT_TRACKER_NAME = "t0";
+
+ const trackers = new Map();
+
+ const run = function (fn, ...args) {
+ if (typeof fn === "function") {
+ try {
+ fn(...args);
+ } catch (e) {
+ console.error(e);
+ }
+ }
+ };
+
+ const create = (id, cookie, name, opts) => {
+ id = id || opts?.trackerId;
+ if (!id) {
+ return undefined;
+ }
+ cookie = cookie || opts?.cookieDomain || "_ga";
+ name = name || opts?.name || DEFAULT_TRACKER_NAME;
+ if (!trackers.has(name)) {
+ let props;
+ try {
+ props = new Map(Object.entries(opts));
+ } catch (_) {
+ props = new Map();
+ }
+ trackers.set(name, {
+ get(p) {
+ if (p === "name") {
+ return name;
+ } else if (p === "trackingId") {
+ return id;
+ } else if (p === "cookieDomain") {
+ return cookie;
+ }
+ return props.get(p);
+ },
+ ma() {},
+ requireSync() {},
+ send() {},
+ set(p, v) {
+ if (typeof p !== "object") {
+ p = Object.fromEntries([[p, v]]);
+ }
+ for (const k in p) {
+ props.set(k, p[k]);
+ if (k === "hitCallback") {
+ run(p[k]);
+ }
+ }
+ },
+ });
+ }
+ return trackers.get(name);
+ };
+
+ const cmdRE = /((?<name>.*?)\.)?((?<plugin>.*?):)?(?<method>.*)/;
+
+ function ga(cmd, ...args) {
+ if (arguments.length === 1 && typeof cmd === "function") {
+ run(cmd, trackers.get(DEFAULT_TRACKER_NAME));
+ return undefined;
+ }
+
+ if (typeof cmd !== "string") {
+ return undefined;
+ }
+
+ const groups = cmdRE.exec(cmd)?.groups;
+ if (!groups) {
+ console.error("Could not parse GA command", cmd);
+ return undefined;
+ }
+
+ let { name, plugin, method } = groups;
+
+ if (plugin) {
+ return undefined;
+ }
+
+ if (cmd === "set") {
+ trackers.get(name)?.set(args[0], args[1]);
+ }
+
+ if (method === "remove") {
+ trackers.delete(name);
+ return undefined;
+ }
+
+ if (cmd === "send") {
+ run(args.at(-1)?.hitCallback);
+ return undefined;
+ }
+
+ if (method === "create") {
+ let id, cookie, fields;
+ for (const param of args.slice(0, 4)) {
+ if (typeof param === "object") {
+ fields = param;
+ break;
+ }
+ if (id === undefined) {
+ id = param;
+ } else if (cookie === undefined) {
+ cookie = param;
+ } else {
+ name = param;
+ }
+ }
+ return create(id, cookie, name, fields);
+ }
+
+ return undefined;
+ }
+
+ Object.assign(ga, {
+ create: (a, b, c, d) => ga("create", a, b, c, d),
+ getAll: () => Array.from(trackers.values()),
+ getByName: name => trackers.get(name),
+ loaded: true,
+ remove: t => ga("remove", t),
+ });
+
+ // Process any GA command queue the site pre-declares (bug 1736850)
+ const q = window[window.GoogleAnalyticsObject || "ga"]?.q;
+ window[window.GoogleAnalyticsObject || "ga"] = ga;
+
+ if (Array.isArray(q)) {
+ const push = o => {
+ ga(...o);
+ return true;
+ };
+ q.push = push;
+ q.forEach(o => push(o));
+ }
+
+ // Also process the Google Tag Manager dataLayer (bug 1713688)
+ const dl = window.dataLayer;
+
+ if (Array.isArray(dl) && !dl.find(e => e["gtm.start"])) {
+ const push = function (o) {
+ setTimeout(() => run(o?.eventCallback), 1);
+ return true;
+ };
+ dl.push = push;
+ dl.forEach(o => push(o));
+ }
+
+ // Run dataLayer.hide.end to handle asynchide (bug 1628151)
+ run(window.dataLayer?.hide?.end);
+}
+
+if (!window?.gaplugins?.Linker) {
+ window.gaplugins = window.gaplugins || {};
+ window.gaplugins.Linker = class {
+ autoLink() {}
+ decorate(url) {
+ return url;
+ }
+ passthrough() {}
+ };
+}