-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathinstrumentation-nodejs.ts
More file actions
314 lines (273 loc) · 10.6 KB
/
instrumentation-nodejs.ts
File metadata and controls
314 lines (273 loc) · 10.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
/**
* Next.js Instrumentation
*
* This file is automatically loaded by Next.js before any other code.
* Use it to initialize monitoring, telemetry, and observability tools.
*
* @see https://nextjs.org/docs/app/building-your-application/optimizing/instrumentation
*/
/**
* Validates that monitoring and observability are properly configured
*/
function validateMonitoringSetup(): void {
const isProduction = process.env.NODE_ENV === 'production'
const isDevelopment = process.env.NODE_ENV === 'development'
const warnings: string[] = []
// Check Sentry configuration
if (!process.env.SENTRY_DSN) {
warnings.push('SENTRY_DSN not configured - error tracking disabled')
}
// Check Redis configuration for rate limiting and caching
if (!process.env.UPSTASH_REDIS_REST_URL || !process.env.UPSTASH_REDIS_REST_TOKEN) {
warnings.push('Upstash Redis not configured - caching and rate limiting degraded')
}
if (warnings.length > 0 && isProduction) {
console.warn('\n' + '⚠'.repeat(40))
console.warn('WARNING: Monitoring not fully configured for production')
console.warn('⚠'.repeat(40))
warnings.forEach((warning) => console.warn(` ⚠️ ${warning}`))
console.warn('\nProduction deployments should have full monitoring configured.')
console.warn('⚠'.repeat(40) + '\n')
}
if (warnings.length === 0 || isDevelopment) {
console.log('[instrumentation] Monitoring configuration validated')
}
}
/**
* Validates that authentication is properly configured
* Implements fail-fast pattern for production deployments
*/
function validateAuthenticationSetup(): void {
const isProduction = process.env.NODE_ENV === 'production'
const isDevelopment = process.env.NODE_ENV === 'development'
// SECURITY: Store only variable names, not values, to prevent secrets in build output
const requiredAuthVars = ['NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY', 'CLERK_SECRET_KEY']
const missingVars: string[] = []
const invalidVars: string[] = []
// Check for missing variables
for (const name of requiredAuthVars) {
const value = process.env[name]
if (!value) {
missingVars.push(name)
} else if (
value.includes('YOUR_') ||
value.includes('CONFIGURE') ||
value.includes('PLACEHOLDER')
) {
invalidVars.push(name)
}
}
// Check specifically for Clerk publishable key format
const publishableKey = process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY
if (publishableKey && !publishableKey.startsWith('pk_')) {
invalidVars.push('NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY (must start with pk_)')
}
const hasAuthErrors = missingVars.length > 0 || invalidVars.length > 0
if (hasAuthErrors && isProduction) {
// FAIL FAST in production
console.error('\n' + '='.repeat(80))
console.error('CRITICAL SECURITY ERROR: Authentication not properly configured')
console.error('='.repeat(80))
if (missingVars.length > 0) {
console.error('\nMissing required authentication variables:')
missingVars.forEach((varName) => console.error(` ❌ ${varName}`))
}
if (invalidVars.length > 0) {
console.error('\nInvalid authentication variables:')
invalidVars.forEach((varName) => console.error(` ❌ ${varName}`))
}
console.error('\nProduction deployments MUST have valid Clerk authentication configured.')
console.error('Without proper authentication, protected routes would be publicly accessible.')
console.error('\nPlease configure valid Clerk keys before deploying to production.')
console.error('Visit https://clerk.com to get your authentication keys.')
console.error('='.repeat(80) + '\n')
throw new Error('CRITICAL: Authentication not configured for production deployment')
}
if (hasAuthErrors && isDevelopment) {
// WARN in development
console.warn('\n' + '⚠'.repeat(40))
console.warn('WARNING: Authentication not properly configured')
console.warn('⚠'.repeat(40))
if (missingVars.length > 0) {
console.warn('\nMissing authentication variables:')
missingVars.forEach((varName) => console.warn(` ⚠️ ${varName}`))
}
if (invalidVars.length > 0) {
console.warn('\nInvalid authentication variables:')
invalidVars.forEach((varName) => console.warn(` ⚠️ ${varName}`))
}
console.warn('\nRunning in INSECURE MODE - protected routes will not require authentication.')
console.warn('This is only acceptable for local development.')
console.warn('Configure Clerk keys from https://clerk.com for full authentication.')
console.warn('⚠'.repeat(40) + '\n')
}
if (!hasAuthErrors) {
console.log('[instrumentation] Authentication configuration validated successfully')
}
}
export async function register() {
// CRITICAL: Skip ALL imports and execution in Edge Runtime
// Edge Runtime has strict CSP that disallows eval/Function which many libraries use
if (process.env.NEXT_RUNTIME === 'edge') {
console.log('[instrumentation] Skipping Edge Runtime - no instrumentation needed')
return
}
// Only run on server (Node.js runtime)
if (process.env.NEXT_RUNTIME === 'nodejs') {
// Validate environment variables before anything else
const { validateEnvironmentOnStartup } = await import('@/lib/utils/env-validator')
validateEnvironmentOnStartup()
// SECURITY: Validate authentication configuration at startup
validateAuthenticationSetup()
// Validate monitoring configuration
validateMonitoringSetup()
// SECURITY: Validate encryption configuration at startup (server-side only)
// Use dynamic import to avoid crypto module issues during build
try {
const { validateEncryptionSetup } = await import('@/lib/security/encryption-validator.server')
await validateEncryptionSetup()
} catch (error) {
console.error('[instrumentation] Failed to load encryption validator:', error)
console.error(
'[instrumentation] Error details:',
error instanceof Error ? error.message : String(error)
)
if (process.env.NODE_ENV === 'production') {
throw error
}
}
// Initialize Sentry for server-side error tracking
// Skip in development to avoid edge runtime issues
if (process.env.SENTRY_DSN && process.env.NODE_ENV === 'production') {
try {
const Sentry = await import('@sentry/nextjs')
Sentry.init({
dsn: process.env.SENTRY_DSN,
// Performance Monitoring
tracesSampleRate: Number(
process.env.SENTRY_TRACES_SAMPLE_RATE ??
(process.env.NODE_ENV === 'production' ? '0.1' : '1.0')
),
profilesSampleRate: Number(process.env.SENTRY_PROFILES_SAMPLE_RATE ?? '0.1'),
// Session Replay
replaysSessionSampleRate: 0.1,
replaysOnErrorSampleRate: 1.0,
// Environment
environment: process.env.NODE_ENV || 'development',
// Release tracking
release:
process.env.VERCEL_GIT_COMMIT_SHA || process.env.NETLIFY_BUILD_ID || 'development',
// Enable profiling for slow transactions
enableTracing: true,
// Filter sensitive data
beforeSend(event) {
// Remove sensitive headers
if (event.request?.headers) {
delete event.request.headers['authorization']
delete event.request.headers['cookie']
delete event.request.headers['x-api-key']
}
// Remove query parameters with sensitive data
if (event.request?.url) {
try {
const url = new URL(event.request.url)
const sensitiveParams = ['key', 'token', 'api_key', 'password']
sensitiveParams.forEach((paramName) => {
if (url.searchParams.has(paramName)) {
url.searchParams.delete(paramName)
}
})
event.request.url = url.toString()
} catch {
// Invalid URL, skip sanitization
}
}
return event
},
// Ignore certain errors
ignoreErrors: [
'ResizeObserver loop limit exceeded',
'Non-Error promise rejection captured',
'Network request failed',
'AbortError',
'cancelled',
],
// Configure integrations
integrations: [
Sentry.httpIntegration({
// Track slow HTTP requests
// Note: shouldCreateSpanForRequest moved to top-level tracesSampler
}),
],
// Custom trace sampling for performance monitoring
tracesSampler: (samplingContext) => {
const url = samplingContext.request?.url || ''
// Track all API routes and external API calls
if (
url.includes('/api/') ||
url.includes('courtlistener') ||
url.includes('supabase')
) {
return 1.0 // 100% sampling for API calls
}
return 0.1 // 10% for other requests
},
})
console.log('[instrumentation] Sentry initialized for nodejs runtime')
} catch (error) {
console.error('[instrumentation] Failed to initialize Sentry:', error)
console.error(
'[instrumentation] Sentry error details:',
error instanceof Error ? error.message : String(error)
)
// Don't throw - allow site to continue without Sentry
}
} else {
console.warn('[instrumentation] SENTRY_DSN not configured - error tracking disabled')
}
}
}
/**
* Optional: onRequestError hook for custom error handling
*
* @see https://nextjs.org/docs/app/api-reference/file-conventions/instrumentation
*/
export async function onRequestError(
err: Error,
request: Request,
context: {
routerKind: 'pages' | 'app'
routePath: string
routeType: 'render' | 'route' | 'action' | 'middleware'
}
) {
// Log error with context
console.error('[instrumentation] Request error:', {
error: err.message,
stack: err.stack,
url: request.url,
method: request.method,
context,
})
// Send to Sentry if initialized
if (process.env.SENTRY_DSN) {
try {
const Sentry = await import('@sentry/nextjs')
Sentry.captureException(err, {
contexts: {
nextjs: {
router_kind: context.routerKind,
route_path: context.routePath,
route_type: context.routeType,
},
},
tags: {
route_path: context.routePath,
route_type: context.routeType,
},
})
} catch {
// Sentry not available, error already logged
}
}
}