{
  "version": 3,
  "sources": ["error_reporting.js"],
  "sourcesContent": ["/*\n * static/js/error_reporting.js \u2014 uncaught JavaScript errors reach the\n * telemetry pipeline (SNOW-894).\n *\n * Until this file, nothing in static/js registered `window.onerror` or an\n * `unhandledrejection` listener. Every uncaught exception and every rejected\n * promise in the browser was invisible in production: nothing logged\n * anywhere we could read, nothing forwarded to PostHog. The 2026-09-10 map\n * review raised it because of the shape of map.js \u2014 one 8,000-line IIFE, so\n * a throw anywhere in it silently kills everything below the throw rather\n * than producing a visibly broken widget \u2014 but the gap was site-wide.\n *\n * The pipeline itself was already built and already running: telemetry.js\n * buffers to IndexedDB, beacons critical events immediately, and posts to\n * /api/telemetry. Nothing was feeding it errors. This file is only the\n * plumbing between the two.\n *\n * WHAT IS SENT, AND WHEN\n * ----------------------\n * `js.error` is a CRITICAL event (telemetry.js's CRITICAL_EVENTS), which\n * means two things: it beacons immediately rather than waiting for the next\n * flush, and it fires whatever the opt-in preference says. Both matter for\n * this event specifically \u2014 the crash most worth seeing is the one that\n * kills the page, and a page that has died does not flush its own buffer.\n *\n * Firing regardless of opt-in is a real cost, so the payload is not the same\n * for both. telemetry.js already strips `user_id` and `session_id` from an\n * opted-out envelope; this module additionally reduces its OWN properties:\n *\n *   opted in   {message, filename, lineno, colno, stack, pathname, kind}\n *   opted out  {pathname, kind}\n *\n * So an opted-out visitor contributes the fact that something broke on this\n * path, and nothing about what. That keeps the signal that would otherwise\n * be lost \u2014 \"the map is crashing for a whole class of device\" is visible\n * from paths alone \u2014 without collecting diagnostics from someone who\n * declined to be diagnosed. Decision recorded on SNOW-894.\n *\n * NO QUERY STRING REACHES THE PAYLOAD \u2014 AND `pathname` IS NOT ENOUGH\n * -------------------------------------------------------------------\n * The map's own URLs carry `?route_share=<token>` and `?trip_share=<token>`.\n * A share token is a capability, and putting one in an analytics payload\n * would put a grant on somebody's private route into a third-party system.\n * The `map.route.shared` event already follows this rule.\n *\n * Sending `pathname` rather than `href` is only the first half. `message`,\n * `filename` and `stack` are strings the runtime hands us, and each can\n * carry the document URL in full: an error thrown from an inline or\n * dynamically-evaluated script is attributed to the DOCUMENT, so `filename`\n * and every stack frame read `/?route_share=SECRET`; and a rejected fetch\n * commonly puts the request URL in its message. So every diagnostic string\n * goes through `redact()` before it is emitted, not just the path.\n *\n * Raised in review on the SNOW-894 PR. Worth stating plainly: the original\n * version satisfied its own comment and still leaked, because the comment\n * described the field it had thought about rather than the guarantee.\n *\n * WHY THE DEDUPE IS NOT OPTIONAL\n * ------------------------------\n * A throw inside a MapLibre `moveend` or `sourcedata` handler fires on every\n * frame of a pan \u2014 hundreds of times in one gesture. Unbounded, that fills\n * `queue:events` and floods the receiver with one fault repeated. Each\n * distinct `message|filename|lineno` is reported at most MAX_PER_KEY times\n * per page, and at most MAX_KEYS distinct faults are tracked at all, so the\n * bookkeeping itself cannot grow without bound either.\n *\n * NOTHING HERE MAY THROW\n * ----------------------\n * An error reporter that throws inside an error handler turns one fault into\n * a loop. Every path is wrapped, and `window.pwaTelemetry` is reached\n * through optional chaining \u2014 the same defensive convention every other\n * consumer of it uses, and the reason load order here is a preference\n * rather than a requirement.\n */\n\n(function () {\n  'use strict';\n\n  // Register exactly once. Two <script> tags for this file \u2014 a partial\n  // included twice, a page that loads it as well as base.html \u2014 would\n  // otherwise attach two sets of listeners and report every fault twice,\n  // which also doubles the dedupe cap. The listeners below read\n  // `window.pwaTelemetry` at call time rather than capturing it, so a second\n  // evaluation has nothing to contribute in any case.\n  if (window.pwaErrorReporting) return;\n\n  /** Longest stack we will send. Enough for the frames that identify a\n   *  fault; short enough that a runaway recursion's stack cannot approach\n   *  sendBeacon's ~64 KB body cap on its own. */\n  const MAX_STACK_CHARS = 2000;\n\n  /** Longest message we will send, for the same reason. */\n  const MAX_MESSAGE_CHARS = 500;\n\n  /** Reports per distinct fault, per page load. Three is enough to show a\n   *  fault is repeating without the repetition being the payload. */\n  const MAX_PER_KEY = 3;\n\n  /** Distinct faults tracked per page load. Past this the map stops\n   *  growing and further NEW faults go unreported \u2014 an acceptable trade at\n   *  twenty, by which point the page is comprehensively broken and the\n   *  first twenty say so. */\n  const MAX_KEYS = 20;\n\n  /** @type {Map<string, number>} fault key -> times already reported. */\n  const seen = new Map();\n\n  /**\n   * Faults raised before `window.pwaTelemetry` existed.\n   *\n   * This module registers ABOVE the content block so its listeners are in\n   * place before any page's boot scripts run \u2014 which necessarily means\n   * before `telemetry.js`, further down the same document. Dropping those\n   * faults would drop precisely the ones worth having: a throw during the\n   * map's boot IIFE is the motivating case for the whole file.\n   *\n   * Bounded by the dedupe, which runs before anything reaches here, so this\n   * cannot exceed MAX_KEYS * MAX_PER_KEY entries however badly the page is\n   * failing.\n   *\n   * @type {Array<{kind: string, properties: object}>}\n   */\n  const pending = [];\n\n  /**\n   * Has this fault already been reported enough times?\n   *\n   * @param {string} key\n   * @returns {boolean} True when the report should be dropped.\n   */\n  function isSuppressed(key) {\n    const count = seen.get(key) || 0;\n    if (count >= MAX_PER_KEY) return true;\n    if (count === 0 && seen.size >= MAX_KEYS) return true;\n    seen.set(key, count + 1);\n    return false;\n  }\n\n  /**\n   * Trim a string to `max` characters, or return '' for a non-string.\n   *\n   * @param {unknown} value\n   * @param {number} max\n   * @returns {string}\n   */\n  function clamp(value, max) {\n    if (typeof value !== 'string') return '';\n    return value.length > max ? value.slice(0, max) : value;\n  }\n\n  /**\n   * Strip query strings and fragments out of anything URL-shaped.\n   *\n   * Applied to every diagnostic string, not only to the path \u2014 see the\n   * header. Two passes, because they catch different things:\n   *\n   *   1. Any `\u2026?\u2026` run inside a URL-ish token loses its query. This is the\n   *      general case and covers a filename, a stack frame and a fetch\n   *      error message alike.\n   *   2. The current page's own `search`, matched literally. Belt and\n   *      braces for a runtime that has already reformatted the URL \u2014 a\n   *      percent-encoded or re-ordered copy would slip past pass 1, and\n   *      this is the one string we know for certain is sensitive.\n   *\n   * Deliberately blunt. Losing a query string that would have been useful\n   * for debugging costs a support conversation; keeping one that carries a\n   * share token costs someone their private route.\n   *\n   * @param {unknown} text\n   * @param {number} max Truncate to this many characters afterwards.\n   * @returns {string} Redacted and clamped, or '' for a non-string.\n   */\n  function redact(text, max) {\n    if (typeof text !== 'string') return '';\n    var out = text;\n    try {\n      // Pass 1 \u2014 drop `?query` and `#fragment` from any URL-ish token. The\n      // token ends at whitespace, a quote, or a bracket, which is how these\n      // strings delimit URLs in practice.\n      out = out.replace(/([^\\s'\"()<>]*?)[?#][^\\s'\"()<>]*/g, '$1?[redacted]');\n      // Pass 2 \u2014 the page's own query, verbatim.\n      var search = window.location.search;\n      if (search && search.length > 1) {\n        while (out.indexOf(search) !== -1) out = out.replace(search, '?[redacted]');\n      }\n    } catch (_err) {\n      // A redaction that throws must not become an unredacted payload.\n      return '[redaction failed]';\n    }\n    return clamp(out, max);\n  }\n\n  /**\n   * The page path, with the query string deliberately dropped.\n   *\n   * @returns {string}\n   */\n  function safePath() {\n    try {\n      return window.location.pathname || '';\n    } catch (_err) {\n      return '';\n    }\n  }\n\n  /**\n   * Emit one fault, reducing the payload when the visitor has opted out.\n   *\n   * @param {string} kind          'error' or 'unhandledrejection'.\n   * @param {object} detail        The full (opted-in) property set.\n   * @param {string} detail.message\n   * @param {string} detail.filename\n   * @param {number} detail.lineno\n   * @param {number} detail.colno\n   * @param {string} detail.stack\n   */\n  /**\n   * Hand anything buffered to telemetry, if it has arrived.\n   *\n   * Called on every new fault and from both document lifecycle events, so a\n   * page whose only fault happened before telemetry loaded still reports it.\n   */\n  function flushPending() {\n    const telemetry = window.pwaTelemetry;\n    if (!telemetry || typeof telemetry.emit !== 'function') return;\n    while (pending.length) {\n      const queued = pending.shift();\n      try {\n        telemetry.emit('js.error', queued.properties);\n      } catch (_err) {\n        // Nowhere left to report to; dropping one queued fault is better\n        // than looping on it.\n      }\n    }\n  }\n\n  function report(kind, detail) {\n    const pathname = safePath();\n    const key = `${detail.message}|${detail.filename}|${detail.lineno}`;\n    if (isSuppressed(key)) return;\n\n    // isOptIn() reads IndexedDB, so it is a promise. Resolving it before\n    // emitting is what lets the payload differ; a rejection (a broken or\n    // reset DB) is treated as opted OUT, which is the conservative branch.\n    // isOptIn() reads IndexedDB, so it is a promise. A missing telemetry\n    // module (this fault beat it to the page) and a rejection (a broken or\n    // reset DB) both resolve to opted OUT, which is the conservative branch:\n    // the buffered report carries the path and nothing else.\n    let optIn;\n    try {\n      const telemetry = window.pwaTelemetry;\n      optIn = telemetry && typeof telemetry.isOptIn === 'function'\n        ? telemetry.isOptIn()\n        : Promise.resolve(false);\n    } catch (_err) {\n      optIn = Promise.resolve(false);\n    }\n\n    Promise.resolve(optIn)\n      .catch(() => false)\n      .then((allowed) => {\n        const telemetry = window.pwaTelemetry;\n        const properties = allowed\n          ? {\n            kind,\n            pathname,\n            message: redact(detail.message, MAX_MESSAGE_CHARS),\n            filename: redact(detail.filename, MAX_MESSAGE_CHARS),\n            lineno: Number.isFinite(detail.lineno) ? detail.lineno : 0,\n            colno: Number.isFinite(detail.colno) ? detail.colno : 0,\n            stack: redact(detail.stack, MAX_STACK_CHARS),\n          }\n          // Opted out: that a fault happened, and where in the site. No\n          // message, no file, no line, no stack.\n          : { kind, pathname };\n        if (telemetry && typeof telemetry.emit === 'function') {\n          flushPending();\n          telemetry.emit('js.error', properties);\n        } else {\n          // telemetry.js has not loaded yet \u2014 see `pending`.\n          pending.push({ kind, properties });\n        }\n      })\n      .catch(() => {\n        // An error reporter that throws inside an error handler turns one\n        // fault into a loop. There is nowhere left to report to.\n      });\n  }\n\n  window.addEventListener('error', (event) => {\n    try {\n      // A failed <img>/<script>/<link> load also fires 'error' on window\n      // during capture, but it does NOT set `message`, and it is not an\n      // exception. Resource failures are a different signal with a\n      // different owner (the SW's cache paths), so they are not this\n      // event's business.\n      if (!event || typeof event.message !== 'string') return;\n      report('error', {\n        message: event.message,\n        filename: event.filename || '',\n        lineno: event.lineno,\n        colno: event.colno,\n        stack: (event.error && event.error.stack) || '',\n      });\n    } catch (_err) {\n      /* see report()'s final catch */\n    }\n  });\n\n  window.addEventListener('unhandledrejection', (event) => {\n    try {\n      if (!event) return;\n      const reason = event.reason;\n      // A promise can be rejected with anything at all \u2014 an Error, a\n      // string, a Response, undefined. Only an Error carries a stack and a\n      // trustworthy message; everything else is stringified, and a\n      // stringification that itself throws (a proxy, a circular toString)\n      // is caught below rather than escaping the handler.\n      const isError = reason instanceof Error;\n      report('unhandledrejection', {\n        message: isError ? reason.message : String(reason),\n        filename: '',\n        lineno: 0,\n        colno: 0,\n        stack: isError ? reason.stack || '' : '',\n      });\n    } catch (_err) {\n      /* see report()'s final catch */\n    }\n  });\n\n  // Exposed for tests only \u2014 the listeners above are the whole public\n  // surface in a browser. `_reset` clears the per-page dedupe state so one\n  // suite can assert the cap and the next can start from zero.\n  // Two chances to drain anything buffered on a page whose faults all\n  // happened before telemetry.js loaded and which then raises no more.\n  window.addEventListener('DOMContentLoaded', flushPending);\n  window.addEventListener('load', flushPending);\n\n  window.pwaErrorReporting = Object.freeze({\n    _flush: flushPending,\n    _reset: () => { seen.clear(); pending.length = 0; },\n    MAX_PER_KEY,\n    MAX_KEYS,\n  });\n})();\n"],
  "mappings": "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",
  "names": ["MAX_STACK_CHARS", "MAX_MESSAGE_CHARS", "MAX_PER_KEY", "MAX_KEYS", "seen", "pending", "isSuppressed", "key", "count", "clamp", "value", "max", "redact", "text", "out", "search", "safePath", "flushPending", "telemetry", "queued", "report", "kind", "detail", "pathname", "optIn", "allowed", "properties", "event", "reason", "isError"]
}
