feat: intégration Sentry + migration middleware.js → proxy.js (Next.js 16)

Sentry (tracking erreurs frontend + API routes) :
- sentry.client.config.js  : erreurs navigateur + Session Replay sur erreurs
- sentry.server.config.js  : erreurs API routes (register, jwt callback)
- sentry.edge.config.js    : runtime edge (middleware proxy)
- instrumentation.js       : point d'entrée Next.js 15+ (register + captureRequestError)
- next.config.mjs          : wrappé avec withSentryConfig (source maps désactivés sans SENTRY_AUTH_TOKEN)
- .env.sample              : ajout de NEXT_PUBLIC_SENTRY_DSN (placeholder)

Migration middleware → proxy (bug pré-existant surfacé par le build Sentry) :
- proxy.js : fusion du rate limiting + auth NextAuth en un seul proxy Next.js 16
- middleware.js : supprimé (Next.js 16 n'accepte plus les deux fichiers simultanément)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-14 06:48:55 +04:00
parent d8a771161c
commit 8016c26e32
10 changed files with 2368 additions and 178 deletions
+3
View File
@@ -19,3 +19,6 @@ COMMENTS_PER_PAGE=5
# WEBSOCKET
NEXT_PUBLIC_DISABLE_WEBSOCKET=false
# SENTRY
NEXT_PUBLIC_SENTRY_DSN=https://xxx@xxx.ingest.sentry.io/xxx
+11
View File
@@ -0,0 +1,11 @@
export async function register() {
if (process.env.NEXT_RUNTIME === 'nodejs') {
await import('./sentry.server.config.js')
}
if (process.env.NEXT_RUNTIME === 'edge') {
await import('./sentry.edge.config.js')
}
}
export {captureRequestError as onRequestError} from '@sentry/nextjs'
+17 -1
View File
@@ -1,4 +1,5 @@
/** @type {import('next').NextConfig} */
import {withSentryConfig} from '@sentry/nextjs'
// Les URL Directus sont lues à l'exécution — elles s'adaptent à l'environnement
// (dev local ou production) sans rebuild.
@@ -70,4 +71,19 @@ const nextConfig = {
},
}
export default nextConfig
export default withSentryConfig(nextConfig, {
// Organisation et projet Sentry pour l'upload des source maps
// Nécessite SENTRY_AUTH_TOKEN en CI pour activer l'upload
silent: !process.env.CI,
// Ne pas uploader les source maps si le token est absent (dev local)
sourcemaps: {
disable: !process.env.SENTRY_AUTH_TOKEN,
},
// Évite d'exposer les source maps aux utilisateurs finaux
hideSourceMaps: true,
// Tunnel Sentry via notre propre domaine (contourne les adblockers)
// tunnelRoute: '/monitoring',
})
+2226 -172
View File
File diff suppressed because it is too large Load Diff
+11 -2
View File
@@ -18,6 +18,7 @@
"@mui/lab": "^7.0.1-beta.20",
"@mui/material": "^7.3.6",
"@mui/material-nextjs": "^7.3.6",
"@sentry/nextjs": "^10.48.0",
"@uiw/react-md-editor": "^4.0.11",
"date-fns": "^4.1.0",
"html2canvas": "^1.4.1",
@@ -58,9 +59,17 @@
"overrides": [
{
"files": "lib/__tests__/**/*.js",
"envs": ["node", "es2020"],
"envs": [
"node",
"es2020"
],
"rules": {
"camelcase": ["error", {"properties": "never"}],
"camelcase": [
"error",
{
"properties": "never"
}
],
"capitalized-comments": "off"
}
}
+55 -1
View File
@@ -1 +1,55 @@
export {auth as proxy} from './auth.js'
import {NextResponse} from 'next/server'
import {auth} from './auth.js'
import {createRateLimiter} from '@/lib/rate-limit.js'
// 5 inscriptions max par IP toutes les 15 minutes
const checkRegister = createRateLimiter({windowMs: 15 * 60 * 1000, max: 5})
// 10 tentatives de connexion max par IP toutes les 5 minutes
const checkSignin = createRateLimiter({windowMs: 5 * 60 * 1000, max: 10})
const limiters = {
'/api/auth/register': checkRegister,
'/api/auth/callback/credentials': checkSignin,
}
/**
* Extrait l'IP cliente depuis les headers HTTP.
* Priorité à X-Real-IP (Nginx), puis X-Forwarded-For.
*/
function getClientIp(request) {
const realIp = request.headers.get('x-real-ip')
if (realIp) {
return realIp.trim()
}
const forwarded = request.headers.get('x-forwarded-for')
if (forwarded) {
return forwarded.split(',')[0].trim()
}
return 'unknown'
}
export async function proxy(request) {
const {pathname} = request.nextUrl
const check = limiters[pathname]
// Rate limiting sur les routes d'authentification critiques
if (check) {
const ip = getClientIp(request)
const result = check(`${ip}:${pathname}`)
if (!result.success) {
return NextResponse.json(
{message: 'Trop de tentatives. Veuillez réessayer dans quelques minutes.'},
{
status: 429,
headers: {'Retry-After': String(result.retryAfter)},
}
)
}
}
return auth(request)
}
+24
View File
@@ -0,0 +1,24 @@
import * as Sentry from '@sentry/nextjs'
Sentry.init({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
// Taux d'échantillonnage des traces de performance (0 = désactivé, 1 = 100%)
// Valeur basse en production pour limiter le volume
tracesSampleRate: 0.1,
// Capture des replays de session uniquement sur les erreurs
replaysOnErrorSampleRate: 1,
replaysSessionSampleRate: 0,
integrations: [
Sentry.replayIntegration({
// Masquer les champs sensibles dans les replays
maskAllText: false,
blockAllMedia: false,
}),
],
// Ne pas afficher les erreurs Sentry dans la console en production
debug: false,
})
+9
View File
@@ -0,0 +1,9 @@
import * as Sentry from '@sentry/nextjs'
Sentry.init({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
tracesSampleRate: 0.1,
debug: false,
})
+10
View File
@@ -0,0 +1,10 @@
import * as Sentry from '@sentry/nextjs'
Sentry.init({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
// Taux d'échantillonnage des traces serveur
tracesSampleRate: 0.1,
debug: false,
})
+2 -2
View File
@@ -14,8 +14,8 @@
- [x] **Tests unitaires** — Vitest sur `lib/format.js`, `lib/version-utils.js`, `lib/rate-limit.js`
- [x] **Tests extensions Directus** — mocks VersionsService
- [x] **Refresh token explicite** — callback `jwt` dans NextAuth options
- [ ] **Pipeline CI**GitHub Actions (lint + test + build)
- [ ] **Sentry** — tracking erreurs frontend + API routes
- [ ] **Pipeline CI**Forgejo Actions / Woodpecker CI (lint + test + build) ⏸ en attente
- [x] **Sentry** — tracking erreurs frontend + API routes
## Améliorations moyennes (P3)