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 }