1import { 2 ANALYTICS_EVENT_TYPE_SET, 3 ANALYTICS_TOUCH_ROTATION_AGE_MS, 4 ANALYTICS_UTM_KEYS, 5 canonicalAnalyticsPath, 6 cleanAnalyticsCampaignValue, 7 cleanAnalyticsHostname, 8 isAllowedAnalyticsPropertyValue, 9 isAnalyticsId, 10 sanitizeAnalyticsEvent, 11 sanitizeAnalyticsProperties, 12 sanitizeAnalyticsTouch, 13} from "./analytics-contract-v1.js"; 14 15(async function () { 16 "use strict"; 17 18 if (window.rtlcaAnalytics || window.__rtlcaSafetyExitActive) return; 19 20 // Anonymous identifiers must be cryptographically random. If Web Crypto is 21 // unavailable, analytics stays off rather than weakening the v1 contract. 22 if (!window.crypto || typeof window.crypto.getRandomValues !== "function") return; 23 // Every queue mutation must be serialized across tabs. If the browser cannot 24 // provide a cross-tab Web Lock, analytics stays off instead of falling back 25 // to a per-document mutex that could lose or recreate queued events. 26 if (!navigator.locks || typeof navigator.locks.request !== "function") return; 27 28 var ENDPOINT = "/api/analytics/event"; 29 var VISITOR_KEY = "rtlca:analytics:visitor:v1"; 30 var FIRST_TOUCH_KEY = "rtlca:analytics:first-touch:v1"; 31 var SESSION_KEY = "rtlca:analytics:session:v1"; 32 var CURRENT_TOUCH_KEY = "rtlca:analytics:current-touch:v1"; 33 var PREVIOUS_PATH_KEY = "rtlca:analytics:previous-path:v1"; 34 var QUEUE_KEY = "rtlca:analytics:queue:v1"; 35 var EXIT_TOMBSTONE_KEY = "rtlca:analytics:exit-tombstone:v1"; 36 var QUEUE_LOCK_NAME = "rtlca-analytics-queue-v1"; 37 var IDENTITY_LOCK_NAME = "rtlca-analytics-identity-v1"; 38 var RETRY_BASE_DELAY_MS = 2_000; 39 var RETRY_MAX_DELAY_MS = 60_000; 40 var MAX_SCHEDULED_RETRIES = 5; 41 var MAX_QUEUE_LENGTH = 50; 42 var MADERA_CAMPAIGN_ID = "madera_fair_2026"; 43 var MADERA_OWNED_PATHS = new Set([ 44 "/madera-fair", 45 "/madera-fair/get-involved", 46 "/madera-fair/the-300", 47 "/es/madera-fair", 48 "/es/madera-fair/get-involved", 49 "/es/madera-fair/the-300", 50 ]); 51 52 function safeRead(storage, key) { 53 try { 54 return storage.getItem(key); 55 } catch (_) { 56 return null; 57 } 58 } 59 60 function safeWrite(storage, key, value) { 61 try { 62 storage.setItem(key, value); 63 } catch (_) {} 64 } 65 66 function safeRemove(storage, key) { 67 try { 68 storage.removeItem(key); 69 } catch (_) {} 70 } 71 72 function readJson(storage, key, fallback) { 73 try { 74 var value = JSON.parse(safeRead(storage, key) || "null"); 75 return value === null ? fallback : value; 76 } catch (_) { 77 return fallback; 78 } 79 } 80 81 function randomId(prefix) { 82 var bytes = new Uint8Array(16); 83 window.crypto.getRandomValues(bytes); 84 var hex = Array.from(bytes, function (value) { 85 return value.toString(16).padStart(2, "0"); 86 }).join(""); 87 return prefix + "_" + hex; 88 } 89 90 function referrerContext() { 91 if (!document.referrer) return {}; 92 try { 93 var referrer = new URL(document.referrer); 94 var host = cleanAnalyticsHostname(referrer.hostname); 95 return host ? { referrer_host: host } : {}; 96 } catch (_) { 97 return {}; 98 } 99 } 100 101 var analyticsPathValue = 102 document.body && document.body.hasAttribute("data-analytics-path") 103 ? document.body.dataset.analyticsPath 104 : window.location.pathname; 105 var currentPath = canonicalAnalyticsPath(analyticsPathValue); 106 if (!currentPath) return; 107 var observedExitTombstone = safeRead(localStorage, EXIT_TOMBSTONE_KEY) || ""; 108 109 function tombstoneTimestamp(value) { 110 var timestamp = Number(String(value || "").split(":", 1)[0]); 111 return Number.isFinite(timestamp) && timestamp > 0 ? timestamp : 0; 112 } 113 114 function makeTouch() { 115 var params = new URLSearchParams(window.location.search); 116 var touch = Object.assign( 117 { 118 landing_path: currentPath, 119 occurred_at: new Date().toISOString(), 120 }, 121 referrerContext(), 122 ); 123 ANALYTICS_UTM_KEYS.forEach(function (key) { 124 var value = cleanAnalyticsCampaignValue(key, params.get(key)); 125 if (value) touch[key] = value; 126 }); 127 return touch; 128 } 129 130 function hasCampaignTouch(touch) { 131 return ANALYTICS_UTM_KEYS.some(function (key) { 132 return Boolean(touch && touch[key]); 133 }); 134 } 135 136 var pageTouch = makeTouch(); 137 var pageTouchTime = Date.parse(pageTouch.occurred_at); 138 139 var identity = await navigator.locks.request( 140 IDENTITY_LOCK_NAME, 141 { mode: "exclusive" }, 142 function () { 143 if ((safeRead(localStorage, EXIT_TOMBSTONE_KEY) || "") !== observedExitTombstone) { 144 return null; 145 } 146 147 var storedVisitorId = safeRead(localStorage, VISITOR_KEY);
148 var storedFirstTouch = sanitizeAnalyticsTouch( 149 readJson(localStorage, FIRST_TOUCH_KEY, null), 150 ANALYTICS_TOUCH_ROTATION_AGE_MS, 151 ); 152 if ( 153 !isAnalyticsId(storedVisitorId, "v") || 154 !storedFirstTouch || 155 Date.parse(storedFirstTouch.occurred_at) > pageTouchTime 156 ) { 157 storedVisitorId = randomId("v"); 158 storedFirstTouch = pageTouch; 159 safeWrite(localStorage, VISITOR_KEY, storedVisitorId); 160 safeWrite(localStorage, FIRST_TOUCH_KEY, JSON.stringify(storedFirstTouch)); 161 } 162 163 // Quick Exit does not wait for this lock, so check the generation again 164 // after the paired writes and undo them if an exit occurred mid-update. 165 if ((safeRead(localStorage, EXIT_TOMBSTONE_KEY) || "") !== observedExitTombstone) { 166 safeRemove(localStorage, VISITOR_KEY); 167 safeRemove(localStorage, FIRST_TOUCH_KEY); 168 return null; 169 } 170 return { visitorId: storedVisitorId, firstTouch: storedFirstTouch }; 171 }, 172 ); 173 if (!identity || window.__rtlcaSafetyExitActive) return; 174 var visitorId = identity.visitorId; 175 var firstTouch = identity.firstTouch; 176 177 var sessionId = safeRead(sessionStorage, SESSION_KEY); 178 var currentTouch = sanitizeAnalyticsTouch( 179 readJson(sessionStorage, CURRENT_TOUCH_KEY, null), 180 ANALYTICS_TOUCH_ROTATION_AGE_MS, 181 ); 182 var mustRotateSession = 183 !isAnalyticsId(sessionId, "s") || 184 !currentTouch || 185 Date.parse(firstTouch.occurred_at) > Date.parse(currentTouch.occurred_at) || 186 Date.parse(currentTouch.occurred_at) > pageTouchTime || 187 hasCampaignTouch(pageTouch); 188 if (mustRotateSession) { 189 sessionId = randomId("s"); 190 currentTouch = pageTouch; 191 safeWrite(sessionStorage, SESSION_KEY, sessionId); 192 safeWrite(sessionStorage, CURRENT_TOUCH_KEY, JSON.stringify(currentTouch)); 193 } 194 195 var previousPath = canonicalAnalyticsPath(safeRead(sessionStorage, PREVIOUS_PATH_KEY)); 196 var referrer = referrerContext(); 197 var documentLanguage = String(document.documentElement.lang || "") 198 .toLowerCase() 199 .split("-", 1)[0]; 200 var pageLanguage = 201 documentLanguage === "en" || documentLanguage === "es" ? documentLanguage : undefined; 202 var flushing = false; 203 var analyticsDisabled = false; 204 var retryTimer = null; 205 var scheduledRetryCount = 0; 206 207 function campaignForCurrentContext() { 208 return MADERA_OWNED_PATHS.has(currentPath) || 209 currentTouch.utm_campaign === MADERA_CAMPAIGN_ID 210 ? MADERA_CAMPAIGN_ID 211 : undefined; 212 } 213 214 function makeEvent(eventType, properties) { 215 var eventProperties = Object.assign({}, properties); 216 var campaignId = campaignForCurrentContext(); 217 if (campaignId) eventProperties.campaign_id = campaignId; 218 219 return sanitizeAnalyticsEvent({ 220 schema_version: 1, 221 event_id: randomId("e"), 222 visitor_id: visitorId, 223 session_id: sessionId, 224 event_type: eventType, 225 occurred_at: new Date().toISOString(), 226 page: { 227 path: currentPath, 228 title: document.title.slice(0, 160), 229 language: pageLanguage, 230 previous_path: previousPath, 231 referrer_host: referrer.referrer_host, 232 }, 233 attribution: { 234 first_touch: firstTouch, 235 current_touch: currentTouch, 236 }, 237 properties: sanitizeAnalyticsProperties(eventType, eventProperties), 238 }); 239 } 240 241 function readQueue() { 242 var queue = readJson(localStorage, QUEUE_KEY, []); 243 var now = Date.now(); 244 var lastExitAt = tombstoneTimestamp(safeRead(localStorage, EXIT_TOMBSTONE_KEY)); 245 return Array.isArray(queue) 246 ? queue 247 .filter(function (event) { 248 // Keep valid legacy events byte-for-byte so their original event, 249 // visitor, session, and touch pairing remains the retry identity. 250 return ( 251 Boolean(sanitizeAnalyticsEvent(event, now)) && 252 Date.parse(event.occurred_at) > lastExitAt 253 ); 254 }) 255 .slice(-MAX_QUEUE_LENGTH) 256 : []; 257 } 258 259 function writeQueue(queue) { 260 safeWrite(localStorage, QUEUE_KEY, JSON.stringify(queue.slice(-MAX_QUEUE_LENGTH))); 261 } 262 263 function withQueueLock(task, signal) { 264 var options = { mode: "exclusive" }; 265 if (signal) options.signal = signal; 266 return navigator.locks.request(QUEUE_LOCK_NAME, options, task); 267 } 268 269 function exitGeneration() { 270 return safeRead(localStorage, EXIT_TOMBSTONE_KEY) || ""; 271 } 272 273 function hasNewExitTombstone() { 274 var current = exitGeneration(); 275 return Boolean(current && current !== observedExitTombstone); 276 } 277 278 function enqueue(event, signal) { 279 return withQueueLock(function () {
280 if (hasNewExitTombstone()) disableAnalyticsForQuickExit(); 281 if (analyticsDisabled) return; 282 var generation = exitGeneration(); 283 var queue = readQueue(); 284 queue.push(event); 285 if (exitGeneration() !== generation) { 286 disableAnalyticsForQuickExit(); 287 return; 288 } 289 writeQueue(queue); 290 if (exitGeneration() !== generation) disableAnalyticsForQuickExit(); 291 }, signal); 292 } 293 294 function retryAfterMs(response) { 295 var value = response.headers.get("Retry-After"); 296 if (!value) return 0; 297 var seconds = Number(value); 298 if (Number.isFinite(seconds) && seconds >= 0) return seconds * 1_000; 299 var date = Date.parse(value); 300 return Number.isFinite(date) ? Math.max(0, date - Date.now()) : 0; 301 } 302 303 function isTransientStatus(status) { 304 return status === 408 || status === 425 || status === 429 || status >= 500; 305 } 306 307 function clearRetrySchedule(resetCount) { 308 if (retryTimer !== null) { 309 window.clearTimeout(retryTimer); 310 retryTimer = null; 311 } 312 if (resetCount) scheduledRetryCount = 0; 313 } 314 315 function scheduleRetry(requestedDelayMs) { 316 if ( 317 analyticsDisabled || 318 retryTimer !== null || 319 !navigator.onLine || 320 scheduledRetryCount >= MAX_SCHEDULED_RETRIES 321 ) { 322 return; 323 } 324 var backoff = RETRY_BASE_DELAY_MS * Math.pow(2, scheduledRetryCount); 325 var minimumDelay = Math.min( 326 RETRY_MAX_DELAY_MS, 327 Math.max(backoff, Number.isFinite(requestedDelayMs) ? requestedDelayMs : 0), 328 ); 329 var jitterRoom = Math.max(0, RETRY_MAX_DELAY_MS - minimumDelay); 330 var jitter = Math.min(jitterRoom, minimumDelay * 0.25 * Math.random()); 331 var delay = minimumDelay + jitter; 332 scheduledRetryCount += 1; 333 retryTimer = window.setTimeout(function () { 334 retryTimer = null; 335 void flush(); 336 }, delay); 337 } 338 339 function disableAnalyticsForQuickExit() { 340 analyticsDisabled = true; 341 observedExitTombstone = safeRead(localStorage, EXIT_TOMBSTONE_KEY) || ""; 342 clearRetrySchedule(true); 343 safeRemove(localStorage, VISITOR_KEY); 344 safeRemove(localStorage, FIRST_TOUCH_KEY); 345 safeRemove(localStorage, QUEUE_KEY); 346 safeRemove(sessionStorage, SESSION_KEY); 347 safeRemove(sessionStorage, CURRENT_TOUCH_KEY); 348 safeRemove(sessionStorage, PREVIOUS_PATH_KEY); 349 safeRemove(sessionStorage, "rtlca:madera-fair-attribution:v1"); 350 safeRemove(sessionStorage, "rtlca:journey-id:v1"); 351 safeRemove(sessionStorage, "rtlca:journey-id"); 352 safeRemove(sessionStorage, "rtlca:analytics-journey:v1"); 353 safeRemove(sessionStorage, "rtlca:quick-gift:v1"); 354 safeRemove(sessionStorage, "rtlca:donate-state:v1"); 355 visitorId = ""; 356 sessionId = ""; 357 firstTouch = {}; 358 currentTouch = {}; 359 previousPath = undefined; 360 if (window.rtlcaAnalytics) window.rtlcaAnalytics.journey_id = ""; 361 } 362 363 function nextQueuedEvent() { 364 return withQueueLock(function () { 365 if (hasNewExitTombstone()) disableAnalyticsForQuickExit(); 366 if (analyticsDisabled) return null; 367 var generation = exitGeneration(); 368 var queue = readQueue(); 369 if (exitGeneration() !== generation) { 370 disableAnalyticsForQuickExit(); 371 return null; 372 } 373 writeQueue(queue); 374 if (exitGeneration() !== generation) { 375 disableAnalyticsForQuickExit(); 376 return null; 377 } 378 return queue[0] || null; 379 }); 380 } 381 382 function removeQueuedEvent(eventId) { 383 return withQueueLock(function () { 384 if (hasNewExitTombstone()) disableAnalyticsForQuickExit(); 385 if (analyticsDisabled) return; 386 var generation = exitGeneration(); 387 var queue = readQueue().filter(function (event) { 388 return event.event_id !== eventId; 389 }); 390 if (exitGeneration() !== generation) { 391 disableAnalyticsForQuickExit(); 392 return; 393 } 394 writeQueue(queue); 395 if (exitGeneration() !== generation) disableAnalyticsForQuickExit(); 396 }); 397 } 398 399 async function drainQueue() { 400 while (!analyticsDisabled) { 401 // Hold the browser lock only for the localStorage transaction. Network 402 // I/O happens after release so a click can enqueue before navigation even 403 // while another tab is delivering an older event. Concurrent deliveries 404 // may repeat the same event ID; Shepherd's unique ID makes that harmless. 405 var queuedEvent = await nextQueuedEvent(); 406 if (!queuedEvent) return { retry: false, delay: 0 };
407 if (hasNewExitTombstone()) disableAnalyticsForQuickExit(); 408 if (analyticsDisabled) return { retry: false, delay: 0 }; 409 var response; 410 try { 411 response = await fetch(ENDPOINT, { 412 method: "POST", 413 headers: { "Content-Type": "application/json" }, 414 body: JSON.stringify(queuedEvent), 415 credentials: "same-origin", 416 keepalive: true, 417 }); 418 } catch (_) { 419 return { retry: true, delay: 0 }; 420 } 421 if (hasNewExitTombstone()) disableAnalyticsForQuickExit(); 422 if (analyticsDisabled) return { retry: false, delay: 0 }; 423 if (!response.ok) { 424 if (isTransientStatus(response.status)) { 425 return { retry: true, delay: retryAfterMs(response) }; 426 } 427 await removeQueuedEvent(queuedEvent.event_id); 428 continue; 429 } 430 await removeQueuedEvent(queuedEvent.event_id); 431 } 432 return { retry: false, delay: 0 }; 433 } 434 435 async function flush() { 436 if (analyticsDisabled || flushing || retryTimer !== null || !navigator.onLine) return; 437 flushing = true; 438 try { 439 var result = await drainQueue(); 440 if (result && result.retry) scheduleRetry(result.delay); 441 else clearRetrySchedule(true); 442 } catch (_) { 443 scheduleRetry(0); 444 } finally { 445 flushing = false; 446 } 447 } 448 449 function queueTrackedEvent(eventType, properties, signal) { 450 if (hasNewExitTombstone()) disableAnalyticsForQuickExit(); 451 if (analyticsDisabled || !ANALYTICS_EVENT_TYPE_SET.has(eventType)) return null; 452 var event = makeEvent(eventType, properties); 453 if (!event) return null; 454 var queued = enqueue(event, signal) 455 .then(function () { 456 void flush(); 457 return true; 458 }) 459 .catch(function (error) { 460 if (!error || error.name !== "AbortError") scheduleRetry(0); 461 return false; 462 }); 463 return { event_id: event.event_id, queued: queued }; 464 } 465 466 function track(eventType, properties) { 467 var pending = queueTrackedEvent(eventType, properties); 468 return pending ? pending.event_id : null; 469 } 470 471 function safeDestination(element) { 472 if (!(element instanceof HTMLAnchorElement) || !element.href) return undefined; 473 try { 474 var destination = new URL(element.href, window.location.href); 475 return destination.origin === window.location.origin 476 ? canonicalAnalyticsPath(destination.pathname) 477 : undefined; 478 } catch (_) { 479 return undefined; 480 } 481 } 482 483 function formId(form) { 484 var value = form.dataset.analyticsForm || form.id || form.getAttribute("name") || "form"; 485 return isAllowedAnalyticsPropertyValue("form_id", value, "form_start") ? value : "form"; 486 } 487 488 var resources = { 489 "/pregnancy-help": "pregnancy_help", 490 "/lifehouse": "lifehouse", 491 "/rachels-vineyard": "rachels_vineyard", 492 "/es/pregnancy-help": "pregnancy_help", 493 "/es/lifehouse": "lifehouse", 494 "/es/rachels-vineyard": "rachels_vineyard", 495 }; 496 497 function outboundHelpResource(target) { 498 var explicitResource = target.dataset.analyticsResource || target.dataset.analyticsResourceId; 499 if ( 500 explicitResource && 501 isAllowedAnalyticsPropertyValue( 502 "resource_id", 503 explicitResource, 504 "outbound_help_click", 505 ) 506 ) { 507 return explicitResource; 508 } 509 510 var pageResource = resources[currentPath]; 511 if (!pageResource || !(target instanceof HTMLAnchorElement) || !target.href) return undefined; 512 try { 513 var destination = new URL(target.href, window.location.href); 514 var isDirectHelp = destination.protocol === "tel:" || destination.protocol === "sms:"; 515 var isExternalResource = 516 (destination.protocol === "http:" || destination.protocol === "https:") && 517 destination.origin !== window.location.origin; 518 return isDirectHelp || isExternalResource ? pageResource : undefined; 519 } catch (_) { 520 return undefined; 521 } 522 } 523 524 function waitForTrackedNavigation( 525 event, 526 target, 527 queuedWrites, 528 abortController, 529 maxWaitMs, 530 ) { 531 if ( 532 !queuedWrites.length || 533 event.defaultPrevented || 534 !(event instanceof MouseEvent) || 535 event.button !== 0 || 536 event.metaKey || 537 event.ctrlKey || 538 event.shiftKey || 539 event.altKey || 540 !(target instanceof HTMLAnchorElement) || 541 !target.href || 542 target.hasAttribute("download") || 543 (target.target && target.target.toLowerCase() !== "_self") 544 ) { 545 return; 546 } 547 548 var destination = target.href; 549 event.preventDefault(); 550 var timeoutId; 551 var deadline = new Promise(function (resolve) { 552 timeoutId = window.setTimeout(function () { 553 abortController.abort("navigation deadline"); 554 resolve(); 555 }, maxWaitMs); 556 }); 557 void Promise.race([Promise.allSettled(queuedWrites), deadline]).then(function () { 558 window.clearTimeout(timeoutId); 559 if (!window.__rtlcaSafetyExitActive) window.location.assign(destination); 560 }); 561 } 562 563 var startedForms = new WeakSet(); 564 document.addEventListener( 565 "focusin", 566 function (event) { 567 var form = event.target instanceof Element ? event.target.closest("form") : null; 568 if (!form || startedForms.has(form)) return; 569 startedForms.add(form); 570 track("form_start", { form_id: formId(form) }); 571 }, 572 true, 573 ); 574 575 document.addEventListener( 576 "submit", 577 function (event) { 578 if (event.target instanceof HTMLFormElement) { 579 track("form_submit", { form_id: formId(event.target) }); 580 } 581 }, 582 true, 583 ); 584 585 document.addEventListener( 586 "click", 587 function (event) { 588 var target = 589 event.target instanceof Element 590 ? event.target.closest( 591 "a, button, [data-quick-exit], [data-quick-gift-exit], [data-analytics-resource], [data-analytics-resource-id]", 592 ) 593 : null; 594 if (!target) return; 595 596 if (target.matches("[data-quick-exit], [data-quick-gift-exit]")) {
597 disableAnalyticsForQuickExit(); 598 return; 599 } 600 601 var queuedWrites = []; 602 var navigationAbortController = new AbortController(); 603 var resourceId = outboundHelpResource(target); 604 if (resourceId) { 605 var helpEvent = queueTrackedEvent( 606 "outbound_help_click", 607 { resource_id: resourceId }, 608 navigationAbortController.signal, 609 ); 610 if (helpEvent) queuedWrites.push(helpEvent.queued); 611 waitForTrackedNavigation( 612 event, 613 target, 614 queuedWrites, 615 navigationAbortController, 616 120, 617 ); 618 return; 619 } 620 621 var destinationPath = safeDestination(target); 622 if (destinationPath === "/donate" || destinationPath === "/es/donate") { 623 var donationEvent = queueTrackedEvent( 624 "donation_click", 625 { 626 cta_id: target.dataset.analyticsCta || "donate", 627 destination_path: destinationPath, 628 }, 629 navigationAbortController.signal, 630 ); 631 if (donationEvent) queuedWrites.push(donationEvent.queued); 632 } 633 634 var ctaId = target.dataset.analyticsCta || target.dataset.analyticsEvent; 635 if (isAllowedAnalyticsPropertyValue("cta_id", ctaId, "cta_click")) { 636 var ctaEvent = queueTrackedEvent( 637 "cta_click", 638 { 639 cta_id: ctaId, 640 destination_path: destinationPath, 641 placement: target.dataset.analyticsPlacement, 642 funnel_step: target.dataset.analyticsStep, 643 }, 644 navigationAbortController.signal, 645 ); 646 if (ctaEvent) queuedWrites.push(ctaEvent.queued); 647 } 648 649 waitForTrackedNavigation( 650 event, 651 target, 652 queuedWrites, 653 navigationAbortController, 654 800, 655 ); 656 }, 657 false, 658 ); 659 660 var donationCompleteTracked = false; 661 function detectDonationSuccess() { 662 if (donationCompleteTracked || !document.querySelector("[data-donation-success]")) return; 663 donationCompleteTracked = true; 664 var isFairCheckout = 665 currentPath === "/madera-fair/the-300" || currentPath === "/es/madera-fair/the-300"; 666 var properties = { 667 funnel_step: isFairCheckout ? "the_300_complete" : "donation_complete", 668 donation_program: isFairCheckout ? "the_300" : "general", 669 outcome: "success", 670 }; 671 if (isFairCheckout) properties.frequency = "monthly"; 672 track("donation_complete", properties); 673 } 674 675 var successObserver = new MutationObserver(detectDonationSuccess); 676 successObserver.observe(document.documentElement, { childList: true, subtree: true }); 677 678 window.rtlcaAnalytics = { 679 journey_id: sessionId, 680 track: track, 681 flush: flush, 682 getJourneyId: function () { 683 return sessionId; 684 }, 685 getContext: function () { 686 return { 687 visitor_id: visitorId, 688 session_id: sessionId, 689 first_touch: Object.assign({}, firstTouch), 690 current_touch: Object.assign({}, currentTouch), 691 }; 692 }, 693 }; 694 695 document.dispatchEvent( 696 new CustomEvent("rtlca:analytics-ready", { detail: { journey_id: sessionId } }), 697 ); 698 699 window.addEventListener("online", function () { 700 clearRetrySchedule(true); 701 void flush(); 702 }); 703 window.addEventListener("storage", function (event) { 704 if ( 705 event.key === EXIT_TOMBSTONE_KEY && 706 event.newValue && 707 event.newValue !== observedExitTombstone 708 ) { 709 window.__rtlcaSafetyExitActive = true; 710 disableAnalyticsForQuickExit(); 711 } 712 }); 713 track("page_view", {}); 714 safeWrite(sessionStorage, PREVIOUS_PATH_KEY, currentPath); 715 716 var storyMatch = currentPath.match(/^\/(?:es\/)?sidewalk-stories\/([^/]+)$/u); 717 if (storyMatch) { 718 track("story_view", { story_slug: storyMatch[1] }); 719 var elapsed = false; 720 var depth = false; 721 var engaged = false; 722 function checkStoryEngagement() { 723 if (engaged || !elapsed || !depth) return; 724 engaged = true; 725 track("story_engaged", { story_slug: storyMatch[1], outcome: "engaged" }); 726 } 727 window.setTimeout(function () { 728 elapsed = true; 729 checkStoryEngagement(); 730 }, 30_000); 731 window.addEventListener( 732 "scroll", 733 function () { 734 var available = document.documentElement.scrollHeight - window.innerHeight; 735 if (available > 0 && window.scrollY / available >= 0.5) { 736 depth = true; 737 checkStoryEngagement(); 738 } 739 }, 740 { passive: true }, 741 ); 742 } 743 744 if (resources[currentPath]) { 745 track("resource_view", { resource_id: resources[currentPath] }); 746 } 747 748 detectDonationSuccess(); 749})().catch(function () { 750 // Analytics is optional. Any initialization failure stays fail-closed. 751});
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.