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:
1 parent
1d904484e6
commit
5b38ddc41d
15 files changed
+101
-13
No files matched your search
@@ -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=
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -16,5 +16,9 @@ DATABASE_URL=postgresql://luciano:luciano%40@localhost:5432/ketopath?schema=publ
|
||||
# GOOGLE_CLIENT_ID=
|
||||
# GOOGLE_CLIENT_SECRET=
|
||||
|
||||
# Sentry (verrà configurato in uno step dedicato)
|
||||
# Sentry — error tracking (lascia vuoto per disabilitare)
|
||||
# NEXT_PUBLIC_SENTRY_DSN=
|
||||
# Per il source-map upload in produzione (opzionale):
|
||||
# SENTRY_AUTH_TOKEN=
|
||||
# SENTRY_ORG=
|
||||
# SENTRY_PROJECT=
|
||||
@@ -1,3 +1,4 @@
|
||||
import { withSentryConfig } from '@sentry/nextjs';
|
||||
import createNextIntlPlugin from 'next-intl/plugin';
|
||||
|
||||
const withNextIntl = createNextIntlPlugin('./src/i18n.ts');
|
||||
@@ -20,4 +21,17 @@ const nextConfig = {
|
||||
},
|
||||
};
|
||||
|
||||
export default withNextIntl(nextConfig);
|
||||
const isSentryEnabled = Boolean(process.env.NEXT_PUBLIC_SENTRY_DSN);
|
||||
|
||||
const sentryWebpackPluginOptions = {
|
||||
silent: true,
|
||||
org: process.env.SENTRY_ORG,
|
||||
project: process.env.SENTRY_PROJECT,
|
||||
// Source map upload requires SENTRY_AUTH_TOKEN — disabled until configured.
|
||||
disableServerWebpackPlugin: !process.env.SENTRY_AUTH_TOKEN,
|
||||
disableClientWebpackPlugin: !process.env.SENTRY_AUTH_TOKEN,
|
||||
};
|
||||
|
||||
const composed = withNextIntl(nextConfig);
|
||||
|
||||
export default isSentryEnabled ? withSentryConfig(composed, sentryWebpackPluginOptions) : composed;
|
||||
@@ -17,6 +17,7 @@
|
||||
"@ketopath/ui": "workspace:*",
|
||||
"@radix-ui/react-label": "^2.1.0",
|
||||
"@radix-ui/react-slot": "^1.1.0",
|
||||
"@sentry/nextjs": "^8.30.0",
|
||||
"better-auth": "^1.0.21",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.1",
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import * as Sentry from '@sentry/nextjs';
|
||||
|
||||
const dsn = process.env.NEXT_PUBLIC_SENTRY_DSN;
|
||||
|
||||
Sentry.init({
|
||||
dsn,
|
||||
enabled: Boolean(dsn),
|
||||
tracesSampleRate: 0.1,
|
||||
environment: process.env.NODE_ENV,
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
import * as Sentry from '@sentry/nextjs';
|
||||
|
||||
const dsn = process.env.NEXT_PUBLIC_SENTRY_DSN;
|
||||
|
||||
Sentry.init({
|
||||
dsn,
|
||||
enabled: Boolean(dsn),
|
||||
tracesSampleRate: 0.1,
|
||||
environment: process.env.NODE_ENV,
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
import * as Sentry from '@sentry/nextjs';
|
||||
|
||||
const dsn = process.env.SENTRY_DSN ?? process.env.NEXT_PUBLIC_SENTRY_DSN;
|
||||
|
||||
Sentry.init({
|
||||
dsn,
|
||||
enabled: Boolean(dsn),
|
||||
tracesSampleRate: 0.1,
|
||||
environment: process.env.NODE_ENV,
|
||||
});
|
||||
@@ -28,17 +28,19 @@ export function ProfileForm({ initial }: { initial: Profile | null }) {
|
||||
formState: { errors, isSubmitting },
|
||||
} = useForm<ProfileInput>({
|
||||
resolver: zodResolver(profileInputSchema),
|
||||
defaultValues: initial
|
||||
...(initial
|
||||
? {
|
||||
age: initial.age,
|
||||
gender: initial.gender,
|
||||
heightCm: initial.heightCm,
|
||||
weightStartKg: initial.weightStartKg,
|
||||
weightCurrentKg: initial.weightCurrentKg,
|
||||
weightGoalKg: initial.weightGoalKg,
|
||||
activityLevel: initial.activityLevel,
|
||||
defaultValues: {
|
||||
age: initial.age,
|
||||
gender: initial.gender,
|
||||
heightCm: initial.heightCm,
|
||||
weightStartKg: initial.weightStartKg,
|
||||
weightCurrentKg: initial.weightCurrentKg,
|
||||
weightGoalKg: initial.weightGoalKg,
|
||||
activityLevel: initial.activityLevel,
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
: {}),
|
||||
});
|
||||
|
||||
const onSubmit = handleSubmit(async (input) => {
|
||||
@@ -142,7 +144,7 @@ function Field({
|
||||
children,
|
||||
}: {
|
||||
label: string;
|
||||
error?: string;
|
||||
error?: string | undefined;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
|
||||
Generated
BIN
Binary file not shown.
Reference in new issue
Block a user