@senzops/apm-node 1.1.2 → 1.1.4
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/README.md +26 -34
- package/dist/index.d.mts +6 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.global.js +1 -1
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/core/client.ts +16 -46
- package/src/core/context.ts +13 -6
- package/src/core/types.ts +12 -5
- package/src/index.ts +10 -7
- package/src/instrumentation/http.ts +73 -20
- package/src/instrumentation/mongo.ts +66 -56
- package/src/middleware/express.ts +17 -4
- package/src/wrappers/fastify.ts +15 -19
- package/src/wrappers/h3.ts +3 -5
- package/src/wrappers/next.ts +14 -16
package/src/core/types.ts
CHANGED
|
@@ -2,36 +2,43 @@ export interface SenzorOptions {
|
|
|
2
2
|
apiKey: string;
|
|
3
3
|
endpoint?: string;
|
|
4
4
|
batchSize?: number;
|
|
5
|
-
flushInterval?: number;
|
|
5
|
+
flushInterval?: number;
|
|
6
6
|
debug?: boolean;
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
export interface Span {
|
|
10
10
|
name: string;
|
|
11
11
|
type: 'db' | 'http' | 'function' | 'custom';
|
|
12
|
-
startTime: number;
|
|
12
|
+
startTime: number;
|
|
13
13
|
duration: number;
|
|
14
14
|
status?: number;
|
|
15
15
|
meta?: Record<string, any>;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
export interface TraceError {
|
|
19
|
+
name: string;
|
|
20
|
+
message: string;
|
|
21
|
+
stack?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
18
24
|
export interface Trace {
|
|
19
25
|
traceId: string;
|
|
20
26
|
method: string;
|
|
21
|
-
route: string;
|
|
22
|
-
path: string;
|
|
27
|
+
route: string;
|
|
28
|
+
path: string;
|
|
23
29
|
status: number;
|
|
24
30
|
duration: number;
|
|
25
31
|
ip?: string;
|
|
26
32
|
userAgent?: string;
|
|
27
33
|
timestamp: string;
|
|
28
34
|
spans: Span[];
|
|
35
|
+
error?: TraceError;
|
|
29
36
|
}
|
|
30
37
|
|
|
31
|
-
// Internal interface for an active trace object
|
|
32
38
|
export interface ActiveTrace {
|
|
33
39
|
id: string;
|
|
34
40
|
startTime: number;
|
|
35
41
|
data: Partial<Trace>;
|
|
36
42
|
spans: Span[];
|
|
43
|
+
error?: TraceError;
|
|
37
44
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,23 +1,26 @@
|
|
|
1
1
|
import { client } from './core/client';
|
|
2
|
-
import { expressMiddleware } from './middleware/express';
|
|
2
|
+
import { expressMiddleware, expressErrorHandler } from './middleware/express';
|
|
3
3
|
import { wrapH3 } from './wrappers/h3';
|
|
4
4
|
import { wrapNextRoute, wrapNextPages } from './wrappers/next';
|
|
5
5
|
import { senzorPlugin } from './wrappers/fastify';
|
|
6
6
|
import { SenzorOptions } from './core/types';
|
|
7
7
|
|
|
8
8
|
const Senzor = {
|
|
9
|
-
// Core
|
|
10
9
|
init: (options: SenzorOptions) => client.init(options),
|
|
11
10
|
flush: () => client.flush(),
|
|
11
|
+
track: client.track.bind(client),
|
|
12
|
+
startSpan: client.startSpan.bind(client),
|
|
13
|
+
captureException: client.captureError.bind(client),
|
|
12
14
|
|
|
13
|
-
// Express
|
|
15
|
+
// Express
|
|
14
16
|
requestHandler: expressMiddleware,
|
|
17
|
+
errorHandler: expressErrorHandler,
|
|
15
18
|
|
|
16
|
-
// Next
|
|
17
|
-
wrapNextRoute,
|
|
18
|
-
wrapNextPages,
|
|
19
|
+
// Next
|
|
20
|
+
wrapNextRoute,
|
|
21
|
+
wrapNextPages,
|
|
19
22
|
|
|
20
|
-
// H3
|
|
23
|
+
// H3
|
|
21
24
|
wrapH3,
|
|
22
25
|
|
|
23
26
|
// Fastify
|
|
@@ -9,15 +9,79 @@ const shimmer = (module: any, methodName: string, wrapper: (original: Function)
|
|
|
9
9
|
module[methodName] = wrapper(original);
|
|
10
10
|
};
|
|
11
11
|
|
|
12
|
+
// --- Native Fetch Instrumentation (Node 18+) ---
|
|
13
|
+
export const instrumentFetch = (ingestUrl: string, debug = false) => {
|
|
14
|
+
if (!globalThis.fetch) return;
|
|
15
|
+
|
|
16
|
+
let ingestHost = '';
|
|
17
|
+
try { ingestHost = new URL(ingestUrl).hostname; } catch (e) { }
|
|
18
|
+
|
|
19
|
+
const originalFetch = globalThis.fetch;
|
|
20
|
+
|
|
21
|
+
// @ts-ignore
|
|
22
|
+
globalThis.fetch = async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
23
|
+
// 1. Extract URL
|
|
24
|
+
let urlStr = '';
|
|
25
|
+
if (typeof input === 'string') urlStr = input;
|
|
26
|
+
else if (input instanceof URL) urlStr = input.toString();
|
|
27
|
+
else if (input && input.url) urlStr = input.url; // Request object
|
|
28
|
+
|
|
29
|
+
// 2. Infinite Loop Guard
|
|
30
|
+
if (ingestHost && urlStr.includes(ingestHost)) {
|
|
31
|
+
return originalFetch(input, init);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// 3. Context Check
|
|
35
|
+
const trace = Context.current();
|
|
36
|
+
if (!trace) {
|
|
37
|
+
return originalFetch(input, init);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// 4. Start Span
|
|
41
|
+
const method = (init?.method || 'GET').toUpperCase();
|
|
42
|
+
const startTime = performance.now() - trace.startTime;
|
|
43
|
+
const spanStartAbs = performance.now();
|
|
44
|
+
let hostname = 'unknown';
|
|
45
|
+
try { hostname = new URL(urlStr).hostname; } catch (e) { }
|
|
46
|
+
|
|
47
|
+
if (debug) console.log(`[Senzor] Tracking Fetch: ${method} ${hostname}`);
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
const response = await originalFetch(input, init);
|
|
51
|
+
|
|
52
|
+
// 5. End Span
|
|
53
|
+
const duration = performance.now() - spanStartAbs;
|
|
54
|
+
Context.addSpan({
|
|
55
|
+
name: `${method} ${hostname}`,
|
|
56
|
+
type: 'http',
|
|
57
|
+
startTime,
|
|
58
|
+
duration,
|
|
59
|
+
status: response.status,
|
|
60
|
+
meta: { url: urlStr, method, library: 'fetch' }
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
return response;
|
|
64
|
+
} catch (err: any) {
|
|
65
|
+
const duration = performance.now() - spanStartAbs;
|
|
66
|
+
Context.addSpan({
|
|
67
|
+
name: `${method} ${hostname}`,
|
|
68
|
+
type: 'http',
|
|
69
|
+
startTime,
|
|
70
|
+
duration,
|
|
71
|
+
status: 500,
|
|
72
|
+
meta: { error: err.message, url: urlStr, library: 'fetch' }
|
|
73
|
+
});
|
|
74
|
+
throw err;
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// --- Standard HTTP/HTTPS Instrumentation ---
|
|
12
80
|
export const instrumentHttp = (ingestUrl: string, debug = false) => {
|
|
13
81
|
let ingestHost = '';
|
|
14
82
|
try {
|
|
15
83
|
ingestHost = new URL(ingestUrl).hostname;
|
|
16
|
-
|
|
17
|
-
} catch (e) {
|
|
18
|
-
// If invalid URL passed, we can't filter loop safely, so we might skip instrumentation
|
|
19
|
-
if (debug) console.error('[Senzor] Invalid Ingest URL for HTTP instrumentation');
|
|
20
|
-
}
|
|
84
|
+
} catch (e) { }
|
|
21
85
|
|
|
22
86
|
const requestWrapper = (original: Function) => {
|
|
23
87
|
return function (this: any, ...args: any[]) {
|
|
@@ -35,16 +99,12 @@ export const instrumentHttp = (ingestUrl: string, debug = false) => {
|
|
|
35
99
|
urlStr = `${protocol}//${host}${path}`;
|
|
36
100
|
}
|
|
37
101
|
|
|
38
|
-
// Safety Guard: Ignore calls to Senzor Ingest API
|
|
39
102
|
if (ingestHost && (urlStr.includes(ingestHost) || (options.hostname && options.hostname.includes(ingestHost)))) {
|
|
40
103
|
return original.apply(this, args);
|
|
41
104
|
}
|
|
42
105
|
|
|
43
106
|
const trace = Context.current();
|
|
44
|
-
if (!trace)
|
|
45
|
-
// Not inside a tracked request
|
|
46
|
-
return original.apply(this, args);
|
|
47
|
-
}
|
|
107
|
+
if (!trace) return original.apply(this, args);
|
|
48
108
|
|
|
49
109
|
const method = (options.method || 'GET').toUpperCase();
|
|
50
110
|
const startTime = performance.now() - trace.startTime;
|
|
@@ -52,8 +112,6 @@ export const instrumentHttp = (ingestUrl: string, debug = false) => {
|
|
|
52
112
|
let hostname = 'unknown';
|
|
53
113
|
try { hostname = new URL(urlStr).hostname; } catch (e) { hostname = options.hostname || 'unknown'; }
|
|
54
114
|
|
|
55
|
-
if (debug) console.log(`[Senzor] Tracking HTTP: ${method} ${hostname}`);
|
|
56
|
-
|
|
57
115
|
const req = original.apply(this, args);
|
|
58
116
|
|
|
59
117
|
const captureSpan = (res: any, error?: Error) => {
|
|
@@ -64,21 +122,16 @@ export const instrumentHttp = (ingestUrl: string, debug = false) => {
|
|
|
64
122
|
startTime,
|
|
65
123
|
duration,
|
|
66
124
|
status: error ? 500 : res?.statusCode || 0,
|
|
67
|
-
meta: { url: urlStr, method }
|
|
125
|
+
meta: { url: urlStr, method, library: 'http', error: error ? error.message : undefined }
|
|
68
126
|
});
|
|
69
127
|
};
|
|
70
128
|
|
|
71
129
|
req.on('response', (res: any) => {
|
|
72
|
-
// We capture on 'response' (headers received) to be safe.
|
|
73
|
-
// Waiting for 'end' might miss requests where body isn't consumed.
|
|
74
130
|
res.once('end', () => captureSpan(res));
|
|
75
|
-
|
|
76
|
-
// setTimeout(() => captureSpan(res), 5000);
|
|
131
|
+
res.once('error', (err: Error) => captureSpan(res, err));
|
|
77
132
|
});
|
|
78
133
|
|
|
79
|
-
req.on('error', (err: Error) =>
|
|
80
|
-
captureSpan(null, err);
|
|
81
|
-
});
|
|
134
|
+
req.on('error', (err: Error) => captureSpan(null, err));
|
|
82
135
|
|
|
83
136
|
return req;
|
|
84
137
|
};
|
|
@@ -2,25 +2,35 @@ import { Context } from '../core/context';
|
|
|
2
2
|
|
|
3
3
|
export const instrumentMongo = (debug = false) => {
|
|
4
4
|
try {
|
|
5
|
-
// Attempt to load the user's installed mongodb driver
|
|
6
|
-
// This works for both native driver users and Mongoose users (as mongoose depends on this)
|
|
7
5
|
const mongodb = require('mongodb');
|
|
8
6
|
const Collection = mongodb.Collection;
|
|
9
7
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
//
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
8
|
+
// Attempt to get Cursor classes
|
|
9
|
+
// Note: The location of these classes varies by driver version,
|
|
10
|
+
// checking common locations
|
|
11
|
+
const FindCursor = mongodb.FindCursor || require('mongodb/lib/cursor/find_cursor').FindCursor;
|
|
12
|
+
const AggregationCursor = mongodb.AggregationCursor || require('mongodb/lib/cursor/aggregation_cursor').AggregationCursor;
|
|
13
|
+
|
|
14
|
+
if (debug) console.log('[Senzor] Instrumenting MongoDB (Collection + Cursors)...');
|
|
15
|
+
|
|
16
|
+
// --- Helper to Record Span ---
|
|
17
|
+
const recordSpan = (name: string, operation: string, collection: string, startAbs: number, traceStart: number, err?: Error) => {
|
|
18
|
+
const duration = performance.now() - startAbs;
|
|
19
|
+
Context.addSpan({
|
|
20
|
+
name: `MongoDB ${name}`,
|
|
21
|
+
type: 'db',
|
|
22
|
+
startTime: performance.now() - traceStart - duration, // Adjust start time to when op actually started
|
|
23
|
+
duration,
|
|
24
|
+
status: err ? 500 : 0,
|
|
25
|
+
meta: { collection, operation, error: err ? err.message : undefined }
|
|
26
|
+
});
|
|
27
|
+
if (debug) console.log(`[Senzor] Captured Mongo: ${name} (${duration.toFixed(2)}ms)`);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// --- 1. Instrument Immediate Operations (Insert/Update/Delete) ---
|
|
31
|
+
const immediateMethods = ['insertOne', 'insertMany', 'updateOne', 'updateMany', 'deleteOne', 'deleteMany', 'countDocuments'];
|
|
32
|
+
|
|
33
|
+
immediateMethods.forEach((method) => {
|
|
24
34
|
if (!Collection.prototype[method]) return;
|
|
25
35
|
const original = Collection.prototype[method];
|
|
26
36
|
|
|
@@ -28,68 +38,68 @@ export const instrumentMongo = (debug = false) => {
|
|
|
28
38
|
const trace = Context.current();
|
|
29
39
|
if (!trace) return original.apply(this, args);
|
|
30
40
|
|
|
31
|
-
const startTime = performance.now() - trace.startTime;
|
|
32
41
|
const spanStartAbs = performance.now();
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
const endSpan = (err?: Error) => {
|
|
36
|
-
const duration = performance.now() - spanStartAbs;
|
|
37
|
-
Context.addSpan({
|
|
38
|
-
name: `MongoDB ${method} (${collectionName})`,
|
|
39
|
-
type: 'db',
|
|
40
|
-
startTime,
|
|
41
|
-
duration,
|
|
42
|
-
status: err ? 500 : 0,
|
|
43
|
-
meta: { collection: collectionName, operation: method }
|
|
44
|
-
});
|
|
45
|
-
if (debug) console.log(`[Senzor] Captured Mongo Span: ${method}`);
|
|
46
|
-
};
|
|
42
|
+
const traceStart = trace.startTime;
|
|
43
|
+
const collName = this.collectionName;
|
|
47
44
|
|
|
48
45
|
try {
|
|
49
46
|
const result = original.apply(this, args);
|
|
50
47
|
if (result && typeof result.then === 'function') {
|
|
51
48
|
return result.then(
|
|
52
|
-
(res: any) => {
|
|
53
|
-
(err: any) => {
|
|
49
|
+
(res: any) => { recordSpan(method, method, collName, spanStartAbs, traceStart); return res; },
|
|
50
|
+
(err: any) => { recordSpan(method, method, collName, spanStartAbs, traceStart, err); throw err; }
|
|
54
51
|
);
|
|
55
52
|
}
|
|
56
53
|
return result;
|
|
57
54
|
} catch (err: any) {
|
|
58
|
-
|
|
55
|
+
recordSpan(method, method, collName, spanStartAbs, traceStart, err);
|
|
59
56
|
throw err;
|
|
60
57
|
}
|
|
61
58
|
};
|
|
62
59
|
});
|
|
63
60
|
|
|
64
|
-
// 2. Instrument Cursor
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
if (!Collection.prototype[method]) return;
|
|
68
|
-
const original = Collection.prototype[method];
|
|
61
|
+
// --- 2. Instrument Cursor Execution (find -> toArray) ---
|
|
62
|
+
const patchCursor = (CursorClass: any, label: string) => {
|
|
63
|
+
if (!CursorClass || !CursorClass.prototype.toArray) return;
|
|
69
64
|
|
|
70
|
-
|
|
71
|
-
const trace = Context.current();
|
|
72
|
-
if (!trace) return original.apply(this, args);
|
|
65
|
+
const originalToArray = CursorClass.prototype.toArray;
|
|
73
66
|
|
|
74
|
-
|
|
67
|
+
CursorClass.prototype.toArray = function (...args: any[]) {
|
|
68
|
+
const trace = Context.current();
|
|
69
|
+
// Cursors are often created in context but executed later.
|
|
70
|
+
// We check context at execution time.
|
|
71
|
+
if (!trace) return originalToArray.apply(this, args);
|
|
75
72
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
startTime,
|
|
81
|
-
duration: 0, // Placeholder, as cursor creation is instant
|
|
82
|
-
status: 0,
|
|
83
|
-
meta: { collection: this.collectionName, operation: method }
|
|
84
|
-
});
|
|
73
|
+
const spanStartAbs = performance.now();
|
|
74
|
+
const traceStart = trace.startTime;
|
|
75
|
+
// Attempt to get collection name from cursor internal state
|
|
76
|
+
const collName = this.namespace?.collection || 'unknown';
|
|
85
77
|
|
|
86
|
-
|
|
78
|
+
const onSuccess = (res: any) => {
|
|
79
|
+
recordSpan(label, label, collName, spanStartAbs, traceStart);
|
|
80
|
+
return res;
|
|
81
|
+
};
|
|
82
|
+
const onError = (err: any) => {
|
|
83
|
+
recordSpan(label, label, collName, spanStartAbs, traceStart, err);
|
|
84
|
+
throw err;
|
|
85
|
+
};
|
|
87
86
|
|
|
88
|
-
|
|
87
|
+
try {
|
|
88
|
+
const result = originalToArray.apply(this, args);
|
|
89
|
+
if (result && typeof result.then === 'function') {
|
|
90
|
+
return result.then(onSuccess, onError);
|
|
91
|
+
}
|
|
92
|
+
return onSuccess(result);
|
|
93
|
+
} catch (e) {
|
|
94
|
+
onError(e);
|
|
95
|
+
}
|
|
89
96
|
};
|
|
90
|
-
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
patchCursor(FindCursor, 'find');
|
|
100
|
+
patchCursor(AggregationCursor, 'aggregate');
|
|
91
101
|
|
|
92
102
|
} catch (e: any) {
|
|
93
|
-
if (debug) console.warn('[Senzor] MongoDB instrumentation
|
|
103
|
+
if (debug) console.warn('[Senzor] MongoDB instrumentation warning:', e.message);
|
|
94
104
|
}
|
|
95
105
|
};
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { client } from '../core/client';
|
|
2
2
|
|
|
3
|
+
// 1. Request Handler (Place before routes)
|
|
3
4
|
export const expressMiddleware = () => {
|
|
4
5
|
return (req: any, res: any, next: () => void) => {
|
|
5
|
-
// We MUST use startTrace to enable Auto-Instrumentation for this request
|
|
6
6
|
client.startTrace({
|
|
7
7
|
method: req.method,
|
|
8
8
|
path: req.originalUrl || req.url,
|
|
@@ -10,9 +10,11 @@ export const expressMiddleware = () => {
|
|
|
10
10
|
userAgent: req.headers['user-agent'],
|
|
11
11
|
}, () => {
|
|
12
12
|
|
|
13
|
+
// Auto-detect status code on finish
|
|
13
14
|
res.once('finish', () => {
|
|
14
15
|
try {
|
|
15
16
|
let route = 'UNKNOWN';
|
|
17
|
+
// Express populates req.route only if a route matched
|
|
16
18
|
if (req.route && req.route.path) {
|
|
17
19
|
route = (req.baseUrl || '') + req.route.path;
|
|
18
20
|
} else if (res.statusCode === 404) {
|
|
@@ -22,12 +24,23 @@ export const expressMiddleware = () => {
|
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
client.endTrace(res.statusCode, { route });
|
|
25
|
-
} catch (e) {
|
|
26
|
-
// Fail open
|
|
27
|
-
}
|
|
27
|
+
} catch (e) { /* Fail open */ }
|
|
28
28
|
});
|
|
29
29
|
|
|
30
30
|
next();
|
|
31
31
|
});
|
|
32
32
|
};
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// 2. Error Handler (Place after routes)
|
|
36
|
+
// This is required in Express to capture the actual Error Object (Stack Trace)
|
|
37
|
+
export const expressErrorHandler = () => {
|
|
38
|
+
return (err: any, req: any, res: any, next: (err?: any) => void) => {
|
|
39
|
+
|
|
40
|
+
// 1. Capture the exception context
|
|
41
|
+
client.captureError(err);
|
|
42
|
+
|
|
43
|
+
// 2. Pass it to the next error handler (don't swallow it)
|
|
44
|
+
next(err);
|
|
45
|
+
};
|
|
33
46
|
};
|
package/src/wrappers/fastify.ts
CHANGED
|
@@ -1,37 +1,33 @@
|
|
|
1
1
|
import { client } from '../core/client';
|
|
2
2
|
import { SenzorOptions } from '../core/types';
|
|
3
3
|
|
|
4
|
-
// We don't import Fastify types to keep zero-deps, but structure matches
|
|
5
4
|
export const senzorPlugin = (fastify: any, options: SenzorOptions, done: Function) => {
|
|
6
5
|
|
|
7
|
-
// Init if options provided inline, otherwise assume global init
|
|
8
6
|
if (options && options.apiKey) {
|
|
9
7
|
client.init(options);
|
|
10
8
|
}
|
|
11
9
|
|
|
12
|
-
//
|
|
10
|
+
// 1. Start Trace
|
|
13
11
|
fastify.addHook('onRequest', (request: any, reply: any, next: Function) => {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
// Hook: On Response (End Timer & Track)
|
|
19
|
-
fastify.addHook('onResponse', (request: any, reply: any, next: Function) => {
|
|
20
|
-
const duration = performance.now() - (request.senzorStart || performance.now());
|
|
21
|
-
|
|
22
|
-
// Fastify provides 'routerPath' (e.g. /user/:id)
|
|
23
|
-
const route = request.routeOptions?.url || request.routerPath;
|
|
24
|
-
|
|
25
|
-
client.track({
|
|
12
|
+
// FIX: Wrap next in a closure to satisfy TS types
|
|
13
|
+
client.startTrace({
|
|
26
14
|
method: request.method,
|
|
27
|
-
route: route || 'UNKNOWN',
|
|
28
15
|
path: request.raw.url || request.url,
|
|
29
|
-
status: reply.statusCode,
|
|
30
|
-
duration: duration,
|
|
31
16
|
ip: request.ip,
|
|
32
17
|
userAgent: request.headers['user-agent']
|
|
33
|
-
});
|
|
18
|
+
}, () => next());
|
|
19
|
+
});
|
|
34
20
|
|
|
21
|
+
// 2. Capture Errors
|
|
22
|
+
fastify.addHook('onError', (request: any, reply: any, error: any, next: Function) => {
|
|
23
|
+
client.captureError(error);
|
|
24
|
+
next();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// 3. End Trace
|
|
28
|
+
fastify.addHook('onResponse', (request: any, reply: any, next: Function) => {
|
|
29
|
+
const route = request.routeOptions?.url || request.routerPath || 'UNKNOWN';
|
|
30
|
+
client.endTrace(reply.statusCode, { route });
|
|
35
31
|
next();
|
|
36
32
|
});
|
|
37
33
|
|
package/src/wrappers/h3.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { client } from '../core/client';
|
|
2
2
|
import { getRoute } from '../core/normalizer';
|
|
3
3
|
|
|
4
|
-
// Minimal types for H3 to avoid peer-deps
|
|
5
4
|
type EventHandler = (event: any) => any;
|
|
6
5
|
|
|
7
6
|
export const wrapH3 = (handler: EventHandler) => {
|
|
@@ -9,7 +8,6 @@ export const wrapH3 = (handler: EventHandler) => {
|
|
|
9
8
|
const req = event.node.req;
|
|
10
9
|
const path = req.originalUrl || req.url || '/';
|
|
11
10
|
|
|
12
|
-
// Start Trace Context
|
|
13
11
|
return client.startTrace({
|
|
14
12
|
method: req.method || 'GET',
|
|
15
13
|
path: path,
|
|
@@ -18,16 +16,16 @@ export const wrapH3 = (handler: EventHandler) => {
|
|
|
18
16
|
}, async () => {
|
|
19
17
|
try {
|
|
20
18
|
const response = await handler(event);
|
|
21
|
-
|
|
22
|
-
// H3/Nitro response status
|
|
23
19
|
let status = 200;
|
|
24
20
|
if (event.node.res.statusCode) status = event.node.res.statusCode;
|
|
25
|
-
// Check if response is an error object
|
|
26
21
|
if (response && response.statusCode) status = response.statusCode;
|
|
27
22
|
|
|
28
23
|
client.endTrace(status, { route: getRoute(event, path) });
|
|
29
24
|
return response;
|
|
30
25
|
} catch (err: any) {
|
|
26
|
+
// AUTOMATIC ERROR CAPTURE
|
|
27
|
+
client.captureError(err);
|
|
28
|
+
|
|
31
29
|
const status = err.statusCode || err.status || 500;
|
|
32
30
|
client.endTrace(status, { route: getRoute(event, path) });
|
|
33
31
|
throw err;
|
package/src/wrappers/next.ts
CHANGED
|
@@ -1,16 +1,14 @@
|
|
|
1
1
|
import { client } from '../core/client';
|
|
2
2
|
import { normalizePath } from '../core/normalizer';
|
|
3
3
|
|
|
4
|
-
// --- App Router Wrapper
|
|
4
|
+
// --- App Router Wrapper ---
|
|
5
5
|
export const wrapNextRoute = (handler: Function) => {
|
|
6
6
|
return async (req: Request | any, context?: any) => {
|
|
7
|
-
// 1. Extract Info
|
|
8
7
|
const url = req.url ? new URL(req.url) : { pathname: '/' };
|
|
9
8
|
const method = req.method || 'GET';
|
|
10
9
|
const ua = req.headers.get ? req.headers.get('user-agent') : undefined;
|
|
11
10
|
const ip = req.headers.get ? req.headers.get('x-forwarded-for') : undefined;
|
|
12
11
|
|
|
13
|
-
// 2. Run in Context
|
|
14
12
|
return client.startTrace({
|
|
15
13
|
method,
|
|
16
14
|
path: url.pathname,
|
|
@@ -20,44 +18,44 @@ export const wrapNextRoute = (handler: Function) => {
|
|
|
20
18
|
try {
|
|
21
19
|
const response = await handler(req, context);
|
|
22
20
|
const status = response?.status || 200;
|
|
23
|
-
|
|
24
21
|
client.endTrace(status, { route: normalizePath(url.pathname) });
|
|
25
22
|
return response;
|
|
26
23
|
} catch (err: any) {
|
|
24
|
+
// AUTOMATIC ERROR CAPTURE
|
|
25
|
+
client.captureError(err);
|
|
27
26
|
client.endTrace(500, { route: normalizePath(url.pathname) });
|
|
28
|
-
throw err;
|
|
27
|
+
throw err; // Re-throw so Next.js handles the error page
|
|
29
28
|
}
|
|
30
29
|
});
|
|
31
30
|
};
|
|
32
31
|
};
|
|
33
32
|
|
|
34
|
-
// --- Pages Router Wrapper
|
|
33
|
+
// --- Pages Router Wrapper ---
|
|
35
34
|
export const wrapNextPages = (handler: Function) => {
|
|
36
35
|
return async (req: any, res: any) => {
|
|
37
36
|
const path = req.url ? req.url.split('?')[0] : '/';
|
|
38
|
-
|
|
39
|
-
// 1. Run in Context
|
|
37
|
+
|
|
40
38
|
return client.startTrace({
|
|
41
39
|
method: req.method || 'GET',
|
|
42
40
|
path: path,
|
|
43
41
|
userAgent: req.headers['user-agent'],
|
|
44
42
|
ip: req.headers['x-forwarded-for'] || req.socket?.remoteAddress,
|
|
45
43
|
}, async () => {
|
|
46
|
-
|
|
47
|
-
// 2. Hook Response
|
|
44
|
+
|
|
48
45
|
const done = () => {
|
|
49
46
|
client.endTrace(res.statusCode || 200, { route: normalizePath(path) });
|
|
50
47
|
};
|
|
51
|
-
|
|
48
|
+
|
|
52
49
|
res.once('finish', done);
|
|
53
|
-
res.once('close', done);
|
|
50
|
+
res.once('close', done);
|
|
54
51
|
|
|
55
|
-
// 3. Execute
|
|
56
52
|
try {
|
|
57
53
|
return await handler(req, res);
|
|
58
|
-
} catch (e) {
|
|
59
|
-
//
|
|
60
|
-
|
|
54
|
+
} catch (e: any) {
|
|
55
|
+
// AUTOMATIC ERROR CAPTURE
|
|
56
|
+
client.captureError(e);
|
|
57
|
+
// Note: In Pages dir, we rely on 'finish' listener above to close trace,
|
|
58
|
+
// but capturing here ensures the error data is attached.
|
|
61
59
|
throw e;
|
|
62
60
|
}
|
|
63
61
|
});
|