@glidevvr/storage-payload-error-logger-pkg 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
- import { L as LogContext, I as InitLoggerConfig, a as LogErrorOptions, R as Reason, S as Severity } from './types-BLz-TUBl.cjs';
2
- export { b as LogChargeLine, c as LogEvent, d as Repo, e as Source } from './types-BLz-TUBl.cjs';
1
+ import { L as LogContext, I as InitLoggerConfig, a as LogErrorOptions, R as Reason, S as Severity } from './types-ue_E93K4.cjs';
2
+ export { b as LogChargeLine, c as LogEvent, d as Repo, e as Source } from './types-ue_E93K4.cjs';
3
3
 
4
4
  /**
5
5
  * @fileoverview Ambient log context store — browser-only.
@@ -77,6 +77,11 @@ declare global {
77
77
  * Idempotent: a `globalThis` sentinel ensures repeated calls do not re-install.
78
78
  * Only the host page should call this — widgets embedded in the host inherit
79
79
  * coverage automatically because errors bubble to page-level listeners.
80
+ *
81
+ * Auto-tags third-party script errors (GTM tags, vendor pixels, etc.) with
82
+ * `reason: "third_party_script"` and — when the browser exposes the host —
83
+ * `context.scriptHost`. Lets admins filter / triage those separately from
84
+ * the host's own errors.
80
85
  */
81
86
  declare function installGlobalHandlers(): void;
82
87
 
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { L as LogContext, I as InitLoggerConfig, a as LogErrorOptions, R as Reason, S as Severity } from './types-BLz-TUBl.js';
2
- export { b as LogChargeLine, c as LogEvent, d as Repo, e as Source } from './types-BLz-TUBl.js';
1
+ import { L as LogContext, I as InitLoggerConfig, a as LogErrorOptions, R as Reason, S as Severity } from './types-ue_E93K4.js';
2
+ export { b as LogChargeLine, c as LogEvent, d as Repo, e as Source } from './types-ue_E93K4.js';
3
3
 
4
4
  /**
5
5
  * @fileoverview Ambient log context store — browser-only.
@@ -77,6 +77,11 @@ declare global {
77
77
  * Idempotent: a `globalThis` sentinel ensures repeated calls do not re-install.
78
78
  * Only the host page should call this — widgets embedded in the host inherit
79
79
  * coverage automatically because errors bubble to page-level listeners.
80
+ *
81
+ * Auto-tags third-party script errors (GTM tags, vendor pixels, etc.) with
82
+ * `reason: "third_party_script"` and — when the browser exposes the host —
83
+ * `context.scriptHost`. Lets admins filter / triage those separately from
84
+ * the host's own errors.
80
85
  */
81
86
  declare function installGlobalHandlers(): void;
82
87
 
package/dist/index.js CHANGED
@@ -304,13 +304,28 @@ function flushPending() {
304
304
  }
305
305
 
306
306
  // src/installGlobalHandlers.ts
307
+ function thirdPartyScriptOpts(event) {
308
+ if (event.message === "Script error." || event.message === "Script error") {
309
+ return { reason: "third_party_script" };
310
+ }
311
+ if (event.filename) {
312
+ try {
313
+ const url = new URL(event.filename);
314
+ if (url.origin !== window.location.origin) {
315
+ return { reason: "third_party_script", context: { scriptHost: url.host } };
316
+ }
317
+ } catch {
318
+ }
319
+ }
320
+ return void 0;
321
+ }
307
322
  function installGlobalHandlers() {
308
323
  if (typeof window === "undefined") return;
309
324
  if (globalThis.__errorLoggerHandlersInstalled__) return;
310
325
  globalThis.__errorLoggerHandlersInstalled__ = true;
311
326
  window.addEventListener("error", (event) => {
312
327
  const err = event.error instanceof Error ? event.error : new Error(String(event.error ?? event.message ?? "Unknown error"));
313
- logError(err);
328
+ logError(err, thirdPartyScriptOpts(event));
314
329
  });
315
330
  window.addEventListener("unhandledrejection", (event) => {
316
331
  const err = event.reason instanceof Error ? event.reason : new Error(String(event.reason ?? "Unhandled promise rejection"));
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/context.ts","../src/initLogger.ts","../src/fingerprint.ts","../src/throttle.ts","../src/batch.ts","../src/circuitBreaker.ts","../src/recaptcha.ts","../src/severity.ts","../src/transport.ts","../src/logError.ts","../src/installGlobalHandlers.ts"],"sourcesContent":["export type {\n LogContext,\n LogErrorOptions,\n LogEvent,\n InitLoggerConfig,\n LogChargeLine,\n Reason,\n Severity,\n Source,\n Repo,\n} from \"./types\";\nexport { setLogContext, withLogContext, getLogContext } from \"./context\";\nexport { initLogger } from \"./initLogger\";\nexport { logError } from \"./logError\";\nexport { installGlobalHandlers } from \"./installGlobalHandlers\";\nexport { getDefaultSeverity } from \"./severity\";\nexport { fingerprint } from \"./fingerprint\";\n","/**\n * @fileoverview Ambient log context store — browser-only.\n *\n * On the browser the module-level `current` object is a per-tab singleton, the\n * scope we want. On Node (SSR), the same module is shared across every\n * incoming request running in the process, so writing to `current` in one\n * request would leak that context into other requests' error events. To stay\n * safe, all three functions no-op (or return `{}`) when there is no `window`.\n *\n * **Server callers must pass context explicitly** via the `opts.context`\n * parameter on `logError`, which is merged into the event regardless of the\n * ambient state.\n */\nimport type { LogContext } from \"./types\";\n\nlet current: LogContext = {};\n\nconst isBrowser = (): boolean => typeof window !== \"undefined\";\n\n/**\n * Add or overwrite keys in the ambient log context. Existing keys you don't\n * pass are preserved; keys you do pass replace existing values (no deep merge).\n *\n * Call at meaningful UX boundaries (page mount, step change, modal open) so\n * later errors automatically pick up the relevant context.\n *\n * No-op on the server — see file-level note above.\n */\nexport function setLogContext(patch: LogContext): void {\n if (!isBrowser()) return;\n current = { ...current, ...patch };\n}\n\n/**\n * Return a copy of the current ambient log context. Used by `logError` to\n * attach context to outgoing events.\n *\n * Returns `{}` on the server — see file-level note above.\n */\nexport function getLogContext(): LogContext {\n if (!isBrowser()) return {};\n return { ...current };\n}\n\n/**\n * Run `fn` with extra context keys layered on top of the ambient context, then\n * put the original context back — even if `fn` throws.\n *\n * Use this when the extra keys should only apply to one synchronous operation\n * and not leak into anything that runs afterwards.\n *\n * On the server `fn` runs without any ambient mutation — see file-level note above.\n */\nexport function withLogContext<T>(scope: LogContext, fn: () => T): T {\n if (!isBrowser()) return fn();\n const prior = current;\n current = { ...current, ...scope };\n try {\n return fn();\n } finally {\n current = prior;\n }\n}\n\n/** @internal — test-only */\nexport function _resetForTests(): void {\n current = {};\n}\n","import type { InitLoggerConfig } from \"./types\";\n\nlet config: InitLoggerConfig | null = null;\n\n/**\n * Set up the logger. Call once at app startup.\n *\n * Required fields: `endpoint` (where the browser sends events) and `repo`\n * (stamped on every event). Optional: `release`, `recaptchaSiteKey`,\n * `recaptchaAction`, `defaultSource`. Throws if a required field is missing\n * so misconfiguration is loud at startup instead of silently dropping events.\n */\nexport function initLogger(cfg: InitLoggerConfig): void {\n if (!cfg?.endpoint) throw new Error(\"initLogger: endpoint is required\");\n if (!cfg?.repo) throw new Error(\"initLogger: repo is required\");\n config = { ...cfg };\n}\n\n/** @internal — read the current config; returns null before `initLogger` runs. */\nexport function _getConfig(): InitLoggerConfig | null {\n return config;\n}\n\n/** @internal — test-only */\nexport function _resetForTests(): void {\n config = null;\n}\n","/**\n * Compute a short stable hash that identifies \"the same bug\" across occurrences.\n * Used by the dedup hook to find-or-create the parent `Issues` row.\n *\n * Only the top frame of the stack is hashed so a bug fired from the same place\n * groups together even if surrounding code paths differ. Returned as a hex\n * string just to keep it compact; we just need stable grouping, not security.\n *\n * `reason` is included so the same code path failing for different reasons\n * (e.g. `invalid_cvv` vs `gateway_decline` at the payment step) becomes two\n * separate Issues. An empty / omitted reason hashes the same as `\"\"`, so\n * existing 3-arg callers stay compatible.\n */\nexport function fingerprint(\n source: string,\n message: string,\n stack: string,\n reason?: string,\n): string {\n const topFrame = stack.split(\"\\n\", 2)[0] ?? \"\";\n const reasonPart = reason ?? \"\";\n const input = `${source}|${message}|${topFrame}|${reasonPart}`;\n let hash = 5381;\n for (let i = 0; i < input.length; i++) {\n hash = ((hash << 5) + hash) + input.charCodeAt(i);\n hash |= 0;\n }\n return (hash >>> 0).toString(16);\n}\n","/** Throttle that decides whether a given fingerprint should be sent right now. */\nexport interface Throttle {\n shouldEmit(fingerprint: string): boolean;\n}\n\n/**\n * Returns a throttle that lets the first occurrence of each fingerprint through\n * and silently drops repeats until `windowMs` passes. Stops a bug firing in a\n * tight loop from flooding the CMS.\n */\nexport function createThrottle(opts: { windowMs: number }): Throttle {\n const lastEmittedAt = new Map<string, number>();\n return {\n shouldEmit(fingerprint: string): boolean {\n const now = Date.now();\n const last = lastEmittedAt.get(fingerprint);\n if (last !== undefined && now - last < opts.windowMs) return false;\n lastEmittedAt.set(fingerprint, now);\n return true;\n },\n };\n}\n","/** A buffer that collects items and flushes them in groups. */\nexport interface Batcher<T> {\n enqueue(item: T): void;\n flushOnce(): Promise<void>;\n}\n\nexport interface BatcherOptions<T> {\n /** Auto-flush this many ms after the first item is enqueued. */\n flushMs: number;\n /** Flush right away once the queue reaches this length. */\n maxQueueLength: number;\n /** Called with the queued items when the batch flushes. */\n onFlush: (events: T[]) => void | Promise<void>;\n}\n\n/**\n * Returns a buffer that flushes when either the time limit or the size limit\n * is reached, whichever comes first. `flushOnce()` empties the queue right\n * away on demand. Lets us send a batch of events in one HTTP request instead\n * of one request per event.\n */\nexport function createBatcher<T>(opts: BatcherOptions<T>): Batcher<T> {\n let queue: T[] = [];\n let timer: ReturnType<typeof setTimeout> | null = null;\n\n async function flush(): Promise<void> {\n if (timer) { clearTimeout(timer); timer = null; }\n if (queue.length === 0) return;\n const drained = queue;\n queue = [];\n await opts.onFlush(drained);\n }\n\n return {\n enqueue(item: T): void {\n queue.push(item);\n if (queue.length >= opts.maxQueueLength) {\n void flush();\n return;\n }\n if (timer === null) {\n timer = setTimeout(() => { void flush(); }, opts.flushMs);\n }\n },\n flushOnce: flush,\n };\n}\n","/** Tracks whether a flaky downstream call should currently be attempted. */\nexport interface CircuitBreaker {\n canTransmit(): boolean;\n recordFailure(): void;\n recordSuccess(): void;\n}\n\nexport interface CircuitBreakerOptions {\n /** Number of recent failures that \"trips\" the breaker (stops sending). */\n failureThreshold: number;\n /** How far back to look when counting failures, in ms. */\n failureWindowMs: number;\n /** Once tripped, how long to wait before allowing sends again. */\n openMs: number;\n}\n\n/**\n * Returns a circuit breaker. After `failureThreshold` failures within\n * `failureWindowMs`, it stops allowing sends for `openMs`, then automatically\n * starts allowing them again.\n *\n * Used by `logError` to back off when the endpoint is failing, so we don't\n * keep hammering a struggling CMS.\n */\nexport function createCircuitBreaker(opts: CircuitBreakerOptions): CircuitBreaker {\n let failures: number[] = [];\n let openUntil = 0;\n\n return {\n canTransmit(): boolean {\n return Date.now() >= openUntil;\n },\n recordFailure(): void {\n const now = Date.now();\n failures = failures.filter((t) => now - t < opts.failureWindowMs);\n failures.push(now);\n if (failures.length >= opts.failureThreshold) {\n openUntil = now + opts.openMs;\n failures = [];\n }\n },\n recordSuccess(): void {\n failures = [];\n },\n };\n}\n","interface GrecaptchaEnterprise {\n ready: (cb: () => void) => void;\n execute: (siteKey: string, opts: { action: string }) => Promise<string>;\n}\n\ninterface GrecaptchaGlobal {\n enterprise: GrecaptchaEnterprise;\n}\n\ndeclare global {\n // eslint-disable-next-line no-var\n var grecaptcha: GrecaptchaGlobal | undefined;\n}\n\n/**\n * Acquire a reCAPTCHA Enterprise v3 token from the browser SDK.\n *\n * Returns null if the SDK isn't loaded yet (e.g. on the server, or before\n * `https://www.google.com/recaptcha/enterprise.js` finishes loading) or if\n * `execute` rejects. Never throws.\n */\nexport async function acquireRecaptchaToken(\n siteKey: string,\n action: string,\n): Promise<string | null> {\n const g = typeof globalThis !== \"undefined\" ? globalThis.grecaptcha : undefined;\n if (!g?.enterprise) return null;\n try {\n await new Promise<void>((resolve) => g.enterprise.ready(resolve));\n return await g.enterprise.execute(siteKey, { action });\n } catch {\n return null;\n }\n}\n","import type { Reason, Severity } from \"./types\";\n\n/**\n * Map a `Reason` to its default `Severity` when the caller hasn't passed\n * one explicitly.\n *\n * Convention:\n * - `error` — blocks the user / unrecoverable / 5xx upstream\n * - `warning` — recoverable, user-facing flow signal (validation,\n * declined card, soft-assert empty result)\n * - `info` — informational signal (`session_expired`)\n *\n * Wire this into integration points (e.g. the RTK Query error middleware)\n * so individual call sites don't have to think about severity unless they\n * want to override the default.\n */\nexport function getDefaultSeverity(reason?: Reason): Severity {\n if (!reason) return \"error\";\n\n switch (reason) {\n // user-recoverable / expected-flow signals\n case \"validation_error\":\n case \"payment_declined\":\n case \"invalid_payment_option\":\n case \"unit_list_empty\":\n case \"expected_data_missing\":\n case \"cache_stale\":\n case \"promotion_not_found\":\n return \"warning\";\n\n // informational\n case \"session_expired\":\n return \"info\";\n\n // everything else (network failures, unauthorized, not-founds we couldn't\n // recover from, hook/scraper/build failures, unhandled exceptions) — the\n // user is blocked, treat as error\n default:\n return \"error\";\n }\n}\n","import type { LogEvent } from \"./types\";\n\nexport interface TransportRequest {\n events: LogEvent[];\n rcToken: string | null;\n}\n\nexport interface TransportResult {\n ok: boolean;\n status: number;\n}\n\n/** Detects whether we're in a browser-like environment (has window). */\nexport function isBrowser(): boolean {\n return typeof window !== \"undefined\";\n}\n\n/** Browser POST: cross-origin to the configured endpoint, no credentials. */\nexport async function postFromBrowser(\n endpoint: string,\n request: TransportRequest,\n): Promise<TransportResult> {\n try {\n const res = await fetch(endpoint, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify(request),\n keepalive: true,\n });\n return { ok: res.ok, status: res.status };\n } catch {\n return { ok: false, status: 0 };\n }\n}\n\n/**\n * Server POST: writes events directly to Payload's REST API for the IssueEvents collection.\n * Each event becomes one POST (the dedup hook on IssueEvents handles aggregation into Issues).\n *\n * `payloadBaseUrl` should be e.g. `https://payloadstorage.golocaldev.com` (no `/cms` suffix —\n * we append `/cms/api/issue-events` per the existing tenantInfo.ts convention).\n */\nexport async function postFromServer(\n payloadBaseUrl: string,\n apiKey: string,\n events: LogEvent[],\n): Promise<TransportResult> {\n let lastStatus = 0;\n for (const event of events) {\n try {\n const res = await fetch(`${payloadBaseUrl}/cms/api/issue-events`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `users API-Key ${apiKey}`,\n },\n body: JSON.stringify(event),\n });\n lastStatus = res.status;\n if (!res.ok) return { ok: false, status: res.status };\n } catch {\n return { ok: false, status: 0 };\n }\n }\n return { ok: true, status: lastStatus };\n}\n","import { _getConfig } from \"./initLogger\";\nimport { fingerprint } from \"./fingerprint\";\nimport { getLogContext } from \"./context\";\nimport { createThrottle } from \"./throttle\";\nimport { createBatcher } from \"./batch\";\nimport { createCircuitBreaker } from \"./circuitBreaker\";\nimport { acquireRecaptchaToken } from \"./recaptcha\";\nimport { getDefaultSeverity } from \"./severity\";\nimport { isBrowser, postFromBrowser, postFromServer } from \"./transport\";\nimport type { LogErrorOptions, LogEvent, Source, Severity } from \"./types\";\n\nconst throttle = createThrottle({ windowMs: 5_000 });\nconst breaker = createCircuitBreaker({\n failureThreshold: 3,\n failureWindowMs: 30_000,\n openMs: 60_000,\n});\n\nlet batcher: ReturnType<typeof createBatcher<LogEvent>> | null = null;\n\nfunction getBatcher(): ReturnType<typeof createBatcher<LogEvent>> {\n if (batcher) return batcher;\n batcher = createBatcher<LogEvent>({\n flushMs: 3_000,\n maxQueueLength: 10,\n onFlush: async (events) => {\n const cfg = _getConfig();\n if (!cfg) return;\n if (!breaker.canTransmit()) return;\n\n const rcToken = cfg.recaptchaSiteKey\n ? await acquireRecaptchaToken(cfg.recaptchaSiteKey, cfg.recaptchaAction ?? \"log_error\")\n : null;\n\n const result = await postFromBrowser(cfg.endpoint, { events, rcToken });\n if (result.ok) breaker.recordSuccess();\n else if (result.status >= 500 || result.status === 0) breaker.recordFailure();\n },\n });\n return batcher;\n}\n\n/**\n * Strip query string and hash from the current URL before logging it.\n *\n * **Why.** The PII contract says we don't accept user data into the pipeline.\n * Query strings can carry tokens, emails, or other inputs (e.g. `?email=…`\n * from a magic-link flow); the fragment can carry routing state that includes\n * the same. Path is enough to identify *which page* the bug fired on.\n */\nfunction safePageUrl(): string {\n if (!isBrowser()) return \"\";\n try {\n const parsed = new URL(window.location.href);\n return parsed.origin + parsed.pathname;\n } catch {\n return \"\";\n }\n}\n\nfunction buildEvent(\n err: Error | string | number | boolean | null | undefined,\n opts?: LogErrorOptions,\n): LogEvent | null {\n const cfg = _getConfig();\n if (!cfg) return null;\n\n const message = err instanceof Error ? err.message : String(err);\n const stack = err instanceof Error && err.stack ? err.stack : \"\";\n const ambient = getLogContext();\n const source: Source = opts?.source ?? ambient.source ?? cfg.defaultSource ?? \"theme\";\n const reason = opts?.reason;\n const severity: Severity = opts?.severity ?? getDefaultSeverity(reason);\n const fp = fingerprint(source, message, stack, reason);\n\n const userAgent =\n isBrowser() && typeof navigator !== \"undefined\" ? navigator.userAgent : \"\";\n\n return {\n source,\n repo: cfg.repo,\n severity,\n fingerprint: fp,\n message: message.slice(0, 500),\n stack: stack.slice(0, 10_000),\n context: { ...ambient, ...(opts?.context ?? {}) },\n url: safePageUrl(),\n userAgent,\n occurredAt: new Date().toISOString(),\n release: cfg.release,\n reason,\n };\n}\n\n/**\n * Capture an error and route it to the CMS.\n *\n * Browser path: throttle → batch → cross-origin POST (with reCAPTCHA token).\n * Server path: throttle → direct POST to Payload's REST API per event.\n *\n * Internal failures never throw — silently degraded so the logger can't break callers.\n *\n * `err` covers the realistic set of thrown values (Error, primitives). For the\n * rare case of throwing a plain object literal, callers should wrap in an Error\n * before passing — e.g. `logError(new Error(JSON.stringify(thing)))`.\n */\nexport function logError(\n err: Error | string | number | boolean | null | undefined,\n opts?: LogErrorOptions,\n): void {\n try {\n const event = buildEvent(err, opts);\n if (!event) return;\n if (!throttle.shouldEmit(event.fingerprint)) return;\n\n if (isBrowser()) {\n getBatcher().enqueue(event);\n } else {\n // Server path: synchronous-ish; we still fire-and-forget but don't batch.\n const cfg = _getConfig();\n if (!cfg) return;\n const apiKey = process.env.TENANT_API_KEY ?? process.env.PAYLOAD_PUBLIC_API_KEY;\n const baseUrl = process.env.PAYLOAD_PUBLIC_URL;\n if (!apiKey || !baseUrl) return;\n void postFromServer(baseUrl, apiKey, [event]).catch(() => {\n /* swallow — cascade guard */\n });\n }\n } catch {\n /* cascade guard: never throw from the logger itself */\n }\n}\n\n/**\n * Drain any queued (but not-yet-flushed) browser events right now. Called from\n * the `pagehide` global handler so a user closing the tab between time-based\n * flushes doesn't drop their queued errors.\n *\n * No-op when no batcher has been lazily initialized yet (nothing was queued).\n */\nexport function flushPending(): Promise<void> {\n if (!batcher) return Promise.resolve();\n return batcher.flushOnce();\n}\n\n/** @internal — test-only: reset batcher state between tests */\nexport function _resetForTests(): void {\n batcher = null;\n}\n","import { flushPending, logError } from \"./logError\";\n\ndeclare global {\n // eslint-disable-next-line no-var\n var __errorLoggerHandlersInstalled__: boolean | undefined;\n}\n\n/**\n * Install browser-global error capture (window.onerror + unhandledrejection)\n * plus a `pagehide` listener that flushes any queued events before unload.\n *\n * Idempotent: a `globalThis` sentinel ensures repeated calls do not re-install.\n * Only the host page should call this — widgets embedded in the host inherit\n * coverage automatically because errors bubble to page-level listeners.\n */\nexport function installGlobalHandlers(): void {\n if (typeof window === \"undefined\") return;\n if (globalThis.__errorLoggerHandlersInstalled__) return;\n globalThis.__errorLoggerHandlersInstalled__ = true;\n\n window.addEventListener(\"error\", (event: ErrorEvent) => {\n const err = event.error instanceof Error ? event.error : new Error(String(event.error ?? event.message ?? \"Unknown error\"));\n logError(err);\n });\n\n window.addEventListener(\"unhandledrejection\", (event: PromiseRejectionEvent) => {\n const err = event.reason instanceof Error ? event.reason : new Error(String(event.reason ?? \"Unhandled promise rejection\"));\n logError(err);\n });\n\n // Flush queued events on tab close / nav so users who leave mid-batch-window don't lose errors.\n window.addEventListener(\"pagehide\", () => {\n void flushPending();\n });\n}\n\n/** @internal — test-only: clears the sentinel so re-install is allowed */\nexport function _resetForTests(): void {\n delete globalThis.__errorLoggerHandlersInstalled__;\n}\n"],"mappings":"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","names":["fingerprint","isBrowser","isBrowser"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/context.ts","../src/initLogger.ts","../src/fingerprint.ts","../src/throttle.ts","../src/batch.ts","../src/circuitBreaker.ts","../src/recaptcha.ts","../src/severity.ts","../src/transport.ts","../src/logError.ts","../src/installGlobalHandlers.ts"],"sourcesContent":["export type {\n LogContext,\n LogErrorOptions,\n LogEvent,\n InitLoggerConfig,\n LogChargeLine,\n Reason,\n Severity,\n Source,\n Repo,\n} from \"./types\";\nexport { setLogContext, withLogContext, getLogContext } from \"./context\";\nexport { initLogger } from \"./initLogger\";\nexport { logError } from \"./logError\";\nexport { installGlobalHandlers } from \"./installGlobalHandlers\";\nexport { getDefaultSeverity } from \"./severity\";\nexport { fingerprint } from \"./fingerprint\";\n","/**\n * @fileoverview Ambient log context store — browser-only.\n *\n * On the browser the module-level `current` object is a per-tab singleton, the\n * scope we want. On Node (SSR), the same module is shared across every\n * incoming request running in the process, so writing to `current` in one\n * request would leak that context into other requests' error events. To stay\n * safe, all three functions no-op (or return `{}`) when there is no `window`.\n *\n * **Server callers must pass context explicitly** via the `opts.context`\n * parameter on `logError`, which is merged into the event regardless of the\n * ambient state.\n */\nimport type { LogContext } from \"./types\";\n\nlet current: LogContext = {};\n\nconst isBrowser = (): boolean => typeof window !== \"undefined\";\n\n/**\n * Add or overwrite keys in the ambient log context. Existing keys you don't\n * pass are preserved; keys you do pass replace existing values (no deep merge).\n *\n * Call at meaningful UX boundaries (page mount, step change, modal open) so\n * later errors automatically pick up the relevant context.\n *\n * No-op on the server — see file-level note above.\n */\nexport function setLogContext(patch: LogContext): void {\n if (!isBrowser()) return;\n current = { ...current, ...patch };\n}\n\n/**\n * Return a copy of the current ambient log context. Used by `logError` to\n * attach context to outgoing events.\n *\n * Returns `{}` on the server — see file-level note above.\n */\nexport function getLogContext(): LogContext {\n if (!isBrowser()) return {};\n return { ...current };\n}\n\n/**\n * Run `fn` with extra context keys layered on top of the ambient context, then\n * put the original context back — even if `fn` throws.\n *\n * Use this when the extra keys should only apply to one synchronous operation\n * and not leak into anything that runs afterwards.\n *\n * On the server `fn` runs without any ambient mutation — see file-level note above.\n */\nexport function withLogContext<T>(scope: LogContext, fn: () => T): T {\n if (!isBrowser()) return fn();\n const prior = current;\n current = { ...current, ...scope };\n try {\n return fn();\n } finally {\n current = prior;\n }\n}\n\n/** @internal — test-only */\nexport function _resetForTests(): void {\n current = {};\n}\n","import type { InitLoggerConfig } from \"./types\";\n\nlet config: InitLoggerConfig | null = null;\n\n/**\n * Set up the logger. Call once at app startup.\n *\n * Required fields: `endpoint` (where the browser sends events) and `repo`\n * (stamped on every event). Optional: `release`, `recaptchaSiteKey`,\n * `recaptchaAction`, `defaultSource`. Throws if a required field is missing\n * so misconfiguration is loud at startup instead of silently dropping events.\n */\nexport function initLogger(cfg: InitLoggerConfig): void {\n if (!cfg?.endpoint) throw new Error(\"initLogger: endpoint is required\");\n if (!cfg?.repo) throw new Error(\"initLogger: repo is required\");\n config = { ...cfg };\n}\n\n/** @internal — read the current config; returns null before `initLogger` runs. */\nexport function _getConfig(): InitLoggerConfig | null {\n return config;\n}\n\n/** @internal — test-only */\nexport function _resetForTests(): void {\n config = null;\n}\n","/**\n * Compute a short stable hash that identifies \"the same bug\" across occurrences.\n * Used by the dedup hook to find-or-create the parent `Issues` row.\n *\n * Only the top frame of the stack is hashed so a bug fired from the same place\n * groups together even if surrounding code paths differ. Returned as a hex\n * string just to keep it compact; we just need stable grouping, not security.\n *\n * `reason` is included so the same code path failing for different reasons\n * (e.g. `invalid_cvv` vs `gateway_decline` at the payment step) becomes two\n * separate Issues. An empty / omitted reason hashes the same as `\"\"`, so\n * existing 3-arg callers stay compatible.\n */\nexport function fingerprint(\n source: string,\n message: string,\n stack: string,\n reason?: string,\n): string {\n const topFrame = stack.split(\"\\n\", 2)[0] ?? \"\";\n const reasonPart = reason ?? \"\";\n const input = `${source}|${message}|${topFrame}|${reasonPart}`;\n let hash = 5381;\n for (let i = 0; i < input.length; i++) {\n hash = ((hash << 5) + hash) + input.charCodeAt(i);\n hash |= 0;\n }\n return (hash >>> 0).toString(16);\n}\n","/** Throttle that decides whether a given fingerprint should be sent right now. */\nexport interface Throttle {\n shouldEmit(fingerprint: string): boolean;\n}\n\n/**\n * Returns a throttle that lets the first occurrence of each fingerprint through\n * and silently drops repeats until `windowMs` passes. Stops a bug firing in a\n * tight loop from flooding the CMS.\n */\nexport function createThrottle(opts: { windowMs: number }): Throttle {\n const lastEmittedAt = new Map<string, number>();\n return {\n shouldEmit(fingerprint: string): boolean {\n const now = Date.now();\n const last = lastEmittedAt.get(fingerprint);\n if (last !== undefined && now - last < opts.windowMs) return false;\n lastEmittedAt.set(fingerprint, now);\n return true;\n },\n };\n}\n","/** A buffer that collects items and flushes them in groups. */\nexport interface Batcher<T> {\n enqueue(item: T): void;\n flushOnce(): Promise<void>;\n}\n\nexport interface BatcherOptions<T> {\n /** Auto-flush this many ms after the first item is enqueued. */\n flushMs: number;\n /** Flush right away once the queue reaches this length. */\n maxQueueLength: number;\n /** Called with the queued items when the batch flushes. */\n onFlush: (events: T[]) => void | Promise<void>;\n}\n\n/**\n * Returns a buffer that flushes when either the time limit or the size limit\n * is reached, whichever comes first. `flushOnce()` empties the queue right\n * away on demand. Lets us send a batch of events in one HTTP request instead\n * of one request per event.\n */\nexport function createBatcher<T>(opts: BatcherOptions<T>): Batcher<T> {\n let queue: T[] = [];\n let timer: ReturnType<typeof setTimeout> | null = null;\n\n async function flush(): Promise<void> {\n if (timer) { clearTimeout(timer); timer = null; }\n if (queue.length === 0) return;\n const drained = queue;\n queue = [];\n await opts.onFlush(drained);\n }\n\n return {\n enqueue(item: T): void {\n queue.push(item);\n if (queue.length >= opts.maxQueueLength) {\n void flush();\n return;\n }\n if (timer === null) {\n timer = setTimeout(() => { void flush(); }, opts.flushMs);\n }\n },\n flushOnce: flush,\n };\n}\n","/** Tracks whether a flaky downstream call should currently be attempted. */\nexport interface CircuitBreaker {\n canTransmit(): boolean;\n recordFailure(): void;\n recordSuccess(): void;\n}\n\nexport interface CircuitBreakerOptions {\n /** Number of recent failures that \"trips\" the breaker (stops sending). */\n failureThreshold: number;\n /** How far back to look when counting failures, in ms. */\n failureWindowMs: number;\n /** Once tripped, how long to wait before allowing sends again. */\n openMs: number;\n}\n\n/**\n * Returns a circuit breaker. After `failureThreshold` failures within\n * `failureWindowMs`, it stops allowing sends for `openMs`, then automatically\n * starts allowing them again.\n *\n * Used by `logError` to back off when the endpoint is failing, so we don't\n * keep hammering a struggling CMS.\n */\nexport function createCircuitBreaker(opts: CircuitBreakerOptions): CircuitBreaker {\n let failures: number[] = [];\n let openUntil = 0;\n\n return {\n canTransmit(): boolean {\n return Date.now() >= openUntil;\n },\n recordFailure(): void {\n const now = Date.now();\n failures = failures.filter((t) => now - t < opts.failureWindowMs);\n failures.push(now);\n if (failures.length >= opts.failureThreshold) {\n openUntil = now + opts.openMs;\n failures = [];\n }\n },\n recordSuccess(): void {\n failures = [];\n },\n };\n}\n","interface GrecaptchaEnterprise {\n ready: (cb: () => void) => void;\n execute: (siteKey: string, opts: { action: string }) => Promise<string>;\n}\n\ninterface GrecaptchaGlobal {\n enterprise: GrecaptchaEnterprise;\n}\n\ndeclare global {\n // eslint-disable-next-line no-var\n var grecaptcha: GrecaptchaGlobal | undefined;\n}\n\n/**\n * Acquire a reCAPTCHA Enterprise v3 token from the browser SDK.\n *\n * Returns null if the SDK isn't loaded yet (e.g. on the server, or before\n * `https://www.google.com/recaptcha/enterprise.js` finishes loading) or if\n * `execute` rejects. Never throws.\n */\nexport async function acquireRecaptchaToken(\n siteKey: string,\n action: string,\n): Promise<string | null> {\n const g = typeof globalThis !== \"undefined\" ? globalThis.grecaptcha : undefined;\n if (!g?.enterprise) return null;\n try {\n await new Promise<void>((resolve) => g.enterprise.ready(resolve));\n return await g.enterprise.execute(siteKey, { action });\n } catch {\n return null;\n }\n}\n","import type { Reason, Severity } from \"./types\";\n\n/**\n * Map a `Reason` to its default `Severity` when the caller hasn't passed\n * one explicitly.\n *\n * Convention:\n * - `error` — blocks the user / unrecoverable / 5xx upstream\n * - `warning` — recoverable, user-facing flow signal (validation,\n * declined card, soft-assert empty result)\n * - `info` — informational signal (`session_expired`)\n *\n * Wire this into integration points (e.g. the RTK Query error middleware)\n * so individual call sites don't have to think about severity unless they\n * want to override the default.\n */\nexport function getDefaultSeverity(reason?: Reason): Severity {\n if (!reason) return \"error\";\n\n switch (reason) {\n // user-recoverable / expected-flow signals\n case \"validation_error\":\n case \"payment_declined\":\n case \"invalid_payment_option\":\n case \"unit_list_empty\":\n case \"expected_data_missing\":\n case \"cache_stale\":\n case \"promotion_not_found\":\n return \"warning\";\n\n // informational\n case \"session_expired\":\n return \"info\";\n\n // everything else (network failures, unauthorized, not-founds we couldn't\n // recover from, hook/scraper/build failures, unhandled exceptions) — the\n // user is blocked, treat as error\n default:\n return \"error\";\n }\n}\n","import type { LogEvent } from \"./types\";\n\nexport interface TransportRequest {\n events: LogEvent[];\n rcToken: string | null;\n}\n\nexport interface TransportResult {\n ok: boolean;\n status: number;\n}\n\n/** Detects whether we're in a browser-like environment (has window). */\nexport function isBrowser(): boolean {\n return typeof window !== \"undefined\";\n}\n\n/** Browser POST: cross-origin to the configured endpoint, no credentials. */\nexport async function postFromBrowser(\n endpoint: string,\n request: TransportRequest,\n): Promise<TransportResult> {\n try {\n const res = await fetch(endpoint, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify(request),\n keepalive: true,\n });\n return { ok: res.ok, status: res.status };\n } catch {\n return { ok: false, status: 0 };\n }\n}\n\n/**\n * Server POST: writes events directly to Payload's REST API for the IssueEvents collection.\n * Each event becomes one POST (the dedup hook on IssueEvents handles aggregation into Issues).\n *\n * `payloadBaseUrl` should be e.g. `https://payloadstorage.golocaldev.com` (no `/cms` suffix —\n * we append `/cms/api/issue-events` per the existing tenantInfo.ts convention).\n */\nexport async function postFromServer(\n payloadBaseUrl: string,\n apiKey: string,\n events: LogEvent[],\n): Promise<TransportResult> {\n let lastStatus = 0;\n for (const event of events) {\n try {\n const res = await fetch(`${payloadBaseUrl}/cms/api/issue-events`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `users API-Key ${apiKey}`,\n },\n body: JSON.stringify(event),\n });\n lastStatus = res.status;\n if (!res.ok) return { ok: false, status: res.status };\n } catch {\n return { ok: false, status: 0 };\n }\n }\n return { ok: true, status: lastStatus };\n}\n","import { _getConfig } from \"./initLogger\";\nimport { fingerprint } from \"./fingerprint\";\nimport { getLogContext } from \"./context\";\nimport { createThrottle } from \"./throttle\";\nimport { createBatcher } from \"./batch\";\nimport { createCircuitBreaker } from \"./circuitBreaker\";\nimport { acquireRecaptchaToken } from \"./recaptcha\";\nimport { getDefaultSeverity } from \"./severity\";\nimport { isBrowser, postFromBrowser, postFromServer } from \"./transport\";\nimport type { LogErrorOptions, LogEvent, Source, Severity } from \"./types\";\n\nconst throttle = createThrottle({ windowMs: 5_000 });\nconst breaker = createCircuitBreaker({\n failureThreshold: 3,\n failureWindowMs: 30_000,\n openMs: 60_000,\n});\n\nlet batcher: ReturnType<typeof createBatcher<LogEvent>> | null = null;\n\nfunction getBatcher(): ReturnType<typeof createBatcher<LogEvent>> {\n if (batcher) return batcher;\n batcher = createBatcher<LogEvent>({\n flushMs: 3_000,\n maxQueueLength: 10,\n onFlush: async (events) => {\n const cfg = _getConfig();\n if (!cfg) return;\n if (!breaker.canTransmit()) return;\n\n const rcToken = cfg.recaptchaSiteKey\n ? await acquireRecaptchaToken(cfg.recaptchaSiteKey, cfg.recaptchaAction ?? \"log_error\")\n : null;\n\n const result = await postFromBrowser(cfg.endpoint, { events, rcToken });\n if (result.ok) breaker.recordSuccess();\n else if (result.status >= 500 || result.status === 0) breaker.recordFailure();\n },\n });\n return batcher;\n}\n\n/**\n * Strip query string and hash from the current URL before logging it.\n *\n * **Why.** The PII contract says we don't accept user data into the pipeline.\n * Query strings can carry tokens, emails, or other inputs (e.g. `?email=…`\n * from a magic-link flow); the fragment can carry routing state that includes\n * the same. Path is enough to identify *which page* the bug fired on.\n */\nfunction safePageUrl(): string {\n if (!isBrowser()) return \"\";\n try {\n const parsed = new URL(window.location.href);\n return parsed.origin + parsed.pathname;\n } catch {\n return \"\";\n }\n}\n\nfunction buildEvent(\n err: Error | string | number | boolean | null | undefined,\n opts?: LogErrorOptions,\n): LogEvent | null {\n const cfg = _getConfig();\n if (!cfg) return null;\n\n const message = err instanceof Error ? err.message : String(err);\n const stack = err instanceof Error && err.stack ? err.stack : \"\";\n const ambient = getLogContext();\n const source: Source = opts?.source ?? ambient.source ?? cfg.defaultSource ?? \"theme\";\n const reason = opts?.reason;\n const severity: Severity = opts?.severity ?? getDefaultSeverity(reason);\n const fp = fingerprint(source, message, stack, reason);\n\n const userAgent =\n isBrowser() && typeof navigator !== \"undefined\" ? navigator.userAgent : \"\";\n\n return {\n source,\n repo: cfg.repo,\n severity,\n fingerprint: fp,\n message: message.slice(0, 500),\n stack: stack.slice(0, 10_000),\n context: { ...ambient, ...(opts?.context ?? {}) },\n url: safePageUrl(),\n userAgent,\n occurredAt: new Date().toISOString(),\n release: cfg.release,\n reason,\n };\n}\n\n/**\n * Capture an error and route it to the CMS.\n *\n * Browser path: throttle → batch → cross-origin POST (with reCAPTCHA token).\n * Server path: throttle → direct POST to Payload's REST API per event.\n *\n * Internal failures never throw — silently degraded so the logger can't break callers.\n *\n * `err` covers the realistic set of thrown values (Error, primitives). For the\n * rare case of throwing a plain object literal, callers should wrap in an Error\n * before passing — e.g. `logError(new Error(JSON.stringify(thing)))`.\n */\nexport function logError(\n err: Error | string | number | boolean | null | undefined,\n opts?: LogErrorOptions,\n): void {\n try {\n const event = buildEvent(err, opts);\n if (!event) return;\n if (!throttle.shouldEmit(event.fingerprint)) return;\n\n if (isBrowser()) {\n getBatcher().enqueue(event);\n } else {\n // Server path: synchronous-ish; we still fire-and-forget but don't batch.\n const cfg = _getConfig();\n if (!cfg) return;\n const apiKey = process.env.TENANT_API_KEY ?? process.env.PAYLOAD_PUBLIC_API_KEY;\n const baseUrl = process.env.PAYLOAD_PUBLIC_URL;\n if (!apiKey || !baseUrl) return;\n void postFromServer(baseUrl, apiKey, [event]).catch(() => {\n /* swallow — cascade guard */\n });\n }\n } catch {\n /* cascade guard: never throw from the logger itself */\n }\n}\n\n/**\n * Drain any queued (but not-yet-flushed) browser events right now. Called from\n * the `pagehide` global handler so a user closing the tab between time-based\n * flushes doesn't drop their queued errors.\n *\n * No-op when no batcher has been lazily initialized yet (nothing was queued).\n */\nexport function flushPending(): Promise<void> {\n if (!batcher) return Promise.resolve();\n return batcher.flushOnce();\n}\n\n/** @internal — test-only: reset batcher state between tests */\nexport function _resetForTests(): void {\n batcher = null;\n}\n","import { flushPending, logError } from \"./logError\";\nimport type { LogErrorOptions } from \"./types\";\n\ndeclare global {\n // eslint-disable-next-line no-var\n var __errorLoggerHandlersInstalled__: boolean | undefined;\n}\n\n/**\n * Classify an ErrorEvent that came from a third-party script (GTM tag,\n * vendor pixel, etc.). Two signals catch the common cases:\n *\n * 1. Browser-sanitized cross-origin error → `message === \"Script error.\"`.\n * The browser stripped details because the script's server didn't send\n * CORS headers. We have no host info in this case.\n * 2. Visible third-party error → `event.filename` parses to a URL on a\n * different origin than `window.location.origin`. Well-behaved third\n * parties (GTM itself, GA, FB Pixel) send proper CORS headers so we\n * get the full stack AND the host.\n *\n * Returns the `LogErrorOptions` to forward to `logError`, or `undefined`\n * when the error doesn't look third-party.\n */\nfunction thirdPartyScriptOpts(event: ErrorEvent): LogErrorOptions | undefined {\n // Case 1: sanitized cross-origin.\n if (event.message === \"Script error.\" || event.message === \"Script error\") {\n return { reason: \"third_party_script\" };\n }\n // Case 2: visible third-party — extract host when filename is a real URL.\n if (event.filename) {\n try {\n const url = new URL(event.filename);\n if (url.origin !== window.location.origin) {\n return { reason: \"third_party_script\", context: { scriptHost: url.host } };\n }\n } catch {\n // Malformed filename; treat as first-party.\n }\n }\n return undefined;\n}\n\n/**\n * Install browser-global error capture (window.onerror + unhandledrejection)\n * plus a `pagehide` listener that flushes any queued events before unload.\n *\n * Idempotent: a `globalThis` sentinel ensures repeated calls do not re-install.\n * Only the host page should call this — widgets embedded in the host inherit\n * coverage automatically because errors bubble to page-level listeners.\n *\n * Auto-tags third-party script errors (GTM tags, vendor pixels, etc.) with\n * `reason: \"third_party_script\"` and — when the browser exposes the host —\n * `context.scriptHost`. Lets admins filter / triage those separately from\n * the host's own errors.\n */\nexport function installGlobalHandlers(): void {\n if (typeof window === \"undefined\") return;\n if (globalThis.__errorLoggerHandlersInstalled__) return;\n globalThis.__errorLoggerHandlersInstalled__ = true;\n\n window.addEventListener(\"error\", (event: ErrorEvent) => {\n const err = event.error instanceof Error ? event.error : new Error(String(event.error ?? event.message ?? \"Unknown error\"));\n logError(err, thirdPartyScriptOpts(event));\n });\n\n window.addEventListener(\"unhandledrejection\", (event: PromiseRejectionEvent) => {\n const err = event.reason instanceof Error ? event.reason : new Error(String(event.reason ?? \"Unhandled promise rejection\"));\n logError(err);\n });\n\n // Flush queued events on tab close / nav so users who leave mid-batch-window don't lose errors.\n window.addEventListener(\"pagehide\", () => {\n void flushPending();\n });\n}\n\n/** @internal — test-only: clears the sentinel so re-install is allowed */\nexport function _resetForTests(): void {\n delete globalThis.__errorLoggerHandlersInstalled__;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACeA,IAAI,UAAsB,CAAC;AAE3B,IAAM,YAAY,MAAe,OAAO,WAAW;AAW5C,SAAS,cAAc,OAAyB;AACrD,MAAI,CAAC,UAAU,EAAG;AAClB,YAAU,EAAE,GAAG,SAAS,GAAG,MAAM;AACnC;AAQO,SAAS,gBAA4B;AAC1C,MAAI,CAAC,UAAU,EAAG,QAAO,CAAC;AAC1B,SAAO,EAAE,GAAG,QAAQ;AACtB;AAWO,SAAS,eAAkB,OAAmB,IAAgB;AACnE,MAAI,CAAC,UAAU,EAAG,QAAO,GAAG;AAC5B,QAAM,QAAQ;AACd,YAAU,EAAE,GAAG,SAAS,GAAG,MAAM;AACjC,MAAI;AACF,WAAO,GAAG;AAAA,EACZ,UAAE;AACA,cAAU;AAAA,EACZ;AACF;;;AC5DA,IAAI,SAAkC;AAU/B,SAAS,WAAW,KAA6B;AACtD,MAAI,CAAC,KAAK,SAAU,OAAM,IAAI,MAAM,kCAAkC;AACtE,MAAI,CAAC,KAAK,KAAM,OAAM,IAAI,MAAM,8BAA8B;AAC9D,WAAS,EAAE,GAAG,IAAI;AACpB;AAGO,SAAS,aAAsC;AACpD,SAAO;AACT;;;ACRO,SAAS,YACd,QACA,SACA,OACA,QACQ;AACR,QAAM,WAAW,MAAM,MAAM,MAAM,CAAC,EAAE,CAAC,KAAK;AAC5C,QAAM,aAAa,UAAU;AAC7B,QAAM,QAAQ,GAAG,MAAM,IAAI,OAAO,IAAI,QAAQ,IAAI,UAAU;AAC5D,MAAI,OAAO;AACX,WAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;AACrC,YAAS,QAAQ,KAAK,OAAQ,MAAM,WAAW,CAAC;AAChD,YAAQ;AAAA,EACV;AACA,UAAQ,SAAS,GAAG,SAAS,EAAE;AACjC;;;AClBO,SAAS,eAAe,MAAsC;AACnE,QAAM,gBAAgB,oBAAI,IAAoB;AAC9C,SAAO;AAAA,IACL,WAAWA,cAA8B;AACvC,YAAM,MAAM,KAAK,IAAI;AACrB,YAAM,OAAO,cAAc,IAAIA,YAAW;AAC1C,UAAI,SAAS,UAAa,MAAM,OAAO,KAAK,SAAU,QAAO;AAC7D,oBAAc,IAAIA,cAAa,GAAG;AAClC,aAAO;AAAA,IACT;AAAA,EACF;AACF;;;ACAO,SAAS,cAAiB,MAAqC;AACpE,MAAI,QAAa,CAAC;AAClB,MAAI,QAA8C;AAElD,iBAAe,QAAuB;AACpC,QAAI,OAAO;AAAE,mBAAa,KAAK;AAAG,cAAQ;AAAA,IAAM;AAChD,QAAI,MAAM,WAAW,EAAG;AACxB,UAAM,UAAU;AAChB,YAAQ,CAAC;AACT,UAAM,KAAK,QAAQ,OAAO;AAAA,EAC5B;AAEA,SAAO;AAAA,IACL,QAAQ,MAAe;AACrB,YAAM,KAAK,IAAI;AACf,UAAI,MAAM,UAAU,KAAK,gBAAgB;AACvC,aAAK,MAAM;AACX;AAAA,MACF;AACA,UAAI,UAAU,MAAM;AAClB,gBAAQ,WAAW,MAAM;AAAE,eAAK,MAAM;AAAA,QAAG,GAAG,KAAK,OAAO;AAAA,MAC1D;AAAA,IACF;AAAA,IACA,WAAW;AAAA,EACb;AACF;;;ACtBO,SAAS,qBAAqB,MAA6C;AAChF,MAAI,WAAqB,CAAC;AAC1B,MAAI,YAAY;AAEhB,SAAO;AAAA,IACL,cAAuB;AACrB,aAAO,KAAK,IAAI,KAAK;AAAA,IACvB;AAAA,IACA,gBAAsB;AACpB,YAAM,MAAM,KAAK,IAAI;AACrB,iBAAW,SAAS,OAAO,CAAC,MAAM,MAAM,IAAI,KAAK,eAAe;AAChE,eAAS,KAAK,GAAG;AACjB,UAAI,SAAS,UAAU,KAAK,kBAAkB;AAC5C,oBAAY,MAAM,KAAK;AACvB,mBAAW,CAAC;AAAA,MACd;AAAA,IACF;AAAA,IACA,gBAAsB;AACpB,iBAAW,CAAC;AAAA,IACd;AAAA,EACF;AACF;;;ACxBA,eAAsB,sBACpB,SACA,QACwB;AACxB,QAAM,IAAI,OAAO,eAAe,cAAc,WAAW,aAAa;AACtE,MAAI,CAAC,GAAG,WAAY,QAAO;AAC3B,MAAI;AACF,UAAM,IAAI,QAAc,CAAC,YAAY,EAAE,WAAW,MAAM,OAAO,CAAC;AAChE,WAAO,MAAM,EAAE,WAAW,QAAQ,SAAS,EAAE,OAAO,CAAC;AAAA,EACvD,QAAQ;AACN,WAAO;AAAA,EACT;AACF;;;ACjBO,SAAS,mBAAmB,QAA2B;AAC5D,MAAI,CAAC,OAAQ,QAAO;AAEpB,UAAQ,QAAQ;AAAA;AAAA,IAEd,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AACH,aAAO;AAAA;AAAA,IAGT,KAAK;AACH,aAAO;AAAA;AAAA;AAAA;AAAA,IAKT;AACE,aAAO;AAAA,EACX;AACF;;;AC3BO,SAASC,aAAqB;AACnC,SAAO,OAAO,WAAW;AAC3B;AAGA,eAAsB,gBACpB,UACA,SAC0B;AAC1B,MAAI;AACF,UAAM,MAAM,MAAM,MAAM,UAAU;AAAA,MAChC,QAAQ;AAAA,MACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,MAC9C,MAAM,KAAK,UAAU,OAAO;AAAA,MAC5B,WAAW;AAAA,IACb,CAAC;AACD,WAAO,EAAE,IAAI,IAAI,IAAI,QAAQ,IAAI,OAAO;AAAA,EAC1C,QAAQ;AACN,WAAO,EAAE,IAAI,OAAO,QAAQ,EAAE;AAAA,EAChC;AACF;AASA,eAAsB,eACpB,gBACA,QACA,QAC0B;AAC1B,MAAI,aAAa;AACjB,aAAW,SAAS,QAAQ;AAC1B,QAAI;AACF,YAAM,MAAM,MAAM,MAAM,GAAG,cAAc,yBAAyB;AAAA,QAChE,QAAQ;AAAA,QACR,SAAS;AAAA,UACP,gBAAgB;AAAA,UAChB,eAAe,iBAAiB,MAAM;AAAA,QACxC;AAAA,QACA,MAAM,KAAK,UAAU,KAAK;AAAA,MAC5B,CAAC;AACD,mBAAa,IAAI;AACjB,UAAI,CAAC,IAAI,GAAI,QAAO,EAAE,IAAI,OAAO,QAAQ,IAAI,OAAO;AAAA,IACtD,QAAQ;AACN,aAAO,EAAE,IAAI,OAAO,QAAQ,EAAE;AAAA,IAChC;AAAA,EACF;AACA,SAAO,EAAE,IAAI,MAAM,QAAQ,WAAW;AACxC;;;ACtDA,IAAM,WAAW,eAAe,EAAE,UAAU,IAAM,CAAC;AACnD,IAAM,UAAU,qBAAqB;AAAA,EACnC,kBAAkB;AAAA,EAClB,iBAAiB;AAAA,EACjB,QAAQ;AACV,CAAC;AAED,IAAI,UAA6D;AAEjE,SAAS,aAAyD;AAChE,MAAI,QAAS,QAAO;AACpB,YAAU,cAAwB;AAAA,IAChC,SAAS;AAAA,IACT,gBAAgB;AAAA,IAChB,SAAS,OAAO,WAAW;AACzB,YAAM,MAAM,WAAW;AACvB,UAAI,CAAC,IAAK;AACV,UAAI,CAAC,QAAQ,YAAY,EAAG;AAE5B,YAAM,UAAU,IAAI,mBAChB,MAAM,sBAAsB,IAAI,kBAAkB,IAAI,mBAAmB,WAAW,IACpF;AAEJ,YAAM,SAAS,MAAM,gBAAgB,IAAI,UAAU,EAAE,QAAQ,QAAQ,CAAC;AACtE,UAAI,OAAO,GAAI,SAAQ,cAAc;AAAA,eAC5B,OAAO,UAAU,OAAO,OAAO,WAAW,EAAG,SAAQ,cAAc;AAAA,IAC9E;AAAA,EACF,CAAC;AACD,SAAO;AACT;AAUA,SAAS,cAAsB;AAC7B,MAAI,CAACC,WAAU,EAAG,QAAO;AACzB,MAAI;AACF,UAAM,SAAS,IAAI,IAAI,OAAO,SAAS,IAAI;AAC3C,WAAO,OAAO,SAAS,OAAO;AAAA,EAChC,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,SAAS,WACP,KACA,MACiB;AACjB,QAAM,MAAM,WAAW;AACvB,MAAI,CAAC,IAAK,QAAO;AAEjB,QAAM,UAAU,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAC/D,QAAM,QAAQ,eAAe,SAAS,IAAI,QAAQ,IAAI,QAAQ;AAC9D,QAAM,UAAU,cAAc;AAC9B,QAAM,SAAiB,MAAM,UAAU,QAAQ,UAAU,IAAI,iBAAiB;AAC9E,QAAM,SAAS,MAAM;AACrB,QAAM,WAAqB,MAAM,YAAY,mBAAmB,MAAM;AACtE,QAAM,KAAK,YAAY,QAAQ,SAAS,OAAO,MAAM;AAErD,QAAM,YACJA,WAAU,KAAK,OAAO,cAAc,cAAc,UAAU,YAAY;AAE1E,SAAO;AAAA,IACL;AAAA,IACA,MAAM,IAAI;AAAA,IACV;AAAA,IACA,aAAa;AAAA,IACb,SAAS,QAAQ,MAAM,GAAG,GAAG;AAAA,IAC7B,OAAO,MAAM,MAAM,GAAG,GAAM;AAAA,IAC5B,SAAS,EAAE,GAAG,SAAS,GAAI,MAAM,WAAW,CAAC,EAAG;AAAA,IAChD,KAAK,YAAY;AAAA,IACjB;AAAA,IACA,aAAY,oBAAI,KAAK,GAAE,YAAY;AAAA,IACnC,SAAS,IAAI;AAAA,IACb;AAAA,EACF;AACF;AAcO,SAAS,SACd,KACA,MACM;AACN,MAAI;AACF,UAAM,QAAQ,WAAW,KAAK,IAAI;AAClC,QAAI,CAAC,MAAO;AACZ,QAAI,CAAC,SAAS,WAAW,MAAM,WAAW,EAAG;AAE7C,QAAIA,WAAU,GAAG;AACf,iBAAW,EAAE,QAAQ,KAAK;AAAA,IAC5B,OAAO;AAEL,YAAM,MAAM,WAAW;AACvB,UAAI,CAAC,IAAK;AACV,YAAM,SAAS,QAAQ,IAAI,kBAAkB,QAAQ,IAAI;AACzD,YAAM,UAAU,QAAQ,IAAI;AAC5B,UAAI,CAAC,UAAU,CAAC,QAAS;AACzB,WAAK,eAAe,SAAS,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,MAAM;AAAA,MAE1D,CAAC;AAAA,IACH;AAAA,EACF,QAAQ;AAAA,EAER;AACF;AASO,SAAS,eAA8B;AAC5C,MAAI,CAAC,QAAS,QAAO,QAAQ,QAAQ;AACrC,SAAO,QAAQ,UAAU;AAC3B;;;ACxHA,SAAS,qBAAqB,OAAgD;AAE5E,MAAI,MAAM,YAAY,mBAAmB,MAAM,YAAY,gBAAgB;AACzE,WAAO,EAAE,QAAQ,qBAAqB;AAAA,EACxC;AAEA,MAAI,MAAM,UAAU;AAClB,QAAI;AACF,YAAM,MAAM,IAAI,IAAI,MAAM,QAAQ;AAClC,UAAI,IAAI,WAAW,OAAO,SAAS,QAAQ;AACzC,eAAO,EAAE,QAAQ,sBAAsB,SAAS,EAAE,YAAY,IAAI,KAAK,EAAE;AAAA,MAC3E;AAAA,IACF,QAAQ;AAAA,IAER;AAAA,EACF;AACA,SAAO;AACT;AAeO,SAAS,wBAA8B;AAC5C,MAAI,OAAO,WAAW,YAAa;AACnC,MAAI,WAAW,iCAAkC;AACjD,aAAW,mCAAmC;AAE9C,SAAO,iBAAiB,SAAS,CAAC,UAAsB;AACtD,UAAM,MAAM,MAAM,iBAAiB,QAAQ,MAAM,QAAQ,IAAI,MAAM,OAAO,MAAM,SAAS,MAAM,WAAW,eAAe,CAAC;AAC1H,aAAS,KAAK,qBAAqB,KAAK,CAAC;AAAA,EAC3C,CAAC;AAED,SAAO,iBAAiB,sBAAsB,CAAC,UAAiC;AAC9E,UAAM,MAAM,MAAM,kBAAkB,QAAQ,MAAM,SAAS,IAAI,MAAM,OAAO,MAAM,UAAU,6BAA6B,CAAC;AAC1H,aAAS,GAAG;AAAA,EACd,CAAC;AAGD,SAAO,iBAAiB,YAAY,MAAM;AACxC,SAAK,aAAa;AAAA,EACpB,CAAC;AACH;","names":["fingerprint","isBrowser","isBrowser"]}
package/dist/index.mjs CHANGED
@@ -10,13 +10,28 @@ import {
10
10
  } from "./chunk-H634EEUT.mjs";
11
11
 
12
12
  // src/installGlobalHandlers.ts
13
+ function thirdPartyScriptOpts(event) {
14
+ if (event.message === "Script error." || event.message === "Script error") {
15
+ return { reason: "third_party_script" };
16
+ }
17
+ if (event.filename) {
18
+ try {
19
+ const url = new URL(event.filename);
20
+ if (url.origin !== window.location.origin) {
21
+ return { reason: "third_party_script", context: { scriptHost: url.host } };
22
+ }
23
+ } catch {
24
+ }
25
+ }
26
+ return void 0;
27
+ }
13
28
  function installGlobalHandlers() {
14
29
  if (typeof window === "undefined") return;
15
30
  if (globalThis.__errorLoggerHandlersInstalled__) return;
16
31
  globalThis.__errorLoggerHandlersInstalled__ = true;
17
32
  window.addEventListener("error", (event) => {
18
33
  const err = event.error instanceof Error ? event.error : new Error(String(event.error ?? event.message ?? "Unknown error"));
19
- logError(err);
34
+ logError(err, thirdPartyScriptOpts(event));
20
35
  });
21
36
  window.addEventListener("unhandledrejection", (event) => {
22
37
  const err = event.reason instanceof Error ? event.reason : new Error(String(event.reason ?? "Unhandled promise rejection"));
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/installGlobalHandlers.ts"],"sourcesContent":["import { flushPending, logError } from \"./logError\";\n\ndeclare global {\n // eslint-disable-next-line no-var\n var __errorLoggerHandlersInstalled__: boolean | undefined;\n}\n\n/**\n * Install browser-global error capture (window.onerror + unhandledrejection)\n * plus a `pagehide` listener that flushes any queued events before unload.\n *\n * Idempotent: a `globalThis` sentinel ensures repeated calls do not re-install.\n * Only the host page should call this — widgets embedded in the host inherit\n * coverage automatically because errors bubble to page-level listeners.\n */\nexport function installGlobalHandlers(): void {\n if (typeof window === \"undefined\") return;\n if (globalThis.__errorLoggerHandlersInstalled__) return;\n globalThis.__errorLoggerHandlersInstalled__ = true;\n\n window.addEventListener(\"error\", (event: ErrorEvent) => {\n const err = event.error instanceof Error ? event.error : new Error(String(event.error ?? event.message ?? \"Unknown error\"));\n logError(err);\n });\n\n window.addEventListener(\"unhandledrejection\", (event: PromiseRejectionEvent) => {\n const err = event.reason instanceof Error ? event.reason : new Error(String(event.reason ?? \"Unhandled promise rejection\"));\n logError(err);\n });\n\n // Flush queued events on tab close / nav so users who leave mid-batch-window don't lose errors.\n window.addEventListener(\"pagehide\", () => {\n void flushPending();\n });\n}\n\n/** @internal — test-only: clears the sentinel so re-install is allowed */\nexport function _resetForTests(): void {\n delete globalThis.__errorLoggerHandlersInstalled__;\n}\n"],"mappings":";;;;;;;;;;;;AAeO,SAAS,wBAA8B;AAC5C,MAAI,OAAO,WAAW,YAAa;AACnC,MAAI,WAAW,iCAAkC;AACjD,aAAW,mCAAmC;AAE9C,SAAO,iBAAiB,SAAS,CAAC,UAAsB;AACtD,UAAM,MAAM,MAAM,iBAAiB,QAAQ,MAAM,QAAQ,IAAI,MAAM,OAAO,MAAM,SAAS,MAAM,WAAW,eAAe,CAAC;AAC1H,aAAS,GAAG;AAAA,EACd,CAAC;AAED,SAAO,iBAAiB,sBAAsB,CAAC,UAAiC;AAC9E,UAAM,MAAM,MAAM,kBAAkB,QAAQ,MAAM,SAAS,IAAI,MAAM,OAAO,MAAM,UAAU,6BAA6B,CAAC;AAC1H,aAAS,GAAG;AAAA,EACd,CAAC;AAGD,SAAO,iBAAiB,YAAY,MAAM;AACxC,SAAK,aAAa;AAAA,EACpB,CAAC;AACH;","names":[]}
1
+ {"version":3,"sources":["../src/installGlobalHandlers.ts"],"sourcesContent":["import { flushPending, logError } from \"./logError\";\nimport type { LogErrorOptions } from \"./types\";\n\ndeclare global {\n // eslint-disable-next-line no-var\n var __errorLoggerHandlersInstalled__: boolean | undefined;\n}\n\n/**\n * Classify an ErrorEvent that came from a third-party script (GTM tag,\n * vendor pixel, etc.). Two signals catch the common cases:\n *\n * 1. Browser-sanitized cross-origin error → `message === \"Script error.\"`.\n * The browser stripped details because the script's server didn't send\n * CORS headers. We have no host info in this case.\n * 2. Visible third-party error → `event.filename` parses to a URL on a\n * different origin than `window.location.origin`. Well-behaved third\n * parties (GTM itself, GA, FB Pixel) send proper CORS headers so we\n * get the full stack AND the host.\n *\n * Returns the `LogErrorOptions` to forward to `logError`, or `undefined`\n * when the error doesn't look third-party.\n */\nfunction thirdPartyScriptOpts(event: ErrorEvent): LogErrorOptions | undefined {\n // Case 1: sanitized cross-origin.\n if (event.message === \"Script error.\" || event.message === \"Script error\") {\n return { reason: \"third_party_script\" };\n }\n // Case 2: visible third-party — extract host when filename is a real URL.\n if (event.filename) {\n try {\n const url = new URL(event.filename);\n if (url.origin !== window.location.origin) {\n return { reason: \"third_party_script\", context: { scriptHost: url.host } };\n }\n } catch {\n // Malformed filename; treat as first-party.\n }\n }\n return undefined;\n}\n\n/**\n * Install browser-global error capture (window.onerror + unhandledrejection)\n * plus a `pagehide` listener that flushes any queued events before unload.\n *\n * Idempotent: a `globalThis` sentinel ensures repeated calls do not re-install.\n * Only the host page should call this — widgets embedded in the host inherit\n * coverage automatically because errors bubble to page-level listeners.\n *\n * Auto-tags third-party script errors (GTM tags, vendor pixels, etc.) with\n * `reason: \"third_party_script\"` and — when the browser exposes the host —\n * `context.scriptHost`. Lets admins filter / triage those separately from\n * the host's own errors.\n */\nexport function installGlobalHandlers(): void {\n if (typeof window === \"undefined\") return;\n if (globalThis.__errorLoggerHandlersInstalled__) return;\n globalThis.__errorLoggerHandlersInstalled__ = true;\n\n window.addEventListener(\"error\", (event: ErrorEvent) => {\n const err = event.error instanceof Error ? event.error : new Error(String(event.error ?? event.message ?? \"Unknown error\"));\n logError(err, thirdPartyScriptOpts(event));\n });\n\n window.addEventListener(\"unhandledrejection\", (event: PromiseRejectionEvent) => {\n const err = event.reason instanceof Error ? event.reason : new Error(String(event.reason ?? \"Unhandled promise rejection\"));\n logError(err);\n });\n\n // Flush queued events on tab close / nav so users who leave mid-batch-window don't lose errors.\n window.addEventListener(\"pagehide\", () => {\n void flushPending();\n });\n}\n\n/** @internal — test-only: clears the sentinel so re-install is allowed */\nexport function _resetForTests(): void {\n delete globalThis.__errorLoggerHandlersInstalled__;\n}\n"],"mappings":";;;;;;;;;;;;AAuBA,SAAS,qBAAqB,OAAgD;AAE5E,MAAI,MAAM,YAAY,mBAAmB,MAAM,YAAY,gBAAgB;AACzE,WAAO,EAAE,QAAQ,qBAAqB;AAAA,EACxC;AAEA,MAAI,MAAM,UAAU;AAClB,QAAI;AACF,YAAM,MAAM,IAAI,IAAI,MAAM,QAAQ;AAClC,UAAI,IAAI,WAAW,OAAO,SAAS,QAAQ;AACzC,eAAO,EAAE,QAAQ,sBAAsB,SAAS,EAAE,YAAY,IAAI,KAAK,EAAE;AAAA,MAC3E;AAAA,IACF,QAAQ;AAAA,IAER;AAAA,EACF;AACA,SAAO;AACT;AAeO,SAAS,wBAA8B;AAC5C,MAAI,OAAO,WAAW,YAAa;AACnC,MAAI,WAAW,iCAAkC;AACjD,aAAW,mCAAmC;AAE9C,SAAO,iBAAiB,SAAS,CAAC,UAAsB;AACtD,UAAM,MAAM,MAAM,iBAAiB,QAAQ,MAAM,QAAQ,IAAI,MAAM,OAAO,MAAM,SAAS,MAAM,WAAW,eAAe,CAAC;AAC1H,aAAS,KAAK,qBAAqB,KAAK,CAAC;AAAA,EAC3C,CAAC;AAED,SAAO,iBAAiB,sBAAsB,CAAC,UAAiC;AAC9E,UAAM,MAAM,MAAM,kBAAkB,QAAQ,MAAM,SAAS,IAAI,MAAM,OAAO,MAAM,UAAU,6BAA6B,CAAC;AAC1H,aAAS,GAAG;AAAA,EACd,CAAC;AAGD,SAAO,iBAAiB,YAAY,MAAM;AACxC,SAAK,aAAa;AAAA,EACpB,CAAC;AACH;","names":[]}
@@ -1,4 +1,4 @@
1
- import { c as LogEvent } from '../types-BLz-TUBl.cjs';
1
+ import { c as LogEvent } from '../types-ue_E93K4.cjs';
2
2
 
3
3
  /**
4
4
  * The shape of a Payload v3 endpoint. Declared here as a plain interface
@@ -1,4 +1,4 @@
1
- import { c as LogEvent } from '../types-BLz-TUBl.js';
1
+ import { c as LogEvent } from '../types-ue_E93K4.js';
2
2
 
3
3
  /**
4
4
  * The shape of a Payload v3 endpoint. Declared here as a plain interface
@@ -14,7 +14,7 @@ type Repo = "gli-payload-multitenant" | "storage-theme-payload" | "unit-table" |
14
14
  * doesn't fit any value, either add one to this list or use
15
15
  * `"unhandled_exception"` (the deliberate "we don't know" bucket).
16
16
  */
17
- type Reason = "unit_not_found" | "unit_not_available" | "unit_unavailable" | "facility_not_found" | "invalid_tenant_credentials" | "unauthorized" | "session_expired" | "tenant_not_found" | "reservation_not_found" | "ledger_not_found" | "promotion_not_found" | "payment_failed" | "payment_declined" | "invalid_payment_option" | "validation_error" | "unit_list_empty" | "expected_data_missing" | "cache_stale" | "build_failure" | "hook_error" | "scraper_failure" | "data_integrity" | "unhandled_exception" | "network_timeout";
17
+ type Reason = "unit_not_found" | "unit_not_available" | "unit_unavailable" | "facility_not_found" | "invalid_tenant_credentials" | "unauthorized" | "session_expired" | "tenant_not_found" | "reservation_not_found" | "ledger_not_found" | "promotion_not_found" | "payment_failed" | "payment_declined" | "invalid_payment_option" | "validation_error" | "unit_list_empty" | "expected_data_missing" | "cache_stale" | "build_failure" | "hook_error" | "scraper_failure" | "data_integrity" | "unhandled_exception" | "network_timeout" | "third_party_script";
18
18
  interface InitLoggerConfig {
19
19
  /** Absolute URL to the Payload endpoint (e.g. https://payloadstorage.golocaldev.com/cms/api/log-error). */
20
20
  endpoint: string;
@@ -155,6 +155,11 @@ interface LogContext {
155
155
  /** Soft-assertion expected count, e.g. "should have at least 1 unit, got 0". */
156
156
  expectedCount?: number;
157
157
  actualCount?: number;
158
+ /** Host of the script that threw, when the error came from a third-party
159
+ * script (GTM tag, vendor pixel, etc.) AND the browser allowed us to see
160
+ * it (i.e. the script is served with CORS headers). Empty for the
161
+ * sanitized "Script error." case where the browser stripped details. */
162
+ scriptHost?: string;
158
163
  collectionSlug?: string;
159
164
  documentId?: string;
160
165
  /** Which Payload hook fired the error (e.g. "beforeChange"). */
@@ -14,7 +14,7 @@ type Repo = "gli-payload-multitenant" | "storage-theme-payload" | "unit-table" |
14
14
  * doesn't fit any value, either add one to this list or use
15
15
  * `"unhandled_exception"` (the deliberate "we don't know" bucket).
16
16
  */
17
- type Reason = "unit_not_found" | "unit_not_available" | "unit_unavailable" | "facility_not_found" | "invalid_tenant_credentials" | "unauthorized" | "session_expired" | "tenant_not_found" | "reservation_not_found" | "ledger_not_found" | "promotion_not_found" | "payment_failed" | "payment_declined" | "invalid_payment_option" | "validation_error" | "unit_list_empty" | "expected_data_missing" | "cache_stale" | "build_failure" | "hook_error" | "scraper_failure" | "data_integrity" | "unhandled_exception" | "network_timeout";
17
+ type Reason = "unit_not_found" | "unit_not_available" | "unit_unavailable" | "facility_not_found" | "invalid_tenant_credentials" | "unauthorized" | "session_expired" | "tenant_not_found" | "reservation_not_found" | "ledger_not_found" | "promotion_not_found" | "payment_failed" | "payment_declined" | "invalid_payment_option" | "validation_error" | "unit_list_empty" | "expected_data_missing" | "cache_stale" | "build_failure" | "hook_error" | "scraper_failure" | "data_integrity" | "unhandled_exception" | "network_timeout" | "third_party_script";
18
18
  interface InitLoggerConfig {
19
19
  /** Absolute URL to the Payload endpoint (e.g. https://payloadstorage.golocaldev.com/cms/api/log-error). */
20
20
  endpoint: string;
@@ -155,6 +155,11 @@ interface LogContext {
155
155
  /** Soft-assertion expected count, e.g. "should have at least 1 unit, got 0". */
156
156
  expectedCount?: number;
157
157
  actualCount?: number;
158
+ /** Host of the script that threw, when the error came from a third-party
159
+ * script (GTM tag, vendor pixel, etc.) AND the browser allowed us to see
160
+ * it (i.e. the script is served with CORS headers). Empty for the
161
+ * sanitized "Script error." case where the browser stripped details. */
162
+ scriptHost?: string;
158
163
  collectionSlug?: string;
159
164
  documentId?: string;
160
165
  /** Which Payload hook fired the error (e.g. "beforeChange"). */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glidevvr/storage-payload-error-logger-pkg",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "Centralized error capture for GLI storage products (theme, unit-table, reservation, rental, login). Publishes a runtime helper for browsers and Node, a React error boundary, and a Payload v3 endpoint handler.",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",