PageSourceSearch

https://righttolifeca.org/scripts/first-party-analytics.js

js righttolifeca.org collected 2026-09-25 20:35:45 UTC 24,285 bytes, 751 lines download raw bytes

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.