node-loop-detective 1.4.0 → 1.5.0

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 CHANGED
@@ -120,7 +120,8 @@ loop-detective -p 12345 --json
120
120
  | Type | What It Tracks |
121
121
  |------|---------------|
122
122
  | 🌐 HTTP/HTTPS | Outgoing HTTP requests — method, target, status code, duration |
123
- | 🔍 DNS | DNS lookupshostname, resolution time |
123
+ | 🌐 Fetch | Global `fetch()` calls (Node.js 18+) method, target, status, duration |
124
+ | 🔍 DNS | DNS lookups — callback and promise API (`dns.lookup` + `dns.promises.lookup`) |
124
125
  | 🔌 TCP | TCP connections — target host:port, connect time (covers databases, Redis, etc.) |
125
126
 
126
127
  Each slow I/O event includes the caller stack trace, so you know exactly which code initiated the slow operation.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-loop-detective",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
4
4
  "description": "Detect event loop blocking & lag in running Node.js apps without code changes or restarts",
5
5
  "main": "src/index.js",
6
6
  "bin": {
package/src/detective.js CHANGED
@@ -261,7 +261,7 @@ class Detective extends EventEmitter {
261
261
  patchHttp('http');
262
262
  patchHttp('https');
263
263
 
264
- // --- Track DNS lookups ---
264
+ // --- Track DNS lookups (callback API) ---
265
265
  (function patchDns() {
266
266
  let dns;
267
267
  try { dns = require('dns'); } catch { return; }
@@ -287,6 +287,40 @@ class Detective extends EventEmitter {
287
287
  if (callback) callback(err, address, family);
288
288
  });
289
289
  };
290
+
291
+ // --- Track DNS lookups (promise API, Node.js 10.6+) ---
292
+ if (dns.promises && dns.promises.lookup) {
293
+ const origPromiseLookup = dns.promises.lookup;
294
+ originals['dns.promises.lookup'] = { mod: dns.promises, key: 'lookup', fn: origPromiseLookup };
295
+
296
+ dns.promises.lookup = function patchedPromiseLookup(hostname, options) {
297
+ const startTime = Date.now();
298
+ const callerStack = captureCallerStack();
299
+
300
+ return origPromiseLookup.call(dns.promises, hostname, options).then(
301
+ (result) => {
302
+ const duration = Date.now() - startTime;
303
+ if (duration >= threshold) {
304
+ recordSlowOp({
305
+ type: 'dns', target: hostname, duration,
306
+ timestamp: Date.now(), stack: callerStack,
307
+ });
308
+ }
309
+ return result;
310
+ },
311
+ (err) => {
312
+ const duration = Date.now() - startTime;
313
+ if (duration >= threshold) {
314
+ recordSlowOp({
315
+ type: 'dns', target: hostname, duration,
316
+ error: err.message, timestamp: Date.now(), stack: callerStack,
317
+ });
318
+ }
319
+ throw err;
320
+ }
321
+ );
322
+ };
323
+ }
290
324
  })();
291
325
 
292
326
  // --- Track TCP socket connections ---
@@ -320,10 +354,63 @@ class Detective extends EventEmitter {
320
354
  };
321
355
  })();
322
356
 
357
+ // --- Track global fetch() (Node.js 18+) ---
358
+ (function patchFetch() {
359
+ if (typeof globalThis.fetch !== 'function') return;
360
+ const origFetch = globalThis.fetch;
361
+ originals['globalThis.fetch'] = { mod: globalThis, key: 'fetch', fn: origFetch };
362
+
363
+ globalThis.fetch = function patchedFetch(input, init) {
364
+ const startTime = Date.now();
365
+ const callerStack = captureCallerStack();
366
+
367
+ // Extract target URL
368
+ let target = 'unknown';
369
+ let method = 'GET';
370
+ if (typeof input === 'string') {
371
+ target = input;
372
+ } else if (input && typeof input === 'object') {
373
+ target = input.url || input.href || String(input);
374
+ method = (input.method || 'GET').toUpperCase();
375
+ }
376
+ if (init && init.method) {
377
+ method = init.method.toUpperCase();
378
+ }
379
+ // Shorten target for display
380
+ try {
381
+ const u = new URL(target);
382
+ target = u.host + u.pathname;
383
+ } catch {}
384
+
385
+ return origFetch.call(this, input, init).then(
386
+ (res) => {
387
+ const duration = Date.now() - startTime;
388
+ if (duration >= threshold) {
389
+ recordSlowOp({
390
+ type: 'fetch', method, target,
391
+ statusCode: res.status, duration, timestamp: Date.now(), stack: callerStack,
392
+ });
393
+ }
394
+ return res;
395
+ },
396
+ (err) => {
397
+ const duration = Date.now() - startTime;
398
+ if (duration >= threshold) {
399
+ recordSlowOp({
400
+ type: 'fetch', method, target,
401
+ error: err.message, duration, timestamp: Date.now(), stack: callerStack,
402
+ });
403
+ }
404
+ throw err;
405
+ }
406
+ );
407
+ };
408
+ })();
409
+
323
410
  globalThis.__loopDetectiveIO = {
324
411
  getSlowOps: () => slowOps.splice(0),
325
412
  cleanup: () => {
326
- // Fix #1: Restore all original functions
413
+ // Restore all original functions
327
414
  for (const entry of Object.values(originals)) {
328
415
  entry.mod[entry.key] = entry.fn;
329
416
  }
package/src/reporter.js CHANGED
@@ -45,8 +45,8 @@ class Reporter {
45
45
  this.slowIOEvents.push(data);
46
46
  if (this.config.json) return;
47
47
  const severity = data.duration > 5000 ? COLORS.red : data.duration > 2000 ? COLORS.yellow : COLORS.magenta;
48
- const icon = data.type === 'http' ? '🌐' : data.type === 'dns' ? '🔍' : '🔌';
49
- const detail = data.type === 'http'
48
+ const icon = data.type === 'http' ? '🌐' : data.type === 'fetch' ? '🌐' : data.type === 'dns' ? '🔍' : '🔌';
49
+ const detail = data.type === 'http' || data.type === 'fetch'
50
50
  ? `${data.method} ${data.target} → ${data.statusCode || data.error || '?'}`
51
51
  : data.type === 'dns'
52
52
  ? `lookup ${data.target}${data.error ? ' (' + data.error + ')' : ''}`
@@ -177,7 +177,7 @@ class Reporter {
177
177
  }
178
178
 
179
179
  for (const [type, ops] of Object.entries(byType)) {
180
- const icon = type === 'http' ? '🌐' : type === 'dns' ? '🔍' : '🔌';
180
+ const icon = type === 'http' ? '🌐' : type === 'fetch' ? '🌐' : type === 'dns' ? '🔍' : '🔌';
181
181
  const maxDur = Math.max(...ops.map(o => o.duration));
182
182
  const avgDur = Math.round(ops.reduce((s, o) => s + o.duration, 0) / ops.length);
183
183
  this._print(`\n ${icon} ${COLORS.bold}${type.toUpperCase()}${COLORS.reset} — ${ops.length} slow ops, avg ${avgDur}ms, max ${maxDur}ms`);
@@ -185,7 +185,7 @@ class Reporter {
185
185
  // Group by target
186
186
  const byTarget = {};
187
187
  for (const op of ops) {
188
- const key = op.type === 'http' ? `${op.method} ${op.target}` : op.target;
188
+ const key = (op.type === 'http' || op.type === 'fetch') ? `${op.method} ${op.target}` : op.target;
189
189
  if (!byTarget[key]) byTarget[key] = { count: 0, totalDuration: 0, maxDuration: 0, errors: 0, stack: op.stack };
190
190
  byTarget[key].count++;
191
191
  byTarget[key].totalDuration += op.duration;