feat(deps): @sentry/nextjs error tracking (DSN-gated, dormant by default)
Wires the Sentry SDK shipped-but-dormant: no-op unless
`NEXT_PUBLIC_SENTRY_DSN` is set in the environment. Production opts
in via the deploy env; dev + CI stay quiet.
- `sentry.client.config.ts` / `sentry.server.config.ts` /
`sentry.edge.config.ts` — runtime init, each guards on the DSN.
- `instrumentation.ts` — Next 13.4+ instrumentation hook that lazy-
imports the server + edge configs when the DSN is present.
- `next.config.ts` — withSentryConfig only wraps the config when
the DSN is set, so dev builds skip source-map upload + middleware
injection.
- `src/lib/env.ts` — added optional NEXT_PUBLIC_SENTRY_DSN +
SENTRY_ENVIRONMENT + SENTRY_TRACES_SAMPLE_RATE (defaults to 0.1).
Env vars to add to .env.example (blocked from this commit by the
.env hook — apply manually):
# Sentry (optional — SDK is a no-op without a DSN)
NEXT_PUBLIC_SENTRY_DSN=
SENTRY_ENVIRONMENT=
# Defaults to 0.1 (10%) when unset
SENTRY_TRACES_SAMPLE_RATE=
Replay is opt-in only — disabled by default for now; we'd need to
audit privacy implications (PII redaction, GDPR) before enabling it.
Verified: tsc clean, vitest 1315/1315, next build green with DSN
unset (Sentry plumbing intact, runtime no-op).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -170,7 +170,6 @@ Remaining (opportunistic, no concrete trigger):
|
||||
| --------------------------------------------------- | ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`.toLocale*` remainder (93 sites)** | ~2-3h opportunistic | Migrate to `formatDate(...)` as you touch each file. Helper already shipped; 17 tests; sweep proven on PDF + template paths. |
|
||||
| **drizzle-zod remainder (~28 simple validators)** | ~30 min per file | Migrate when a validator file is touched. Pattern proven in tags + brochures. |
|
||||
| **PWA assets** (per `MEMORY.md`) | ~30 min | Add `public/icon-192.png`, `public/icon-512.png`, `public/icon-512-maskable.png` before shipping Phase B PWA scanner. |
|
||||
| **Wire `<DataTable virtual />`** on big tables | ~15 min per site | Prop is shipped + opt-in. Apply to: admin/audit-log-list (10k rows possible), super-admin port switcher (50+ ports), client export modal preview. None blocking. |
|
||||
| **Tier 2 polish — when product UX surfaces emerge** | each 30 min – 1 h | `embla-carousel-react` + `yet-another-react-lightbox` for berth / yacht photo galleries · `react-resizable-panels` for docs hub sidebar · `@use-gesture/react` for kanban swipe. |
|
||||
|
||||
@@ -183,7 +182,7 @@ Decisions / parked:
|
||||
- `@sentry/nextjs` — needs SaaS-dep decision.
|
||||
- `@tiptap/core` upgrade — needs product decision on rich notes.
|
||||
- `pdfjs-dist` / `@react-pdf-viewer/core` — in-browser PDF preview in docs hub (paired with Phase 2 docs-hub UX work).
|
||||
- `next-pwa` / `@serwist/next` — blocked on missing PWA icons per MEMORY.md.
|
||||
- `next-pwa` / `@serwist/next` — icons already in `public/`; revisit only when we want fuller service-worker integration (offline shell, install prompt UX).
|
||||
- `next-intl` — no current i18n target.
|
||||
- `posthog-js` — analytics scope decision.
|
||||
- `react-virtuoso` — only useful if inbox grows past ~hundreds of items; current `<ScrollArea max-h-[400px]>` handles realistic volumes fine.
|
||||
|
||||
20
instrumentation.ts
Normal file
20
instrumentation.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* Next.js instrumentation hook (Next 13.4+ / 15+ / 16+).
|
||||
*
|
||||
* Runs once at server startup. We use it to wire Sentry's server +
|
||||
* edge runtimes. The client init is auto-bundled by withSentryConfig
|
||||
* from `sentry.client.config.ts`.
|
||||
*
|
||||
* The Sentry imports are gated behind the DSN check so the SDK stays
|
||||
* a no-op when unconfigured.
|
||||
*/
|
||||
|
||||
export async function register() {
|
||||
if (!process.env.NEXT_PUBLIC_SENTRY_DSN) return;
|
||||
if (process.env.NEXT_RUNTIME === 'nodejs') {
|
||||
await import('./sentry.server.config');
|
||||
}
|
||||
if (process.env.NEXT_RUNTIME === 'edge') {
|
||||
await import('./sentry.edge.config');
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { NextConfig } from 'next';
|
||||
import bundleAnalyzer from '@next/bundle-analyzer';
|
||||
import { withSentryConfig } from '@sentry/nextjs';
|
||||
|
||||
const isProd = process.env.NODE_ENV === 'production';
|
||||
|
||||
@@ -117,4 +118,21 @@ const nextConfig: NextConfig = {
|
||||
},
|
||||
};
|
||||
|
||||
export default withBundleAnalyzer(nextConfig);
|
||||
// Sentry wrapper is conditional: if NEXT_PUBLIC_SENTRY_DSN isn't set we
|
||||
// skip its build-time source-map upload + middleware injection so dev
|
||||
// builds stay fast and CI doesn't need credentials. When the DSN is
|
||||
// present, withSentryConfig adds instrumentation hooks that route
|
||||
// errors + traces to Sentry.
|
||||
const withSentry = process.env.NEXT_PUBLIC_SENTRY_DSN
|
||||
? (cfg: NextConfig) =>
|
||||
withSentryConfig(cfg, {
|
||||
silent: true,
|
||||
widenClientFileUpload: true,
|
||||
// We host on our own infra — disable Vercel-specific tunneling.
|
||||
tunnelRoute: undefined,
|
||||
// Strip Sentry debug logger from prod bundle.
|
||||
disableLogger: true,
|
||||
})
|
||||
: (cfg: NextConfig) => cfg;
|
||||
|
||||
export default withSentry(withBundleAnalyzer(nextConfig));
|
||||
|
||||
@@ -55,6 +55,7 @@
|
||||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
"@react-email/components": "^1.0.12",
|
||||
"@react-pdf/renderer": "^4.5.1",
|
||||
"@sentry/nextjs": "^10.53.1",
|
||||
"@socket.io/redis-adapter": "^8.3.0",
|
||||
"@tanstack/query-broadcast-client-experimental": "^5.100.10",
|
||||
"@tanstack/react-query": "^5.100.10",
|
||||
|
||||
1754
pnpm-lock.yaml
generated
1754
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
25
sentry.client.config.ts
Normal file
25
sentry.client.config.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Sentry client-side init.
|
||||
*
|
||||
* No-op when `NEXT_PUBLIC_SENTRY_DSN` is unset — Sentry stays
|
||||
* shipped-but-dormant in dev. Production sets the DSN via the
|
||||
* deploy env. Sampling rate is env-driven via
|
||||
* `SENTRY_TRACES_SAMPLE_RATE` (defaults to 0.1 = 10% of transactions
|
||||
* to avoid quota burn).
|
||||
*/
|
||||
|
||||
import * as Sentry from '@sentry/nextjs';
|
||||
|
||||
const dsn = process.env.NEXT_PUBLIC_SENTRY_DSN;
|
||||
|
||||
if (dsn) {
|
||||
Sentry.init({
|
||||
dsn,
|
||||
environment: process.env.SENTRY_ENVIRONMENT ?? process.env.NODE_ENV,
|
||||
tracesSampleRate: Number(process.env.SENTRY_TRACES_SAMPLE_RATE ?? 0.1),
|
||||
// Replay is opt-in — we'd need to verify privacy implications
|
||||
// before enabling. Leave disabled by default.
|
||||
replaysOnErrorSampleRate: 0,
|
||||
replaysSessionSampleRate: 0,
|
||||
});
|
||||
}
|
||||
17
sentry.edge.config.ts
Normal file
17
sentry.edge.config.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Sentry edge-runtime init (proxy.ts / middleware).
|
||||
*
|
||||
* No-op when `NEXT_PUBLIC_SENTRY_DSN` is unset.
|
||||
*/
|
||||
|
||||
import * as Sentry from '@sentry/nextjs';
|
||||
|
||||
const dsn = process.env.NEXT_PUBLIC_SENTRY_DSN;
|
||||
|
||||
if (dsn) {
|
||||
Sentry.init({
|
||||
dsn,
|
||||
environment: process.env.SENTRY_ENVIRONMENT ?? process.env.NODE_ENV,
|
||||
tracesSampleRate: Number(process.env.SENTRY_TRACES_SAMPLE_RATE ?? 0.1),
|
||||
});
|
||||
}
|
||||
18
sentry.server.config.ts
Normal file
18
sentry.server.config.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* Sentry server-side init.
|
||||
*
|
||||
* No-op when `NEXT_PUBLIC_SENTRY_DSN` is unset. Same DSN as the client
|
||||
* config — Sentry routes events to the right project automatically.
|
||||
*/
|
||||
|
||||
import * as Sentry from '@sentry/nextjs';
|
||||
|
||||
const dsn = process.env.NEXT_PUBLIC_SENTRY_DSN;
|
||||
|
||||
if (dsn) {
|
||||
Sentry.init({
|
||||
dsn,
|
||||
environment: process.env.SENTRY_ENVIRONMENT ?? process.env.NODE_ENV,
|
||||
tracesSampleRate: Number(process.env.SENTRY_TRACES_SAMPLE_RATE ?? 0.1),
|
||||
});
|
||||
}
|
||||
@@ -61,6 +61,11 @@ const envSchema = z
|
||||
// OpenAI (optional)
|
||||
OPENAI_API_KEY: z.string().optional(),
|
||||
|
||||
// Sentry (optional — when unset the SDK is a no-op)
|
||||
NEXT_PUBLIC_SENTRY_DSN: z.string().url().optional(),
|
||||
SENTRY_ENVIRONMENT: z.string().optional(),
|
||||
SENTRY_TRACES_SAMPLE_RATE: z.coerce.number().min(0).max(1).default(0.1),
|
||||
|
||||
// App
|
||||
APP_URL: z.string().url(),
|
||||
PUBLIC_SITE_URL: z.string().url(),
|
||||
|
||||
Reference in New Issue
Block a user