Sentry
Sentry is the error-tracking and performance-monitoring backend for the frontend SPA. It is deliberately production-only and silently no-ops in development and in builds without a DSN.
Where it is configured
File: src/lib/observability.ts.
The sole Sentry.init call in the codebase lives at
src/lib/observability.ts:48-63. initObservability() is called once from
src/main.tsx:6 at app startup and is guarded so it is idempotent.
Sentry.init({
dsn,
environment: getEnv().MODE ?? 'production',
release: getEnv().VITE_APP_VERSION,
sendDefaultPii: false,
integrations: [
Sentry.browserTracingIntegration(),
Sentry.replayIntegration({ maskAllText: true, blockAllMedia: true }),
],
tracesSampleRate: 0.1,
replaysSessionSampleRate: 0,
replaysOnErrorSampleRate: 1.0,
});
Activation rules (src/lib/observability.ts:43-46)
import.meta.env.PRODmust betrue(Vite prod build).VITE_SENTRY_DSNmust be a non-empty string.- Both conditions must hold — otherwise every capture falls through to a
console.*log (dev) or is silently dropped (prod without DSN).
This is why a local npm run dev session never shows Sentry traffic and
why preview builds without a DSN set in Cloudflare stay quiet.
What we capture
| Signal | Configured by | Notes |
|---|---|---|
| Exceptions | Sentry.captureException |
Raw errors and the context bag passed as extra. sendDefaultPii: false — user identifiers are not auto-attached. |
| Performance traces | browserTracingIntegration |
tracesSampleRate: 0.1 — 10% of transactions are traced. |
| Session replays | replayIntegration |
replaysSessionSampleRate: 0 (no random replays), replaysOnErrorSampleRate: 1.0 (every errored session is replayed). Text is masked, media is blocked. |
| Web Vitals | reportWebVital |
CLS / INP / LCP / FCP / TTFB are attached as attributes on the active span (observability.ts:115-124). onFID was removed in web-vitals v4 — onINP replaces it. |
| Release tag | release: VITE_APP_VERSION |
Set this in CI from the semantic-release tag so issues group by deploy. |
Replay masking
maskAllText: true + blockAllMedia: true — Sentry replays show the
DOM structure and interaction timing but not PII text or uploaded
images. If you change these flags, check whether the captured data
crosses a regulatory line before merging.
DSN — environment variable
| Variable | Required? | Exposure |
|---|---|---|
VITE_SENTRY_DSN |
Prod only | Baked into the JS bundle at build time. Safe to expose — DSNs are public identifiers. |
VITE_APP_VERSION |
Optional | Used as the Sentry release. CI should set this to the semver tag for the build. |
Set the DSN in the Cloudflare Pages app project → Environment variables →
Production environment. Leave it blank in .env.local and in the
Preview environment so development and preview builds stay quiet.
Rotating the DSN
A DSN is not a secret in the strict sense — it cannot write to other projects — but rotating it requires redeploying every environment that embeds the old value. Coordinate with a release.
Adding breadcrumbs, tags, and user context
observability.ts exports the thin wrappers the app is expected to use.
Prefer these over reaching for the Sentry SDK directly — they make the
dev-mode fallback work and keep Sentry pluggable.
Capture an exception with extra context
import { captureException } from "@/lib/observability";
try {
await riskyThing();
} catch (err) {
captureException(err, { bookingId, stepName: "finance-approve" });
}
In prod the context goes to Sentry as extra. In dev it prints to the
console.
Capture a message
import { captureMessage } from "@/lib/observability";
captureMessage("Payment webhook arrived with missing bookingId", "warning", {
razorpayPaymentId,
});
Breadcrumbs and custom tags
There is no project-local wrapper for breadcrumbs or tags yet — if you need
them, import from @sentry/react directly and guard on getDsn():
import * as Sentry from "@sentry/react";
// Breadcrumb (cheap, bulk dropped when no error fires)
Sentry.addBreadcrumb({
category: "booking",
message: "user clicked finance-approve",
level: "info",
data: { bookingId },
});
// Tag (appears as a first-class facet in Sentry issue list)
Sentry.setTag("module", "finance");
// User (only set fields you've explicitly consented to; sendDefaultPii is false)
Sentry.setUser({ id: userId });
ErrorBoundary
src/components/common/ErrorBoundary.tsx wraps the app shell and forwards
caught render errors through captureException. If you add a route-level
boundary, route its onError through the same helper rather than calling
Sentry.captureException directly.
Testing
src/lib/observability.test.ts exercises the environment-gating logic
against a mocked import.meta.env. Run with the normal Vitest suite:
Live captures are verified by toggling a real DSN in a preview build and triggering a deliberate error — there is no CI smoke test that hits Sentry.
Operational notes
- Quota. Replays are the costly bucket.
replaysOnErrorSampleRate: 1.0means any error session is recorded — watch the Sentry quota if error volume spikes after a release. - Source maps.
- PII.
sendDefaultPii: falseand the replay masking are the safety net when a component accidentally logs user input. Do not weaken either without a DPIA review.