1/* eslint-disable no-undef */ 2/** 3 * @file 4 * Defer GTM until first interaction. chentheme_page_attachments_alter() 5 * strips the google_tag script libraries and passes their resolved URLs via 6 * drupalSettings.chenGtmDefer.scripts; this re-loads them on first interaction 7 * (idle fallback for non-interacting visitors). 8 */ 9(() => { 10 const EVENTS = ['scroll', 'mousemove', 'mousedown', 'touchstart', 'keydown']; 11 const LISTENER_OPTS = { once: true, passive: true, capture: true }; 12 // Fallback for visitors who never interact. Measured at 5000: behaviours 13 // attach after DCL (~2.4s on a throttled mobile load), so GTM was starting at 14 // ~7.9s and googletagmanager.com was still transferring at ~10s. That keeps 15 // the page network-busy well past the point Lighthouse looks for quiet, 16 // inflating TTI, while buying real users nothing â LCP had long since 17 // painted. 2000 keeps GTM clear of the LCP window but lands it in the same 18 // couple of seconds eager loading would have. [CMDIST-471] 19 const IDLE_FALLBACK_MS = 2000; 20 21 Drupal.behaviors.chenGtmDefer = { 22 attach: () => { 23 if (window.chenGtmDeferBound) { 24 return; 25 } 26 window.chenGtmDeferBound = true; 27 28 const settings = drupalSettings.chenGtmDefer || {}; 29 const scripts = settings.scripts || []; 30 if (!scripts.length) { 31 return; 32 } 33 34 let loaded = false; 35 let timer; 36 37 const loadGtm = () => { 38 if (loaded) { 39 return; 40 } 41 loaded = true; 42 clearTimeout(timer); 43 EVENTS.forEach((evt) => 44 window.removeEventListener(evt, loadGtm, LISTENER_OPTS), 45 ); 46 47 // No attachBehaviors() re-run after injection: none of the google_tag 48 // scripts define Drupal behaviours, and the pass would fire at script 49 // file onload â before GTM's container tags have executed â so it 50 // can't serve late GTM-delivered globals either (chen_ketch polls for 51 // its own readiness instead). A full-document pass also re-runs every 52 // behaviour on the page, double-binding any not guarded by once(), 53 // which several legacy theme components are not. 54 // 55 // `src` values are full URLs resolved server-side from the google_tag 56 // library definition, so no module path is assumed here. 57 scripts.forEach((src) => { 58 const script = document.createElement('script'); 59 script.src = src; 60 script.async = true; 61 script.onerror = () => { 62 // Surface the failure instead of silently dropping analytics. 63 window.dataLayer = window.dataLayer || []; 64 window.dataLayer.push({ 65 event: 'gtm_defer_load_error', 66 src: script.src, 67 }); 68 // eslint-disable-next-line no-console 69 window.console.error( 70 `[gtm-defer] failed to load ${script.src} â GTM/analytics will not fire`, 71 ); 72 }; 73 document.body.appendChild(script); 74 }); 75 }; 76 77 EVENTS.forEach((evt) => 78 window.addEventListener(evt, loadGtm, LISTENER_OPTS), 79 ); 80 // Fallback for visitors who never interact; keeps GTM well clear of the 81 // LCP measurement window while still collecting analytics. 82 timer = setTimeout(loadGtm, IDLE_FALLBACK_MS); 83 // A visitor who leaves (tab switch / close) before interacting or before 84 // the idle timer still gets GTM loaded on tab-hide, so short bounces 85 // aren't silently dropped. loadGtm is idempotent, so no removal needed. 86 // NOTE: this covers tab-hide only; capturing a true instant close would 87 // need navigator.sendBeacon. 88 document.addEventListener('visibilitychange', () => { 89 if (document.visibilityState === 'hidden') { 90 loadGtm(); 91 } 92 }); 93 }, 94 }; 95})();
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.