kohi-node 0.1.2 → 0.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/dist/index.d.ts +62 -1
- package/dist/index.js +189 -27
- package/package.json +3 -11
package/dist/index.d.ts
CHANGED
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
import { IncomingMessage, ServerResponse } from 'http';
|
|
4
4
|
import { gzip } from 'zlib';
|
|
5
5
|
|
|
6
|
-
export declare const VERSION = "node:0.1.2";
|
|
7
6
|
export declare const DEFAULT_INGEST_ENDPOINT = "https://kohicorp.com/api/ingest";
|
|
8
7
|
export declare const DEFAULT_MAX_CONCURRENT_SENDS = 5;
|
|
9
8
|
export declare const DEFAULT_QUEUE_SIZE = 5000;
|
|
@@ -76,16 +75,78 @@ export declare class Monitor {
|
|
|
76
75
|
addEvent(evt: Event$1): void;
|
|
77
76
|
shutdown(): Promise<void>;
|
|
78
77
|
close(): Promise<void>;
|
|
78
|
+
captureException(error: Error, opts?: {
|
|
79
|
+
fingerprint?: string;
|
|
80
|
+
}): void;
|
|
81
|
+
captureExceptionWithRequest(error: Error, reqURL: string, reqMethod: string, clientIP?: string): void;
|
|
82
|
+
captureMessage(msg: string, level?: "fatal" | "error" | "warning"): void;
|
|
83
|
+
captureHttpError(statusCode: number, responseBody: string, reqURL: string, reqMethod: string, clientIP?: string): void;
|
|
84
|
+
private enqueue;
|
|
79
85
|
private startWorker;
|
|
80
86
|
private sendBatch;
|
|
81
87
|
private sendBatchAttempt;
|
|
82
88
|
}
|
|
83
89
|
export declare function createMonitor(config: Config): Monitor;
|
|
90
|
+
export interface StackFrame {
|
|
91
|
+
file: string;
|
|
92
|
+
line: number;
|
|
93
|
+
column?: number;
|
|
94
|
+
function: string;
|
|
95
|
+
context?: string[];
|
|
96
|
+
in_app: boolean;
|
|
97
|
+
}
|
|
98
|
+
export interface ErrorEvent {
|
|
99
|
+
_type: "error";
|
|
100
|
+
error_type: string;
|
|
101
|
+
error_message: string;
|
|
102
|
+
level: "fatal" | "error" | "warning";
|
|
103
|
+
fingerprint?: string;
|
|
104
|
+
stack_frames: StackFrame[];
|
|
105
|
+
request_url?: string;
|
|
106
|
+
request_method?: string;
|
|
107
|
+
timestamp: number;
|
|
108
|
+
client_ip?: string;
|
|
109
|
+
}
|
|
110
|
+
export declare function parseNodeStack(stack: string): StackFrame[];
|
|
111
|
+
declare let _monitorRef: {
|
|
112
|
+
captureException(err: Error): void;
|
|
113
|
+
captureExceptionWithRequest(err: Error, url: string, method: string, clientIP?: string): void;
|
|
114
|
+
isEnabled(): boolean;
|
|
115
|
+
} | null;
|
|
116
|
+
/**
|
|
117
|
+
* Installs process-level error handlers for uncaught exceptions and unhandled
|
|
118
|
+
* rejections. Captured errors are queued for the next batch send.
|
|
119
|
+
*
|
|
120
|
+
* Uses AsyncLocalStorage to propagate request context — async errors thrown
|
|
121
|
+
* inside request handlers are captured with the originating URL and method.
|
|
122
|
+
*
|
|
123
|
+
* NOTE: Adding an `uncaughtException` listener prevents Node.js from
|
|
124
|
+
* terminating the process on unhandled exceptions. If your application relies
|
|
125
|
+
* on crash-and-restart behavior (e.g., with PM2 or systemd), you should still
|
|
126
|
+
* call `process.exit(1)` in your own `uncaughtException` handler after the
|
|
127
|
+
* Kohi handler runs.
|
|
128
|
+
*/
|
|
129
|
+
export declare function installGlobalHooks(monitor: typeof _monitorRef): void;
|
|
84
130
|
export declare function expressMiddleware(monitor: Monitor): (req: IncomingMessage, res: ServerResponse, next: () => void) => void;
|
|
131
|
+
export declare function expressErrorHandler(monitor: Monitor): (err: Error, req: IncomingMessage, res: ServerResponse, next: (err?: unknown) => void) => void;
|
|
85
132
|
export declare function instrumentNextJs(cfg: Config): void;
|
|
133
|
+
/**
|
|
134
|
+
* Next.js onRequestError handler for instrumentation.ts.
|
|
135
|
+
* Usage in instrumentation.ts:
|
|
136
|
+
* import { onRequestError } from '@kohi/node';
|
|
137
|
+
* export { onRequestError };
|
|
138
|
+
*/
|
|
139
|
+
export declare function onRequestError(err: {
|
|
140
|
+
digest: string;
|
|
141
|
+
} & Error, request: {
|
|
142
|
+
path: string;
|
|
143
|
+
method: string;
|
|
144
|
+
headers: Record<string, string | string[]>;
|
|
145
|
+
}, _context: unknown): void;
|
|
86
146
|
export declare function init(cfg: Config): {
|
|
87
147
|
monitor: Monitor;
|
|
88
148
|
expressMiddleware: () => (req: import("http").IncomingMessage, res: import("http").ServerResponse, next: () => void) => void;
|
|
149
|
+
expressErrorHandler: () => (err: Error, req: import("http").IncomingMessage, res: import("http").ServerResponse, next: (err?: unknown) => void) => void;
|
|
89
150
|
instrumentNextJs: () => void;
|
|
90
151
|
};
|
|
91
152
|
declare const defaultExport: typeof Monitor & {
|
package/dist/index.js
CHANGED
|
@@ -37,22 +37,25 @@ __export(index_exports, {
|
|
|
37
37
|
Monitor: () => Monitor,
|
|
38
38
|
REDACTION_MASK: () => REDACTION_MASK,
|
|
39
39
|
SENSITIVE_KEYS: () => SENSITIVE_KEYS,
|
|
40
|
-
VERSION: () => VERSION,
|
|
41
40
|
canonicalHeaders: () => canonicalHeaders,
|
|
42
41
|
createMonitor: () => createMonitor,
|
|
43
42
|
default: () => index_default,
|
|
43
|
+
expressErrorHandler: () => expressErrorHandler,
|
|
44
44
|
expressMiddleware: () => expressMiddleware,
|
|
45
45
|
extractClientIP: () => extractClientIP,
|
|
46
46
|
firstIPFromList: () => firstIPFromList,
|
|
47
47
|
gzipAsync: () => gzipAsync,
|
|
48
48
|
init: () => init,
|
|
49
|
+
installGlobalHooks: () => installGlobalHooks,
|
|
49
50
|
instrumentNextJs: () => instrumentNextJs,
|
|
50
51
|
isRetryableStatus: () => isRetryableStatus,
|
|
51
52
|
isValidIP: () => isValidIP,
|
|
52
53
|
jitter: () => jitter,
|
|
53
54
|
normalizeIP: () => normalizeIP,
|
|
55
|
+
onRequestError: () => onRequestError,
|
|
54
56
|
parseForwardedHeader: () => parseForwardedHeader,
|
|
55
57
|
parseJSONBody: () => parseJSONBody,
|
|
58
|
+
parseNodeStack: () => parseNodeStack,
|
|
56
59
|
peerIPFromRemoteAddr: () => peerIPFromRemoteAddr,
|
|
57
60
|
redactEvent: () => redactEvent,
|
|
58
61
|
redactHeaders: () => redactHeaders,
|
|
@@ -67,8 +70,8 @@ module.exports = __toCommonJS(index_exports);
|
|
|
67
70
|
var import_crypto = require("crypto");
|
|
68
71
|
var import_zlib = require("zlib");
|
|
69
72
|
var import_util = require("util");
|
|
70
|
-
var VERSION = "node:0.1.2";
|
|
71
73
|
var DEFAULT_INGEST_ENDPOINT = "https://kohicorp.com/api/ingest";
|
|
74
|
+
var MAX_REQUEST_BODY = 64 * 1024;
|
|
72
75
|
var MAX_RESPONSE_BODY = 64 * 1024;
|
|
73
76
|
var DEFAULT_MAX_CONCURRENT_SENDS = 5;
|
|
74
77
|
var DEFAULT_QUEUE_SIZE = 5e3;
|
|
@@ -190,6 +193,70 @@ function parseJSONBody(raw) {
|
|
|
190
193
|
}
|
|
191
194
|
}
|
|
192
195
|
|
|
196
|
+
// src/errors.ts
|
|
197
|
+
var import_node_async_hooks = require("node:async_hooks");
|
|
198
|
+
function parseNodeStack(stack) {
|
|
199
|
+
const frames = [];
|
|
200
|
+
for (const line of stack.split("\n")) {
|
|
201
|
+
const trimmed = line.trim();
|
|
202
|
+
if (!trimmed.startsWith("at ")) continue;
|
|
203
|
+
const frame = parseStackLine(trimmed.slice(3));
|
|
204
|
+
if (frame) frames.push(frame);
|
|
205
|
+
}
|
|
206
|
+
return frames;
|
|
207
|
+
}
|
|
208
|
+
function parseStackLine(line) {
|
|
209
|
+
const parenMatch = line.match(/^(.+?)\s+\((.+):(\d+):(\d+)\)$/);
|
|
210
|
+
if (parenMatch) {
|
|
211
|
+
return { function: parenMatch[1], file: parenMatch[2], line: +parenMatch[3], column: +parenMatch[4], in_app: isInApp(parenMatch[2]) };
|
|
212
|
+
}
|
|
213
|
+
const anonMatch = line.match(/^(.+):(\d+):(\d+)$/);
|
|
214
|
+
if (anonMatch) {
|
|
215
|
+
return { function: "<anonymous>", file: anonMatch[1], line: +anonMatch[2], column: +anonMatch[3], in_app: isInApp(anonMatch[1]) };
|
|
216
|
+
}
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
function isInApp(file) {
|
|
220
|
+
return !file.includes("node_modules/") && !file.includes("node_modules\\") && !file.startsWith("node:") && !file.startsWith("internal/");
|
|
221
|
+
}
|
|
222
|
+
function buildErrorEvent(err, level = "error", reqURL, reqMethod, clientIP) {
|
|
223
|
+
const evt = {
|
|
224
|
+
_type: "error",
|
|
225
|
+
error_type: err.constructor?.name ?? "Error",
|
|
226
|
+
error_message: err.message,
|
|
227
|
+
level,
|
|
228
|
+
stack_frames: err.stack ? parseNodeStack(err.stack) : [],
|
|
229
|
+
request_url: reqURL,
|
|
230
|
+
request_method: reqMethod,
|
|
231
|
+
timestamp: Date.now()
|
|
232
|
+
};
|
|
233
|
+
if (clientIP) evt.client_ip = clientIP;
|
|
234
|
+
return evt;
|
|
235
|
+
}
|
|
236
|
+
var requestContext = new import_node_async_hooks.AsyncLocalStorage();
|
|
237
|
+
var _hooksInstalled = false;
|
|
238
|
+
var _monitorRef = null;
|
|
239
|
+
function installGlobalHooks(monitor) {
|
|
240
|
+
_monitorRef = monitor;
|
|
241
|
+
if (_hooksInstalled) return;
|
|
242
|
+
_hooksInstalled = true;
|
|
243
|
+
const capture = (raw) => {
|
|
244
|
+
if (!_monitorRef?.isEnabled()) return;
|
|
245
|
+
const error = raw instanceof Error ? raw : new Error(String(raw));
|
|
246
|
+
const reqCtx = requestContext.getStore();
|
|
247
|
+
try {
|
|
248
|
+
if (reqCtx) {
|
|
249
|
+
_monitorRef.captureExceptionWithRequest(error, reqCtx.url, reqCtx.method, reqCtx.clientIP);
|
|
250
|
+
} else {
|
|
251
|
+
_monitorRef.captureException(error);
|
|
252
|
+
}
|
|
253
|
+
} catch {
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
process.on("uncaughtException", capture);
|
|
257
|
+
process.on("unhandledRejection", capture);
|
|
258
|
+
}
|
|
259
|
+
|
|
193
260
|
// src/monitor.ts
|
|
194
261
|
var Monitor = class _Monitor {
|
|
195
262
|
projectKey;
|
|
@@ -218,7 +285,10 @@ var Monitor = class _Monitor {
|
|
|
218
285
|
this.secret = Buffer.from(config.secretKey, "base64url");
|
|
219
286
|
this.logger = config.logger ?? noopLogger;
|
|
220
287
|
this.enabled = enabled;
|
|
221
|
-
if (this.enabled)
|
|
288
|
+
if (this.enabled) {
|
|
289
|
+
this.startWorker();
|
|
290
|
+
installGlobalHooks(this);
|
|
291
|
+
}
|
|
222
292
|
}
|
|
223
293
|
static noop() {
|
|
224
294
|
return new _Monitor({ projectKey: "pk_00000000000000000000AA", secretKey: "0000000000000000000000000000000000000000000", enabled: false });
|
|
@@ -229,11 +299,7 @@ var Monitor = class _Monitor {
|
|
|
229
299
|
addEvent(evt) {
|
|
230
300
|
if (!this.enabled || this.closed) return;
|
|
231
301
|
const normalized = { ...evt, method: evt.method.toUpperCase(), request_headers: canonicalHeaders(evt.request_headers), response_headers: canonicalHeaders(evt.response_headers) };
|
|
232
|
-
|
|
233
|
-
this.logger.warn("kohi monitor queue is full; dropping event");
|
|
234
|
-
return;
|
|
235
|
-
}
|
|
236
|
-
this.queue.push(normalized);
|
|
302
|
+
this.enqueue(normalized);
|
|
237
303
|
}
|
|
238
304
|
async shutdown() {
|
|
239
305
|
if (this.closed) return;
|
|
@@ -244,6 +310,51 @@ var Monitor = class _Monitor {
|
|
|
244
310
|
async close() {
|
|
245
311
|
return this.shutdown();
|
|
246
312
|
}
|
|
313
|
+
captureException(error, opts) {
|
|
314
|
+
if (!this.enabled || this.closed) return;
|
|
315
|
+
const evt = buildErrorEvent(error, "error");
|
|
316
|
+
if (opts?.fingerprint) evt.fingerprint = opts.fingerprint;
|
|
317
|
+
this.enqueue(evt);
|
|
318
|
+
}
|
|
319
|
+
captureExceptionWithRequest(error, reqURL, reqMethod, clientIP) {
|
|
320
|
+
if (!this.enabled || this.closed) return;
|
|
321
|
+
const evt = buildErrorEvent(error, "error", reqURL, reqMethod, clientIP);
|
|
322
|
+
this.enqueue(evt);
|
|
323
|
+
}
|
|
324
|
+
captureMessage(msg, level = "error") {
|
|
325
|
+
if (!this.enabled || this.closed) return;
|
|
326
|
+
const evt = {
|
|
327
|
+
_type: "error",
|
|
328
|
+
error_type: "Message",
|
|
329
|
+
error_message: msg,
|
|
330
|
+
level,
|
|
331
|
+
stack_frames: [],
|
|
332
|
+
timestamp: Date.now()
|
|
333
|
+
};
|
|
334
|
+
this.enqueue(evt);
|
|
335
|
+
}
|
|
336
|
+
captureHttpError(statusCode, responseBody, reqURL, reqMethod, clientIP) {
|
|
337
|
+
if (!this.enabled || this.closed) return;
|
|
338
|
+
const evt = {
|
|
339
|
+
_type: "error",
|
|
340
|
+
error_type: `HTTP ${statusCode}`,
|
|
341
|
+
error_message: responseBody,
|
|
342
|
+
level: "error",
|
|
343
|
+
stack_frames: [],
|
|
344
|
+
request_url: reqURL,
|
|
345
|
+
request_method: reqMethod,
|
|
346
|
+
timestamp: Date.now()
|
|
347
|
+
};
|
|
348
|
+
if (clientIP) evt.client_ip = clientIP;
|
|
349
|
+
this.enqueue(evt);
|
|
350
|
+
}
|
|
351
|
+
enqueue(evt) {
|
|
352
|
+
if (this.queue.length >= this.queueSize) {
|
|
353
|
+
this.logger.warn("kohi monitor queue is full; dropping event");
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
this.queue.push(evt);
|
|
357
|
+
}
|
|
247
358
|
startWorker() {
|
|
248
359
|
const tick = async () => {
|
|
249
360
|
let lastFlush = Date.now();
|
|
@@ -266,7 +377,7 @@ var Monitor = class _Monitor {
|
|
|
266
377
|
tick().catch((err) => this.logger.error(`kohi: worker fatal: ${err}`));
|
|
267
378
|
}
|
|
268
379
|
async sendBatch(events) {
|
|
269
|
-
const redacted = events.map(redactEvent);
|
|
380
|
+
const redacted = events.map((e) => "_type" in e && e._type === "error" ? e : redactEvent(e));
|
|
270
381
|
let payload;
|
|
271
382
|
try {
|
|
272
383
|
payload = await gzipAsync(Buffer.from(JSON.stringify(redacted), "utf8"));
|
|
@@ -325,9 +436,24 @@ function expressMiddleware(monitor) {
|
|
|
325
436
|
return;
|
|
326
437
|
}
|
|
327
438
|
const start = Date.now();
|
|
439
|
+
const reqURL = req.url ?? "/";
|
|
440
|
+
const reqMethod = (req.method ?? "GET").toUpperCase();
|
|
441
|
+
const reqHeaders = canonicalHeaders(req.headers);
|
|
442
|
+
const peerIP = peerIPFromRemoteAddr(req.socket?.remoteAddress ?? "");
|
|
443
|
+
const clientIP = extractClientIP(reqHeaders, peerIP);
|
|
328
444
|
const chunks = [], resChunks = [];
|
|
329
|
-
let reqBodyBuf = Buffer.alloc(0), totalRes = 0, capped = false, reqBodyReady = false, sent = false;
|
|
330
|
-
req.on("data", (c) =>
|
|
445
|
+
let reqBodyBuf = Buffer.alloc(0), totalReq = 0, totalRes = 0, capped = false, reqCapped = false, reqBodyReady = false, sent = false, errorCaptured = false;
|
|
446
|
+
req.on("data", (c) => {
|
|
447
|
+
if (reqCapped) return;
|
|
448
|
+
const rem = MAX_REQUEST_BODY - totalReq;
|
|
449
|
+
if (rem <= 0) {
|
|
450
|
+
reqCapped = true;
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
chunks.push(c.subarray(0, Math.min(c.length, rem)));
|
|
454
|
+
totalReq += Math.min(c.length, rem);
|
|
455
|
+
if (c.length > rem) reqCapped = true;
|
|
456
|
+
});
|
|
331
457
|
req.once("end", () => {
|
|
332
458
|
reqBodyBuf = Buffer.concat(chunks);
|
|
333
459
|
reqBodyReady = true;
|
|
@@ -356,10 +482,6 @@ function expressMiddleware(monitor) {
|
|
|
356
482
|
const finalize = () => {
|
|
357
483
|
if (sent) return;
|
|
358
484
|
sent = true;
|
|
359
|
-
const reqHeaders = canonicalHeaders(req.headers);
|
|
360
|
-
reqHeaders["x-kohi-version"] = VERSION;
|
|
361
|
-
const peerIP = peerIPFromRemoteAddr(req.socket?.remoteAddress ?? "");
|
|
362
|
-
const clientIP = extractClientIP(reqHeaders, peerIP);
|
|
363
485
|
const status = res.statusCode || 200;
|
|
364
486
|
let resBody = parseJSONBody(Buffer.concat(resChunks));
|
|
365
487
|
if (status >= 500 && resChunks.length === 0) resBody = { error: res.statusMessage || "Internal Server Error" };
|
|
@@ -377,6 +499,11 @@ function expressMiddleware(monitor) {
|
|
|
377
499
|
client_ip: clientIP
|
|
378
500
|
};
|
|
379
501
|
monitor.addEvent(evt);
|
|
502
|
+
if (status >= 500 && !errorCaptured && !res._kohiErrorCaptured) {
|
|
503
|
+
const body = Buffer.concat(resChunks).toString("utf8").trim();
|
|
504
|
+
const msg = body.slice(0, 256) || res.statusMessage || "Internal Server Error";
|
|
505
|
+
monitor.captureHttpError(status, msg, reqURL, reqMethod, clientIP);
|
|
506
|
+
}
|
|
380
507
|
};
|
|
381
508
|
const fail = (err) => {
|
|
382
509
|
if (sent) return;
|
|
@@ -388,18 +515,34 @@ function expressMiddleware(monitor) {
|
|
|
388
515
|
res.on("finish", finalize);
|
|
389
516
|
res.on("close", finalize);
|
|
390
517
|
res.on("error", fail);
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
518
|
+
requestContext.run({ url: reqURL, method: reqMethod, clientIP }, () => {
|
|
519
|
+
try {
|
|
520
|
+
next();
|
|
521
|
+
} catch (err) {
|
|
522
|
+
errorCaptured = true;
|
|
523
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
524
|
+
monitor.captureExceptionWithRequest(error, reqURL, reqMethod, clientIP);
|
|
525
|
+
fail(err);
|
|
526
|
+
throw err;
|
|
527
|
+
}
|
|
528
|
+
});
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
function expressErrorHandler(monitor) {
|
|
532
|
+
return (err, req, res, next) => {
|
|
533
|
+
if (monitor.isEnabled()) {
|
|
534
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
535
|
+
const headers = canonicalHeaders(req.headers);
|
|
536
|
+
const ip = extractClientIP(headers, peerIPFromRemoteAddr(req.socket?.remoteAddress ?? ""));
|
|
537
|
+
monitor.captureExceptionWithRequest(error, req.url ?? "/", (req.method ?? "GET").toUpperCase(), ip);
|
|
538
|
+
res._kohiErrorCaptured = true;
|
|
396
539
|
}
|
|
540
|
+
next(err);
|
|
397
541
|
};
|
|
398
542
|
}
|
|
399
543
|
|
|
400
544
|
// src/integrations/nextjs.ts
|
|
401
545
|
var http = __toESM(require("http"));
|
|
402
|
-
var MAX_REQUEST_BODY = 64 * 1024;
|
|
403
546
|
var sharedMonitor = null;
|
|
404
547
|
var instrumented = false;
|
|
405
548
|
var requestBodies = /* @__PURE__ */ new WeakMap();
|
|
@@ -435,7 +578,9 @@ function instrumentNextJs(cfg) {
|
|
|
435
578
|
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, encoding);
|
|
436
579
|
const remaining = MAX_REQUEST_BODY - bodyData.size;
|
|
437
580
|
if (remaining > 0) {
|
|
438
|
-
bodyData.chunks.push(
|
|
581
|
+
bodyData.chunks.push(
|
|
582
|
+
buf.subarray(0, Math.min(buf.length, remaining))
|
|
583
|
+
);
|
|
439
584
|
bodyData.size += Math.min(buf.length, remaining);
|
|
440
585
|
}
|
|
441
586
|
}
|
|
@@ -454,12 +599,14 @@ function instrumentNextJs(cfg) {
|
|
|
454
599
|
function instrumentRequest(req, res) {
|
|
455
600
|
if (!sharedMonitor) return;
|
|
456
601
|
const url = req.url ?? "/";
|
|
457
|
-
if (url.startsWith("/_next") || url.startsWith("/__next") || url.includes("/_next/"))
|
|
602
|
+
if (url.startsWith("/_next") || url.startsWith("/__next") || url.includes("/_next/"))
|
|
603
|
+
return;
|
|
458
604
|
if (!url.startsWith("/api/")) return;
|
|
459
605
|
const start = Date.now();
|
|
460
606
|
const method = req.method ?? "GET";
|
|
461
|
-
const reqHeaders = canonicalHeaders(
|
|
462
|
-
|
|
607
|
+
const reqHeaders = canonicalHeaders(
|
|
608
|
+
req.headers
|
|
609
|
+
);
|
|
463
610
|
const peerIP = peerIPFromRemoteAddr(req.socket?.remoteAddress ?? "");
|
|
464
611
|
const clientIP = extractClientIP(reqHeaders, peerIP);
|
|
465
612
|
const captureState = { total: 0, capped: false };
|
|
@@ -485,7 +632,9 @@ function instrumentRequest(req, res) {
|
|
|
485
632
|
status_code: res.statusCode,
|
|
486
633
|
request_headers: reqHeaders,
|
|
487
634
|
request_body: reqBody,
|
|
488
|
-
response_headers: canonicalHeaders(
|
|
635
|
+
response_headers: canonicalHeaders(
|
|
636
|
+
res.getHeaders()
|
|
637
|
+
),
|
|
489
638
|
response_body: parseJSONBody(resBody),
|
|
490
639
|
duration_ms: duration,
|
|
491
640
|
client_ip: clientIP
|
|
@@ -494,6 +643,15 @@ function instrumentRequest(req, res) {
|
|
|
494
643
|
return originalEnd(chunk, ...args);
|
|
495
644
|
};
|
|
496
645
|
}
|
|
646
|
+
function onRequestError(err, request, _context) {
|
|
647
|
+
if (!sharedMonitor) return;
|
|
648
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
649
|
+
const url = request?.path ?? "/";
|
|
650
|
+
const method = (request?.method ?? "GET").toUpperCase();
|
|
651
|
+
const headers = canonicalHeaders(request?.headers ?? {});
|
|
652
|
+
const clientIP = extractClientIP(headers, "");
|
|
653
|
+
sharedMonitor.captureExceptionWithRequest(error, url, method, clientIP);
|
|
654
|
+
}
|
|
497
655
|
|
|
498
656
|
// src/index.ts
|
|
499
657
|
function init(cfg) {
|
|
@@ -501,6 +659,7 @@ function init(cfg) {
|
|
|
501
659
|
return {
|
|
502
660
|
monitor,
|
|
503
661
|
expressMiddleware: () => expressMiddleware(monitor),
|
|
662
|
+
expressErrorHandler: () => expressErrorHandler(monitor),
|
|
504
663
|
instrumentNextJs: () => instrumentNextJs(cfg)
|
|
505
664
|
};
|
|
506
665
|
}
|
|
@@ -515,21 +674,24 @@ var index_default = defaultExport;
|
|
|
515
674
|
Monitor,
|
|
516
675
|
REDACTION_MASK,
|
|
517
676
|
SENSITIVE_KEYS,
|
|
518
|
-
VERSION,
|
|
519
677
|
canonicalHeaders,
|
|
520
678
|
createMonitor,
|
|
679
|
+
expressErrorHandler,
|
|
521
680
|
expressMiddleware,
|
|
522
681
|
extractClientIP,
|
|
523
682
|
firstIPFromList,
|
|
524
683
|
gzipAsync,
|
|
525
684
|
init,
|
|
685
|
+
installGlobalHooks,
|
|
526
686
|
instrumentNextJs,
|
|
527
687
|
isRetryableStatus,
|
|
528
688
|
isValidIP,
|
|
529
689
|
jitter,
|
|
530
690
|
normalizeIP,
|
|
691
|
+
onRequestError,
|
|
531
692
|
parseForwardedHeader,
|
|
532
693
|
parseJSONBody,
|
|
694
|
+
parseNodeStack,
|
|
533
695
|
peerIPFromRemoteAddr,
|
|
534
696
|
redactEvent,
|
|
535
697
|
redactHeaders,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kohi-node",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Kohi API monitoring SDK for Node.js",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
"README.md"
|
|
17
17
|
],
|
|
18
18
|
"scripts": {
|
|
19
|
-
"build": "rm -rf dist && esbuild src/index.ts --bundle --platform=node --target=node18 --outfile=dist/index.js && dts-bundle-generator -o dist/index.d.ts src/index.ts --no-check",
|
|
20
|
-
"test": "npx tsx --test tests/index.test.ts",
|
|
19
|
+
"build": "rm -rf dist && esbuild src/index.ts --bundle --platform=node --target=node18 --outfile=dist/index.js && dts-bundle-generator -o dist/index.d.ts src/index.ts --no-check --export-referenced-types=false",
|
|
20
|
+
"test": "npx tsx --test tests/index.test.ts tests/errors.test.ts",
|
|
21
21
|
"prepublishOnly": "npm run build"
|
|
22
22
|
},
|
|
23
23
|
"keywords": [
|
|
@@ -27,15 +27,7 @@
|
|
|
27
27
|
"kohi"
|
|
28
28
|
],
|
|
29
29
|
"license": "MIT",
|
|
30
|
-
"repository": {
|
|
31
|
-
"type": "git",
|
|
32
|
-
"url": "https://github.com/kohicorp/kohi",
|
|
33
|
-
"directory": "sdk/node"
|
|
34
|
-
},
|
|
35
30
|
"homepage": "https://kohicorp.com",
|
|
36
|
-
"bugs": {
|
|
37
|
-
"url": "https://github.com/kohicorp/kohi/issues"
|
|
38
|
-
},
|
|
39
31
|
"devDependencies": {
|
|
40
32
|
"@types/node": "^20.19.30",
|
|
41
33
|
"dts-bundle-generator": "^9.5.1",
|