Commit 8e4e262b authored by Thomas Wisniewski's avatar Thomas Wisniewski
Browse files

Bug 1713687 - Improve the ETP shim for Google Analytics and fold the Tag...

Bug 1713687 - Improve the ETP shim for Google Analytics and fold the Tag Manager shim into it; r=webcompat-reviewers,denschub

- add wildcards to the URLs, as some sites access the scripts with GET parameters.
- we don't actually need a standalone Tag Manager shim, so just re-use the GA one.
- this will also mitigate breakage on some sites which load only one of the two scripts.
- more thoroughly stub out the Analytics window object to mitigate more site breakage.

Differential Revision: https://phabricator.services.mozilla.com/D117678
parent 8ac4b278
Loading
Loading
Loading
Loading
+9 −17
Original line number Diff line number Diff line
@@ -183,12 +183,16 @@ const AVAILABLE_SHIMS = [
    ],
  },
  {
    id: "GoogleAnalytics",
    id: "GoogleAnalyticsAndTagManager",
    platform: "all",
    name: "Google Analytics",
    bug: "1493602",
    file: "google-analytics.js",
    matches: ["*://www.google-analytics.com/analytics.js"],
    name: "Google Analytics and Tag Manager",
    bug: "1713687",
    file: "google-analytics-and-tag-manager.js",
    matches: [
      "*://www.google-analytics.com/analytics.js*",
      "*://www.google-analytics.com/gtm/js*",
      "*://www.googletagmanager.com/gtm.js*",
    ],
    onlyIfBlockedByETP: true,
  },
  {
@@ -200,18 +204,6 @@ const AVAILABLE_SHIMS = [
    matches: ["*://www.google-analytics.com/plugins/ua/ec.js"],
    onlyIfBlockedByETP: true,
  },
  {
    id: "GoogleAnalyticsTagManager",
    platform: "all",
    name: "Google Analytics Tag Manager",
    bug: "1478593",
    file: "google-analytics-tag-manager.js",
    matches: [
      "*://www.google-analytics.com/gtm/js",
      "*://www.googletagmanager.com/gtm.js",
    ],
    onlyIfBlockedByETP: true,
  },
  {
    id: "GoogleAnalyticsLegacy",
    platform: "all",
+2 −3
Original line number Diff line number Diff line
@@ -2,7 +2,7 @@
  "manifest_version": 2,
  "name": "Web Compatibility Interventions",
  "description": "Urgent post-release fixes for web compatibility.",
  "version": "23.3.0",
  "version": "23.4.0",

  "applications": {
    "gecko": {
@@ -96,10 +96,9 @@
    "shims/empty-script.js",
    "shims/facebook-sdk.js",
    "shims/facebook.svg",
    "shims/google-analytics-and-tag-manager.js",
    "shims/google-analytics-ecommerce-plugin.js",
    "shims/google-analytics-legacy.js",
    "shims/google-analytics-tag-manager.js",
    "shims/google-analytics.js",
    "shims/google-publisher-tags.js",
    "shims/live-test-shim.js",
    "shims/mochitest-shim-1.js",
+1 −2
Original line number Diff line number Diff line
@@ -83,10 +83,9 @@ FINAL_TARGET_FILES.features["webcompat@mozilla.org"]["shims"] += [
    "shims/empty-script.js",
    "shims/facebook-sdk.js",
    "shims/facebook.svg",
    "shims/google-analytics-and-tag-manager.js",
    "shims/google-analytics-ecommerce-plugin.js",
    "shims/google-analytics-legacy.js",
    "shims/google-analytics-tag-manager.js",
    "shims/google-analytics.js",
    "shims/google-publisher-tags.js",
    "shims/live-test-shim.js",
    "shims/mochitest-shim-1.js",
+164 −0
Original line number Diff line number Diff line
/* 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: () => trackers.values(),
    getByName: name => trackers.get(name),
    loaded: true,
    remove: t => ga("remove", t),
  });

  window[window.GoogleAnalyticsObject || "ga"] = ga;

  // Also process the Google Tag Manager dataLayer (bug 1713688)
  const dl = window.dataLayer;

  if (Array.isArray(dl)) {
    const push = o => {
      setTimeout(() => run(o?.eventCallback), 1);
    };
    dl.push = push;
    dl.forEach(o => push(o));
  }

  // Run dataLayer.hide.end to handle asynchide (bug 1628151)
  run(window.dataLayer?.hide?.end);
}
+0 −24
Original line number Diff line number Diff line
/* 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/. */

// based on https://github.com/gorhill/uBlock/blob/caa8e7d35ba61214a9d13e7d324b2bd2aa73237f/src/web_accessible_resources/googletagmanager_gtm.js

"use strict";

if (!window.ga) {
  window.ga = () => {};

  try {
    window.dataLayer.hide.end();
  } catch (_) {}

  const dl = window.dataLayer;
  if (typeof dl.push === "function") {
    dl.push = o => {
      if (o instanceof Object && typeof o.eventCallback === "function") {
        setTimeout(o.eventCallback, 1);
      }
    };
  }
}
Loading