Skip to content

Observability

GiftWrapt ships with no third-party observability by default. Operators who want error reporting or a metrics endpoint can opt in to either family independently, pointed at whichever backend they operate.

Two families, two gates each:

  • Env owns the host / secret. SENTRY_DSN for error reporting, METRICS_TOKEN for the metrics endpoint. Set per deploy, not editable from the admin UI.
  • Admin owns the runtime kill-switch. enableSentry and enableMetrics toggles in Admin > Observability, default off, hot-reloadable per request.

Both gates must be true to emit. If you flip the env without flipping the toggle (or vice versa), nothing leaves the server.

Client-side analytics (Plausible, Umami, PostHog, Vercel Analytics) is deliberately not integrated. This page covers only operator-side error reports and metrics. See Privacy and Telemetry for the broader stance.

Server-side errors (caught at the Nitro error hook and the TanStack server-fn loggingMiddleware) and browser-side errors (caught at the React ErrorBoundary plus Sentry’s React SDK) are forwarded to the backend you point SENTRY_DSN at. Works with sentry.io, self-hosted Glitchtip, or any compatible HTTP ingest.

  • SENTRY_DSN - the project DSN from your Sentry / Glitchtip backend.
  • SENTRY_ENVIRONMENT - defaults to NODE_ENV.
  • SENTRY_RELEASE - typically the build SHA. Set in CI for source-map alignment.
  • SENTRY_TRACES_SAMPLE_RATE - 0 to 1, defaults to 0 (errors only, no tracing spans).
  1. Set SENTRY_DSN (and any optional env vars above) in your deployment env.
  2. Restart the app so CSP picks up the DSN origin for connect-src.
  3. In Admin > Observability, flip Enable Sentry Error Reporting on.

Flip the admin toggle off. Server-side capture stops on the next request. Browser SDKs already loaded in open tabs keep running until those tabs reload or the deploy restarts.

  • Stack traces, error messages, the matched request route, the request URL with query string stripped.
  • Item / list / claim / user-id fields that happen to be in scope at the throw site - deliberate debugging signal so the operator can act on real bugs.
  • Sentry’s sendDefaultPii: false is set, so the SDK does not auto-attach IPs or default user info.

What is stripped (in src/lib/observability/scrubber.ts):

  • Query strings on the request URL.
  • Cookie + Authorization headers (and any header key matching password / token / apiKey / secret).
  • Recursive strip of those same keys inside event extras, contexts, and breadcrumb data.

Recipients of gift lists never see Sentry events; the SDK runs server-side and in the operator’s own browser sessions, not in a recipient’s UI surface. Spoiler protection in the app is enforced at the rendering layer regardless of what does or doesn’t end up in the operator’s error tracker.

Counters and histograms accumulate in memory on every deployment regardless of toggle state; the admin toggle and bearer token gate exposure, not accumulation. When you turn the endpoint on, the counters reflect activity from the moment the process started, not the moment you flipped the toggle.

  • METRICS_TOKEN - bearer token, minimum 16 chars. Generate one with:

    Terminal window
    openssl rand -hex 32
  1. Set METRICS_TOKEN in your deployment env.
  2. In Admin > Observability, flip Enable Prometheus Metrics Endpoint on.
  3. Configure your Prometheus scraper to hit /api/metrics with Authorization: Bearer $METRICS_TOKEN.
  • Returns 404 (not 401) when disabled or the token is wrong, to avoid endpoint-existence disclosure to internet scanners.
  • Returns Prometheus exposition format (text/plain; version=0.0.4) on success.
  • All labels are enum-bounded. Routes with dynamic segments (/lists/$listId, etc.) are normalized to /lists/<id> so cardinality stays bounded.

Synced from the core registry at build time. Edit core/src/lib/observability/metrics.ts and re-run pnpm metrics:catalog in core/ to refresh.

MetricTypeLabelsDescription
claims_created_totalcounter(none)Gift claims created (gifter assigned to an item).
claims_deleted_totalcounter(none)Gift claims deleted (unclaim).
cron_run_duration_mshistogramjob, outcomeCron run duration in milliseconds, by job and resolved outcome.
cron_run_outcomes_totalcounterjob, outcomeCron run outcomes, by job. Outcomes match cron_runs.status (success, error, skipped).
http_request_duration_mshistogrammethod, route, status_classHTTP request duration in milliseconds, by matched route template, method, and status class.
intelligence_runs_completed_totalcounteroutcomeIntelligence (recommendations) cron runs completed, by outcome.
items_archived_totalcounter(none)Items archived (revealed to recipient).
items_created_totalcounter(none)Items created on a list.
items_pending_deletion_totalcounter(none)Items flipped to pending-deletion via recipient delete on a claimed item.
lists_created_totalcountertypeLists created, by type.
orphan_claims_cleaned_up_totalcounter(none)Orphan claims hard-deleted by the daily cleanup pass.
reveals_triggered_totalcountertriggerReveals (item archive) triggered, by trigger source.

A deliberate skip. Funnel and retention analytics on a family-scale wish-list app aren’t worth the CSP / privacy / cookie-banner cost. Re-evaluate if a future feature concretely needs it.