@glidevvr/storage-payload-error-logger-pkg
v0.4.0
Published
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.
Downloads
554
Readme
@glidevvr/storage-payload-error-logger-pkg
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.
Subpaths
@glidevvr/storage-payload-error-logger-pkg— core browser/Node helper (initLogger, logError, setLogContext, installGlobalHandlers, fingerprint)@glidevvr/storage-payload-error-logger-pkg/react—<ErrorBoundary>@glidevvr/storage-payload-error-logger-pkg/payload-endpoint— Payload v3Endpointhandler (the HTTP receiver for browser-sidelogErrorcalls)
Init
initLogger({ endpoint, repo, release, recaptchaSiteKey })
PII contract
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.
This contract is enforced on five surfaces:
1. LogContext is a closed allowlist
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:
| Category | Examples |
| --- | --- |
| Identity | source, sessionId, componentStack |
| Organization (storage org) | organizationId (Payload Mongo ID), organizationName, seCompanyId (SE numeric ID) |
| Renter (anonymous, never name/email/phone) | seTenantId, vendorTenantId, vendorType |
| Facility | seFacilityId, vendorFacilityId, siteLocationCode |
| Unit | seUnitId, vendorUnitId, unitCategorySlug, unitTier, ratePlan |
| Reservation (pre-move-in) | seReservationId, vendorReservationId, reservationType |
| Rental (post-move-in) | seRentalId, vendorRentalId, seTransactionId |
| Ledger / billing | seLedgerId, seReceiptId, chargeId |
| Charges + recurring fees | charges[], recurringFees[] (sub-keys: id, name, amount, cycle only — memo/note stripped) |
| Selections (codes, never typed values) | promotionId, promotionSlug, insurancePlanId, paymentMethodType, autopayEnabled, billingCycle, moveInDate, quotedRate |
| Flow context | flow (closed: reservation / rental / cancellation / payment-update / onboarding), step (paired with flow), slug, apiMethod, apiEndpoint, httpStatus, cacheKey, errorTag, validationFieldNames[], expectedCount, actualCount |
| Payload-only | collectionSlug, documentId, hookName, userRole, scraperBatchId, buildId |
Naming convention:
se*prefix → identifier from the SE API (matches SE's wire term)vendor*prefix → identifier from the underlying property-management system (SiteLink / StorEdge / DoorSwap)- everything else → user-facing terms shared with the admin UI (e.g.
organizationIdmatches the "Organization" label in CMS)
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.
2. reason is a closed enum — the WHY, never user input
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.
15 values mirror the errorTag enum in @glidevvr/se-components; 9 internal values cover failure modes outside the SE API:
// SE errorTags (mirror se-components)
"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"
// Internal additions
| "unit_list_empty" | "expected_data_missing" | "cache_stale"
| "build_failure" | "hook_error" | "scraper_failure"
| "data_integrity" | "unhandled_exception" | "network_timeout"Pass via LogErrorOptions.reason:
logError(err, { reason: "invalid_cvv", context: { step: "payment" } })
logError(err, { reason: "unit_list_empty", context: { seFacilityId, expectedCount: 1, actualCount: 0 } })reason is part of the event's fingerprint, so the same code path failing for different reasons becomes distinct Issues.
3. severity defaults from reason
Severity convention:
error— blocks the user / unrecoverable / 5xx (default for uncaught and most reasons)warning— recoverable, user-facing flow signal (validation_error,payment_declined,unit_list_empty,cache_stale, etc.)info— informational signal (session_expired)
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.
4. Error.message must not contain user input
logError() reads err.message and persists it. Throw errors with constant messages:
// ✅ good — fixed message, classification via `reason`
if (!isValidCvv(cvv)) {
logError(new Error("payment validation failed"), {
reason: "invalid_payment_option",
context: { step: "payment", validationFieldNames: ["cvv"] },
});
}
// ❌ bad — user input lands in the persisted message
throw new Error(`Card ${cardNumber} has invalid CVV ${cvv}`);Code review is the only guard for this surface; the helper has no automated way to detect interpolated PII inside an Error.message string.
5. URL is path-only
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=…).
Patterns
Soft-assert empty / unexpected results
For "should-have-data-but-doesn't" cases, log a warning at the boundary where bad data hits the UI:
if (facility.isActive && units.length === 0) {
logError(new Error("Unit list empty for active facility"), {
reason: "unit_list_empty",
context: {
seFacilityId,
apiMethod: "getUnits",
expectedCount: 1,
actualCount: 0,
},
});
}CMS / scraper / build hook errors
On the server side, wrap the suspect spot in try/catch and re-throw after logging:
try {
await runScraper(batchId);
} catch (err) {
logError(err, {
source: "payload",
reason: "scraper_failure",
context: { scraperBatchId: batchId, apiMethod: "runScraper" },
});
throw err;
}The helper's server transport writes via the api-user key. Don't wrap collection-internal hooks with logError — they'd loop on themselves.
RTK Query middleware (apps using @glidevvr/se-components)
Map errorTag → reason + default severity:
// inside rtkQueryErrorMiddleware
logError(err, {
reason: payload.errorTag, // 1:1 with se-components
context: {
apiMethod: action.meta.arg.endpointName,
httpStatus: err.status,
validationFieldNames: Object.keys(payload.validationErrors ?? {}),
},
})Server-side defense
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).
Local Development
npm install
npm test # vitest
npm run build # tsup → dist/{esm,cjs,d.ts} for all three subpathsPrerequisites
- Node.js 20.x or later
- npm 10.x or later
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.
Branching Strategy
This package follows a Dirty Trunk repo structure to enable fast, continuous integration.
Trunk (Main Branch)
masteris the trunk where all development merges.- Trunk may contain work-in-progress code and is not guaranteed to be in a release-ready state at all times.
Feature Branches
- Use short-lived branches for features or fixes:
- Naming convention:
feature/<feature-name>orbugfix/<issue-description>.
- Naming convention:
- Merge into
masterfrequently to avoid long-lived branches and reduce conflicts. - Use squash merges to keep the history clean.
Release Branches
- Create
release/x.y.zbranches frommasterwhen ready to stabilize for a release. - Only bug fixes and release preparation changes go into the release branch.
- After release, merge back into
masterand tag with the version.
Hotfixes
- Create
hotfix/x.y.zbranches from the latest tagged release. - Merge back into
masterand the active release branch.
Managing CI/CD
This package uses Bitbucket Pipelines for testing and publishing:
- Branch Tests: Automated build and test run on all branches (
feature,bugfix,release,hotfix). - Publishing: npm releases are triggered manually via the
npm-publishcustom pipeline in Bitbucket. - Versioning: Uses semantic versioning:
- Increment minor versions on releases.
- Increment patch versions for hotfixes.
Publishing a new version
- Update the version in
package.json(npm version minor/npm version patch) on the release or hotfix branch. - Push the branch and tag.
- In Bitbucket, run the
npm-publishcustom pipeline from the release/hotfix branch. RequiresNPM_TOKENto be set as a Repository Variable.
Consumers
- gli-payload-multitenant: hosts the
/api/log-errorendpoint via thepayload-endpointsubpath. Server-only. - storage-theme-payload: Tier 1 + 2 retrofit (Phase 3 of WEB-1413). Wraps the app root with
<ErrorBoundary>and callsinitLogger+installGlobalHandlers; explicitlogError(...)at high-value catch paths (SE rate-limiter, tenantInfo, form submission). - 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).
- Future consumers:
reservation-app,rental-app,npm-golocal-cloud-wrapper.
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.
