feat: add browser Sentry tracing

This commit is contained in:
Codex Review Integration 2026-09-03 13:59:13 -03:00
parent c70587941c
commit c1863c4274
8 changed files with 206 additions and 1 deletions

View file

@ -8,5 +8,9 @@
# Production: absolute URLs MUST include the /api path segment
VITE_API_URL=https://api.seahavenindustries.com/api
# Sentry browser DSN. This value is public configuration and is baked into the bundle.
# Leave blank to disable telemetry for that build.
VITE_SENTRY_DSN=
# Development proxy target (used by vite.config.ts)
#VITE_API_TARGET=http://localhost:5141

View file

@ -2,3 +2,6 @@
# NOTE: baked into the build at `vite build`, so this is the DEV value. Staging
# and prod builds must override VITE_API_URL per environment (api.staging..., etc.).
VITE_API_URL=https://api.dev.seahaven.com/api
# Public browser configuration; deployment must provide the environment's Sentry DSN.
VITE_SENTRY_DSN=

View file

@ -0,0 +1,56 @@
# 0002. Browser error and transaction telemetry with Sentry
## Status
Accepted
## Context
The SeaHaven admin SPA has no centralized browser error reporting or distributed
transaction tracing. Operational failures therefore have to be reconstructed
from user reports and isolated backend logs. We need browser navigation and API
request transactions to join the backend trace while preserving the existing UI,
routing, authentication, and server-state behavior.
## Decision
Use the pinned `@sentry/react` 10.73.0 SDK. Initialize it before the application
router is created, and wrap `createBrowserRouter` with Sentry's current React
Router compatibility API. Configure the browser tracing integration with a 100%
transaction sample rate, and propagate trace headers only to the SPA origin and
the configured `VITE_API_URL` base.
The DSN comes from `VITE_SENTRY_DSN`. It is intentionally treated as public
browser configuration because every `VITE_` value is embedded in the built
JavaScript. A missing or blank DSN leaves telemetry inactive without preventing
the application from starting. Default personally identifiable information
collection remains disabled.
Source-map upload is outside this change. It requires a Sentry organization,
project, and scoped upload token that are not currently available, plus an
explicit deployment design for handling that credential without exposing it in
the browser bundle.
### Alternatives considered
- **Manual error and timing calls** — rejected because they would miss route
transitions and distributed request context, and would require every feature
to maintain its own instrumentation.
- **Propagate tracing headers to every request** — rejected because third-party
requests must not receive SeaHaven trace metadata and broad propagation can
introduce cross-origin request failures.
- **Add the Sentry Vite plugin now** — rejected until the organization, project,
upload credential, and deployment ownership are defined.
## Consequences
- Browser navigations and API requests are recorded as connected transactions
when the deployment supplies a valid DSN.
- Every transaction is sampled. This meets the current requirement but increases
event volume and should be reviewed against Sentry quota and retention after
real traffic is measured.
- Cross-origin API tracing depends on the backend accepting `sentry-trace` and
`baggage` request headers; the current API CORS policy allows request headers.
- Production stack traces remain minified until source-map upload is designed.
- Rollback is isolated: remove `@sentry/react`, the initialization module and
import, restore the unwrapped router factory, and remove the Sentry env entries.

107
package-lock.json generated
View file

@ -21,6 +21,7 @@
"@hookform/resolvers": "^5.4.0",
"@mui/icons-material": "^9.2.0",
"@mui/material": "^9.2.0",
"@sentry/react": "10.73.0",
"@tanstack/query-broadcast-client-experimental": "5.101.2",
"@tanstack/react-query": "5.101.2",
"@tanstack/react-query-devtools": "5.101.2",
@ -1974,6 +1975,112 @@
"integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",
"license": "MIT"
},
"node_modules/@sentry/browser": {
"version": "10.73.0",
"resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-10.73.0.tgz",
"integrity": "sha512-HqTe1S5RrWLufhX2LaFP3yNoMxfNDroh120bq1zdGHZfFDBMJQ0CDXxHO+L4UJfQ5dWdCCzWbXIAiZuWGa/DFQ==",
"license": "MIT",
"dependencies": {
"@sentry/browser-utils": "10.73.0",
"@sentry/conventions": "^0.16.0",
"@sentry/core": "10.73.0",
"@sentry/feedback": "10.73.0",
"@sentry/replay": "10.73.0",
"@sentry/replay-canvas": "10.73.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@sentry/browser-utils": {
"version": "10.73.0",
"resolved": "https://registry.npmjs.org/@sentry/browser-utils/-/browser-utils-10.73.0.tgz",
"integrity": "sha512-qQygxJZ+RV779+iL1+lrJ4f4sZLgbgW0/JWPNp0YlcEAE62yCsdKbqoTEjB/EugdS4mSjBMX0chZC6rblu2Ycw==",
"license": "MIT",
"dependencies": {
"@sentry/conventions": "^0.16.0",
"@sentry/core": "10.73.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@sentry/conventions": {
"version": "0.16.0",
"resolved": "https://registry.npmjs.org/@sentry/conventions/-/conventions-0.16.0.tgz",
"integrity": "sha512-fO9PLmHdVURcSPUpWCItWAtgKiMwGdJHbovoSEyLplX5sxs2ugvI4CBPTrkkgqhObnZOD0CnWBKDzSVQYBKEyQ==",
"license": "MIT",
"engines": {
"node": ">=14"
}
},
"node_modules/@sentry/core": {
"version": "10.73.0",
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.73.0.tgz",
"integrity": "sha512-FLO1UgH19RyasVpofu612WCOgb2nEH0dZy+R72d7p65XU9i0wxlMKm3+sgfwKmiSJp1Qhilaaxs4Jg6BbiM5HA==",
"license": "MIT",
"dependencies": {
"@sentry/conventions": "^0.16.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@sentry/feedback": {
"version": "10.73.0",
"resolved": "https://registry.npmjs.org/@sentry/feedback/-/feedback-10.73.0.tgz",
"integrity": "sha512-D6nSngX+e46Mae2/oh2bxBvxNK1z2NERbuMAhB5sx9x4xMBWIyGnYYTECehvEqV9+AqGAgxxhOZoYIG3AmRwww==",
"license": "MIT",
"dependencies": {
"@sentry/core": "10.73.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@sentry/react": {
"version": "10.73.0",
"resolved": "https://registry.npmjs.org/@sentry/react/-/react-10.73.0.tgz",
"integrity": "sha512-wJrzS98ddPvhGS/MKNHZyE8X7ecd4KwKdz7fHino2qkqBBrF4cxWr+q/uLTIk5PYWMkeJ4oKMjHAjOqmzGR4tg==",
"license": "MIT",
"dependencies": {
"@sentry/browser": "10.73.0",
"@sentry/conventions": "^0.16.0",
"@sentry/core": "10.73.0"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"react": "^16.14.0 || 17.x || 18.x || 19.x"
}
},
"node_modules/@sentry/replay": {
"version": "10.73.0",
"resolved": "https://registry.npmjs.org/@sentry/replay/-/replay-10.73.0.tgz",
"integrity": "sha512-nN2wjN/Y0J5BOJV5hqRHUEBfxwUsipp1PKjcDHh6Fpxnrtfldu3Y99E8cQInseo5heFdzEvrOHBBrqWZXOHVKQ==",
"license": "MIT",
"dependencies": {
"@sentry/browser-utils": "10.73.0",
"@sentry/core": "10.73.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@sentry/replay-canvas": {
"version": "10.73.0",
"resolved": "https://registry.npmjs.org/@sentry/replay-canvas/-/replay-canvas-10.73.0.tgz",
"integrity": "sha512-sxa2lKkHPfF/j5xFpW7gocthWXRqyoHz8KCPy6yGc8plT477nl57iGSKhQDYsx1Ny10TLjs7YkIuPP1GY/Ax2Q==",
"license": "MIT",
"dependencies": {
"@sentry/core": "10.73.0",
"@sentry/replay": "10.73.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@simple-libs/child-process-utils": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@simple-libs/child-process-utils/-/child-process-utils-2.0.0.tgz",

View file

@ -55,6 +55,7 @@
"@hookform/resolvers": "^5.4.0",
"@mui/icons-material": "^9.2.0",
"@mui/material": "^9.2.0",
"@sentry/react": "10.73.0",
"@tanstack/query-broadcast-client-experimental": "5.101.2",
"@tanstack/react-query": "5.101.2",
"@tanstack/react-query-devtools": "5.101.2",

View file

@ -1,3 +1,5 @@
import "@/observability/sentry";
import React from "react";
import ReactDOM from "react-dom/client";
import { AppRoot } from "@/app-root";

View file

@ -0,0 +1,30 @@
import * as Sentry from "@sentry/react";
import { useEffect } from "react";
import {
createRoutesFromChildren,
matchRoutes,
useLocation,
useNavigationType,
} from "react-router";
import { env } from "@/lib/env";
const sentryDsn = import.meta.env.VITE_SENTRY_DSN?.trim() || undefined;
const apiBaseUrl = new URL(env.apiUrl, window.location.origin).toString();
Sentry.init({
dsn: sentryDsn,
environment: import.meta.env.MODE,
integrations: [
Sentry.reactRouterBrowserTracingIntegration({
useEffect,
useLocation,
useNavigationType,
createRoutesFromChildren,
matchRoutes,
}),
],
sendDefaultPii: false,
tracesSampleRate: 1.0,
tracePropagationTargets: [window.location.origin, apiBaseUrl],
});

View file

@ -1,8 +1,10 @@
import { createBrowserRouter } from "react-router";
import * as Sentry from "@sentry/react";
import { createBrowserRouter as createBrowserRouterBase } from "react-router";
import { RouterProvider } from "react-router/dom";
import { routes } from "@/routing/build-route-tree";
const createBrowserRouter = Sentry.wrapCreateBrowserRouter(createBrowserRouterBase);
const router = createBrowserRouter(routes);
export function Routes() {