@glidevvr/storage-payload-error-logger-pkg 0.3.0 → 0.4.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 +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +37 -0
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +36 -0
- package/dist/index.mjs.map +1 -1
- package/dist/payload-endpoint/index.d.cts +1 -1
- package/dist/payload-endpoint/index.d.ts +1 -1
- package/dist/{types-ue_E93K4.d.cts → types-eUQvPHXr.d.cts} +13 -3
- package/dist/{types-ue_E93K4.d.ts → types-eUQvPHXr.d.ts} +13 -3
- package/package.json +1 -1
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-
|
|
2
|
-
export { b as LogChargeLine, c as LogEvent, d as
|
|
1
|
+
import { L as LogContext, I as InitLoggerConfig, a as LogErrorOptions, R as Reason, S as Severity } from './types-eUQvPHXr.cjs';
|
|
2
|
+
export { b as LogChargeLine, c as LogEvent, d as REASON_VALUES, e as Repo, f as Source } from './types-eUQvPHXr.cjs';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* @fileoverview Ambient log context store — browser-only.
|
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-
|
|
2
|
-
export { b as LogChargeLine, c as LogEvent, d as
|
|
1
|
+
import { L as LogContext, I as InitLoggerConfig, a as LogErrorOptions, R as Reason, S as Severity } from './types-eUQvPHXr.js';
|
|
2
|
+
export { b as LogChargeLine, c as LogEvent, d as REASON_VALUES, e as Repo, f as Source } from './types-eUQvPHXr.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* @fileoverview Ambient log context store — browser-only.
|
package/dist/index.js
CHANGED
|
@@ -20,6 +20,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/index.ts
|
|
21
21
|
var src_exports = {};
|
|
22
22
|
__export(src_exports, {
|
|
23
|
+
REASON_VALUES: () => REASON_VALUES,
|
|
23
24
|
fingerprint: () => fingerprint,
|
|
24
25
|
getDefaultSeverity: () => getDefaultSeverity,
|
|
25
26
|
getLogContext: () => getLogContext,
|
|
@@ -31,6 +32,41 @@ __export(src_exports, {
|
|
|
31
32
|
});
|
|
32
33
|
module.exports = __toCommonJS(src_exports);
|
|
33
34
|
|
|
35
|
+
// src/types.ts
|
|
36
|
+
var REASON_VALUES = [
|
|
37
|
+
// SE errorTags (mirror @glidevvr/se-components)
|
|
38
|
+
"unit_not_found",
|
|
39
|
+
"unit_not_available",
|
|
40
|
+
"unit_unavailable",
|
|
41
|
+
"facility_not_found",
|
|
42
|
+
"invalid_tenant_credentials",
|
|
43
|
+
"unauthorized",
|
|
44
|
+
"session_expired",
|
|
45
|
+
"tenant_not_found",
|
|
46
|
+
"reservation_not_found",
|
|
47
|
+
"ledger_not_found",
|
|
48
|
+
"promotion_not_found",
|
|
49
|
+
"payment_failed",
|
|
50
|
+
"payment_declined",
|
|
51
|
+
"invalid_payment_option",
|
|
52
|
+
"validation_error",
|
|
53
|
+
// Internal additions
|
|
54
|
+
"unit_list_empty",
|
|
55
|
+
"expected_data_missing",
|
|
56
|
+
"cache_stale",
|
|
57
|
+
"build_failure",
|
|
58
|
+
"hook_error",
|
|
59
|
+
"scraper_failure",
|
|
60
|
+
"data_integrity",
|
|
61
|
+
"unhandled_exception",
|
|
62
|
+
"network_timeout",
|
|
63
|
+
"third_party_script",
|
|
64
|
+
// Server-side fallback bucket — written by normalizeIssueEventReason when
|
|
65
|
+
// an incoming `reason` doesn't match any of the values above. Not intended
|
|
66
|
+
// for callers to pass directly.
|
|
67
|
+
"unknown_reason"
|
|
68
|
+
];
|
|
69
|
+
|
|
34
70
|
// src/context.ts
|
|
35
71
|
var current = {};
|
|
36
72
|
var isBrowser = () => typeof window !== "undefined";
|
|
@@ -337,6 +373,7 @@ function installGlobalHandlers() {
|
|
|
337
373
|
}
|
|
338
374
|
// Annotate the CommonJS export names for ESM import in node:
|
|
339
375
|
0 && (module.exports = {
|
|
376
|
+
REASON_VALUES,
|
|
340
377
|
fingerprint,
|
|
341
378
|
getDefaultSeverity,
|
|
342
379
|
getLogContext,
|
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\";\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":"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","names":["fingerprint","isBrowser","isBrowser"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/types.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 { REASON_VALUES } 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","export type Severity = \"error\" | \"warning\" | \"info\";\n\nexport type Source =\n | \"payload\"\n | \"theme\"\n | \"unit-table\"\n | \"reservation-app\"\n | \"rental-app\"\n | \"login\";\n\nexport type Repo =\n | \"gli-payload-multitenant\"\n | \"storage-theme-payload\"\n | \"unit-table\"\n | \"reservation-app\"\n | \"rental-app\"\n | \"npm-golocal-cloud-wrapper\"\n | \"golocal-cloud-wrapper\";\n\n/**\n * Closed enum of failure-classification codes.\n *\n * Splits into two groups:\n * 1. The 15 SE `errorTag` values shared with `@glidevvr/se-components` —\n * keep these in lockstep with that library.\n * 2. Internal codes for failures that aren't an SE-API call (silent empty\n * results, build/scraper/hook errors, uncaught exceptions, etc.).\n *\n * Plus one server-side fallback bucket: `\"unknown_reason\"`. Callers shouldn't\n * pass this directly — it's what server-side normalization writes when a\n * direct REST POST or out-of-sync caller passes a value not in this list,\n * so the event still lands instead of being rejected.\n *\n * Adding a new value here is intentional. If the failure mode you're logging\n * doesn't fit any value, either add one to this list or use\n * `\"unhandled_exception\"` (the deliberate \"we don't know what threw\" bucket).\n *\n * Exported as both a value (`REASON_VALUES` runtime array) and a type\n * (`Reason` derived union). The CMS imports the runtime array to keep its\n * select-field allowlist in lockstep without a parallel constant.\n */\nexport const REASON_VALUES = [\n // SE errorTags (mirror @glidevvr/se-components)\n \"unit_not_found\",\n \"unit_not_available\",\n \"unit_unavailable\",\n \"facility_not_found\",\n \"invalid_tenant_credentials\",\n \"unauthorized\",\n \"session_expired\",\n \"tenant_not_found\",\n \"reservation_not_found\",\n \"ledger_not_found\",\n \"promotion_not_found\",\n \"payment_failed\",\n \"payment_declined\",\n \"invalid_payment_option\",\n \"validation_error\",\n // Internal additions\n \"unit_list_empty\",\n \"expected_data_missing\",\n \"cache_stale\",\n \"build_failure\",\n \"hook_error\",\n \"scraper_failure\",\n \"data_integrity\",\n \"unhandled_exception\",\n \"network_timeout\",\n \"third_party_script\",\n // Server-side fallback bucket — written by normalizeIssueEventReason when\n // an incoming `reason` doesn't match any of the values above. Not intended\n // for callers to pass directly.\n \"unknown_reason\",\n] as const;\n\nexport type Reason = (typeof REASON_VALUES)[number];\n\nexport interface InitLoggerConfig {\n /** Absolute URL to the Payload endpoint (e.g. https://payloadstorage.golocaldev.com/cms/api/log-error). */\n endpoint: string;\n /** Repo identifier — stamped on every event. */\n repo: Repo;\n /** Build identifier (git SHA / build ID) — stamped on every event. */\n release?: string;\n /** reCAPTCHA v3 site key (browser-only). If absent, browser-side logging is disabled. */\n recaptchaSiteKey?: string;\n /** reCAPTCHA action name; defaults to \"log_error\". */\n recaptchaAction?: string;\n /** Default source for events that don't pass an explicit source. */\n defaultSource?: Source;\n}\n\n/**\n * One line item in `LogContext.charges` / `LogContext.recurringFees`.\n *\n * Sub-keys are allowlisted: `id` / `name` / `amount` / `cycle`. Anything else\n * (memo, note, internal_comment, etc.) is stripped by the server-side\n * `sanitizeIssueEventContext` hook before persistence.\n */\nexport interface LogChargeLine {\n id?: string;\n name?: string;\n amount?: number;\n /** Only meaningful on `recurringFees`, e.g. \"monthly\". */\n cycle?: string;\n}\n\n/**\n * Closed allowlist for log context fields.\n *\n * **Why this is a closed type (no index signature).** The PII contract for\n * this logger is \"never accept user-input data into the pipeline in the first\n * place\" — not \"scrub it after\". A closed type makes TypeScript reject unknown\n * keys at every call site, so card numbers, names, emails, addresses, etc.\n * cannot accidentally land in an event's `context`.\n *\n * Naming convention:\n * - `se*` prefix → identifier from the SE API (matches SE's wire term)\n * - `vendor*` prefix → identifier from the underlying property-management\n * system (SiteLink / StorEdge / DoorSwap)\n * - everything else → user-facing terms shared with the admin UI\n * (e.g. `organizationId` matches the \"Organization\" label used in CMS)\n *\n * If a new context field is genuinely needed, add it here intentionally and\n * confirm it carries no PII. The server has a defense-in-depth filter\n * (`sanitizeIssueEventContext`) that drops unknown keys before persistence.\n */\nexport interface LogContext {\n // ─── Identity ─────────────────────────────────────────────────────────\n /** Default source for events that don't pass an explicit source. */\n source?: Source;\n /** Anonymous browser session identifier. NEVER a logged-in user identity. */\n sessionId?: string;\n /** React component-tree path from `<ErrorBoundary>`. No user input. */\n componentStack?: string;\n\n // ─── Organization (the storage org running the site) ──────────────────\n /** Payload-side ID for the org (Mongo ObjectID). Maps 1:1 with the SE\n * company below — they're two IDs for the same entity in different systems. */\n organizationId?: string;\n /** Display name, e.g. \"Go Local Interactive\". */\n organizationName?: string;\n /** SE-side numeric ID for the same org (e.g. 7). Used to pivot into SE admin. */\n seCompanyId?: number;\n\n // ─── Renter (the person renting; anonymous, never a name/email/phone) ─\n /** SE renter UUID. SE calls this \"tenant\" — confusing in our multi-tenant\n * CMS context, hence the `se` prefix. */\n seTenantId?: string;\n /** Vendor-system renter ID (SiteLink/StorEdge/DoorSwap). */\n vendorTenantId?: string;\n /** Which underlying property-management system the org uses. */\n vendorType?: \"SiteLink\" | \"StorEdge\" | \"DoorSwap\";\n\n // ─── Facility ─────────────────────────────────────────────────────────\n seFacilityId?: string | number;\n vendorFacilityId?: string | number;\n /** Vendor-side site/region code. */\n siteLocationCode?: string;\n\n // ─── Unit ─────────────────────────────────────────────────────────────\n seUnitId?: string | number;\n vendorUnitId?: string | number;\n /** Unit category slug (e.g. \"5x10-storage\"). */\n unitCategorySlug?: string;\n /** Pricing tier tag, e.g. \"Tier 1\" / \"Tier 2\". */\n unitTier?: string;\n /** Rate plan family, e.g. \"standard_rate\" | \"tiered_rate\" | \"channel_rate\" | \"managed_rate\". */\n ratePlan?: string;\n\n // ─── Reservation (pre-move-in hold) ───────────────────────────────────\n seReservationId?: string;\n vendorReservationId?: string | number;\n /** Hard hold (units locked) vs soft inquiry. */\n reservationType?: \"hard\" | \"soft\";\n\n // ─── Rental (post-move-in lease) ──────────────────────────────────────\n /** SE-side UUID on the `vendor_rentals` table. */\n seRentalId?: string;\n /** Vendor-system rental ID (e.g. \"R-1234567\"). */\n vendorRentalId?: string;\n /** SE move-in event / transaction ID. */\n seTransactionId?: string | number;\n\n // ─── Ledger / billing ─────────────────────────────────────────────────\n /** Vendor's ledger ID, surfaced via SE's `_se_ledger_id`. */\n seLedgerId?: string | number;\n /** Payment receipt ID (SE `_se_receipt_id`). */\n seReceiptId?: string | number;\n /** Single charge ID when one specific line item is the focus of the error. */\n chargeId?: string;\n\n // ─── Charges + recurring fees (sanitized line-items) ──────────────────\n /** Current invoice / one-time charges. Sub-keys are allowlisted by\n * `sanitizeIssueEventContext` — memo/note/comment fields are stripped. */\n charges?: LogChargeLine[];\n /** Recurring billing schedule. Same sub-key allowlist as `charges`. */\n recurringFees?: LogChargeLine[];\n\n // ─── User selections (codes, never user-typed values) ─────────────────\n promotionId?: string | number;\n promotionSlug?: string;\n insurancePlanId?: string | number;\n paymentMethodType?: \"cc\" | \"ach\";\n autopayEnabled?: boolean;\n billingCycle?: \"monthly\" | \"prepay\";\n /** ISO date the user picked. The date itself is not PII — it's a calendar\n * click, not who-they-are. */\n moveInDate?: string;\n /** Numeric rate the user *saw* on the page. Useful for \"user saw $X but\n * was charged $Y\" debugging without logging payment data. */\n quotedRate?: number;\n\n // ─── Flow context (cross-cutting) ─────────────────────────────────────\n /** Which multi-step user flow the error occurred in. Pair with `step` —\n * `step` is meaningless without a flow reference (step 3 of *what*?).\n * Closed union so values stay grep-able across apps. */\n flow?:\n | \"reservation\"\n | \"rental\"\n | \"cancellation\"\n | \"payment-update\"\n | \"onboarding\";\n /** Position within `flow` — number for ordered flows, slug for named ones.\n * Meaningful only when paired with `flow`. Different from `slug` (which\n * entity is in focus) and from the top-level `url` (where the user was). */\n step?: number | string;\n /** Stable identifier for the entity being acted on — page slug, market\n * slug, unit-category slug, etc. Decoupled from URL: a build-time SSG\n * error has no URL yet but the slug for the page being built does. */\n slug?: string;\n /** Which API/RPC was being called (e.g. \"createReservationV2\"). */\n apiMethod?: string;\n /** URL of the called endpoint, query string already stripped. */\n apiEndpoint?: string;\n httpStatus?: number;\n /** Cache identifier when the error came from a stale-cache lookup. */\n cacheKey?: string;\n /** Mirrors `LogErrorOptions.reason` for filter convenience. */\n errorTag?: string;\n /** Closed schema field names, never user-typed values. e.g. `[\"cvv\", \"email\"]`. */\n validationFieldNames?: string[];\n /** Soft-assertion expected count, e.g. \"should have at least 1 unit, got 0\". */\n expectedCount?: number;\n actualCount?: number;\n /** Host of the script that threw, when the error came from a third-party\n * script (GTM tag, vendor pixel, etc.) AND the browser allowed us to see\n * it (i.e. the script is served with CORS headers). Empty for the\n * sanitized \"Script error.\" case where the browser stripped details. */\n scriptHost?: string;\n\n // ─── Payload-only (only meaningful when source: \"payload\") ────────────\n collectionSlug?: string;\n documentId?: string;\n /** Which Payload hook fired the error (e.g. \"beforeChange\"). */\n hookName?: string;\n userRole?: \"admin\" | \"super-admin\" | \"user\" | \"api-user\";\n /** AI-scraper batch ID. */\n scraperBatchId?: string;\n /** Build/deploy ID for build-trigger hooks. */\n buildId?: string;\n}\n\nexport interface LogErrorOptions {\n source?: Source;\n severity?: Severity;\n context?: LogContext;\n /**\n * Failure-classification code — the WHY behind the error. Closed enum\n * (see `Reason`). Same code path failing for different reasons becomes\n * distinct Issues, since `reason` is part of the fingerprint.\n *\n * If omitted, the event is uncaught/unclassified — fingerprint groups\n * by `(source, message, top-stack-frame)` only.\n */\n reason?: Reason;\n}\n\nexport interface LogEvent {\n source: Source;\n repo: Repo;\n severity: Severity;\n fingerprint: string;\n message: string;\n stack: string;\n context: LogContext;\n url: string;\n userAgent: string;\n occurredAt: string;\n release?: string;\n /** Failure-classification code copied from `LogErrorOptions.reason`. */\n reason?: Reason;\n}\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":"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","names":["fingerprint","isBrowser","isBrowser"]}
|
package/dist/index.mjs
CHANGED
|
@@ -9,6 +9,41 @@ import {
|
|
|
9
9
|
withLogContext
|
|
10
10
|
} from "./chunk-H634EEUT.mjs";
|
|
11
11
|
|
|
12
|
+
// src/types.ts
|
|
13
|
+
var REASON_VALUES = [
|
|
14
|
+
// SE errorTags (mirror @glidevvr/se-components)
|
|
15
|
+
"unit_not_found",
|
|
16
|
+
"unit_not_available",
|
|
17
|
+
"unit_unavailable",
|
|
18
|
+
"facility_not_found",
|
|
19
|
+
"invalid_tenant_credentials",
|
|
20
|
+
"unauthorized",
|
|
21
|
+
"session_expired",
|
|
22
|
+
"tenant_not_found",
|
|
23
|
+
"reservation_not_found",
|
|
24
|
+
"ledger_not_found",
|
|
25
|
+
"promotion_not_found",
|
|
26
|
+
"payment_failed",
|
|
27
|
+
"payment_declined",
|
|
28
|
+
"invalid_payment_option",
|
|
29
|
+
"validation_error",
|
|
30
|
+
// Internal additions
|
|
31
|
+
"unit_list_empty",
|
|
32
|
+
"expected_data_missing",
|
|
33
|
+
"cache_stale",
|
|
34
|
+
"build_failure",
|
|
35
|
+
"hook_error",
|
|
36
|
+
"scraper_failure",
|
|
37
|
+
"data_integrity",
|
|
38
|
+
"unhandled_exception",
|
|
39
|
+
"network_timeout",
|
|
40
|
+
"third_party_script",
|
|
41
|
+
// Server-side fallback bucket — written by normalizeIssueEventReason when
|
|
42
|
+
// an incoming `reason` doesn't match any of the values above. Not intended
|
|
43
|
+
// for callers to pass directly.
|
|
44
|
+
"unknown_reason"
|
|
45
|
+
];
|
|
46
|
+
|
|
12
47
|
// src/installGlobalHandlers.ts
|
|
13
48
|
function thirdPartyScriptOpts(event) {
|
|
14
49
|
if (event.message === "Script error." || event.message === "Script error") {
|
|
@@ -42,6 +77,7 @@ function installGlobalHandlers() {
|
|
|
42
77
|
});
|
|
43
78
|
}
|
|
44
79
|
export {
|
|
80
|
+
REASON_VALUES,
|
|
45
81
|
fingerprint,
|
|
46
82
|
getDefaultSeverity,
|
|
47
83
|
getLogContext,
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
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
|
+
{"version":3,"sources":["../src/types.ts","../src/installGlobalHandlers.ts"],"sourcesContent":["export type Severity = \"error\" | \"warning\" | \"info\";\n\nexport type Source =\n | \"payload\"\n | \"theme\"\n | \"unit-table\"\n | \"reservation-app\"\n | \"rental-app\"\n | \"login\";\n\nexport type Repo =\n | \"gli-payload-multitenant\"\n | \"storage-theme-payload\"\n | \"unit-table\"\n | \"reservation-app\"\n | \"rental-app\"\n | \"npm-golocal-cloud-wrapper\"\n | \"golocal-cloud-wrapper\";\n\n/**\n * Closed enum of failure-classification codes.\n *\n * Splits into two groups:\n * 1. The 15 SE `errorTag` values shared with `@glidevvr/se-components` —\n * keep these in lockstep with that library.\n * 2. Internal codes for failures that aren't an SE-API call (silent empty\n * results, build/scraper/hook errors, uncaught exceptions, etc.).\n *\n * Plus one server-side fallback bucket: `\"unknown_reason\"`. Callers shouldn't\n * pass this directly — it's what server-side normalization writes when a\n * direct REST POST or out-of-sync caller passes a value not in this list,\n * so the event still lands instead of being rejected.\n *\n * Adding a new value here is intentional. If the failure mode you're logging\n * doesn't fit any value, either add one to this list or use\n * `\"unhandled_exception\"` (the deliberate \"we don't know what threw\" bucket).\n *\n * Exported as both a value (`REASON_VALUES` runtime array) and a type\n * (`Reason` derived union). The CMS imports the runtime array to keep its\n * select-field allowlist in lockstep without a parallel constant.\n */\nexport const REASON_VALUES = [\n // SE errorTags (mirror @glidevvr/se-components)\n \"unit_not_found\",\n \"unit_not_available\",\n \"unit_unavailable\",\n \"facility_not_found\",\n \"invalid_tenant_credentials\",\n \"unauthorized\",\n \"session_expired\",\n \"tenant_not_found\",\n \"reservation_not_found\",\n \"ledger_not_found\",\n \"promotion_not_found\",\n \"payment_failed\",\n \"payment_declined\",\n \"invalid_payment_option\",\n \"validation_error\",\n // Internal additions\n \"unit_list_empty\",\n \"expected_data_missing\",\n \"cache_stale\",\n \"build_failure\",\n \"hook_error\",\n \"scraper_failure\",\n \"data_integrity\",\n \"unhandled_exception\",\n \"network_timeout\",\n \"third_party_script\",\n // Server-side fallback bucket — written by normalizeIssueEventReason when\n // an incoming `reason` doesn't match any of the values above. Not intended\n // for callers to pass directly.\n \"unknown_reason\",\n] as const;\n\nexport type Reason = (typeof REASON_VALUES)[number];\n\nexport interface InitLoggerConfig {\n /** Absolute URL to the Payload endpoint (e.g. https://payloadstorage.golocaldev.com/cms/api/log-error). */\n endpoint: string;\n /** Repo identifier — stamped on every event. */\n repo: Repo;\n /** Build identifier (git SHA / build ID) — stamped on every event. */\n release?: string;\n /** reCAPTCHA v3 site key (browser-only). If absent, browser-side logging is disabled. */\n recaptchaSiteKey?: string;\n /** reCAPTCHA action name; defaults to \"log_error\". */\n recaptchaAction?: string;\n /** Default source for events that don't pass an explicit source. */\n defaultSource?: Source;\n}\n\n/**\n * One line item in `LogContext.charges` / `LogContext.recurringFees`.\n *\n * Sub-keys are allowlisted: `id` / `name` / `amount` / `cycle`. Anything else\n * (memo, note, internal_comment, etc.) is stripped by the server-side\n * `sanitizeIssueEventContext` hook before persistence.\n */\nexport interface LogChargeLine {\n id?: string;\n name?: string;\n amount?: number;\n /** Only meaningful on `recurringFees`, e.g. \"monthly\". */\n cycle?: string;\n}\n\n/**\n * Closed allowlist for log context fields.\n *\n * **Why this is a closed type (no index signature).** The PII contract for\n * this logger is \"never accept user-input data into the pipeline in the first\n * place\" — not \"scrub it after\". A closed type makes TypeScript reject unknown\n * keys at every call site, so card numbers, names, emails, addresses, etc.\n * cannot accidentally land in an event's `context`.\n *\n * Naming convention:\n * - `se*` prefix → identifier from the SE API (matches SE's wire term)\n * - `vendor*` prefix → identifier from the underlying property-management\n * system (SiteLink / StorEdge / DoorSwap)\n * - everything else → user-facing terms shared with the admin UI\n * (e.g. `organizationId` matches the \"Organization\" label used in CMS)\n *\n * If a new context field is genuinely needed, add it here intentionally and\n * confirm it carries no PII. The server has a defense-in-depth filter\n * (`sanitizeIssueEventContext`) that drops unknown keys before persistence.\n */\nexport interface LogContext {\n // ─── Identity ─────────────────────────────────────────────────────────\n /** Default source for events that don't pass an explicit source. */\n source?: Source;\n /** Anonymous browser session identifier. NEVER a logged-in user identity. */\n sessionId?: string;\n /** React component-tree path from `<ErrorBoundary>`. No user input. */\n componentStack?: string;\n\n // ─── Organization (the storage org running the site) ──────────────────\n /** Payload-side ID for the org (Mongo ObjectID). Maps 1:1 with the SE\n * company below — they're two IDs for the same entity in different systems. */\n organizationId?: string;\n /** Display name, e.g. \"Go Local Interactive\". */\n organizationName?: string;\n /** SE-side numeric ID for the same org (e.g. 7). Used to pivot into SE admin. */\n seCompanyId?: number;\n\n // ─── Renter (the person renting; anonymous, never a name/email/phone) ─\n /** SE renter UUID. SE calls this \"tenant\" — confusing in our multi-tenant\n * CMS context, hence the `se` prefix. */\n seTenantId?: string;\n /** Vendor-system renter ID (SiteLink/StorEdge/DoorSwap). */\n vendorTenantId?: string;\n /** Which underlying property-management system the org uses. */\n vendorType?: \"SiteLink\" | \"StorEdge\" | \"DoorSwap\";\n\n // ─── Facility ─────────────────────────────────────────────────────────\n seFacilityId?: string | number;\n vendorFacilityId?: string | number;\n /** Vendor-side site/region code. */\n siteLocationCode?: string;\n\n // ─── Unit ─────────────────────────────────────────────────────────────\n seUnitId?: string | number;\n vendorUnitId?: string | number;\n /** Unit category slug (e.g. \"5x10-storage\"). */\n unitCategorySlug?: string;\n /** Pricing tier tag, e.g. \"Tier 1\" / \"Tier 2\". */\n unitTier?: string;\n /** Rate plan family, e.g. \"standard_rate\" | \"tiered_rate\" | \"channel_rate\" | \"managed_rate\". */\n ratePlan?: string;\n\n // ─── Reservation (pre-move-in hold) ───────────────────────────────────\n seReservationId?: string;\n vendorReservationId?: string | number;\n /** Hard hold (units locked) vs soft inquiry. */\n reservationType?: \"hard\" | \"soft\";\n\n // ─── Rental (post-move-in lease) ──────────────────────────────────────\n /** SE-side UUID on the `vendor_rentals` table. */\n seRentalId?: string;\n /** Vendor-system rental ID (e.g. \"R-1234567\"). */\n vendorRentalId?: string;\n /** SE move-in event / transaction ID. */\n seTransactionId?: string | number;\n\n // ─── Ledger / billing ─────────────────────────────────────────────────\n /** Vendor's ledger ID, surfaced via SE's `_se_ledger_id`. */\n seLedgerId?: string | number;\n /** Payment receipt ID (SE `_se_receipt_id`). */\n seReceiptId?: string | number;\n /** Single charge ID when one specific line item is the focus of the error. */\n chargeId?: string;\n\n // ─── Charges + recurring fees (sanitized line-items) ──────────────────\n /** Current invoice / one-time charges. Sub-keys are allowlisted by\n * `sanitizeIssueEventContext` — memo/note/comment fields are stripped. */\n charges?: LogChargeLine[];\n /** Recurring billing schedule. Same sub-key allowlist as `charges`. */\n recurringFees?: LogChargeLine[];\n\n // ─── User selections (codes, never user-typed values) ─────────────────\n promotionId?: string | number;\n promotionSlug?: string;\n insurancePlanId?: string | number;\n paymentMethodType?: \"cc\" | \"ach\";\n autopayEnabled?: boolean;\n billingCycle?: \"monthly\" | \"prepay\";\n /** ISO date the user picked. The date itself is not PII — it's a calendar\n * click, not who-they-are. */\n moveInDate?: string;\n /** Numeric rate the user *saw* on the page. Useful for \"user saw $X but\n * was charged $Y\" debugging without logging payment data. */\n quotedRate?: number;\n\n // ─── Flow context (cross-cutting) ─────────────────────────────────────\n /** Which multi-step user flow the error occurred in. Pair with `step` —\n * `step` is meaningless without a flow reference (step 3 of *what*?).\n * Closed union so values stay grep-able across apps. */\n flow?:\n | \"reservation\"\n | \"rental\"\n | \"cancellation\"\n | \"payment-update\"\n | \"onboarding\";\n /** Position within `flow` — number for ordered flows, slug for named ones.\n * Meaningful only when paired with `flow`. Different from `slug` (which\n * entity is in focus) and from the top-level `url` (where the user was). */\n step?: number | string;\n /** Stable identifier for the entity being acted on — page slug, market\n * slug, unit-category slug, etc. Decoupled from URL: a build-time SSG\n * error has no URL yet but the slug for the page being built does. */\n slug?: string;\n /** Which API/RPC was being called (e.g. \"createReservationV2\"). */\n apiMethod?: string;\n /** URL of the called endpoint, query string already stripped. */\n apiEndpoint?: string;\n httpStatus?: number;\n /** Cache identifier when the error came from a stale-cache lookup. */\n cacheKey?: string;\n /** Mirrors `LogErrorOptions.reason` for filter convenience. */\n errorTag?: string;\n /** Closed schema field names, never user-typed values. e.g. `[\"cvv\", \"email\"]`. */\n validationFieldNames?: string[];\n /** Soft-assertion expected count, e.g. \"should have at least 1 unit, got 0\". */\n expectedCount?: number;\n actualCount?: number;\n /** Host of the script that threw, when the error came from a third-party\n * script (GTM tag, vendor pixel, etc.) AND the browser allowed us to see\n * it (i.e. the script is served with CORS headers). Empty for the\n * sanitized \"Script error.\" case where the browser stripped details. */\n scriptHost?: string;\n\n // ─── Payload-only (only meaningful when source: \"payload\") ────────────\n collectionSlug?: string;\n documentId?: string;\n /** Which Payload hook fired the error (e.g. \"beforeChange\"). */\n hookName?: string;\n userRole?: \"admin\" | \"super-admin\" | \"user\" | \"api-user\";\n /** AI-scraper batch ID. */\n scraperBatchId?: string;\n /** Build/deploy ID for build-trigger hooks. */\n buildId?: string;\n}\n\nexport interface LogErrorOptions {\n source?: Source;\n severity?: Severity;\n context?: LogContext;\n /**\n * Failure-classification code — the WHY behind the error. Closed enum\n * (see `Reason`). Same code path failing for different reasons becomes\n * distinct Issues, since `reason` is part of the fingerprint.\n *\n * If omitted, the event is uncaught/unclassified — fingerprint groups\n * by `(source, message, top-stack-frame)` only.\n */\n reason?: Reason;\n}\n\nexport interface LogEvent {\n source: Source;\n repo: Repo;\n severity: Severity;\n fingerprint: string;\n message: string;\n stack: string;\n context: LogContext;\n url: string;\n userAgent: string;\n occurredAt: string;\n release?: string;\n /** Failure-classification code copied from `LogErrorOptions.reason`. */\n reason?: Reason;\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":";;;;;;;;;;;;AAyCO,IAAM,gBAAgB;AAAA;AAAA,EAE3B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA;AAAA,EAEA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA;AAAA;AAAA;AAAA,EAIA;AACF;;;AClDA,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":[]}
|
|
@@ -10,11 +10,21 @@ type Repo = "gli-payload-multitenant" | "storage-theme-payload" | "unit-table" |
|
|
|
10
10
|
* 2. Internal codes for failures that aren't an SE-API call (silent empty
|
|
11
11
|
* results, build/scraper/hook errors, uncaught exceptions, etc.).
|
|
12
12
|
*
|
|
13
|
+
* Plus one server-side fallback bucket: `"unknown_reason"`. Callers shouldn't
|
|
14
|
+
* pass this directly — it's what server-side normalization writes when a
|
|
15
|
+
* direct REST POST or out-of-sync caller passes a value not in this list,
|
|
16
|
+
* so the event still lands instead of being rejected.
|
|
17
|
+
*
|
|
13
18
|
* Adding a new value here is intentional. If the failure mode you're logging
|
|
14
19
|
* doesn't fit any value, either add one to this list or use
|
|
15
|
-
* `"unhandled_exception"` (the deliberate "we don't know" bucket).
|
|
20
|
+
* `"unhandled_exception"` (the deliberate "we don't know what threw" bucket).
|
|
21
|
+
*
|
|
22
|
+
* Exported as both a value (`REASON_VALUES` runtime array) and a type
|
|
23
|
+
* (`Reason` derived union). The CMS imports the runtime array to keep its
|
|
24
|
+
* select-field allowlist in lockstep without a parallel constant.
|
|
16
25
|
*/
|
|
17
|
-
|
|
26
|
+
declare const REASON_VALUES: readonly ["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", "unknown_reason"];
|
|
27
|
+
type Reason = (typeof REASON_VALUES)[number];
|
|
18
28
|
interface InitLoggerConfig {
|
|
19
29
|
/** Absolute URL to the Payload endpoint (e.g. https://payloadstorage.golocaldev.com/cms/api/log-error). */
|
|
20
30
|
endpoint: string;
|
|
@@ -200,4 +210,4 @@ interface LogEvent {
|
|
|
200
210
|
reason?: Reason;
|
|
201
211
|
}
|
|
202
212
|
|
|
203
|
-
export type
|
|
213
|
+
export { type InitLoggerConfig as I, type LogContext as L, type Reason as R, type Severity as S, type LogErrorOptions as a, type LogChargeLine as b, type LogEvent as c, REASON_VALUES as d, type Repo as e, type Source as f };
|
|
@@ -10,11 +10,21 @@ type Repo = "gli-payload-multitenant" | "storage-theme-payload" | "unit-table" |
|
|
|
10
10
|
* 2. Internal codes for failures that aren't an SE-API call (silent empty
|
|
11
11
|
* results, build/scraper/hook errors, uncaught exceptions, etc.).
|
|
12
12
|
*
|
|
13
|
+
* Plus one server-side fallback bucket: `"unknown_reason"`. Callers shouldn't
|
|
14
|
+
* pass this directly — it's what server-side normalization writes when a
|
|
15
|
+
* direct REST POST or out-of-sync caller passes a value not in this list,
|
|
16
|
+
* so the event still lands instead of being rejected.
|
|
17
|
+
*
|
|
13
18
|
* Adding a new value here is intentional. If the failure mode you're logging
|
|
14
19
|
* doesn't fit any value, either add one to this list or use
|
|
15
|
-
* `"unhandled_exception"` (the deliberate "we don't know" bucket).
|
|
20
|
+
* `"unhandled_exception"` (the deliberate "we don't know what threw" bucket).
|
|
21
|
+
*
|
|
22
|
+
* Exported as both a value (`REASON_VALUES` runtime array) and a type
|
|
23
|
+
* (`Reason` derived union). The CMS imports the runtime array to keep its
|
|
24
|
+
* select-field allowlist in lockstep without a parallel constant.
|
|
16
25
|
*/
|
|
17
|
-
|
|
26
|
+
declare const REASON_VALUES: readonly ["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", "unknown_reason"];
|
|
27
|
+
type Reason = (typeof REASON_VALUES)[number];
|
|
18
28
|
interface InitLoggerConfig {
|
|
19
29
|
/** Absolute URL to the Payload endpoint (e.g. https://payloadstorage.golocaldev.com/cms/api/log-error). */
|
|
20
30
|
endpoint: string;
|
|
@@ -200,4 +210,4 @@ interface LogEvent {
|
|
|
200
210
|
reason?: Reason;
|
|
201
211
|
}
|
|
202
212
|
|
|
203
|
-
export type
|
|
213
|
+
export { type InitLoggerConfig as I, type LogContext as L, type Reason as R, type Severity as S, type LogErrorOptions as a, type LogChargeLine as b, type LogEvent as c, REASON_VALUES as d, type Repo as e, type Source as f };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glidevvr/storage-payload-error-logger-pkg",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.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",
|