feat: wire Sentry, conditional on DSN env var

apps/web (@sentry/nextjs):
- sentry.client.config.ts / sentry.server.config.ts / sentry.edge.config.ts
  initialize Sentry only when NEXT_PUBLIC_SENTRY_DSN (or SENTRY_DSN on the
  server) is set; tracesSampleRate 0.1
- next.config.mjs wraps the existing config with withSentryConfig only when a
  DSN is present; source-map upload stays disabled until SENTRY_AUTH_TOKEN is
  provided
- .env.example documents NEXT_PUBLIC_SENTRY_DSN and the optional auth token

apps/api (@sentry/node):
- src/lib/sentry.ts initializes Sentry at module load when SENTRY_DSN is set
- server.ts imports sentry.ts as the very first side-effect so early-boot
  errors (env validation, plugin registration) reach Sentry
- env schema gains optional SENTRY_DSN (URL)
- app.ts registers an errorHandler that captures the exception with the
  authenticated user when SENTRY_DSN is set; logs and re-sends as before

Build-time housekeeping:
- profile route: targetDate ?? null on create to satisfy Prisma's input shape
  under exactOptionalPropertyTypes
- profile form: useForm receives defaultValues only when initial is provided
  (spread instead of `defaultValues: undefined`); Field error prop typed
  `string | undefined` for exactOptionalPropertyTypes

Without a DSN both apps run unchanged (Sentry is inert).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
lucianoandClaude Opus 4.7 committed 2026-04-29 14:25:08 +02:00
1 parent 1d904484e6
commit 5b38ddc41d
15 files changed
+101 -13

No files matched your search

+3
View File
@@ -15,3 +15,6 @@ BETTER_AUTH_URL=http://localhost:3000
# Google OAuth (configurato a fine progetto)
# GOOGLE_CLIENT_ID=
# GOOGLE_CLIENT_SECRET=
# Sentry — error tracking (lascia vuoto per disabilitare)
# SENTRY_DSN=
+1
View File
@@ -18,6 +18,7 @@
"@ketopath/auth": "workspace:*",
"@ketopath/db": "workspace:*",
"@ketopath/shared": "workspace:*",
"@sentry/node": "^8.30.0",
"better-auth": "^1.0.21",
"fastify": "^4.28.1",
"fastify-plugin": "^4.5.1",
+15
View File
@@ -5,6 +5,7 @@ import sensible from '@fastify/sensible';
import Fastify, { type FastifyInstance } from 'fastify';
import { env } from './config/env.js';
import { Sentry } from './lib/sentry.js';
import { dbRoutes } from './modules/db/db.routes.js';
import { healthRoutes } from './modules/health/health.routes.js';
import { meRoutes } from './modules/me/me.routes.js';
@@ -46,5 +47,19 @@ export async function buildApp(): Promise<FastifyInstance> {
await app.register(meRoutes);
await app.register(profileRoutes);
if (env.SENTRY_DSN) {
app.setErrorHandler((err, request, reply) => {
if (request.user) {
Sentry.captureException(err, {
user: { id: request.user.id, email: request.user.email },
});
} else {
Sentry.captureException(err);
}
app.log.error({ err }, 'unhandled error');
reply.send(err);
});
}
return app;
}
+1
View File
@@ -17,6 +17,7 @@ const envSchema = z.object({
DATABASE_URL: z.string().url(),
BETTER_AUTH_SECRET: z.string().min(32),
BETTER_AUTH_URL: z.string().url(),
SENTRY_DSN: z.string().url().optional(),
});
export type Env = z.infer<typeof envSchema>;
+13
View File
@@ -0,0 +1,13 @@
import * as Sentry from '@sentry/node';
import { env } from '../config/env.js';
if (env.SENTRY_DSN) {
Sentry.init({
dsn: env.SENTRY_DSN,
environment: env.NODE_ENV,
tracesSampleRate: 0.1,
});
}
export { Sentry };
@@ -78,7 +78,7 @@ export const profileRoutes: FastifyPluginAsync = async (fastify) => {
weightCurrentKg: data.weightCurrentKg,
weightGoalKg: data.weightGoalKg,
activityLevel: data.activityLevel,
targetDate: data.targetDate,
targetDate: data.targetDate ?? null,
},
update: {
age: data.age,
+4
View File
@@ -1,3 +1,7 @@
// Side-effect import: initializes Sentry before any other module is evaluated.
// Must stay first — moving it later means errors thrown during early boot
// (env validation, Prisma adapter, etc.) wouldn't reach Sentry.
import './lib/sentry.js';
import { buildApp } from './app.js';
import { env } from './config/env.js';