Skip to content

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.PROD must be true (Vite prod build).
  • VITE_SENTRY_DSN must 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,
});

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:

npm test -- observability

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.0 means any error session is recorded — watch the Sentry quota if error volume spikes after a release.
  • Source maps.
  • PII. sendDefaultPii: false and the replay masking are the safety net when a component accidentally logs user input. Do not weaken either without a DPIA review.