@glidevvr/storage-payload-error-logger-pkg 0.1.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/README.md ADDED
@@ -0,0 +1,229 @@
1
+ # @glidevvr/storage-payload-error-logger-pkg
2
+
3
+ Centralized error capture for GLI storage products. See the WEB-1413 design doc in `gli-payload-multitenant/docs/plans/2026-05-05-web-1413-error-logger-design.md` for details.
4
+
5
+ ## Subpaths
6
+ - `@glidevvr/storage-payload-error-logger-pkg` — core (initLogger, logError, setLogContext, installGlobalHandlers)
7
+ - `@glidevvr/storage-payload-error-logger-pkg/react` — `<ErrorBoundary>`
8
+ - `@glidevvr/storage-payload-error-logger-pkg/payload-endpoint` — Payload v3 `Endpoint` handler
9
+
10
+ ## Init
11
+ `initLogger({ endpoint, repo, release, recaptchaSiteKey })`
12
+
13
+ ## PII contract
14
+
15
+ The logger is built on a strict rule: **never accept user-input data into the pipeline in the first place.** Scrubbing PII downstream is too late — the goal is to never receive it.
16
+
17
+ This contract is enforced on five surfaces:
18
+
19
+ ### 1. `LogContext` is a closed allowlist
20
+ `LogContext` has no index signature. TypeScript rejects unknown keys at the call site. The allowlist is grouped by domain — see `types.ts` for the full list and per-field JSDoc. Headline categories:
21
+
22
+ | Category | Examples |
23
+ | --- | --- |
24
+ | **Identity** | `source`, `sessionId`, `componentStack` |
25
+ | **Organization** (storage org) | `organizationId` (Payload Mongo ID), `organizationName`, `seCompanyId` (SE numeric ID) |
26
+ | **Renter** (anonymous, never name/email/phone) | `seTenantId`, `vendorTenantId`, `vendorType` |
27
+ | **Facility** | `seFacilityId`, `vendorFacilityId`, `siteLocationCode` |
28
+ | **Unit** | `seUnitId`, `vendorUnitId`, `unitCategorySlug`, `unitTier`, `ratePlan` |
29
+ | **Reservation** (pre-move-in) | `seReservationId`, `vendorReservationId`, `reservationType` |
30
+ | **Rental** (post-move-in) | `seRentalId`, `vendorRentalId`, `seTransactionId` |
31
+ | **Ledger / billing** | `seLedgerId`, `seReceiptId`, `chargeId` |
32
+ | **Charges + recurring fees** | `charges[]`, `recurringFees[]` (sub-keys: `id`, `name`, `amount`, `cycle` only — memo/note stripped) |
33
+ | **Selections** (codes, never typed values) | `promotionId`, `promotionSlug`, `insurancePlanId`, `paymentMethodType`, `autopayEnabled`, `billingCycle`, `moveInDate`, `quotedRate` |
34
+ | **Flow context** | `flow` (closed: `reservation` / `rental` / `cancellation` / `payment-update` / `onboarding`), `step` (paired with `flow`), `slug`, `apiMethod`, `apiEndpoint`, `httpStatus`, `cacheKey`, `errorTag`, `validationFieldNames[]`, `expectedCount`, `actualCount` |
35
+ | **Payload-only** | `collectionSlug`, `documentId`, `hookName`, `userRole`, `scraperBatchId`, `buildId` |
36
+
37
+ **Naming convention:**
38
+ - `se*` prefix → identifier from the SE API (matches SE's wire term)
39
+ - `vendor*` prefix → identifier from the underlying property-management system (SiteLink / StorEdge / DoorSwap)
40
+ - everything else → user-facing terms shared with the admin UI (e.g. `organizationId` matches the "Organization" label in CMS)
41
+
42
+ Need a new field? Add it intentionally to `LogContext` in `types.ts` AND to `ALLOWED_CONTEXT_KEYS` in `gli-payload-multitenant/src/collections/IssueEvents/hooks/sanitizeContext.ts` (the server-side defense). Confirm it carries no PII before adding.
43
+
44
+ ### 2. `reason` is a closed enum — the WHY, never user input
45
+ The `Reason` type in `types.ts` is the closed vocabulary, mirrored by `ISSUE_REASON_VALUES` in `gli-payload-multitenant/src/utilities/errorLogger/reasonValues.ts`. The Issues + IssueEvents collection schemas use Payload's `select` field type with this enum, so direct REST POSTs that send an unknown reason are rejected with a 400 — closing the gap that the helper-side TypeScript guard alone can't cover.
46
+
47
+ 15 values mirror the `errorTag` enum in `@glidevvr/se-components`; 9 internal values cover failure modes outside the SE API:
48
+
49
+ ```ts
50
+ // SE errorTags (mirror se-components)
51
+ "unit_not_found" | "unit_not_available" | "unit_unavailable" | "facility_not_found"
52
+ | "invalid_tenant_credentials" | "unauthorized" | "session_expired"
53
+ | "tenant_not_found" | "reservation_not_found" | "ledger_not_found" | "promotion_not_found"
54
+ | "payment_failed" | "payment_declined" | "invalid_payment_option"
55
+ | "validation_error"
56
+
57
+ // Internal additions
58
+ | "unit_list_empty" | "expected_data_missing" | "cache_stale"
59
+ | "build_failure" | "hook_error" | "scraper_failure"
60
+ | "data_integrity" | "unhandled_exception" | "network_timeout"
61
+ ```
62
+
63
+ Pass via `LogErrorOptions.reason`:
64
+
65
+ ```ts
66
+ logError(err, { reason: "invalid_cvv", context: { step: "payment" } })
67
+ logError(err, { reason: "unit_list_empty", context: { seFacilityId, expectedCount: 1, actualCount: 0 } })
68
+ ```
69
+
70
+ `reason` is part of the event's fingerprint, so the same code path failing for different reasons becomes distinct Issues.
71
+
72
+ ### 3. `severity` defaults from `reason`
73
+ Severity convention:
74
+ - **`error`** — blocks the user / unrecoverable / 5xx (default for uncaught and most reasons)
75
+ - **`warning`** — recoverable, user-facing flow signal (`validation_error`, `payment_declined`, `unit_list_empty`, `cache_stale`, etc.)
76
+ - **`info`** — informational signal (`session_expired`)
77
+
78
+ `getDefaultSeverity(reason)` does the mapping. `logError()` uses it automatically; callers can override via `LogErrorOptions.severity`. Wire `getDefaultSeverity` into integration points (e.g. RTK Query error middleware) so individual call sites don't think about it.
79
+
80
+ ### 4. `Error.message` must not contain user input
81
+ `logError()` reads `err.message` and persists it. **Throw errors with constant messages**:
82
+
83
+ ```ts
84
+ // ✅ good — fixed message, classification via `reason`
85
+ if (!isValidCvv(cvv)) {
86
+ logError(new Error("payment validation failed"), {
87
+ reason: "invalid_payment_option",
88
+ context: { step: "payment", validationFieldNames: ["cvv"] },
89
+ });
90
+ }
91
+
92
+ // ❌ bad — user input lands in the persisted message
93
+ throw new Error(`Card ${cardNumber} has invalid CVV ${cvv}`);
94
+ ```
95
+
96
+ Code review is the only guard for this surface; the helper has no automated way to detect interpolated PII inside an `Error.message` string.
97
+
98
+ ### 5. URL is path-only
99
+ `logError()` strips the query string and fragment from `window.location.href` before sending. Only `origin + pathname` is logged. This protects against tokens or emails appearing in URL params (e.g. `?token=…`, `?email=…`).
100
+
101
+ ## Patterns
102
+
103
+ ### Soft-assert empty / unexpected results
104
+ For "should-have-data-but-doesn't" cases, log a `warning` at the boundary where bad data hits the UI:
105
+
106
+ ```ts
107
+ if (facility.isActive && units.length === 0) {
108
+ logError(new Error("Unit list empty for active facility"), {
109
+ reason: "unit_list_empty",
110
+ context: {
111
+ seFacilityId,
112
+ apiMethod: "getUnits",
113
+ expectedCount: 1,
114
+ actualCount: 0,
115
+ },
116
+ });
117
+ }
118
+ ```
119
+
120
+ ### CMS / scraper / build hook errors
121
+ On the server side, wrap the suspect spot in try/catch and re-throw after logging:
122
+
123
+ ```ts
124
+ try {
125
+ await runScraper(batchId);
126
+ } catch (err) {
127
+ logError(err, {
128
+ source: "payload",
129
+ reason: "scraper_failure",
130
+ context: { scraperBatchId: batchId, apiMethod: "runScraper" },
131
+ });
132
+ throw err;
133
+ }
134
+ ```
135
+
136
+ The helper's server transport writes via the api-user key. Don't wrap collection-internal hooks with `logError` — they'd loop on themselves.
137
+
138
+ ### RTK Query middleware (apps using @glidevvr/se-components)
139
+ Map `errorTag` → `reason` + default severity:
140
+
141
+ ```ts
142
+ // inside rtkQueryErrorMiddleware
143
+ logError(err, {
144
+ reason: payload.errorTag, // 1:1 with se-components
145
+ context: {
146
+ apiMethod: action.meta.arg.endpointName,
147
+ httpStatus: err.status,
148
+ validationFieldNames: Object.keys(payload.validationErrors ?? {}),
149
+ },
150
+ })
151
+ ```
152
+
153
+ ## Server-side defense
154
+ The CMS-side `IssueEvents.beforeValidate` hook (`sanitizeIssueEventContext`) is the matching runtime guard for the `LogContext` allowlist. Direct REST POSTs that bypass the helper still get unknown keys stripped before persistence — and per-row keys inside `charges` / `recurringFees` are filtered too (memo/note dropped).
155
+
156
+ ---
157
+
158
+ ## Local Development
159
+
160
+ ```bash
161
+ npm install
162
+ npm test # vitest
163
+ npm run build # tsup → dist/{esm,cjs,d.ts} for all three subpaths
164
+ ```
165
+
166
+ ### Prerequisites
167
+
168
+ - Node.js 20.x or later
169
+ - npm 10.x or later
170
+
171
+ The published package's wire shape (entry/exports/peerDeps) is defined in `package.json`. The CMS server defense lives in `gli-payload-multitenant/src/collections/IssueEvents/hooks/sanitizeContext.ts` — keep `ALLOWED_CONTEXT_KEYS` there in sync with the `LogContext` interface in `src/types.ts`.
172
+
173
+ ---
174
+
175
+ ## Branching Strategy
176
+
177
+ This package follows a **Dirty Trunk** repo structure to enable fast, continuous integration.
178
+
179
+ ### Trunk (Main Branch)
180
+
181
+ - **`master`** is the trunk where all development merges.
182
+ - Trunk may contain work-in-progress code and is not guaranteed to be in a release-ready state at all times.
183
+
184
+ ### Feature Branches
185
+
186
+ - Use short-lived branches for features or fixes:
187
+ - Naming convention: `feature/<feature-name>` or `bugfix/<issue-description>`.
188
+ - Merge into `master` frequently to avoid long-lived branches and reduce conflicts.
189
+ - Use squash merges to keep the history clean.
190
+
191
+ ### Release Branches
192
+
193
+ - Create `release/x.y.z` branches from `master` when ready to stabilize for a release.
194
+ - Only bug fixes and release preparation changes go into the release branch.
195
+ - After release, merge back into `master` and tag with the version.
196
+
197
+ ### Hotfixes
198
+
199
+ - Create `hotfix/x.y.z` branches from the latest tagged release.
200
+ - Merge back into `master` and the active release branch.
201
+
202
+ ---
203
+
204
+ ## Managing CI/CD
205
+
206
+ This package uses Bitbucket Pipelines for testing and publishing:
207
+
208
+ - **Branch Tests**: Automated build and test run on all branches (`feature`, `bugfix`, `release`, `hotfix`).
209
+ - **Publishing**: npm releases are triggered manually via the `npm-publish` custom pipeline in Bitbucket.
210
+ - **Versioning**: Uses semantic versioning:
211
+ - Increment minor versions on releases.
212
+ - Increment patch versions for hotfixes.
213
+
214
+ ### Publishing a new version
215
+
216
+ 1. Update the version in `package.json` (`npm version minor` / `npm version patch`) on the release or hotfix branch.
217
+ 2. Push the branch and tag.
218
+ 3. In Bitbucket, run the `npm-publish` custom pipeline from the release/hotfix branch. Requires `NPM_TOKEN` to be set as a Repository Variable.
219
+
220
+ ---
221
+
222
+ ## Consumers
223
+
224
+ - **gli-payload-multitenant**: hosts the `/api/log-error` endpoint via the `payload-endpoint` subpath. Server-only.
225
+ - **storage-theme-payload**: Tier 1 + 2 retrofit (Phase 3 of WEB-1413). Wraps the app root with `<ErrorBoundary>` and calls `initLogger` + `installGlobalHandlers`; explicit `logError(...)` at high-value catch paths (SE rate-limiter, tenantInfo, form submission).
226
+ - **unit-table**: Tier 1 + 2 retrofit (Phase 5 of WEB-1413). Augments the existing widget ErrorBoundary; does NOT install global handlers (the host page owns those).
227
+ - Future consumers: `reservation-app`, `rental-app`, `npm-golocal-cloud-wrapper`.
228
+
229
+ See the WEB-1413 design doc in `gli-payload-multitenant/docs/plans/2026-05-05-web-1413-error-logger-design.md` for the full rollout plan.
@@ -0,0 +1,282 @@
1
+ // src/context.ts
2
+ var current = {};
3
+ var isBrowser = () => typeof window !== "undefined";
4
+ function setLogContext(patch) {
5
+ if (!isBrowser()) return;
6
+ current = { ...current, ...patch };
7
+ }
8
+ function getLogContext() {
9
+ if (!isBrowser()) return {};
10
+ return { ...current };
11
+ }
12
+ function withLogContext(scope, fn) {
13
+ if (!isBrowser()) return fn();
14
+ const prior = current;
15
+ current = { ...current, ...scope };
16
+ try {
17
+ return fn();
18
+ } finally {
19
+ current = prior;
20
+ }
21
+ }
22
+
23
+ // src/initLogger.ts
24
+ var config = null;
25
+ function initLogger(cfg) {
26
+ if (!cfg?.endpoint) throw new Error("initLogger: endpoint is required");
27
+ if (!cfg?.repo) throw new Error("initLogger: repo is required");
28
+ config = { ...cfg };
29
+ }
30
+ function _getConfig() {
31
+ return config;
32
+ }
33
+
34
+ // src/severity.ts
35
+ function getDefaultSeverity(reason) {
36
+ if (!reason) return "error";
37
+ switch (reason) {
38
+ // user-recoverable / expected-flow signals
39
+ case "validation_error":
40
+ case "payment_declined":
41
+ case "invalid_payment_option":
42
+ case "unit_list_empty":
43
+ case "expected_data_missing":
44
+ case "cache_stale":
45
+ case "promotion_not_found":
46
+ return "warning";
47
+ // informational
48
+ case "session_expired":
49
+ return "info";
50
+ // everything else (network failures, unauthorized, not-founds we couldn't
51
+ // recover from, hook/scraper/build failures, unhandled exceptions) — the
52
+ // user is blocked, treat as error
53
+ default:
54
+ return "error";
55
+ }
56
+ }
57
+
58
+ // src/fingerprint.ts
59
+ function fingerprint(source, message, stack, reason) {
60
+ const topFrame = stack.split("\n", 2)[0] ?? "";
61
+ const reasonPart = reason ?? "";
62
+ const input = `${source}|${message}|${topFrame}|${reasonPart}`;
63
+ let hash = 5381;
64
+ for (let i = 0; i < input.length; i++) {
65
+ hash = (hash << 5) + hash + input.charCodeAt(i);
66
+ hash |= 0;
67
+ }
68
+ return (hash >>> 0).toString(16);
69
+ }
70
+
71
+ // src/throttle.ts
72
+ function createThrottle(opts) {
73
+ const lastEmittedAt = /* @__PURE__ */ new Map();
74
+ return {
75
+ shouldEmit(fingerprint2) {
76
+ const now = Date.now();
77
+ const last = lastEmittedAt.get(fingerprint2);
78
+ if (last !== void 0 && now - last < opts.windowMs) return false;
79
+ lastEmittedAt.set(fingerprint2, now);
80
+ return true;
81
+ }
82
+ };
83
+ }
84
+
85
+ // src/batch.ts
86
+ function createBatcher(opts) {
87
+ let queue = [];
88
+ let timer = null;
89
+ async function flush() {
90
+ if (timer) {
91
+ clearTimeout(timer);
92
+ timer = null;
93
+ }
94
+ if (queue.length === 0) return;
95
+ const drained = queue;
96
+ queue = [];
97
+ await opts.onFlush(drained);
98
+ }
99
+ return {
100
+ enqueue(item) {
101
+ queue.push(item);
102
+ if (queue.length >= opts.maxQueueLength) {
103
+ void flush();
104
+ return;
105
+ }
106
+ if (timer === null) {
107
+ timer = setTimeout(() => {
108
+ void flush();
109
+ }, opts.flushMs);
110
+ }
111
+ },
112
+ flushOnce: flush
113
+ };
114
+ }
115
+
116
+ // src/circuitBreaker.ts
117
+ function createCircuitBreaker(opts) {
118
+ let failures = [];
119
+ let openUntil = 0;
120
+ return {
121
+ canTransmit() {
122
+ return Date.now() >= openUntil;
123
+ },
124
+ recordFailure() {
125
+ const now = Date.now();
126
+ failures = failures.filter((t) => now - t < opts.failureWindowMs);
127
+ failures.push(now);
128
+ if (failures.length >= opts.failureThreshold) {
129
+ openUntil = now + opts.openMs;
130
+ failures = [];
131
+ }
132
+ },
133
+ recordSuccess() {
134
+ failures = [];
135
+ }
136
+ };
137
+ }
138
+
139
+ // src/recaptcha.ts
140
+ async function acquireRecaptchaToken(siteKey, action) {
141
+ const g = typeof globalThis !== "undefined" ? globalThis.grecaptcha : void 0;
142
+ if (!g?.enterprise) return null;
143
+ try {
144
+ await new Promise((resolve) => g.enterprise.ready(resolve));
145
+ return await g.enterprise.execute(siteKey, { action });
146
+ } catch {
147
+ return null;
148
+ }
149
+ }
150
+
151
+ // src/transport.ts
152
+ function isBrowser2() {
153
+ return typeof window !== "undefined";
154
+ }
155
+ async function postFromBrowser(endpoint, request) {
156
+ try {
157
+ const res = await fetch(endpoint, {
158
+ method: "POST",
159
+ headers: { "Content-Type": "application/json" },
160
+ body: JSON.stringify(request),
161
+ keepalive: true
162
+ });
163
+ return { ok: res.ok, status: res.status };
164
+ } catch {
165
+ return { ok: false, status: 0 };
166
+ }
167
+ }
168
+ async function postFromServer(payloadBaseUrl, apiKey, events) {
169
+ let lastStatus = 0;
170
+ for (const event of events) {
171
+ try {
172
+ const res = await fetch(`${payloadBaseUrl}/cms/api/issue-events`, {
173
+ method: "POST",
174
+ headers: {
175
+ "Content-Type": "application/json",
176
+ Authorization: `users API-Key ${apiKey}`
177
+ },
178
+ body: JSON.stringify(event)
179
+ });
180
+ lastStatus = res.status;
181
+ if (!res.ok) return { ok: false, status: res.status };
182
+ } catch {
183
+ return { ok: false, status: 0 };
184
+ }
185
+ }
186
+ return { ok: true, status: lastStatus };
187
+ }
188
+
189
+ // src/logError.ts
190
+ var throttle = createThrottle({ windowMs: 5e3 });
191
+ var breaker = createCircuitBreaker({
192
+ failureThreshold: 3,
193
+ failureWindowMs: 3e4,
194
+ openMs: 6e4
195
+ });
196
+ var batcher = null;
197
+ function getBatcher() {
198
+ if (batcher) return batcher;
199
+ batcher = createBatcher({
200
+ flushMs: 3e3,
201
+ maxQueueLength: 10,
202
+ onFlush: async (events) => {
203
+ const cfg = _getConfig();
204
+ if (!cfg) return;
205
+ if (!breaker.canTransmit()) return;
206
+ const rcToken = cfg.recaptchaSiteKey ? await acquireRecaptchaToken(cfg.recaptchaSiteKey, cfg.recaptchaAction ?? "log_error") : null;
207
+ const result = await postFromBrowser(cfg.endpoint, { events, rcToken });
208
+ if (result.ok) breaker.recordSuccess();
209
+ else if (result.status >= 500 || result.status === 0) breaker.recordFailure();
210
+ }
211
+ });
212
+ return batcher;
213
+ }
214
+ function safePageUrl() {
215
+ if (!isBrowser2()) return "";
216
+ try {
217
+ const parsed = new URL(window.location.href);
218
+ return parsed.origin + parsed.pathname;
219
+ } catch {
220
+ return "";
221
+ }
222
+ }
223
+ function buildEvent(err, opts) {
224
+ const cfg = _getConfig();
225
+ if (!cfg) return null;
226
+ const message = err instanceof Error ? err.message : String(err);
227
+ const stack = err instanceof Error && err.stack ? err.stack : "";
228
+ const ambient = getLogContext();
229
+ const source = opts?.source ?? ambient.source ?? cfg.defaultSource ?? "theme";
230
+ const reason = opts?.reason;
231
+ const severity = opts?.severity ?? getDefaultSeverity(reason);
232
+ const fp = fingerprint(source, message, stack, reason);
233
+ const userAgent = isBrowser2() && typeof navigator !== "undefined" ? navigator.userAgent : "";
234
+ return {
235
+ source,
236
+ repo: cfg.repo,
237
+ severity,
238
+ fingerprint: fp,
239
+ message: message.slice(0, 500),
240
+ stack: stack.slice(0, 1e4),
241
+ context: { ...ambient, ...opts?.context ?? {} },
242
+ url: safePageUrl(),
243
+ userAgent,
244
+ occurredAt: (/* @__PURE__ */ new Date()).toISOString(),
245
+ release: cfg.release,
246
+ reason
247
+ };
248
+ }
249
+ function logError(err, opts) {
250
+ try {
251
+ const event = buildEvent(err, opts);
252
+ if (!event) return;
253
+ if (!throttle.shouldEmit(event.fingerprint)) return;
254
+ if (isBrowser2()) {
255
+ getBatcher().enqueue(event);
256
+ } else {
257
+ const cfg = _getConfig();
258
+ if (!cfg) return;
259
+ const apiKey = process.env.TENANT_API_KEY ?? process.env.PAYLOAD_PUBLIC_API_KEY;
260
+ const baseUrl = process.env.PAYLOAD_PUBLIC_URL;
261
+ if (!apiKey || !baseUrl) return;
262
+ void postFromServer(baseUrl, apiKey, [event]).catch(() => {
263
+ });
264
+ }
265
+ } catch {
266
+ }
267
+ }
268
+ function flushPending() {
269
+ if (!batcher) return Promise.resolve();
270
+ return batcher.flushOnce();
271
+ }
272
+
273
+ export {
274
+ setLogContext,
275
+ getLogContext,
276
+ withLogContext,
277
+ initLogger,
278
+ getDefaultSeverity,
279
+ logError,
280
+ flushPending
281
+ };
282
+ //# sourceMappingURL=chunk-EKIOMBUO.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/context.ts","../src/initLogger.ts","../src/severity.ts","../src/fingerprint.ts","../src/throttle.ts","../src/batch.ts","../src/circuitBreaker.ts","../src/recaptcha.ts","../src/transport.ts","../src/logError.ts"],"sourcesContent":["/**\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","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","/**\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 { 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"],"mappings":"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","names":["fingerprint","isBrowser","isBrowser"]}
@@ -0,0 +1,99 @@
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';
3
+
4
+ /**
5
+ * @fileoverview Ambient log context store — browser-only.
6
+ *
7
+ * On the browser the module-level `current` object is a per-tab singleton, the
8
+ * scope we want. On Node (SSR), the same module is shared across every
9
+ * incoming request running in the process, so writing to `current` in one
10
+ * request would leak that context into other requests' error events. To stay
11
+ * safe, all three functions no-op (or return `{}`) when there is no `window`.
12
+ *
13
+ * **Server callers must pass context explicitly** via the `opts.context`
14
+ * parameter on `logError`, which is merged into the event regardless of the
15
+ * ambient state.
16
+ */
17
+
18
+ /**
19
+ * Add or overwrite keys in the ambient log context. Existing keys you don't
20
+ * pass are preserved; keys you do pass replace existing values (no deep merge).
21
+ *
22
+ * Call at meaningful UX boundaries (page mount, step change, modal open) so
23
+ * later errors automatically pick up the relevant context.
24
+ *
25
+ * No-op on the server — see file-level note above.
26
+ */
27
+ declare function setLogContext(patch: LogContext): void;
28
+ /**
29
+ * Return a copy of the current ambient log context. Used by `logError` to
30
+ * attach context to outgoing events.
31
+ *
32
+ * Returns `{}` on the server — see file-level note above.
33
+ */
34
+ declare function getLogContext(): LogContext;
35
+ /**
36
+ * Run `fn` with extra context keys layered on top of the ambient context, then
37
+ * put the original context back — even if `fn` throws.
38
+ *
39
+ * Use this when the extra keys should only apply to one synchronous operation
40
+ * and not leak into anything that runs afterwards.
41
+ *
42
+ * On the server `fn` runs without any ambient mutation — see file-level note above.
43
+ */
44
+ declare function withLogContext<T>(scope: LogContext, fn: () => T): T;
45
+
46
+ /**
47
+ * Set up the logger. Call once at app startup.
48
+ *
49
+ * Required fields: `endpoint` (where the browser sends events) and `repo`
50
+ * (stamped on every event). Optional: `release`, `recaptchaSiteKey`,
51
+ * `recaptchaAction`, `defaultSource`. Throws if a required field is missing
52
+ * so misconfiguration is loud at startup instead of silently dropping events.
53
+ */
54
+ declare function initLogger(cfg: InitLoggerConfig): void;
55
+
56
+ /**
57
+ * Capture an error and route it to the CMS.
58
+ *
59
+ * Browser path: throttle → batch → cross-origin POST (with reCAPTCHA token).
60
+ * Server path: throttle → direct POST to Payload's REST API per event.
61
+ *
62
+ * Internal failures never throw — silently degraded so the logger can't break callers.
63
+ *
64
+ * `err` covers the realistic set of thrown values (Error, primitives). For the
65
+ * rare case of throwing a plain object literal, callers should wrap in an Error
66
+ * before passing — e.g. `logError(new Error(JSON.stringify(thing)))`.
67
+ */
68
+ declare function logError(err: Error | string | number | boolean | null | undefined, opts?: LogErrorOptions): void;
69
+
70
+ declare global {
71
+ var __errorLoggerHandlersInstalled__: boolean | undefined;
72
+ }
73
+ /**
74
+ * Install browser-global error capture (window.onerror + unhandledrejection)
75
+ * plus a `pagehide` listener that flushes any queued events before unload.
76
+ *
77
+ * Idempotent: a `globalThis` sentinel ensures repeated calls do not re-install.
78
+ * Only the host page should call this — widgets embedded in the host inherit
79
+ * coverage automatically because errors bubble to page-level listeners.
80
+ */
81
+ declare function installGlobalHandlers(): void;
82
+
83
+ /**
84
+ * Map a `Reason` to its default `Severity` when the caller hasn't passed
85
+ * one explicitly.
86
+ *
87
+ * Convention:
88
+ * - `error` — blocks the user / unrecoverable / 5xx upstream
89
+ * - `warning` — recoverable, user-facing flow signal (validation,
90
+ * declined card, soft-assert empty result)
91
+ * - `info` — informational signal (`session_expired`)
92
+ *
93
+ * Wire this into integration points (e.g. the RTK Query error middleware)
94
+ * so individual call sites don't have to think about severity unless they
95
+ * want to override the default.
96
+ */
97
+ declare function getDefaultSeverity(reason?: Reason): Severity;
98
+
99
+ export { InitLoggerConfig, LogContext, LogErrorOptions, Reason, Severity, getDefaultSeverity, getLogContext, initLogger, installGlobalHandlers, logError, setLogContext, withLogContext };