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 OAuth (configurato a fine progetto)
|
||||||
# GOOGLE_CLIENT_ID=
|
# GOOGLE_CLIENT_ID=
|
||||||
# GOOGLE_CLIENT_SECRET=
|
# GOOGLE_CLIENT_SECRET=
|
||||||
|
|
||||||
|
# Sentry — error tracking (lascia vuoto per disabilitare)
|
||||||
|
# SENTRY_DSN=
|
||||||
@@ -18,6 +18,7 @@
|
|||||||
"@ketopath/auth": "workspace:*",
|
"@ketopath/auth": "workspace:*",
|
||||||
"@ketopath/db": "workspace:*",
|
"@ketopath/db": "workspace:*",
|
||||||
"@ketopath/shared": "workspace:*",
|
"@ketopath/shared": "workspace:*",
|
||||||
|
"@sentry/node": "^8.30.0",
|
||||||
"better-auth": "^1.0.21",
|
"better-auth": "^1.0.21",
|
||||||
"fastify": "^4.28.1",
|
"fastify": "^4.28.1",
|
||||||
"fastify-plugin": "^4.5.1",
|
"fastify-plugin": "^4.5.1",
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import sensible from '@fastify/sensible';
|
|||||||
import Fastify, { type FastifyInstance } from 'fastify';
|
import Fastify, { type FastifyInstance } from 'fastify';
|
||||||
|
|
||||||
import { env } from './config/env.js';
|
import { env } from './config/env.js';
|
||||||
|
import { Sentry } from './lib/sentry.js';
|
||||||
import { dbRoutes } from './modules/db/db.routes.js';
|
import { dbRoutes } from './modules/db/db.routes.js';
|
||||||
import { healthRoutes } from './modules/health/health.routes.js';
|
import { healthRoutes } from './modules/health/health.routes.js';
|
||||||
import { meRoutes } from './modules/me/me.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(meRoutes);
|
||||||
await app.register(profileRoutes);
|
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;
|
return app;
|
||||||
}
|
}
|
||||||
@@ -17,6 +17,7 @@ const envSchema = z.object({
|
|||||||
DATABASE_URL: z.string().url(),
|
DATABASE_URL: z.string().url(),
|
||||||
BETTER_AUTH_SECRET: z.string().min(32),
|
BETTER_AUTH_SECRET: z.string().min(32),
|
||||||
BETTER_AUTH_URL: z.string().url(),
|
BETTER_AUTH_URL: z.string().url(),
|
||||||
|
SENTRY_DSN: z.string().url().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type Env = z.infer<typeof envSchema>;
|
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,
|
weightCurrentKg: data.weightCurrentKg,
|
||||||
weightGoalKg: data.weightGoalKg,
|
weightGoalKg: data.weightGoalKg,
|
||||||
activityLevel: data.activityLevel,
|
activityLevel: data.activityLevel,
|
||||||
targetDate: data.targetDate,
|
targetDate: data.targetDate ?? null,
|
||||||
},
|
},
|
||||||
update: {
|
update: {
|
||||||
age: data.age,
|
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 { buildApp } from './app.js';
|
||||||
import { env } from './config/env.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_ID=
|
||||||
# GOOGLE_CLIENT_SECRET=
|
# GOOGLE_CLIENT_SECRET=
|
||||||
|
|
||||||
# Sentry (verrà configurato in uno step dedicato)
|
# Sentry — error tracking (lascia vuoto per disabilitare)
|
||||||
# NEXT_PUBLIC_SENTRY_DSN=
|
# 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';
|
import createNextIntlPlugin from 'next-intl/plugin';
|
||||||
|
|
||||||
const withNextIntl = createNextIntlPlugin('./src/i18n.ts');
|
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:*",
|
"@ketopath/ui": "workspace:*",
|
||||||
"@radix-ui/react-label": "^2.1.0",
|
"@radix-ui/react-label": "^2.1.0",
|
||||||
"@radix-ui/react-slot": "^1.1.0",
|
"@radix-ui/react-slot": "^1.1.0",
|
||||||
|
"@sentry/nextjs": "^8.30.0",
|
||||||
"better-auth": "^1.0.21",
|
"better-auth": "^1.0.21",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"clsx": "^2.1.1",
|
"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 },
|
formState: { errors, isSubmitting },
|
||||||
} = useForm<ProfileInput>({
|
} = useForm<ProfileInput>({
|
||||||
resolver: zodResolver(profileInputSchema),
|
resolver: zodResolver(profileInputSchema),
|
||||||
defaultValues: initial
|
...(initial
|
||||||
? {
|
? {
|
||||||
age: initial.age,
|
defaultValues: {
|
||||||
gender: initial.gender,
|
age: initial.age,
|
||||||
heightCm: initial.heightCm,
|
gender: initial.gender,
|
||||||
weightStartKg: initial.weightStartKg,
|
heightCm: initial.heightCm,
|
||||||
weightCurrentKg: initial.weightCurrentKg,
|
weightStartKg: initial.weightStartKg,
|
||||||
weightGoalKg: initial.weightGoalKg,
|
weightCurrentKg: initial.weightCurrentKg,
|
||||||
activityLevel: initial.activityLevel,
|
weightGoalKg: initial.weightGoalKg,
|
||||||
|
activityLevel: initial.activityLevel,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
: undefined,
|
: {}),
|
||||||
});
|
});
|
||||||
|
|
||||||
const onSubmit = handleSubmit(async (input) => {
|
const onSubmit = handleSubmit(async (input) => {
|
||||||
@@ -142,7 +144,7 @@ function Field({
|
|||||||
children,
|
children,
|
||||||
}: {
|
}: {
|
||||||
label: string;
|
label: string;
|
||||||
error?: string;
|
error?: string | undefined;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
Generated
BIN
Binary file not shown.
Reference in new issue
Block a user