@engjts/nexus 0.1.8 → 0.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/BENCHMARK_REPORT.md +0 -343
- package/documentation/01-getting-started.md +0 -240
- package/documentation/02-context.md +0 -335
- package/documentation/03-routing.md +0 -397
- package/documentation/04-middleware.md +0 -483
- package/documentation/05-validation.md +0 -514
- package/documentation/06-error-handling.md +0 -465
- package/documentation/07-performance.md +0 -364
- package/documentation/08-adapters.md +0 -470
- package/documentation/09-api-reference.md +0 -548
- package/documentation/10-examples.md +0 -582
- package/documentation/11-deployment.md +0 -477
- package/documentation/12-sentry.md +0 -620
- package/documentation/13-sentry-data-storage.md +0 -996
- package/documentation/14-sentry-data-reference.md +0 -457
- package/documentation/15-sentry-summary.md +0 -409
- package/documentation/16-alerts-system.md +0 -745
- package/documentation/17-alert-adapters.md +0 -696
- package/documentation/18-alerts-implementation-summary.md +0 -385
- package/documentation/19-class-based-routing.md +0 -840
- package/documentation/20-websocket-realtime.md +0 -813
- package/documentation/21-cache-system.md +0 -510
- package/documentation/22-job-queue.md +0 -772
- package/documentation/23-sentry-plugin.md +0 -551
- package/documentation/24-testing-utilities.md +0 -1287
- package/documentation/25-api-versioning.md +0 -533
- package/documentation/26-context-store.md +0 -607
- package/documentation/27-dependency-injection.md +0 -329
- package/documentation/28-lifecycle-hooks.md +0 -521
- package/documentation/29-package-structure.md +0 -196
- package/documentation/30-plugin-system.md +0 -414
- package/documentation/31-jwt-authentication.md +0 -597
- package/documentation/32-cli.md +0 -268
- package/documentation/ALERTS-COMPLETE-SUMMARY.md +0 -429
- package/documentation/ALERTS-INDEX.md +0 -330
- package/documentation/ALERTS-QUICK-REFERENCE.md +0 -286
- package/documentation/README.md +0 -178
- package/documentation/index.html +0 -34
- package/modern_framework_paper.md +0 -1870
- package/public/css/style.css +0 -87
- package/public/index.html +0 -34
- package/public/js/app.js +0 -27
- package/src/advanced/cache/InMemoryCacheStore.ts +0 -68
- package/src/advanced/cache/MultiTierCache.ts +0 -194
- package/src/advanced/cache/RedisCacheStore.ts +0 -341
- package/src/advanced/cache/index.ts +0 -5
- package/src/advanced/cache/types.ts +0 -40
- package/src/advanced/graphql/SimpleDataLoader.ts +0 -42
- package/src/advanced/graphql/index.ts +0 -22
- package/src/advanced/graphql/server.ts +0 -252
- package/src/advanced/graphql/types.ts +0 -42
- package/src/advanced/jobs/InMemoryQueueStore.ts +0 -68
- package/src/advanced/jobs/JobQueue.ts +0 -556
- package/src/advanced/jobs/RedisQueueStore.ts +0 -367
- package/src/advanced/jobs/index.ts +0 -5
- package/src/advanced/jobs/types.ts +0 -70
- package/src/advanced/observability/APMManager.ts +0 -163
- package/src/advanced/observability/AlertManager.ts +0 -109
- package/src/advanced/observability/MetricRegistry.ts +0 -151
- package/src/advanced/observability/ObservabilityCenter.ts +0 -304
- package/src/advanced/observability/StructuredLogger.ts +0 -154
- package/src/advanced/observability/TracingManager.ts +0 -117
- package/src/advanced/observability/adapters.ts +0 -304
- package/src/advanced/observability/createObservabilityMiddleware.ts +0 -63
- package/src/advanced/observability/index.ts +0 -11
- package/src/advanced/observability/types.ts +0 -174
- package/src/advanced/playground/extractPathParams.ts +0 -6
- package/src/advanced/playground/generateFieldExample.ts +0 -31
- package/src/advanced/playground/generatePlaygroundHTML.ts +0 -1956
- package/src/advanced/playground/generateSummary.ts +0 -19
- package/src/advanced/playground/getTagFromPath.ts +0 -9
- package/src/advanced/playground/index.ts +0 -8
- package/src/advanced/playground/playground.ts +0 -250
- package/src/advanced/playground/types.ts +0 -49
- package/src/advanced/playground/zodToExample.ts +0 -16
- package/src/advanced/playground/zodToParams.ts +0 -15
- package/src/advanced/postman/buildAuth.ts +0 -31
- package/src/advanced/postman/buildBody.ts +0 -15
- package/src/advanced/postman/buildQueryParams.ts +0 -27
- package/src/advanced/postman/buildRequestItem.ts +0 -36
- package/src/advanced/postman/buildResponses.ts +0 -11
- package/src/advanced/postman/buildUrl.ts +0 -33
- package/src/advanced/postman/capitalize.ts +0 -4
- package/src/advanced/postman/generateCollection.ts +0 -59
- package/src/advanced/postman/generateEnvironment.ts +0 -34
- package/src/advanced/postman/generateExampleFromZod.ts +0 -21
- package/src/advanced/postman/generateFieldExample.ts +0 -45
- package/src/advanced/postman/generateName.ts +0 -20
- package/src/advanced/postman/generateUUID.ts +0 -11
- package/src/advanced/postman/getTagFromPath.ts +0 -10
- package/src/advanced/postman/index.ts +0 -28
- package/src/advanced/postman/postman.ts +0 -156
- package/src/advanced/postman/slugify.ts +0 -7
- package/src/advanced/postman/types.ts +0 -140
- package/src/advanced/realtime/index.ts +0 -18
- package/src/advanced/realtime/websocket.ts +0 -231
- package/src/advanced/sentry/index.ts +0 -1236
- package/src/advanced/sentry/types.ts +0 -355
- package/src/advanced/static/generateDirectoryListing.ts +0 -47
- package/src/advanced/static/generateETag.ts +0 -7
- package/src/advanced/static/getMimeType.ts +0 -9
- package/src/advanced/static/index.ts +0 -32
- package/src/advanced/static/isSafePath.ts +0 -13
- package/src/advanced/static/publicDir.ts +0 -21
- package/src/advanced/static/serveStatic.ts +0 -225
- package/src/advanced/static/spa.ts +0 -24
- package/src/advanced/static/types.ts +0 -159
- package/src/advanced/swagger/SwaggerGenerator.ts +0 -66
- package/src/advanced/swagger/buildOperation.ts +0 -61
- package/src/advanced/swagger/buildParameters.ts +0 -61
- package/src/advanced/swagger/buildRequestBody.ts +0 -21
- package/src/advanced/swagger/buildResponses.ts +0 -54
- package/src/advanced/swagger/capitalize.ts +0 -5
- package/src/advanced/swagger/convertPath.ts +0 -9
- package/src/advanced/swagger/createSwagger.ts +0 -12
- package/src/advanced/swagger/generateOperationId.ts +0 -21
- package/src/advanced/swagger/generateSpec.ts +0 -105
- package/src/advanced/swagger/generateSummary.ts +0 -24
- package/src/advanced/swagger/generateSwaggerUI.ts +0 -70
- package/src/advanced/swagger/generateThemeCss.ts +0 -53
- package/src/advanced/swagger/index.ts +0 -25
- package/src/advanced/swagger/swagger.ts +0 -237
- package/src/advanced/swagger/types.ts +0 -206
- package/src/advanced/swagger/zodFieldToOpenAPI.ts +0 -94
- package/src/advanced/swagger/zodSchemaToOpenAPI.ts +0 -50
- package/src/advanced/swagger/zodToOpenAPI.ts +0 -22
- package/src/advanced/testing/factory.ts +0 -509
- package/src/advanced/testing/harness.ts +0 -612
- package/src/advanced/testing/index.ts +0 -430
- package/src/advanced/testing/load-test.ts +0 -618
- package/src/advanced/testing/mock-server.ts +0 -498
- package/src/advanced/testing/mock.ts +0 -670
- package/src/cli/bin.ts +0 -9
- package/src/cli/cli.ts +0 -158
- package/src/cli/commands/add.ts +0 -178
- package/src/cli/commands/build.ts +0 -73
- package/src/cli/commands/create.ts +0 -166
- package/src/cli/commands/dev.ts +0 -85
- package/src/cli/commands/generate.ts +0 -99
- package/src/cli/commands/help.ts +0 -95
- package/src/cli/commands/init.ts +0 -91
- package/src/cli/commands/version.ts +0 -38
- package/src/cli/index.ts +0 -6
- package/src/cli/templates/generators.ts +0 -359
- package/src/cli/templates/index.ts +0 -680
- package/src/cli/utils/exec.ts +0 -52
- package/src/cli/utils/file-system.ts +0 -78
- package/src/cli/utils/logger.ts +0 -111
- package/src/core/adapter.ts +0 -88
- package/src/core/application.ts +0 -1453
- package/src/core/context-pool.ts +0 -79
- package/src/core/context.ts +0 -856
- package/src/core/index.ts +0 -94
- package/src/core/middleware.ts +0 -272
- package/src/core/performance/buffer-pool.ts +0 -108
- package/src/core/performance/middleware-optimizer.ts +0 -162
- package/src/core/plugin/PluginManager.ts +0 -435
- package/src/core/plugin/builder.ts +0 -358
- package/src/core/plugin/index.ts +0 -50
- package/src/core/plugin/types.ts +0 -214
- package/src/core/router/file-router.ts +0 -623
- package/src/core/router/index.ts +0 -260
- package/src/core/router/radix-tree.ts +0 -242
- package/src/core/serializer.ts +0 -397
- package/src/core/store/index.ts +0 -30
- package/src/core/store/registry.ts +0 -178
- package/src/core/store/request-store.ts +0 -240
- package/src/core/store/types.ts +0 -233
- package/src/core/types.ts +0 -616
- package/src/database/adapter.ts +0 -35
- package/src/database/adapters/index.ts +0 -1
- package/src/database/adapters/mysql.ts +0 -669
- package/src/database/database.ts +0 -70
- package/src/database/dialect.ts +0 -388
- package/src/database/index.ts +0 -12
- package/src/database/migrations.ts +0 -86
- package/src/database/optimizer.ts +0 -125
- package/src/database/query-builder.ts +0 -404
- package/src/database/realtime.ts +0 -53
- package/src/database/schema.ts +0 -71
- package/src/database/transactions.ts +0 -56
- package/src/database/types.ts +0 -87
- package/src/deployment/cluster.ts +0 -471
- package/src/deployment/config.ts +0 -454
- package/src/deployment/docker.ts +0 -599
- package/src/deployment/graceful-shutdown.ts +0 -373
- package/src/deployment/index.ts +0 -56
- package/src/index.ts +0 -281
- package/src/security/adapter.ts +0 -318
- package/src/security/auth/JWTPlugin.ts +0 -234
- package/src/security/auth/JWTProvider.ts +0 -316
- package/src/security/auth/adapter.ts +0 -12
- package/src/security/auth/jwt.ts +0 -234
- package/src/security/auth/middleware.ts +0 -188
- package/src/security/csrf.ts +0 -220
- package/src/security/headers.ts +0 -108
- package/src/security/index.ts +0 -60
- package/src/security/rate-limit/adapter.ts +0 -7
- package/src/security/rate-limit/memory.ts +0 -108
- package/src/security/rate-limit/middleware.ts +0 -181
- package/src/security/sanitization.ts +0 -75
- package/src/security/types.ts +0 -240
- package/src/security/utils.ts +0 -52
- package/tsconfig.json +0 -39
|
@@ -1,1236 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Sentry Integration for Nexus Framework
|
|
3
|
-
*
|
|
4
|
-
* Built-in error tracking, performance monitoring, and session replay
|
|
5
|
-
* with seamless integration to Sentry.io
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { Context, Middleware, Response } from '../../core/types';
|
|
9
|
-
import { randomUUID } from 'crypto';
|
|
10
|
-
import { APMOptions, MemoryLeakStats, MemorySnapshot, ParsedDSN, SentryBreadcrumb, SentryEvent, SentryMiddlewareOptions, SentryOptions, SentrySpan, SentryStackFrame, SentryTransaction, SentryUser, SeverityLevel, SlowQueryRecord } from './types';
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
// ============================================
|
|
14
|
-
// APM Manager (Integrated into Sentry)
|
|
15
|
-
// ============================================
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Built-in APM (Application Performance Monitoring)
|
|
19
|
-
* Automatically enabled when Sentry is initialized
|
|
20
|
-
*/
|
|
21
|
-
class APMIntegration {
|
|
22
|
-
private options: Required<APMOptions>;
|
|
23
|
-
private slowQueries: SlowQueryRecord[] = [];
|
|
24
|
-
private memorySnapshots: MemorySnapshot[] = [];
|
|
25
|
-
private memoryCheckInterval?: NodeJS.Timeout;
|
|
26
|
-
private sentryClient?: SentryClient;
|
|
27
|
-
|
|
28
|
-
constructor(options: APMOptions = {}, sentryClient?: SentryClient) {
|
|
29
|
-
this.sentryClient = sentryClient;
|
|
30
|
-
this.options = {
|
|
31
|
-
enabled: options.enabled ?? true,
|
|
32
|
-
slowQueryThreshold: options.slowQueryThreshold ?? 1000,
|
|
33
|
-
maxSlowQueries: options.maxSlowQueries ?? 100,
|
|
34
|
-
memoryLeakDetection: {
|
|
35
|
-
enabled: options.memoryLeakDetection?.enabled ?? true,
|
|
36
|
-
interval: options.memoryLeakDetection?.interval ?? 60000,
|
|
37
|
-
growthThreshold: options.memoryLeakDetection?.growthThreshold ?? 0.5,
|
|
38
|
-
maxSnapshots: options.memoryLeakDetection?.maxSnapshots ?? 60
|
|
39
|
-
},
|
|
40
|
-
onMemoryLeak: options.onMemoryLeak,
|
|
41
|
-
onSlowQuery: options.onSlowQuery
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
if (this.options.enabled && this.options.memoryLeakDetection.enabled) {
|
|
45
|
-
this.startMemoryMonitoring();
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Record a database/external query
|
|
51
|
-
* Automatically reports slow queries to Sentry as breadcrumbs
|
|
52
|
-
*/
|
|
53
|
-
recordQuery(query: string, durationMs: number, metadata?: Record<string, any>): void {
|
|
54
|
-
if (!this.options.enabled) return;
|
|
55
|
-
|
|
56
|
-
const threshold = this.options.slowQueryThreshold;
|
|
57
|
-
if (durationMs >= threshold) {
|
|
58
|
-
const record: SlowQueryRecord = {
|
|
59
|
-
query: query.slice(0, 500), // Truncate long queries
|
|
60
|
-
duration: durationMs,
|
|
61
|
-
timestamp: Date.now()
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
this.slowQueries.push(record);
|
|
65
|
-
|
|
66
|
-
// Keep limited slow queries
|
|
67
|
-
if (this.slowQueries.length > this.options.maxSlowQueries) {
|
|
68
|
-
this.slowQueries.shift();
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// Report to Sentry as breadcrumb
|
|
72
|
-
if (this.sentryClient) {
|
|
73
|
-
this.sentryClient.addBreadcrumb({
|
|
74
|
-
type: 'query',
|
|
75
|
-
category: 'db.query',
|
|
76
|
-
message: `Slow query: ${query.slice(0, 100)}...`,
|
|
77
|
-
data: {
|
|
78
|
-
duration_ms: durationMs,
|
|
79
|
-
threshold_ms: threshold,
|
|
80
|
-
...metadata
|
|
81
|
-
},
|
|
82
|
-
level: durationMs > threshold * 2 ? 'warning' : 'info'
|
|
83
|
-
});
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// Callback
|
|
87
|
-
this.options.onSlowQuery?.(record);
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Get all recorded slow queries
|
|
93
|
-
*/
|
|
94
|
-
getSlowQueries(): SlowQueryRecord[] {
|
|
95
|
-
return [...this.slowQueries];
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Clear slow query history
|
|
100
|
-
*/
|
|
101
|
-
clearSlowQueries(): void {
|
|
102
|
-
this.slowQueries = [];
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
private startMemoryMonitoring(): void {
|
|
106
|
-
const { interval, maxSnapshots } = this.options.memoryLeakDetection;
|
|
107
|
-
|
|
108
|
-
this.memoryCheckInterval = setInterval(() => {
|
|
109
|
-
const usage = process.memoryUsage();
|
|
110
|
-
this.memorySnapshots.push({
|
|
111
|
-
timestamp: Date.now(),
|
|
112
|
-
heapUsed: usage.heapUsed,
|
|
113
|
-
heapTotal: usage.heapTotal
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
// Keep limited snapshots
|
|
117
|
-
if (this.memorySnapshots.length > maxSnapshots) {
|
|
118
|
-
this.memorySnapshots.shift();
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// Check for potential memory leak
|
|
122
|
-
this.checkMemoryLeak();
|
|
123
|
-
}, interval);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
private checkMemoryLeak(): void {
|
|
127
|
-
if (this.memorySnapshots.length < 10) return;
|
|
128
|
-
|
|
129
|
-
const { growthThreshold } = this.options.memoryLeakDetection;
|
|
130
|
-
const recent = this.memorySnapshots.slice(-10);
|
|
131
|
-
const oldest = recent[0].heapUsed;
|
|
132
|
-
const newest = recent[recent.length - 1].heapUsed;
|
|
133
|
-
const growth = (newest - oldest) / oldest;
|
|
134
|
-
|
|
135
|
-
// Alert if memory grew more than threshold in the monitoring window
|
|
136
|
-
if (growth > growthThreshold) {
|
|
137
|
-
const stats: MemoryLeakStats = {
|
|
138
|
-
growth,
|
|
139
|
-
growthPercent: `${(growth * 100).toFixed(1)}%`,
|
|
140
|
-
fromMB: `${(oldest / 1024 / 1024).toFixed(1)}MB`,
|
|
141
|
-
toMB: `${(newest / 1024 / 1024).toFixed(1)}MB`,
|
|
142
|
-
snapshots: recent
|
|
143
|
-
};
|
|
144
|
-
|
|
145
|
-
// Report to Sentry
|
|
146
|
-
if (this.sentryClient) {
|
|
147
|
-
this.sentryClient.captureMessage(
|
|
148
|
-
`Potential memory leak detected: ${stats.growthPercent} growth`,
|
|
149
|
-
{
|
|
150
|
-
level: 'warning',
|
|
151
|
-
tags: { type: 'memory_leak' },
|
|
152
|
-
extra: stats
|
|
153
|
-
}
|
|
154
|
-
);
|
|
155
|
-
|
|
156
|
-
this.sentryClient.addBreadcrumb({
|
|
157
|
-
type: 'debug',
|
|
158
|
-
category: 'memory',
|
|
159
|
-
message: `Memory grew ${stats.growthPercent}`,
|
|
160
|
-
data: {
|
|
161
|
-
from: stats.fromMB,
|
|
162
|
-
to: stats.toMB
|
|
163
|
-
},
|
|
164
|
-
level: 'warning'
|
|
165
|
-
});
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// Callback
|
|
169
|
-
this.options.onMemoryLeak?.(stats);
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
/**
|
|
174
|
-
* Get current memory statistics
|
|
175
|
-
*/
|
|
176
|
-
getMemoryStats(): {
|
|
177
|
-
current: {
|
|
178
|
-
heapUsed: number;
|
|
179
|
-
heapTotal: number;
|
|
180
|
-
external: number;
|
|
181
|
-
rss: number;
|
|
182
|
-
};
|
|
183
|
-
history: MemorySnapshot[];
|
|
184
|
-
} {
|
|
185
|
-
const current = process.memoryUsage();
|
|
186
|
-
return {
|
|
187
|
-
current: {
|
|
188
|
-
heapUsed: current.heapUsed,
|
|
189
|
-
heapTotal: current.heapTotal,
|
|
190
|
-
external: current.external,
|
|
191
|
-
rss: current.rss
|
|
192
|
-
},
|
|
193
|
-
history: [...this.memorySnapshots]
|
|
194
|
-
};
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
/**
|
|
198
|
-
* Stop APM monitoring
|
|
199
|
-
*/
|
|
200
|
-
stop(): void {
|
|
201
|
-
if (this.memoryCheckInterval) {
|
|
202
|
-
clearInterval(this.memoryCheckInterval);
|
|
203
|
-
this.memoryCheckInterval = undefined;
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
// ============================================
|
|
211
|
-
// Sentry Client
|
|
212
|
-
// ============================================
|
|
213
|
-
|
|
214
|
-
/**
|
|
215
|
-
* Nexus Sentry Client
|
|
216
|
-
* Lightweight, built-in Sentry integration with APM
|
|
217
|
-
*/
|
|
218
|
-
export class SentryClient {
|
|
219
|
-
private options: Required<Omit<SentryOptions, 'apm'>> & { apm: APMOptions };
|
|
220
|
-
private dsn: ParsedDSN;
|
|
221
|
-
private breadcrumbs: SentryBreadcrumb[] = [];
|
|
222
|
-
private user?: SentryUser;
|
|
223
|
-
private globalTags: Record<string, string> = {};
|
|
224
|
-
private globalExtra: Record<string, any> = {};
|
|
225
|
-
private activeTransactions: Map<string, SentryTransaction> = new Map();
|
|
226
|
-
private isEnabled: boolean = true;
|
|
227
|
-
private apm: APMIntegration;
|
|
228
|
-
|
|
229
|
-
constructor(options: SentryOptions) {
|
|
230
|
-
// Parse and validate DSN
|
|
231
|
-
this.dsn = this.parseDSN(options.dsn);
|
|
232
|
-
|
|
233
|
-
this.options = {
|
|
234
|
-
dsn: options.dsn,
|
|
235
|
-
environment: options.environment ?? process.env.NODE_ENV ?? 'production',
|
|
236
|
-
release: options.release ?? process.env.npm_package_version ?? 'unknown',
|
|
237
|
-
serverName: options.serverName ?? this.getServerName(),
|
|
238
|
-
sampleRate: options.sampleRate ?? 1.0,
|
|
239
|
-
tracesSampleRate: options.tracesSampleRate ?? 0.1,
|
|
240
|
-
enableTracing: options.enableTracing ?? true,
|
|
241
|
-
maxBreadcrumbs: options.maxBreadcrumbs ?? 100,
|
|
242
|
-
debug: options.debug ?? false,
|
|
243
|
-
attachStacktrace: options.attachStacktrace ?? false,
|
|
244
|
-
sendDefaultPii: options.sendDefaultPii ?? false,
|
|
245
|
-
beforeSend: options.beforeSend ?? ((event) => event),
|
|
246
|
-
beforeSendTransaction: options.beforeSendTransaction ?? ((tx) => tx),
|
|
247
|
-
tags: options.tags ?? {},
|
|
248
|
-
extra: options.extra ?? {},
|
|
249
|
-
ignorePaths: options.ignorePaths ?? [],
|
|
250
|
-
ignoreErrors: options.ignoreErrors ?? [],
|
|
251
|
-
timeout: options.timeout ?? 5000,
|
|
252
|
-
integrations: {
|
|
253
|
-
http: options.integrations?.http ?? true,
|
|
254
|
-
console: options.integrations?.console ?? false,
|
|
255
|
-
unhandledRejection: options.integrations?.unhandledRejection ?? true,
|
|
256
|
-
uncaughtException: options.integrations?.uncaughtException ?? true
|
|
257
|
-
},
|
|
258
|
-
apm: options.apm ?? {}
|
|
259
|
-
};
|
|
260
|
-
|
|
261
|
-
this.globalTags = { ...this.options.tags };
|
|
262
|
-
this.globalExtra = { ...this.options.extra };
|
|
263
|
-
|
|
264
|
-
// Initialize APM integration
|
|
265
|
-
this.apm = new APMIntegration(this.options.apm, this);
|
|
266
|
-
|
|
267
|
-
// Setup global handlers
|
|
268
|
-
this.setupIntegrations();
|
|
269
|
-
|
|
270
|
-
this.log('Sentry initialized', { environment: this.options.environment });
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
// ============================================
|
|
274
|
-
// DSN Parsing
|
|
275
|
-
// ============================================
|
|
276
|
-
|
|
277
|
-
private parseDSN(dsn: string): ParsedDSN {
|
|
278
|
-
try {
|
|
279
|
-
const url = new URL(dsn);
|
|
280
|
-
const pathParts = url.pathname.split('/').filter(Boolean);
|
|
281
|
-
|
|
282
|
-
return {
|
|
283
|
-
protocol: url.protocol.replace(':', ''),
|
|
284
|
-
publicKey: url.username,
|
|
285
|
-
host: url.host,
|
|
286
|
-
projectId: pathParts[pathParts.length - 1]
|
|
287
|
-
};
|
|
288
|
-
} catch {
|
|
289
|
-
throw new Error(`Invalid Sentry DSN: ${dsn}`);
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
private getEnvelopeEndpoint(): string {
|
|
294
|
-
return `${this.dsn.protocol}://${this.dsn.host}/api/${this.dsn.projectId}/envelope/`;
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
private getServerName(): string {
|
|
298
|
-
try {
|
|
299
|
-
return require('os').hostname();
|
|
300
|
-
} catch {
|
|
301
|
-
return 'unknown';
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
// ============================================
|
|
306
|
-
// Integrations
|
|
307
|
-
// ============================================
|
|
308
|
-
|
|
309
|
-
private setupIntegrations(): void {
|
|
310
|
-
if (this.options.integrations.unhandledRejection) {
|
|
311
|
-
process.on('unhandledRejection', (reason) => {
|
|
312
|
-
this.captureException(reason instanceof Error ? reason : new Error(String(reason)), {
|
|
313
|
-
tags: { mechanism: 'unhandledRejection' }
|
|
314
|
-
});
|
|
315
|
-
});
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
if (this.options.integrations.uncaughtException) {
|
|
319
|
-
process.on('uncaughtException', (error) => {
|
|
320
|
-
this.captureException(error, {
|
|
321
|
-
tags: { mechanism: 'uncaughtException' },
|
|
322
|
-
level: 'fatal'
|
|
323
|
-
});
|
|
324
|
-
// Allow time for the event to be sent
|
|
325
|
-
setTimeout(() => process.exit(1), 2000);
|
|
326
|
-
});
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
// ============================================
|
|
331
|
-
// Core Methods
|
|
332
|
-
// ============================================
|
|
333
|
-
|
|
334
|
-
/**
|
|
335
|
-
* Capture an exception and send to Sentry
|
|
336
|
-
*/
|
|
337
|
-
captureException(
|
|
338
|
-
error: Error | unknown,
|
|
339
|
-
options: {
|
|
340
|
-
level?: SeverityLevel;
|
|
341
|
-
tags?: Record<string, string>;
|
|
342
|
-
extra?: Record<string, any>;
|
|
343
|
-
user?: SentryUser;
|
|
344
|
-
fingerprint?: string[];
|
|
345
|
-
contexts?: Record<string, any>;
|
|
346
|
-
} = {}
|
|
347
|
-
): string {
|
|
348
|
-
if (!this.isEnabled || !this.shouldSample(this.options.sampleRate)) {
|
|
349
|
-
return '';
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
const err = error instanceof Error ? error : new Error(String(error));
|
|
353
|
-
|
|
354
|
-
// Check ignore patterns
|
|
355
|
-
if (this.shouldIgnoreError(err)) {
|
|
356
|
-
return '';
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
const eventId = this.generateEventId();
|
|
360
|
-
const event = this.buildErrorEvent(err, eventId, options);
|
|
361
|
-
|
|
362
|
-
// Apply beforeSend hook
|
|
363
|
-
const processedEvent = this.options.beforeSend(event, { originalException: err });
|
|
364
|
-
if (!processedEvent) {
|
|
365
|
-
this.log('Event dropped by beforeSend');
|
|
366
|
-
return '';
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
this.sendEvent(processedEvent);
|
|
370
|
-
return eventId;
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
/**
|
|
374
|
-
* Capture a message
|
|
375
|
-
*/
|
|
376
|
-
captureMessage(
|
|
377
|
-
message: string,
|
|
378
|
-
options: {
|
|
379
|
-
level?: SeverityLevel;
|
|
380
|
-
tags?: Record<string, string>;
|
|
381
|
-
extra?: Record<string, any>;
|
|
382
|
-
user?: SentryUser;
|
|
383
|
-
fingerprint?: string[];
|
|
384
|
-
} = {}
|
|
385
|
-
): string {
|
|
386
|
-
if (!this.isEnabled || !this.shouldSample(this.options.sampleRate)) {
|
|
387
|
-
return '';
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
const eventId = this.generateEventId();
|
|
391
|
-
const event: SentryEvent = {
|
|
392
|
-
event_id: eventId,
|
|
393
|
-
timestamp: Date.now() / 1000,
|
|
394
|
-
platform: 'node',
|
|
395
|
-
level: options.level ?? 'info',
|
|
396
|
-
message,
|
|
397
|
-
environment: this.options.environment,
|
|
398
|
-
release: this.options.release,
|
|
399
|
-
server_name: this.options.serverName,
|
|
400
|
-
user: options.user ?? this.user,
|
|
401
|
-
tags: { ...this.globalTags, ...options.tags },
|
|
402
|
-
extra: { ...this.globalExtra, ...options.extra },
|
|
403
|
-
breadcrumbs: [...this.breadcrumbs],
|
|
404
|
-
fingerprint: options.fingerprint
|
|
405
|
-
};
|
|
406
|
-
|
|
407
|
-
const processedEvent = this.options.beforeSend(event);
|
|
408
|
-
if (!processedEvent) {
|
|
409
|
-
return '';
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
this.sendEvent(processedEvent);
|
|
413
|
-
return eventId;
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
/**
|
|
417
|
-
* Add a breadcrumb
|
|
418
|
-
*/
|
|
419
|
-
addBreadcrumb(breadcrumb: SentryBreadcrumb): void {
|
|
420
|
-
const crumb: SentryBreadcrumb = {
|
|
421
|
-
timestamp: Date.now() / 1000,
|
|
422
|
-
...breadcrumb
|
|
423
|
-
};
|
|
424
|
-
|
|
425
|
-
this.breadcrumbs.push(crumb);
|
|
426
|
-
|
|
427
|
-
// Limit breadcrumbs
|
|
428
|
-
if (this.breadcrumbs.length > this.options.maxBreadcrumbs) {
|
|
429
|
-
this.breadcrumbs.shift();
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
/**
|
|
434
|
-
* Set user context
|
|
435
|
-
*/
|
|
436
|
-
setUser(user: SentryUser | null): void {
|
|
437
|
-
this.user = user ?? undefined;
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
/**
|
|
441
|
-
* Set a global tag
|
|
442
|
-
*/
|
|
443
|
-
setTag(key: string, value: string): void {
|
|
444
|
-
this.globalTags[key] = value;
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
/**
|
|
448
|
-
* Set multiple tags
|
|
449
|
-
*/
|
|
450
|
-
setTags(tags: Record<string, string>): void {
|
|
451
|
-
Object.assign(this.globalTags, tags);
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
/**
|
|
455
|
-
* Set extra context
|
|
456
|
-
*/
|
|
457
|
-
setExtra(key: string, value: any): void {
|
|
458
|
-
this.globalExtra[key] = value;
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
/**
|
|
462
|
-
* Set multiple extra values
|
|
463
|
-
*/
|
|
464
|
-
setExtras(extras: Record<string, any>): void {
|
|
465
|
-
Object.assign(this.globalExtra, extras);
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
/**
|
|
469
|
-
* Clear breadcrumbs
|
|
470
|
-
*/
|
|
471
|
-
clearBreadcrumbs(): void {
|
|
472
|
-
this.breadcrumbs = [];
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
// ============================================
|
|
476
|
-
// Performance Monitoring
|
|
477
|
-
// ============================================
|
|
478
|
-
|
|
479
|
-
/**
|
|
480
|
-
* Start a transaction for performance monitoring
|
|
481
|
-
*/
|
|
482
|
-
startTransaction(options: {
|
|
483
|
-
name: string;
|
|
484
|
-
op: string;
|
|
485
|
-
tags?: Record<string, string>;
|
|
486
|
-
data?: Record<string, any>;
|
|
487
|
-
}): SentryTransaction | null {
|
|
488
|
-
if (!this.options.enableTracing || !this.shouldSample(this.options.tracesSampleRate)) {
|
|
489
|
-
return null;
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
const transaction: SentryTransaction = {
|
|
493
|
-
trace_id: this.generateTraceId(),
|
|
494
|
-
span_id: this.generateSpanId(),
|
|
495
|
-
name: options.name,
|
|
496
|
-
op: options.op,
|
|
497
|
-
status: 'ok',
|
|
498
|
-
start_timestamp: Date.now() / 1000,
|
|
499
|
-
tags: options.tags,
|
|
500
|
-
data: options.data,
|
|
501
|
-
spans: []
|
|
502
|
-
};
|
|
503
|
-
|
|
504
|
-
this.activeTransactions.set(transaction.span_id, transaction);
|
|
505
|
-
return transaction;
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
/**
|
|
509
|
-
* Start a child span within a transaction
|
|
510
|
-
*/
|
|
511
|
-
startSpan(
|
|
512
|
-
transaction: SentryTransaction,
|
|
513
|
-
options: {
|
|
514
|
-
op: string;
|
|
515
|
-
description?: string;
|
|
516
|
-
tags?: Record<string, string>;
|
|
517
|
-
data?: Record<string, any>;
|
|
518
|
-
}
|
|
519
|
-
): SentrySpan {
|
|
520
|
-
const span: SentrySpan = {
|
|
521
|
-
trace_id: transaction.trace_id,
|
|
522
|
-
span_id: this.generateSpanId(),
|
|
523
|
-
parent_span_id: transaction.span_id,
|
|
524
|
-
op: options.op,
|
|
525
|
-
description: options.description,
|
|
526
|
-
start_timestamp: Date.now() / 1000,
|
|
527
|
-
tags: options.tags,
|
|
528
|
-
data: options.data
|
|
529
|
-
};
|
|
530
|
-
|
|
531
|
-
transaction.spans = transaction.spans ?? [];
|
|
532
|
-
transaction.spans.push(span);
|
|
533
|
-
|
|
534
|
-
return span;
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
/**
|
|
538
|
-
* Finish a span
|
|
539
|
-
*/
|
|
540
|
-
finishSpan(span: SentrySpan, status?: string): void {
|
|
541
|
-
span.timestamp = Date.now() / 1000;
|
|
542
|
-
span.status = status ?? 'ok';
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
/**
|
|
546
|
-
* Finish a transaction and send to Sentry
|
|
547
|
-
*/
|
|
548
|
-
finishTransaction(transaction: SentryTransaction, status?: SentryTransaction['status']): void {
|
|
549
|
-
transaction.timestamp = Date.now() / 1000;
|
|
550
|
-
transaction.status = status ?? 'ok';
|
|
551
|
-
|
|
552
|
-
this.activeTransactions.delete(transaction.span_id);
|
|
553
|
-
|
|
554
|
-
const processedTx = this.options.beforeSendTransaction(transaction);
|
|
555
|
-
if (!processedTx) {
|
|
556
|
-
this.log('Transaction dropped by beforeSendTransaction');
|
|
557
|
-
return;
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
this.sendTransaction(processedTx);
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
// ============================================
|
|
564
|
-
// Event Building
|
|
565
|
-
// ============================================
|
|
566
|
-
|
|
567
|
-
private buildErrorEvent(
|
|
568
|
-
error: Error,
|
|
569
|
-
eventId: string,
|
|
570
|
-
options: {
|
|
571
|
-
level?: SeverityLevel;
|
|
572
|
-
tags?: Record<string, string>;
|
|
573
|
-
extra?: Record<string, any>;
|
|
574
|
-
user?: SentryUser;
|
|
575
|
-
fingerprint?: string[];
|
|
576
|
-
contexts?: Record<string, any>;
|
|
577
|
-
}
|
|
578
|
-
): SentryEvent {
|
|
579
|
-
const stacktrace = this.parseStackTrace(error);
|
|
580
|
-
|
|
581
|
-
return {
|
|
582
|
-
event_id: eventId,
|
|
583
|
-
timestamp: Date.now() / 1000,
|
|
584
|
-
platform: 'node',
|
|
585
|
-
level: options.level ?? 'error',
|
|
586
|
-
logger: 'nexus',
|
|
587
|
-
server_name: this.options.serverName,
|
|
588
|
-
release: this.options.release,
|
|
589
|
-
environment: this.options.environment,
|
|
590
|
-
exception: {
|
|
591
|
-
values: [{
|
|
592
|
-
type: error.name || 'Error',
|
|
593
|
-
value: error.message,
|
|
594
|
-
stacktrace,
|
|
595
|
-
mechanism: {
|
|
596
|
-
type: 'generic',
|
|
597
|
-
handled: true
|
|
598
|
-
}
|
|
599
|
-
}]
|
|
600
|
-
},
|
|
601
|
-
user: options.user ?? this.user,
|
|
602
|
-
tags: { ...this.globalTags, ...options.tags },
|
|
603
|
-
extra: {
|
|
604
|
-
...this.globalExtra,
|
|
605
|
-
...options.extra,
|
|
606
|
-
...(error.cause ? { cause: String(error.cause) } : {})
|
|
607
|
-
},
|
|
608
|
-
contexts: {
|
|
609
|
-
runtime: {
|
|
610
|
-
name: 'node',
|
|
611
|
-
version: process.version
|
|
612
|
-
},
|
|
613
|
-
os: {
|
|
614
|
-
name: process.platform,
|
|
615
|
-
version: process.arch
|
|
616
|
-
},
|
|
617
|
-
...options.contexts
|
|
618
|
-
},
|
|
619
|
-
breadcrumbs: [...this.breadcrumbs],
|
|
620
|
-
fingerprint: options.fingerprint
|
|
621
|
-
};
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
private parseStackTrace(error: Error): { frames: SentryStackFrame[] } | undefined {
|
|
625
|
-
if (!error.stack) return undefined;
|
|
626
|
-
|
|
627
|
-
const lines = error.stack.split('\n').slice(1);
|
|
628
|
-
const frames: SentryStackFrame[] = [];
|
|
629
|
-
|
|
630
|
-
for (const line of lines) {
|
|
631
|
-
const match = line.match(/^\s*at\s+(?:(.+?)\s+\()?(.+?):(\d+):(\d+)\)?$/);
|
|
632
|
-
if (match) {
|
|
633
|
-
frames.unshift({
|
|
634
|
-
function: match[1] || '<anonymous>',
|
|
635
|
-
filename: match[2],
|
|
636
|
-
abs_path: match[2],
|
|
637
|
-
lineno: parseInt(match[3], 10),
|
|
638
|
-
colno: parseInt(match[4], 10),
|
|
639
|
-
in_app: !match[2].includes('node_modules')
|
|
640
|
-
});
|
|
641
|
-
}
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
return { frames };
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
// ============================================
|
|
648
|
-
// HTTP Transport
|
|
649
|
-
// ============================================
|
|
650
|
-
|
|
651
|
-
private async sendEvent(event: SentryEvent): Promise<void> {
|
|
652
|
-
const envelope = this.buildEnvelope(event, 'event');
|
|
653
|
-
await this.sendEnvelope(envelope);
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
private async sendTransaction(transaction: SentryTransaction): Promise<void> {
|
|
657
|
-
const envelope = this.buildTransactionEnvelope(transaction);
|
|
658
|
-
await this.sendEnvelope(envelope);
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
private buildEnvelope(event: SentryEvent, type: 'event' | 'transaction'): string {
|
|
662
|
-
const header = JSON.stringify({
|
|
663
|
-
event_id: event.event_id,
|
|
664
|
-
sent_at: new Date().toISOString(),
|
|
665
|
-
dsn: this.options.dsn
|
|
666
|
-
});
|
|
667
|
-
|
|
668
|
-
const itemHeader = JSON.stringify({
|
|
669
|
-
type,
|
|
670
|
-
content_type: 'application/json'
|
|
671
|
-
});
|
|
672
|
-
|
|
673
|
-
const payload = JSON.stringify(event);
|
|
674
|
-
|
|
675
|
-
return `${header}\n${itemHeader}\n${payload}`;
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
private buildTransactionEnvelope(transaction: SentryTransaction): string {
|
|
679
|
-
const header = JSON.stringify({
|
|
680
|
-
event_id: this.generateEventId(),
|
|
681
|
-
sent_at: new Date().toISOString(),
|
|
682
|
-
dsn: this.options.dsn
|
|
683
|
-
});
|
|
684
|
-
|
|
685
|
-
const itemHeader = JSON.stringify({
|
|
686
|
-
type: 'transaction',
|
|
687
|
-
content_type: 'application/json'
|
|
688
|
-
});
|
|
689
|
-
|
|
690
|
-
const payload = JSON.stringify({
|
|
691
|
-
...transaction,
|
|
692
|
-
type: 'transaction',
|
|
693
|
-
platform: 'node',
|
|
694
|
-
environment: this.options.environment,
|
|
695
|
-
release: this.options.release,
|
|
696
|
-
server_name: this.options.serverName,
|
|
697
|
-
contexts: {
|
|
698
|
-
trace: {
|
|
699
|
-
trace_id: transaction.trace_id,
|
|
700
|
-
span_id: transaction.span_id,
|
|
701
|
-
parent_span_id: transaction.parent_span_id,
|
|
702
|
-
op: transaction.op
|
|
703
|
-
}
|
|
704
|
-
}
|
|
705
|
-
});
|
|
706
|
-
|
|
707
|
-
return `${header}\n${itemHeader}\n${payload}`;
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
private async sendEnvelope(envelope: string): Promise<void> {
|
|
711
|
-
const endpoint = this.getEnvelopeEndpoint();
|
|
712
|
-
|
|
713
|
-
try {
|
|
714
|
-
const controller = new AbortController();
|
|
715
|
-
const timeoutId = setTimeout(() => controller.abort(), this.options.timeout);
|
|
716
|
-
|
|
717
|
-
const response = await fetch(endpoint, {
|
|
718
|
-
method: 'POST',
|
|
719
|
-
headers: {
|
|
720
|
-
'Content-Type': 'application/x-sentry-envelope',
|
|
721
|
-
'X-Sentry-Auth': this.buildAuthHeader()
|
|
722
|
-
},
|
|
723
|
-
body: envelope,
|
|
724
|
-
signal: controller.signal
|
|
725
|
-
});
|
|
726
|
-
|
|
727
|
-
clearTimeout(timeoutId);
|
|
728
|
-
|
|
729
|
-
if (!response.ok) {
|
|
730
|
-
this.log(`Failed to send event: ${response.status} ${response.statusText}`);
|
|
731
|
-
} else {
|
|
732
|
-
this.log('Event sent successfully');
|
|
733
|
-
}
|
|
734
|
-
} catch (error: any) {
|
|
735
|
-
if (error.name === 'AbortError') {
|
|
736
|
-
this.log('Request timed out');
|
|
737
|
-
} else {
|
|
738
|
-
this.log(`Failed to send event: ${error.message}`);
|
|
739
|
-
}
|
|
740
|
-
}
|
|
741
|
-
}
|
|
742
|
-
|
|
743
|
-
private buildAuthHeader(): string {
|
|
744
|
-
return `Sentry sentry_version=7,sentry_client=nexus-sentry/1.0.0,sentry_key=${this.dsn.publicKey}`;
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
// ============================================
|
|
748
|
-
// Helpers
|
|
749
|
-
// ============================================
|
|
750
|
-
|
|
751
|
-
private generateEventId(): string {
|
|
752
|
-
return randomUUID().replace(/-/g, '');
|
|
753
|
-
}
|
|
754
|
-
|
|
755
|
-
private generateTraceId(): string {
|
|
756
|
-
return randomUUID().replace(/-/g, '');
|
|
757
|
-
}
|
|
758
|
-
|
|
759
|
-
private generateSpanId(): string {
|
|
760
|
-
return randomUUID().replace(/-/g, '').substring(0, 16);
|
|
761
|
-
}
|
|
762
|
-
|
|
763
|
-
private shouldSample(rate: number): boolean {
|
|
764
|
-
return Math.random() < rate;
|
|
765
|
-
}
|
|
766
|
-
|
|
767
|
-
private shouldIgnoreError(error: Error): boolean {
|
|
768
|
-
const message = error.message;
|
|
769
|
-
|
|
770
|
-
for (const pattern of this.options.ignoreErrors) {
|
|
771
|
-
if (typeof pattern === 'string') {
|
|
772
|
-
if (message.includes(pattern)) return true;
|
|
773
|
-
} else {
|
|
774
|
-
if (pattern.test(message)) return true;
|
|
775
|
-
}
|
|
776
|
-
}
|
|
777
|
-
|
|
778
|
-
return false;
|
|
779
|
-
}
|
|
780
|
-
|
|
781
|
-
private log(message: string, data?: any): void {
|
|
782
|
-
if (this.options.debug) {
|
|
783
|
-
console.log(`[Sentry] ${message}`, data ?? '');
|
|
784
|
-
}
|
|
785
|
-
}
|
|
786
|
-
|
|
787
|
-
/**
|
|
788
|
-
* Enable/disable Sentry
|
|
789
|
-
*/
|
|
790
|
-
setEnabled(enabled: boolean): void {
|
|
791
|
-
this.isEnabled = enabled;
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
/**
|
|
795
|
-
* Get current state
|
|
796
|
-
*/
|
|
797
|
-
isInitialized(): boolean {
|
|
798
|
-
return this.isEnabled;
|
|
799
|
-
}
|
|
800
|
-
|
|
801
|
-
/**
|
|
802
|
-
* Flush pending events (useful before shutdown)
|
|
803
|
-
*/
|
|
804
|
-
async flush(timeout?: number): Promise<boolean> {
|
|
805
|
-
// In a more complete implementation, this would wait for pending requests
|
|
806
|
-
return new Promise((resolve) => {
|
|
807
|
-
setTimeout(() => resolve(true), timeout ?? 2000);
|
|
808
|
-
});
|
|
809
|
-
}
|
|
810
|
-
|
|
811
|
-
/**
|
|
812
|
-
* Close the client
|
|
813
|
-
*/
|
|
814
|
-
async close(timeout?: number): Promise<boolean> {
|
|
815
|
-
this.apm.stop();
|
|
816
|
-
await this.flush(timeout);
|
|
817
|
-
this.isEnabled = false;
|
|
818
|
-
return true;
|
|
819
|
-
}
|
|
820
|
-
|
|
821
|
-
// ============================================
|
|
822
|
-
// APM Methods
|
|
823
|
-
// ============================================
|
|
824
|
-
|
|
825
|
-
/**
|
|
826
|
-
* Record a database/external query for slow query tracking
|
|
827
|
-
*
|
|
828
|
-
* @example
|
|
829
|
-
* ```typescript
|
|
830
|
-
* const start = Date.now();
|
|
831
|
-
* const result = await db.query('SELECT * FROM users');
|
|
832
|
-
* sentry.recordQuery('SELECT * FROM users', Date.now() - start);
|
|
833
|
-
* ```
|
|
834
|
-
*/
|
|
835
|
-
recordQuery(query: string, durationMs: number, metadata?: Record<string, any>): void {
|
|
836
|
-
this.apm.recordQuery(query, durationMs, metadata);
|
|
837
|
-
}
|
|
838
|
-
|
|
839
|
-
/**
|
|
840
|
-
* Get all recorded slow queries
|
|
841
|
-
*/
|
|
842
|
-
getSlowQueries(): SlowQueryRecord[] {
|
|
843
|
-
return this.apm.getSlowQueries();
|
|
844
|
-
}
|
|
845
|
-
|
|
846
|
-
/**
|
|
847
|
-
* Get current memory statistics
|
|
848
|
-
*/
|
|
849
|
-
getMemoryStats() {
|
|
850
|
-
return this.apm.getMemoryStats();
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
/**
|
|
854
|
-
* Get APM integration instance for advanced usage
|
|
855
|
-
*/
|
|
856
|
-
getAPM(): APMIntegration {
|
|
857
|
-
return this.apm;
|
|
858
|
-
}
|
|
859
|
-
}
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
/**
|
|
863
|
-
* Create Sentry middleware for Nexus
|
|
864
|
-
*/
|
|
865
|
-
export function createSentryMiddleware(
|
|
866
|
-
client: SentryClient,
|
|
867
|
-
options: SentryMiddlewareOptions = {}
|
|
868
|
-
): Middleware {
|
|
869
|
-
const {
|
|
870
|
-
includeRequestBody = false,
|
|
871
|
-
includeHeaders = true,
|
|
872
|
-
excludeHeaders = ['authorization', 'cookie', 'x-api-key'],
|
|
873
|
-
ignorePaths = [],
|
|
874
|
-
extractUser = () => null,
|
|
875
|
-
getTransactionName = (ctx) => `${ctx.method} ${ctx.path}`
|
|
876
|
-
} = options;
|
|
877
|
-
|
|
878
|
-
return async (ctx: Context, next, _deps) => {
|
|
879
|
-
// Check if path should be ignored
|
|
880
|
-
if (ignorePaths.some(p => ctx.path.startsWith(p))) {
|
|
881
|
-
return next(ctx);
|
|
882
|
-
}
|
|
883
|
-
|
|
884
|
-
// Add breadcrumb for request
|
|
885
|
-
client.addBreadcrumb({
|
|
886
|
-
type: 'http',
|
|
887
|
-
category: 'http.request',
|
|
888
|
-
message: `${ctx.method} ${ctx.path}`,
|
|
889
|
-
data: {
|
|
890
|
-
method: ctx.method,
|
|
891
|
-
url: ctx.path,
|
|
892
|
-
query: ctx.query
|
|
893
|
-
},
|
|
894
|
-
level: 'info'
|
|
895
|
-
});
|
|
896
|
-
|
|
897
|
-
// Set user context
|
|
898
|
-
const user = extractUser(ctx);
|
|
899
|
-
if (user) {
|
|
900
|
-
client.setUser({
|
|
901
|
-
...user,
|
|
902
|
-
ip_address: ctx.headers['x-forwarded-for'] as string || ctx.headers['x-real-ip'] as string
|
|
903
|
-
});
|
|
904
|
-
}
|
|
905
|
-
|
|
906
|
-
// Start transaction for performance monitoring
|
|
907
|
-
const transaction = client.startTransaction({
|
|
908
|
-
name: getTransactionName(ctx),
|
|
909
|
-
op: 'http.server',
|
|
910
|
-
tags: {
|
|
911
|
-
'http.method': ctx.method,
|
|
912
|
-
'http.url': ctx.path
|
|
913
|
-
}
|
|
914
|
-
});
|
|
915
|
-
|
|
916
|
-
// Store transaction in context for child spans
|
|
917
|
-
if (transaction) {
|
|
918
|
-
ctx.sentryTransaction = transaction;
|
|
919
|
-
ctx.sentryClient = client;
|
|
920
|
-
}
|
|
921
|
-
|
|
922
|
-
const startTime = Date.now();
|
|
923
|
-
|
|
924
|
-
try {
|
|
925
|
-
const response = await next(ctx);
|
|
926
|
-
|
|
927
|
-
// Finish transaction with success
|
|
928
|
-
if (transaction) {
|
|
929
|
-
transaction.data = {
|
|
930
|
-
...transaction.data,
|
|
931
|
-
'http.status_code': response.statusCode,
|
|
932
|
-
'http.response_time_ms': Date.now() - startTime
|
|
933
|
-
};
|
|
934
|
-
client.finishTransaction(transaction,
|
|
935
|
-
response.statusCode >= 500 ? 'internal_error' : 'ok'
|
|
936
|
-
);
|
|
937
|
-
}
|
|
938
|
-
|
|
939
|
-
// Add response breadcrumb
|
|
940
|
-
client.addBreadcrumb({
|
|
941
|
-
type: 'http',
|
|
942
|
-
category: 'http.response',
|
|
943
|
-
message: `${ctx.method} ${ctx.path} - ${response.statusCode}`,
|
|
944
|
-
data: {
|
|
945
|
-
status_code: response.statusCode,
|
|
946
|
-
duration_ms: Date.now() - startTime
|
|
947
|
-
},
|
|
948
|
-
level: response.statusCode >= 400 ? 'warning' : 'info'
|
|
949
|
-
});
|
|
950
|
-
|
|
951
|
-
return response;
|
|
952
|
-
|
|
953
|
-
} catch (error: any) {
|
|
954
|
-
// Build request context
|
|
955
|
-
const requestData: SentryEvent['request'] = {
|
|
956
|
-
url: ctx.url?.toString(),
|
|
957
|
-
method: ctx.method,
|
|
958
|
-
query_string: ctx.url?.search
|
|
959
|
-
};
|
|
960
|
-
|
|
961
|
-
if (includeHeaders) {
|
|
962
|
-
const headers: Record<string, string> = {};
|
|
963
|
-
for (const [key, value] of Object.entries(ctx.headers)) {
|
|
964
|
-
if (!excludeHeaders.includes(key.toLowerCase())) {
|
|
965
|
-
headers[key] = Array.isArray(value) ? value.join(', ') : (value ?? '');
|
|
966
|
-
}
|
|
967
|
-
}
|
|
968
|
-
requestData.headers = headers;
|
|
969
|
-
}
|
|
970
|
-
|
|
971
|
-
if (includeRequestBody && ctx.body) {
|
|
972
|
-
requestData.data = ctx.body;
|
|
973
|
-
}
|
|
974
|
-
|
|
975
|
-
// Capture the exception
|
|
976
|
-
client.captureException(error, {
|
|
977
|
-
tags: {
|
|
978
|
-
'http.method': ctx.method,
|
|
979
|
-
'http.url': ctx.path
|
|
980
|
-
},
|
|
981
|
-
extra: {
|
|
982
|
-
request: requestData,
|
|
983
|
-
params: ctx.params,
|
|
984
|
-
query: ctx.query
|
|
985
|
-
},
|
|
986
|
-
user: user ?? undefined
|
|
987
|
-
});
|
|
988
|
-
|
|
989
|
-
// Finish transaction with error
|
|
990
|
-
if (transaction) {
|
|
991
|
-
client.finishTransaction(transaction, 'internal_error');
|
|
992
|
-
}
|
|
993
|
-
|
|
994
|
-
throw error;
|
|
995
|
-
}
|
|
996
|
-
};
|
|
997
|
-
}
|
|
998
|
-
|
|
999
|
-
// ============================================
|
|
1000
|
-
// Request Handler Error Wrapper
|
|
1001
|
-
// ============================================
|
|
1002
|
-
|
|
1003
|
-
/**
|
|
1004
|
-
* Create an error handler that reports to Sentry
|
|
1005
|
-
*/
|
|
1006
|
-
export function createSentryErrorHandler(client: SentryClient) {
|
|
1007
|
-
return (error: Error, ctx: Context): Response => {
|
|
1008
|
-
client.captureException(error, {
|
|
1009
|
-
tags: {
|
|
1010
|
-
'http.method': ctx.method,
|
|
1011
|
-
'http.url': ctx.path
|
|
1012
|
-
},
|
|
1013
|
-
extra: {
|
|
1014
|
-
params: ctx.params,
|
|
1015
|
-
query: ctx.query
|
|
1016
|
-
}
|
|
1017
|
-
});
|
|
1018
|
-
|
|
1019
|
-
return {
|
|
1020
|
-
statusCode: 500,
|
|
1021
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1022
|
-
body: JSON.stringify({
|
|
1023
|
-
error: 'Internal Server Error',
|
|
1024
|
-
message: process.env.NODE_ENV === 'development' ? error.message : undefined
|
|
1025
|
-
})
|
|
1026
|
-
};
|
|
1027
|
-
};
|
|
1028
|
-
}
|
|
1029
|
-
|
|
1030
|
-
// ============================================
|
|
1031
|
-
// Context Extensions
|
|
1032
|
-
// ============================================
|
|
1033
|
-
declare module '../../core/types' {
|
|
1034
|
-
interface Context {
|
|
1035
|
-
sentryTransaction?: SentryTransaction;
|
|
1036
|
-
sentryClient?: SentryClient;
|
|
1037
|
-
}
|
|
1038
|
-
}
|
|
1039
|
-
|
|
1040
|
-
// ============================================
|
|
1041
|
-
// Helper Functions
|
|
1042
|
-
// ============================================
|
|
1043
|
-
|
|
1044
|
-
/**
|
|
1045
|
-
* Create a child span in the current transaction
|
|
1046
|
-
*/
|
|
1047
|
-
export function withSpan<T>(
|
|
1048
|
-
ctx: Context,
|
|
1049
|
-
options: { op: string; description?: string },
|
|
1050
|
-
fn: () => T | Promise<T>
|
|
1051
|
-
): T | Promise<T> {
|
|
1052
|
-
const client = ctx.sentryClient;
|
|
1053
|
-
const transaction = ctx.sentryTransaction;
|
|
1054
|
-
|
|
1055
|
-
if (!client || !transaction) {
|
|
1056
|
-
return fn();
|
|
1057
|
-
}
|
|
1058
|
-
|
|
1059
|
-
const span = client.startSpan(transaction, options);
|
|
1060
|
-
|
|
1061
|
-
try {
|
|
1062
|
-
const result = fn();
|
|
1063
|
-
|
|
1064
|
-
if (result instanceof Promise) {
|
|
1065
|
-
return result.then(
|
|
1066
|
-
(value) => {
|
|
1067
|
-
client.finishSpan(span, 'ok');
|
|
1068
|
-
return value;
|
|
1069
|
-
},
|
|
1070
|
-
(error) => {
|
|
1071
|
-
client.finishSpan(span, 'internal_error');
|
|
1072
|
-
throw error;
|
|
1073
|
-
}
|
|
1074
|
-
);
|
|
1075
|
-
}
|
|
1076
|
-
|
|
1077
|
-
client.finishSpan(span, 'ok');
|
|
1078
|
-
return result;
|
|
1079
|
-
|
|
1080
|
-
} catch (error) {
|
|
1081
|
-
client.finishSpan(span, 'internal_error');
|
|
1082
|
-
throw error;
|
|
1083
|
-
}
|
|
1084
|
-
}
|
|
1085
|
-
|
|
1086
|
-
// ============================================
|
|
1087
|
-
// Factory Functions
|
|
1088
|
-
// ============================================
|
|
1089
|
-
|
|
1090
|
-
let globalClient: SentryClient | null = null;
|
|
1091
|
-
|
|
1092
|
-
/**
|
|
1093
|
-
* Initialize Sentry
|
|
1094
|
-
*/
|
|
1095
|
-
export function initSentry(options: SentryOptions): SentryClient {
|
|
1096
|
-
globalClient = new SentryClient(options);
|
|
1097
|
-
return globalClient;
|
|
1098
|
-
}
|
|
1099
|
-
|
|
1100
|
-
/**
|
|
1101
|
-
* Get the global Sentry client
|
|
1102
|
-
*/
|
|
1103
|
-
export function getSentry(): SentryClient | null {
|
|
1104
|
-
return globalClient;
|
|
1105
|
-
}
|
|
1106
|
-
|
|
1107
|
-
/**
|
|
1108
|
-
* Capture exception using global client
|
|
1109
|
-
*/
|
|
1110
|
-
export function captureException(
|
|
1111
|
-
error: Error | unknown,
|
|
1112
|
-
options?: Parameters<SentryClient['captureException']>[1]
|
|
1113
|
-
): string {
|
|
1114
|
-
if (!globalClient) {
|
|
1115
|
-
console.warn('[Sentry] Client not initialized. Call initSentry() first.');
|
|
1116
|
-
return '';
|
|
1117
|
-
}
|
|
1118
|
-
return globalClient.captureException(error, options);
|
|
1119
|
-
}
|
|
1120
|
-
|
|
1121
|
-
/**
|
|
1122
|
-
* Capture message using global client
|
|
1123
|
-
*/
|
|
1124
|
-
export function captureMessage(
|
|
1125
|
-
message: string,
|
|
1126
|
-
options?: Parameters<SentryClient['captureMessage']>[1]
|
|
1127
|
-
): string {
|
|
1128
|
-
if (!globalClient) {
|
|
1129
|
-
console.warn('[Sentry] Client not initialized. Call initSentry() first.');
|
|
1130
|
-
return '';
|
|
1131
|
-
}
|
|
1132
|
-
return globalClient.captureMessage(message, options);
|
|
1133
|
-
}
|
|
1134
|
-
|
|
1135
|
-
/**
|
|
1136
|
-
* Add breadcrumb using global client
|
|
1137
|
-
*/
|
|
1138
|
-
export function addBreadcrumb(breadcrumb: SentryBreadcrumb): void {
|
|
1139
|
-
globalClient?.addBreadcrumb(breadcrumb);
|
|
1140
|
-
}
|
|
1141
|
-
|
|
1142
|
-
/**
|
|
1143
|
-
* Set user using global client
|
|
1144
|
-
*/
|
|
1145
|
-
export function setUser(user: SentryUser | null): void {
|
|
1146
|
-
globalClient?.setUser(user);
|
|
1147
|
-
}
|
|
1148
|
-
|
|
1149
|
-
/**
|
|
1150
|
-
* Set tag using global client
|
|
1151
|
-
*/
|
|
1152
|
-
export function setTag(key: string, value: string): void {
|
|
1153
|
-
globalClient?.setTag(key, value);
|
|
1154
|
-
}
|
|
1155
|
-
|
|
1156
|
-
/**
|
|
1157
|
-
* Set extra using global client
|
|
1158
|
-
*/
|
|
1159
|
-
export function setExtra(key: string, value: any): void {
|
|
1160
|
-
globalClient?.setExtra(key, value);
|
|
1161
|
-
}
|
|
1162
|
-
|
|
1163
|
-
// ============================================
|
|
1164
|
-
// Sentry Plugin
|
|
1165
|
-
// ============================================
|
|
1166
|
-
|
|
1167
|
-
export interface SentryPluginOptions extends SentryOptions {
|
|
1168
|
-
/**
|
|
1169
|
-
* Middleware options
|
|
1170
|
-
*/
|
|
1171
|
-
middleware?: SentryMiddlewareOptions;
|
|
1172
|
-
}
|
|
1173
|
-
|
|
1174
|
-
/**
|
|
1175
|
-
* Create Sentry plugin for Nexus
|
|
1176
|
-
*
|
|
1177
|
-
* @example
|
|
1178
|
-
* ```typescript
|
|
1179
|
-
* import { sentry } from './src/advanced/sentry';
|
|
1180
|
-
*
|
|
1181
|
-
* app.plugin(sentry({
|
|
1182
|
-
* dsn: 'https://xxx@xxx.ingest.sentry.io/xxx',
|
|
1183
|
-
* environment: 'production',
|
|
1184
|
-
* tracesSampleRate: 0.1,
|
|
1185
|
-
* middleware: {
|
|
1186
|
-
* includeRequestBody: true,
|
|
1187
|
-
* ignorePaths: ['/health', '/metrics']
|
|
1188
|
-
* }
|
|
1189
|
-
* }));
|
|
1190
|
-
* ```
|
|
1191
|
-
*/
|
|
1192
|
-
export function sentry(options: SentryPluginOptions): import('../../core/types').Plugin {
|
|
1193
|
-
return {
|
|
1194
|
-
name: 'sentry',
|
|
1195
|
-
version: '1.0.0',
|
|
1196
|
-
install(app) {
|
|
1197
|
-
// Initialize Sentry client
|
|
1198
|
-
const client = initSentry(options);
|
|
1199
|
-
|
|
1200
|
-
// Add middleware
|
|
1201
|
-
app.use(createSentryMiddleware(client, options.middleware));
|
|
1202
|
-
|
|
1203
|
-
// Wrap error handler to capture exceptions
|
|
1204
|
-
const originalOnError = app.onError.bind(app);
|
|
1205
|
-
app.onError((error, ctx) => {
|
|
1206
|
-
client.captureException(error, {
|
|
1207
|
-
tags: {
|
|
1208
|
-
'http.method': ctx.method,
|
|
1209
|
-
'http.url': ctx.path
|
|
1210
|
-
},
|
|
1211
|
-
extra: {
|
|
1212
|
-
params: ctx.params,
|
|
1213
|
-
query: ctx.query
|
|
1214
|
-
}
|
|
1215
|
-
});
|
|
1216
|
-
|
|
1217
|
-
// Return default error response
|
|
1218
|
-
return {
|
|
1219
|
-
statusCode: 500,
|
|
1220
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1221
|
-
body: JSON.stringify({
|
|
1222
|
-
error: 'Internal Server Error',
|
|
1223
|
-
message: process.env.NODE_ENV === 'development' ? error.message : undefined
|
|
1224
|
-
})
|
|
1225
|
-
};
|
|
1226
|
-
});
|
|
1227
|
-
|
|
1228
|
-
// Add shutdown hook if graceful shutdown is enabled
|
|
1229
|
-
if (typeof app.onShutdown === 'function') {
|
|
1230
|
-
app.onShutdown('sentry-flush', async () => {
|
|
1231
|
-
await client.flush(5000);
|
|
1232
|
-
}, 1);
|
|
1233
|
-
}
|
|
1234
|
-
}
|
|
1235
|
-
};
|
|
1236
|
-
}
|