1(function (window, document) { 2 "use strict"; 3 4 var sessionId = 5 Date.now().toString(36) + "-" + Math.random().toString(36).slice(2, 10); 6 var seen = {}; 7 var funnelSeen = {}; 8 var backendOperations = {}; 9 var blockedParamKeys = { 10 url: true, 11 href: true, 12 ip: true, 13 email: true, 14 user_agent: true, 15 userAgent: true, 16 attemptedUrl: true, 17 attempted_url: true, 18 link: true, 19 selected_domain: true, 20 }; 21 var funnelEvents = { 22 ad_eligible: true, 23 ad_impression: true, 24 start: true, 25 ready: true, 26 api_response: true, 27 download_started: true, 28 completed: true, 29 }; 30 31 function hasAnalytics() { 32 return typeof window.gtag === "function" || typeof window.ga === "function"; 33 } 34 35 function normalize(value, fallback) { 36 if (value === undefined || value === null || value === "") { 37 return fallback || "unknown"; 38 } 39 40 return String(value).slice(0, 100); 41 } 42 43 function isValidOperationId(value) { 44 return typeof value === "string" && /^[a-f0-9]{32}$/.test(value); 45 } 46 47 function beginOperation() { 48 if (!window.crypto || typeof window.crypto.getRandomValues !== "function") { 49 return null; 50 } 51 52 var bytes = new Uint8Array(16); 53 window.crypto.getRandomValues(bytes); 54 var hex = ""; 55 var index; 56 57 for (index = 0; index < bytes.length; index += 1) { 58 hex += ("0" + bytes[index].toString(16)).slice(-2); 59 } 60 61 return hex; 62 } 63 64 function bindBackendId(operationId, backendId) { 65 if (!isValidOperationId(operationId) || backendId === undefined || backendId === null || backendId === "") { 66 return; 67 } 68 69 backendOperations[String(backendId)] = operationId; 70 } 71 72 function resolveOperationId(params) { 73 params = params || {}; 74 75 if (isValidOperationId(params.operation_id)) { 76 return params.operation_id; 77 } 78 79 if (params.download_id !== undefined && backendOperations[String(params.download_id)]) { 80 return backendOperations[String(params.download_id)]; 81 } 82 83 return null; 84 } 85 86 function getHost(url) { 87 try { 88 return new URL(url).hostname || "unknown"; 89 } catch (e) { 90 return "unknown"; 91 } 92 } 93 94 function detectSource(url) { 95 var host = getHost(url).replace(/^www\./, ""); 96 97 if (host.indexOf("youtu.be") > -1 || host.indexOf("youtube.com") > -1) { 98 return "youtube"; 99 } 100 101 if (host.indexOf("soundcloud.com") > -1) { 102 return "soundcloud"; 103 } 104 105 if (host.indexOf("tiktok.com") > -1) { 106 return "tiktok"; 107 } 108 109 if (host.indexOf("vimeo.com") > -1) { 110 return "vimeo"; 111 } 112 113 return host || "unknown"; 114 } 115 116 function detectUrlType(url) { 117 if (!url) { 118 return "unknown"; 119 } 120 121 if (url.indexOf("list=") > -1 || url.indexOf("/playlist") > -1) { 122 return "playlist"; 123 } 124 125 return "single"; 126 } 127 128 function trafficSource() { 129 var params = new URLSearchParams(window.location.search); 130 var campaignSource = params.get("utm_source"); 131 132 if (campaignSource) { 133 return normalize(campaignSource); 134 } 135 136 return getHost(document.referrer || "direct").replace(/^www\./, "") || "direct"; 137 } 138 139 function deviceType() { 140 var userAgent = navigator.userAgent || ""; 141 142 if (/Tablet|iPad/i.test(userAgent)) { 143 return "tablet"; 144 } 145 146 if (/Android|Mobile|iPhone|iPod/i.test(userAgent)) { 147 return "mobile"; 148 } 149 150 return "desktop"; 151 } 152 153 function eventName(name) { 154 return "download_" + name; 155 } 156 157 function attribution(params) { 158 params = params || {}; 159 160 return { 161 download_session_id: sessionId, 162 download_id: normalize(params.download_id), 163 download_format: normalize(params.format), 164 download_quality: normalize(params.quality || params.format), 165 download_source: normalize(params.source || detectSource(params.url)), 166 download_url_type: normalize(params.url_type || detectUrlType(params.url)), 167 traffic_source: trafficSource(), 168 page_path: window.location.pathname, 169 page_host: window.location.hostname, 170 page_language: normalize(document.documentElement.lang || "unknown"), 171 device_type: deviceType(), 172 download_trigger: normalize(params.trigger), 173 download_status: normalize(params.status), 174 ad_eligible: params.ad_eligible === undefined ? undefined : Boolean(params.ad_eligible), 175 ad_provider: normalize(params.ad_provider), 176 ad_placement: normalize(params.ad_placement), 177 }; 178 } 179 180 function beaconFunnel(name, params, attrs) { 181 if (!funnelEvents[name]) { 182 return; 183 } 184 185 var operationId = resolveOperationId(params); 186 var funnelKey = (operationId || sessionId) + ":" + name; 187 188 if (funnelSeen[funnelKey]) { 189 return; 190 } 191 192 funnelSeen[funnelKey] = true; 193 194 var payload = { 195 event: name, 196 download_session_id: sessionId, 197 source: attrs.download_source, 198 traffic_source: attrs.traffic_source, 199 page_host: attrs.page_host, 200 page_path: attrs.page_path, 201 language: attrs.page_language, 202 device: attrs.device_type, 203 format: attrs.download_format, 204 status: attrs.download_status, 205 }; 206 207 if (operationId) { 208 payload.contract_version = 2; 209 payload.operation_id = operationId; 210 } else if (attrs.download_id && attrs.download_id !== "unknown") { 211 payload.download_id = attrs.download_id; 212 } 213 214 if (params.ad_eligible !== undefined) { 215 payload.ad_eligible = Boolean(params.ad_eligible); 216 } 217 218 try { 219 var body = JSON.stringify(payload); 220 var queued = false;
221 222 if (typeof navigator.sendBeacon === "function") { 223 queued = navigator.sendBeacon( 224 "/api/analytics/ad-download-funnel", 225 new Blob([body], { type: "application/json" }) 226 ); 227 } 228 229 if (queued) { 230 return; 231 } 232 233 fetch("/api/analytics/ad-download-funnel", { 234 method: "POST", 235 headers: { 236 Accept: "application/json", 237 "Content-Type": "application/json", 238 }, 239 body: body, 240 keepalive: true, 241 }).catch(function () {}); 242 } catch (e) {} 243 } 244 245 function send(name, params) { 246 params = params || {}; 247 248 var attrs = attribution(params); 249 var payload = { 250 event_category: "Download", 251 event_label: normalize(params.label || params.format || params.download_id), 252 value: params.value || 1, 253 }; 254 255 Object.keys(attrs).forEach(function (key) { 256 if (attrs[key] !== undefined) { 257 payload[key] = attrs[key]; 258 } 259 }); 260 261 Object.keys(params).forEach(function (key) { 262 if ( 263 payload[key] === undefined && 264 params[key] !== undefined && 265 params[key] !== null && 266 !blockedParamKeys[key] 267 ) { 268 payload[key] = String(params[key]).slice(0, 100); 269 } 270 }); 271 272 beaconFunnel(name, params, attrs); 273 274 if (!hasAnalytics()) { 275 return; 276 } 277 278 if (typeof window.gtag === "function") { 279 window.gtag("event", eventName(name), payload); 280 return; 281 } 282 283 if (typeof window.ga === "function") { 284 window.ga("send", "event", "Download", eventName(name), payload.event_label); 285 } 286 } 287 288 function once(key, name, params) { 289 if (seen[key]) { 290 return; 291 } 292 293 seen[key] = true; 294 send(name, params); 295 } 296 297 window.DownloadAnalytics = { 298 send: send, 299 once: once, 300 source: detectSource, 301 urlType: detectUrlType, 302 beginOperation: beginOperation, 303 bindBackendId: bindBackendId, 304 resolveOperationId: resolveOperationId, 305 }; 306 307 window.trackDownloadEvent = send; 308 309 document.addEventListener( 310 "click", 311 function (event) { 312 var target = event.target; 313 while (target && target !== document) { 314 if (target.tagName === "A" && target.href) { 315 var id = target.id || ""; 316 if ( 317 id.indexOf("download") > -1 || 318 target.hasAttribute("download") || 319 target.className.indexOf("btn-download") > -1 320 ) { 321 send("link_click", { 322 download_id: id.replace(/(_downloadLink|download_|card-downloadButton_)/g, ""), 323 trigger: "result_link", 324 status: target.className.indexOf("disabled") > -1 ? "disabled" : "enabled", 325 }); 326 } 327 return; 328 } 329 target = target.parentNode; 330 } 331 }, 332 true 333 ); 334})(window, document);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.