Ctrl K
ComunidadGuías…

static/analytics.js

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
// Betterlytics - Privacy-focused, cookieless analytics
(function () {
  if (window.__betterlytics_analytics_initialized__) {
    return;
  }
  window.__betterlytics_analytics_initialized__ = true;

  // currentScript is null when an optimizer (Rocket Loader, defer/combine) injects us; never match another vendor's analytics.js
  var script =
    document.currentScript ||
    document.querySelector("script[data-betterlytics-tracker]") ||
    document.querySelector('script[data-site-id][src*="analytics"]');
  if (!script) {
    console.error("Betterlytics: could not find own script tag");
    return;
  }
  var siteId = script.getAttribute("data-site-id");

  var serverUrl = script.getAttribute("data-server-url");
  if (!serverUrl) {
    serverUrl = "https://betterlytics.io/event";
    script.setAttribute("data-server-url", serverUrl);
  }

  var urlPatterns =
    script
      .getAttribute("data-dynamic-urls")
      ?.split(",")
      .filter(Boolean)
      .map(function (p) {
        p = p.trim();
        return {
          original: p,
          regex: new RegExp(
            `^${p.replace(/\*\*/g, "(.+)").replace(/\*/g, "([^/]+)")}`,
          ),
        };
      }) ?? [];

  var scriptsBaseUrl = script.getAttribute("data-scripts-base-url");
  if (!scriptsBaseUrl) {
    try {
      scriptsBaseUrl = new URL(script.src).origin;
    } catch (e) {}
    scriptsBaseUrl = scriptsBaseUrl || "https://betterlytics.io";
  }

  // Content hash of replay.js, replaced by esbuild --define in static/build.mjs; unbuilt copies stay on "dev"
  var replayScriptHash =
    typeof __BL_REPLAY_HASH__ === "string" ? __BL_REPLAY_HASH__ : "dev";
  var replayScriptUrl = `${scriptsBaseUrl}/replay.js?v=${replayScriptHash}`;

  // "off" | "domain" | "full" (defaults to "domain")
  var outboundLinks = script.getAttribute("data-outbound-links") ?? "domain";

  var coreWebVitals = script.getAttribute("data-web-vitals") === "true";

  var enableReplay = script.getAttribute("data-replay") === "true";
  var consentReplay = script.getAttribute("data-consent-replay") === "true";

  var enableErrors = script.getAttribute("data-track-errors") === "true";
  var enableReplayOnError =
    script.getAttribute("data-replay-on-error") === "true";
  var enableConsoleErrors =
    script.getAttribute("data-track-console-errors") === "true";

  var replaySamplePct = parseInt(
    script.getAttribute("data-replay-sample") || "5",
    10,
  );

  if (isNaN(replaySamplePct) || replaySamplePct < 0 || replaySamplePct > 100) {
    replaySamplePct = 0;
  }

  var initialGlobalProperties = null;
  var initialGlobalPropertiesRaw = script.getAttribute(
    "data-global-properties",
  );
  if (initialGlobalPropertiesRaw) {
    try {
      initialGlobalProperties = JSON.parse(initialGlobalPropertiesRaw);
    } catch (e) {
      console.warn("Betterlytics: data-global-properties is not valid JSON", e);
    }
  }

  if (!siteId) {
    return console.error("Betterlytics: data-site-id attribute missing");
  }

  // Store current URL for SPA navigation
  var currentPath = window.location.pathname;

  var globalProperties = {};

  var automation =
    !!(
      navigator.webdriver ||
      window._phantom ||
      window.__nightmare ||
      window.Cypress
    ) ||
    ((navigator.userAgentData && navigator.userAgentData.brands) || []).some(
      function (b) {
        return /headless/i.test(b.brand);
      },
    );

  // Engagement tracking state (duration + scroll depth)
  var pageStartTime = performance.now();
  var currentUrl = null;
  var maxScrollDepthPx = 0;
  var currentDocHeight = 0;

  function normalize(url) {
    var urlObj = new URL(url);
    var pathname = urlObj.pathname;
    for (var i = 0; i < urlPatterns.length; i++) {
      var match = urlPatterns[i].regex.exec(pathname);
      if (match) {
        var normalizedPath = `${urlPatterns[i].original.replace(
          /\*\*/g,
          "*",
        )}${pathname.slice(match[0].length)}`;
        return `${urlObj.origin}${normalizedPath}${urlObj.search}${urlObj.hash}`;
      }
    }
    return url;
  }

  function sendEvent(eventName, overrides = {}) {
    var url = normalize(window.location.href);
    var referrer = document.referrer || null;
    var userAgent = navigator.userAgent;
    var screenResolution = window.screen.width + "x" + window.screen.height;

    fetch(serverUrl, {
      method: "POST",
      keepalive: true,
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        site_id: siteId,
        event_name: eventName,
        is_custom_event: false,
        properties: "{}",
        url: url,
        referrer: referrer,
        user_agent: userAgent,
        screen_resolution: screenResolution,
        timestamp: Math.floor(Date.now() / 1000),
        ...(automation && { automation: true }),
        ...(Object.keys(globalProperties).length > 0 && {
          global_properties: Object.assign({}, globalProperties),
        }),
        ...overrides,
      }),
    })
      .then((res) => res.text())
      .catch(function () {});
  }

  var queuedEvents = (window.betterlytics && window.betterlytics.q) || [];
  var queuedGlobalProperties =
    (window.betterlytics && window.betterlytics.gq) || [];

  var replayConsentCallbacks = [];

  window.betterlytics = {
    event: (eventName, eventProps = {}) =>
      sendEvent(eventName, {
        is_custom_event: true,
        properties: JSON.stringify(eventProps),
      }),
    setGlobalProperties: function (props) {
      if (props == null || typeof props !== "object" || Array.isArray(props)) {
        return console.error(
          "Betterlytics: setGlobalProperties requires a flat object",
        );
      }
      Object.assign(globalProperties, props);
    },
    clearGlobalProperties: function () {
      globalProperties = {};
    },
    getGlobalProperties: function () {
      return Object.assign({}, globalProperties);
    },
    setReplayConsent: function (consented) {
      var CONSENT_KEY = "betterlytics:replay_consent";
      try {
        if (consented) {
          localStorage.setItem(
            CONSENT_KEY,
            JSON.stringify({
              consented: true,
              source: "custom",
              timestamp: Date.now(),
            }),
          );
        } else {
          localStorage.removeItem(CONSENT_KEY);
        }
        for (var i = 0; i < replayConsentCallbacks.length; i++) {
          replayConsentCallbacks[i](consented);
        }
      } catch (e) {
        console.error("Failed to set replay consent:", e);
      }
    },
  };

  if (initialGlobalProperties !== null) {
    window.betterlytics.setGlobalProperties(initialGlobalProperties);
  }

  for (var i = 0; i < queuedGlobalProperties.length; i++) {
    window.betterlytics.setGlobalProperties.apply(
      this,
      queuedGlobalProperties[i],
    );
  }

  for (var i = 0; i < queuedEvents.length; i++) {
    window.betterlytics.event.apply(this, queuedEvents[i]);
  }

  function loadScript(src) {
    return new Promise(function (resolve, reject) {
      var s = document.createElement("script");
      s.src = src;
      s.async = true;

      s.onload = function () {
        resolve();
      };
      s.onerror = function (err) {
        reject(err);
      };
      document.head.appendChild(s);
    });
  }

  // Web Vitals batching (send once per page lifecycle)
  if (coreWebVitals) {
    var cwvQueue = new Map();
    var cwvFlushed = false;

    function addCwvMetric(m) {
      cwvQueue.set(m.name, {
        name: m.name,
        value: m.value,
        id: m.id,
        rating: m.rating,
        delta: m.delta,
        navigationType: m.navigationType,
      });
    }

    function flushCwvQueue() {
      if (cwvFlushed || cwvQueue.size === 0) return;
      cwvFlushed = true;
      var metrics = Array.from(cwvQueue.values());
      var byName = metrics.reduce(function (acc, m) {
        acc[m.name] = m.value;
        return acc;
      }, {});
      sendEvent("cwv", {
        cwv_cls: byName.CLS,
        cwv_lcp: byName.LCP,
        cwv_inp: byName.INP,
        cwv_fcp: byName.FCP,
        cwv_ttfb: byName.TTFB,
      });
      cwvQueue.clear();
    }

    // Flush on visibility change to hidden and on pagehide (Safari/iOS)
    document.addEventListener("visibilitychange", function () {
      if (document.visibilityState === "hidden") {
        flushCwvQueue();
      }
    });
    window.addEventListener("pagehide", function () {
      flushCwvQueue();
    });

    var s = document.createElement("script");
    s.src = `${scriptsBaseUrl}/web-vitals.js`;
    s.async = true;
    s.onload = function () {
      if (typeof webVitals !== "undefined") {
        webVitals.onCLS(addCwvMetric);
        webVitals.onINP(addCwvMetric);
        webVitals.onLCP(addCwvMetric);
        webVitals.onFCP(addCwvMetric);
        webVitals.onTTFB(addCwvMetric);
      }
    };
    document.head.appendChild(s);
  }

  function getDocumentHeight() {
    var body = document.body || {};
    var el = document.documentElement || {};
    return Math.max(
      body.scrollHeight || 0,
      body.offsetHeight || 0,
      body.clientHeight || 0,
      el.scrollHeight || 0,
      el.offsetHeight || 0,
      el.clientHeight || 0,
    );
  }

  function updateScrollDepth() {
    if (currentUrl && normalize(window.location.href) !== currentUrl) {
      return;
    }

    var body = document.body || {};
    var el = document.documentElement || {};
    var scrollTop = window.scrollY || el.scrollTop || body.scrollTop || 0;
    var viewportHeight = window.innerHeight || el.clientHeight || 0;

    currentDocHeight = getDocumentHeight();

    var scrollPosition = Math.min(scrollTop + viewportHeight, currentDocHeight);

    if (scrollPosition > maxScrollDepthPx) {
      maxScrollDepthPx = scrollPosition;
    }
  }

  function flushEngagement(urlOverride) {
    var duration = Math.round((performance.now() - pageStartTime) / 1000);
    if (duration <= 0 && maxScrollDepthPx <= 0) return;

    var overrides = {
      page_duration_seconds: duration > 0 ? duration : undefined,
    };

    if (maxScrollDepthPx > 0 && currentDocHeight > 0) {
      overrides.scroll_depth_percentage = Math.min(
        100,
        Math.round((maxScrollDepthPx / currentDocHeight) * 100),
      );
      overrides.scroll_depth_pixels = maxScrollDepthPx;
    }

    if (urlOverride) overrides.url = urlOverride;

    sendEvent("engagement", overrides);

    // Reset after sending so this function become idempotent: a second consecutive call will compute
    // duration=0 and scroll=0 and hit the early-return guard above.
    pageStartTime = performance.now();
    maxScrollDepthPx = 0;
  }

  function resetEngagement() {
    pageStartTime = performance.now();
    currentUrl = normalize(window.location.href);
    maxScrollDepthPx = 0;
  }

  window.addEventListener("scroll", () => updateScrollDepth(), {
    passive: true,
  });

  document.addEventListener("visibilitychange", function () {
    if (document.visibilityState === "hidden") {
      flushEngagement();
    } else {
      pageStartTime = performance.now();
    }
  });

  window.addEventListener("pagehide", () => flushEngagement());

  // Initialize currentUrl and capture initial viewport
  currentUrl = normalize(window.location.href);
  updateScrollDepth();

  function monitorContentHeight() {
    requestAnimationFrame(() =>
      requestAnimationFrame(() => updateScrollDepth()),
    );
    var count = 0;
    var interval = setInterval(function () {
      updateScrollDepth();
      if (++count === 15) clearInterval(interval);
    }, 200);
  }

  window.addEventListener("load", monitorContentHeight);

  // Send initial page view
  sendEvent("pageview");

  // Detect SPA navigation
  if (window.history.pushState) {
    var originalPushState = history.pushState;
    history.pushState = function () {
      flushEngagement(); // Flush before URL changes (uses current URL)
      originalPushState.apply(this, arguments); // URL changes here
      if (currentPath !== window.location.pathname) {
        currentPath = window.location.pathname;
        resetEngagement();
        monitorContentHeight();
        sendEvent("pageview");
      }
    };

    window.addEventListener("popstate", function () {
      if (currentPath !== window.location.pathname) {
        currentPath = window.location.pathname;
        flushEngagement(currentUrl); // Pass old URL as override
        resetEngagement();
        monitorContentHeight();
        sendEvent("pageview");
      }
    });
  }

  function parseOutboundLink(link) {
    var linkUrl = URL.parse(link.href, window.location.origin);
    if (
      linkUrl &&
      linkUrl.hostname !== window.location.hostname &&
      linkUrl.hostname !== ""
    ) {
      return outboundLinks === "domain"
        ? linkUrl.origin
        : linkUrl.origin + linkUrl.pathname;
    }
    return false;
  }

  // Only enable if outbounds is set to "domain" or "full"
  if (["domain", "full"].includes(outboundLinks)) {
    document.addEventListener("click", function (event) {
      var target = event.target.closest("a");
      if (target && target.href) {
        const parsed = parseOutboundLink(target);
        if (parsed) {
          sendEvent("outbound_link", {
            outbound_link_url: parsed,
          });
        }
      }
    });
  }

  if (enableErrors || enableReplayOnError) {
    var errorCounts = {},
      errorWindowStart = Date.now();
    function isRateLimited(type) {
      var now = Date.now();
      if (now - errorWindowStart > 10000) {
        errorCounts = {};
        errorWindowStart = now;
      }
      errorCounts[type] = (errorCounts[type] || 0) + 1;
      return errorCounts[type] > 10;
    }

    function captureError(err, mechanismType) {
      var type, value, stack;
      if (err != null && typeof err === "object") {
        type = err.name ? String(err.name) : "Error";
        if (err.message != null) {
          value = String(err.message);
        } else {
          try {
            value = JSON.stringify(err);
          } catch (e) {
            value = String(err);
          }
        }
        stack = err.stack ? String(err.stack).replace(/\?[^:)\s\n]+/g, "") : "";
      } else {
        type = "Error";
        value = err != null ? String(err) : "unknown error";
        stack = "";
      }
      if (
        /chrome-extension:|moz-extension:|safari-(web-)?extension:/.test(stack)
      )
        return;
      if (isRateLimited(type)) return;
      var errorExceptionsJson = JSON.stringify([
        {
          type: type,
          value: value.substring(0, 1000),
          mechanism: mechanismType,
          stack: stack.substring(0, 10000),
        },
      ]);
      sendEvent("client_error", { error_exceptions: errorExceptionsJson });
      if (window.__betterlytics_replay__?.notifyError) {
        window.__betterlytics_replay__.notifyError(type, errorExceptionsJson);
      }
    }

    window.addEventListener("error", function (event) {
      if (!event.error && !event.message) return;
      captureError(
        event.error || { name: "Error", message: event.message },
        "onuncaughtexception",
      );
    });

    window.addEventListener("unhandledrejection", function (event) {
      captureError(
        event.reason != null
          ? event.reason
          : {
              name: "UnhandledRejection",
              message: "Promise rejected with no reason",
            },
        "onunhandledrejection",
      );
    });

    if (enableConsoleErrors) {
      // Fallback for browsers without Error.isError: the tag check also covers Errors from other frames
      function isError(a) {
        if (Error.isError) return Error.isError(a);
        return a instanceof Error || Object.prototype.toString.call(a) === "[object Error]";
      }

      function formatError(e) {
        var name = e.name ? String(e.name) : "Error";
        var message = e.message != null ? String(e.message) : "";
        return message ? name + ": " + message : name;
      }

      // Errors serialize to {}, so write them as text; circular objects and BigInts throw, so fall back to String
      function formatArg(a) {
        if (isError(a)) return formatError(a);
        if (a == null || typeof a !== "object") return String(a);
        try {
          return JSON.stringify(a, function (k, v) {
            return isError(v) ? formatError(v) : v;
          });
        } catch (e) {
          return String(a);
        }
      }

      var _origConsoleError = console.error;
      var _inConsoleCapture = false;
      console.error = function () {
        _origConsoleError.apply(console, arguments);
        if (_inConsoleCapture) return;
        _inConsoleCapture = true;
        try {
          var args = Array.prototype.slice.call(arguments);
          var first = args[0];
          if (isError(first)) {
            var message = first.message != null ? String(first.message) : "";
            if (args.length > 1) {
              message =
                (message ? message + " " : "") +
                args.slice(1).map(formatArg).join(" ");
            }
            captureError(
              { name: first.name, message: message, stack: first.stack },
              "onconsole",
            );
          } else if (first != null && typeof first === "object") {
            if (!first.name) {
              first = Object.assign({ name: "ConsoleError" }, first);
            }
            if (first.message == null) {
              first = {
                name: first.name,
                message: formatArg(first),
                stack: first.stack,
              };
            }
            captureError(first, "onconsole");
          } else {
            var firstError = args.filter(isError)[0];
            captureError(
              {
                name: "ConsoleError",
                message: args.map(formatArg).join(" "),
                stack: firstError ? firstError.stack : undefined,
              },
              "onconsole",
            );
          }
        } catch (e) {
          // Capture must never throw into the page's console.error call
        } finally {
          _inConsoleCapture = false;
        }
      };
    }
  }

  if ((enableReplay || enableReplayOnError) && !automation) {
    // replay.js reads this instead of searching the DOM, where another "analytics.js" (Segment, GA) can come first
    window.__betterlytics_replay_config__ = {
      siteId: siteId,
      serverUrl: serverUrl,
      script: script,
    };

    var REPLAY_STORAGE_KEY = "betterlytics:replay_sample";
    var CONSENT_KEY = "betterlytics:replay_consent";
    var THIRTY_MIN_MS = 30 * 60 * 1000;
    var replayLoaded = false;

    function checkReplayConsent() {
      if (consentReplay) {
        return true;
      }
      try {
        var stored = localStorage.getItem(CONSENT_KEY);
        if (stored) {
          var data = JSON.parse(stored);
          return data.consented === true && data.source === "custom";
        }
      } catch (e) {}
      return false;
    }

    function shouldSample() {
      try {
        var stored = localStorage.getItem(REPLAY_STORAGE_KEY);
        var now = Date.now();

        if (stored) {
          var data = JSON.parse(stored);
          var age = now - data.timestamp;

          if (age < THIRTY_MIN_MS) {
            localStorage.setItem(
              REPLAY_STORAGE_KEY,
              JSON.stringify({ sampled: data.sampled, timestamp: now }),
            );
            return data.sampled;
          }
        }

        var sampled = Math.round(Math.random() * 100) <= replaySamplePct;

        localStorage.setItem(
          REPLAY_STORAGE_KEY,
          JSON.stringify({ sampled, timestamp: now }),
        );
        return sampled;
      } catch (e) {
        return Math.round(Math.random() * 100) <= replaySamplePct;
      }
    }

    function initReplay() {
      if (replayLoaded) return;

      var hasConsent = checkReplayConsent();
      if (!hasConsent) return;

      var sampled = enableReplay && shouldSample();
      var shouldLoadForError = enableReplayOnError;

      if (sampled || shouldLoadForError) {
        window.__betterlytics_replay_sampled__ = sampled;
        replayLoaded = true;
        loadScript(replayScriptUrl);
      }
    }

    replayConsentCallbacks.push(function (consented) {
      if (consented && !replayLoaded) {
        var sampled = enableReplay && shouldSample();
        if (sampled || enableReplayOnError) {
          window.__betterlytics_replay_sampled__ = sampled;
          replayLoaded = true;
          loadScript(replayScriptUrl);
        }
      } else if (!consented && replayLoaded) {
        window.__betterlytics_replay__?.stop();
      }
    });

    initReplay();
  }
})();