peekinside 1.2.5 → 1.3.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 +5 -2
- package/index.d.ts +41 -12
- package/package.json +10 -1
- package/src/PeekInside.js +70 -14
- package/src/capture/exchange-recorder.js +5 -12
- package/src/capture/http-message.js +17 -0
- package/src/dumps/dump-capture.js +100 -0
- package/src/dumps/dump-record.js +57 -0
- package/src/dumps/dump-value.js +428 -0
- package/src/exceptions/exception-capture.js +5 -4
- package/src/exceptions/request-context.js +118 -10
- package/src/hub/ingest-payload.js +66 -6
- package/src/hub/project-log.js +71 -4
- package/src/hub/server.js +18 -7
- package/src/hub/spool-ingester.js +1 -0
- package/src/hub/store.js +44 -1
- package/src/instrumentation/index.js +48 -3
- package/src/instrumentation/jobs.js +93 -2
- package/src/instrumentation/loggers.js +78 -4
- package/src/integrations/fastify.js +18 -1
- package/src/integrations/hono.js +209 -0
- package/src/integrations/nest.js +277 -0
- package/src/integrations/server-binder.js +9 -6
- package/src/logs/log-capture.js +5 -5
- package/src/logs/log-record.js +2 -2
- package/src/record.js +6 -2
- package/src/timeline/timeline.js +24 -3
- package/src/transport/batch.js +4 -1
- package/src/transport/cloud.js +12 -0
- package/src/transport/local.js +7 -5
package/README.md
CHANGED
|
@@ -21,11 +21,13 @@ const app = express();
|
|
|
21
21
|
PeekInside.attach(app);
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
-
Start your app, and PeekInside prints the link to its dashboard. It works with Express, Koa, Fastify and the native http module.
|
|
24
|
+
Start your app, and PeekInside prints the link to its dashboard. It works with Express, Koa, Fastify, NestJS, Hono and the native http module.
|
|
25
25
|
|
|
26
26
|
Check the setup before anything breaks on its own: `npx peekinside test` sends a test error from your project.
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
While you develop, `PeekInside.dump(value)` shows any value in the local dashboard instead of your terminal: as a tree you can open, under the request it belongs to, with the line that dumped it.
|
|
29
|
+
|
|
30
|
+
Also recorded: background jobs (node-cron, cron, BullMQ, @nestjs/schedule, or `PeekInside.job()`, with an email when a scheduled job misses its run), log lines from the console, pino, winston and NestJS's Logger, event-loop blocks and memory, the deploy each error came from, the user it happened to (`PeekInside.setUser()`), and TypeScript lines through your source maps.
|
|
29
31
|
|
|
30
32
|
## Documentation
|
|
31
33
|
|
|
@@ -38,6 +40,7 @@ Everything else is in the docs: **[peekinside.dev/docs](https://peekinside.dev/d
|
|
|
38
40
|
- [Timeline and queries](https://peekinside.dev/docs/timeline)
|
|
39
41
|
- [Logs](https://peekinside.dev/docs/logs)
|
|
40
42
|
- [Jobs and schedules](https://peekinside.dev/docs/jobs)
|
|
43
|
+
- [Dumps](https://peekinside.dev/docs/dumps)
|
|
41
44
|
- [Runtime](https://peekinside.dev/docs/runtime)
|
|
42
45
|
- [Alerts](https://peekinside.dev/docs/alerts)
|
|
43
46
|
- [Configuration](https://peekinside.dev/docs/configuration)
|
package/index.d.ts
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
* and the exceptions that happen while handling them. In development they go to the free local
|
|
4
4
|
* dashboard; with an API key they are encrypted and sent to PeekInside Cloud.
|
|
5
5
|
*
|
|
6
|
-
* The signatures are structural, so Express, Connect, Koa, Fastify and node:http
|
|
7
|
-
* without PeekInside depending on any of those packages.
|
|
6
|
+
* The signatures are structural, so Express, Connect, Koa, Fastify, NestJS, Hono and node:http
|
|
7
|
+
* objects fit without PeekInside depending on any of those packages.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import type { IncomingMessage, Server, ServerResponse } from "node:http";
|
|
@@ -67,8 +67,8 @@ export interface PeekInsideOptions {
|
|
|
67
67
|
projectRoot?: string;
|
|
68
68
|
|
|
69
69
|
/**
|
|
70
|
-
* Records exceptions automatically: errors that reach Express, Koa or
|
|
71
|
-
* uncaught exceptions and unhandled rejections. `report()` works either way.
|
|
70
|
+
* Records exceptions automatically: errors that reach Express, Koa, Fastify, NestJS or Hono error
|
|
71
|
+
* handling, uncaught exceptions and unhandled rejections. `report()` works either way.
|
|
72
72
|
* @default true
|
|
73
73
|
*/
|
|
74
74
|
captureExceptions?: boolean;
|
|
@@ -109,8 +109,8 @@ export interface PeekInsideOptions {
|
|
|
109
109
|
captureOutgoing?: boolean;
|
|
110
110
|
|
|
111
111
|
/**
|
|
112
|
-
* Records log lines from the console, pino and
|
|
113
|
-
* run) they are written in, and on the Logs page, from anywhere in the app.
|
|
112
|
+
* Records log lines from the console, pino, winston and NestJS's Logger: on the timeline of the
|
|
113
|
+
* request (or job run) they are written in, and on the Logs page, from anywhere in the app.
|
|
114
114
|
* @default true
|
|
115
115
|
*/
|
|
116
116
|
captureLogs?: boolean;
|
|
@@ -122,7 +122,8 @@ export interface PeekInsideOptions {
|
|
|
122
122
|
logLevel?: "debug" | "info" | "warn" | "error";
|
|
123
123
|
|
|
124
124
|
/**
|
|
125
|
-
* Records runs of node-cron, cron and
|
|
125
|
+
* Records runs of node-cron, cron, BullMQ and @nestjs/schedule jobs automatically (see `job()`
|
|
126
|
+
* for other jobs).
|
|
126
127
|
* @default true
|
|
127
128
|
*/
|
|
128
129
|
captureJobs?: boolean;
|
|
@@ -217,9 +218,14 @@ export type CaptureOptions = Pick<PeekInsideOptions, "maxBodySize" | "ignorePath
|
|
|
217
218
|
|
|
218
219
|
/**
|
|
219
220
|
* What `PeekInside.attach()` understands: an Express or Connect app, a Koa app, a Fastify
|
|
220
|
-
* instance, or a node:http (or https) server.
|
|
221
|
+
* instance, a NestJS app or application context, a Hono app, or a node:http (or https) server.
|
|
221
222
|
*/
|
|
222
|
-
export type AttachTarget =
|
|
223
|
+
export type AttachTarget =
|
|
224
|
+
| Server
|
|
225
|
+
| { use(...args: any[]): unknown }
|
|
226
|
+
| { addHook(...args: any[]): unknown }
|
|
227
|
+
| { select(...args: any[]): unknown; useLogger(...args: any[]): unknown }
|
|
228
|
+
| { fetch(...args: any[]): unknown; onError(...args: any[]): unknown };
|
|
223
229
|
|
|
224
230
|
/** Express and Connect style middleware. */
|
|
225
231
|
export type NodeMiddleware = (req: IncomingMessage, res: ServerResponse, next: (error?: unknown) => void) => void;
|
|
@@ -236,7 +242,7 @@ export type KoaMiddleware = (
|
|
|
236
242
|
* @example
|
|
237
243
|
* import PeekInside from "peekinside";
|
|
238
244
|
*
|
|
239
|
-
* PeekInside.attach(app); // Express, Koa, Fastify or an http.Server
|
|
245
|
+
* PeekInside.attach(app); // Express, Koa, Fastify, NestJS, Hono or an http.Server
|
|
240
246
|
*/
|
|
241
247
|
declare class PeekInside {
|
|
242
248
|
/**
|
|
@@ -255,9 +261,11 @@ declare class PeekInside {
|
|
|
255
261
|
readonly release: string | null;
|
|
256
262
|
|
|
257
263
|
/**
|
|
258
|
-
* One-line setup: records every request the app handles.
|
|
264
|
+
* One-line setup: records every request the app handles. Attach a NestJS app before
|
|
265
|
+
* `app.listen()`, a Hono app before `serve()`, and a Fastify app before `fastify.listen()`.
|
|
259
266
|
*
|
|
260
|
-
* @param app - Express or Connect app, Koa app, Fastify instance, or
|
|
267
|
+
* @param app - Express or Connect app, Koa app, Fastify instance, NestJS app or application
|
|
268
|
+
* context, Hono app, or http.Server
|
|
261
269
|
*/
|
|
262
270
|
static attach(app: AttachTarget, options?: PeekInsideOptions): PeekInside;
|
|
263
271
|
|
|
@@ -301,6 +309,27 @@ declare class PeekInside {
|
|
|
301
309
|
*/
|
|
302
310
|
static measure<T>(name: string, fn: () => T): T;
|
|
303
311
|
|
|
312
|
+
/**
|
|
313
|
+
* Shows a value in the local dashboard instead of your terminal: on the Dumps page and under the
|
|
314
|
+
* request (or job run) it was dumped in, as a tree that opens level by level, with the line that
|
|
315
|
+
* dumped it. Returns the value, so it can wrap an expression in place.
|
|
316
|
+
*
|
|
317
|
+
* Dumps are for development: they go to the local dashboard only. With PeekInside Cloud, or when
|
|
318
|
+
* nothing is recorded, `dump()` just returns the value.
|
|
319
|
+
*
|
|
320
|
+
* @param label - Shown with the dump, such as `"cart before checkout"`
|
|
321
|
+
*
|
|
322
|
+
* @example
|
|
323
|
+
* const order = PeekInside.dump(await buildOrder(cart), "order");
|
|
324
|
+
*/
|
|
325
|
+
dump<T>(value: T, label?: string): T;
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* `dump()` with the instance that tracks the current request, or else the most recently created
|
|
329
|
+
* instance that records.
|
|
330
|
+
*/
|
|
331
|
+
static dump<T>(value: T, label?: string): T;
|
|
332
|
+
|
|
304
333
|
/**
|
|
305
334
|
* The id of the request the calling code runs for, as the dashboard knows it: put it in your
|
|
306
335
|
* own logs to find the request from a log line. `null` outside a recorded request.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "peekinside",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Debug Node.js from the inside: errors with the requests, queries, logs and jobs behind them. A free local dashboard, and encrypted monitoring for production",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "./index.d.ts",
|
|
@@ -57,6 +57,12 @@
|
|
|
57
57
|
},
|
|
58
58
|
"homepage": "https://peekinside.dev",
|
|
59
59
|
"devDependencies": {
|
|
60
|
+
"@hono/node-server": "2.1.3",
|
|
61
|
+
"@nestjs/common": "12.1.3",
|
|
62
|
+
"@nestjs/core": "12.1.3",
|
|
63
|
+
"@nestjs/platform-express": "12.1.3",
|
|
64
|
+
"@nestjs/platform-fastify": "12.1.3",
|
|
65
|
+
"@nestjs/schedule": "12.0.2",
|
|
60
66
|
"@types/express": "4.17.25",
|
|
61
67
|
"@types/koa": "2.15.0",
|
|
62
68
|
"@types/node": "24.10.1",
|
|
@@ -65,6 +71,7 @@
|
|
|
65
71
|
"express": "4.22.3",
|
|
66
72
|
"express5": "npm:express@5.1.0",
|
|
67
73
|
"fastify": "5.12.5",
|
|
74
|
+
"hono": "4.13.13",
|
|
68
75
|
"koa": "2.16.4",
|
|
69
76
|
"mongodb": "6.21.0",
|
|
70
77
|
"mongodb-memory-server-core": "11.3.0",
|
|
@@ -73,6 +80,8 @@
|
|
|
73
80
|
"node-cron": "4.6.0",
|
|
74
81
|
"pg": "8.23.1",
|
|
75
82
|
"pino": "10.4.0",
|
|
83
|
+
"reflect-metadata": "0.2.2",
|
|
84
|
+
"rxjs": "7.8.2",
|
|
76
85
|
"typescript": "5.9.3",
|
|
77
86
|
"winston": "3.19.0"
|
|
78
87
|
}
|
package/src/PeekInside.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Records every request and response your app handles. During development they go to the free
|
|
5
5
|
* local dashboard, which is shared by all your projects and keeps its data on this machine.
|
|
6
6
|
* With an API key they are encrypted and sent to PeekInside Cloud instead.
|
|
7
|
-
* Works with Express, Koa, Fastify and the native http module.
|
|
7
|
+
* Works with Express, Koa, Fastify, NestJS, Hono and the native http module.
|
|
8
8
|
*
|
|
9
9
|
* Exceptions are recorded too: errors that reach the framework's error handling, crashes
|
|
10
10
|
* (uncaught exceptions and unhandled rejections), and errors the app reports. Each is linked to the
|
|
@@ -13,6 +13,9 @@
|
|
|
13
13
|
* Each request also gets a timeline: the database queries, outgoing HTTP calls, console lines and
|
|
14
14
|
* exceptions that happened while it was handled (see src/timeline and src/instrumentation).
|
|
15
15
|
*
|
|
16
|
+
* In development, dump() shows values in the local dashboard, under the request they were dumped
|
|
17
|
+
* in (see src/dumps).
|
|
18
|
+
*
|
|
16
19
|
* This class is the public API. It decides where requests go and which requests are recorded,
|
|
17
20
|
* and leaves the work to the capture (src/capture), exception (src/exceptions), framework
|
|
18
21
|
* (src/integrations) and transport (src/transport) classes.
|
|
@@ -26,6 +29,7 @@ import { isIgnoredPath } from './capture/ignored-paths.js';
|
|
|
26
29
|
import { LOG_PREFIX, VERSION, settings } from './config.js';
|
|
27
30
|
import { TrafficClassifier } from './deployment.js';
|
|
28
31
|
import { ExceptionCapture } from './exceptions/exception-capture.js';
|
|
32
|
+
import { DumpCapture } from './dumps/dump-capture.js';
|
|
29
33
|
import { ProcessMonitor } from './exceptions/process-monitor.js';
|
|
30
34
|
import { RequestContext } from './exceptions/request-context.js';
|
|
31
35
|
import { Instrumentation } from './instrumentation/index.js';
|
|
@@ -33,8 +37,10 @@ import { isNativePromise } from './instrumentation/patch.js';
|
|
|
33
37
|
import { TIMELINE_DEFAULTS, Timeline, TimelineSettings } from './timeline/timeline.js';
|
|
34
38
|
import { ExpressAdapter } from './integrations/express.js';
|
|
35
39
|
import { FastifyAdapter } from './integrations/fastify.js';
|
|
40
|
+
import { HonoAdapter } from './integrations/hono.js';
|
|
36
41
|
import { HttpServerAdapter } from './integrations/http-server.js';
|
|
37
42
|
import { KoaAdapter } from './integrations/koa.js';
|
|
43
|
+
import { NestAdapter } from './integrations/nest.js';
|
|
38
44
|
import { ServerBinder } from './integrations/server-binder.js';
|
|
39
45
|
import { ProjectIdentity } from './project.js';
|
|
40
46
|
import { Release } from './release.js';
|
|
@@ -60,9 +66,10 @@ const DEFAULT_OPTIONS = {
|
|
|
60
66
|
...TIMELINE_DEFAULTS
|
|
61
67
|
};
|
|
62
68
|
|
|
63
|
-
// Tried in this order by attachTo():
|
|
64
|
-
//
|
|
65
|
-
|
|
69
|
+
// Tried in this order by attachTo(): Nest apps (which pass Express's or Fastify's methods
|
|
70
|
+
// through), Hono, Fastify and Koa apps also have use(), and anything with use() is handled as
|
|
71
|
+
// middleware before falling back to a plain server.
|
|
72
|
+
const ADAPTERS = [NestAdapter, HonoAdapter, FastifyAdapter, KoaAdapter, ExpressAdapter, HttpServerAdapter];
|
|
66
73
|
|
|
67
74
|
// Marks requests already being recorded, so a request that passes through several integrations
|
|
68
75
|
// (or PeekInside instances) is recorded once.
|
|
@@ -73,7 +80,7 @@ const kTracked = Symbol('peekInside.tracked');
|
|
|
73
80
|
*
|
|
74
81
|
* @class PeekInside
|
|
75
82
|
* @example
|
|
76
|
-
* // Express, Koa, Fastify or an http.Server
|
|
83
|
+
* // Express, Koa, Fastify, NestJS, Hono or an http.Server
|
|
77
84
|
* PeekInside.attach(app);
|
|
78
85
|
*
|
|
79
86
|
* @example
|
|
@@ -104,8 +111,8 @@ class PeekInside {
|
|
|
104
111
|
* "v2.4.0". Defaults to PEEKINSIDE_RELEASE, then the commit your host deployed (Vercel, Render,
|
|
105
112
|
* Railway, Heroku and others set it), then the commit checked out in the project folder.
|
|
106
113
|
* @param {boolean} [options.captureExceptions=true] - Record exceptions automatically: errors that
|
|
107
|
-
* reach Express, Koa or
|
|
108
|
-
* report() works either way.
|
|
114
|
+
* reach Express, Koa, Fastify, NestJS or Hono error handling, uncaught exceptions and unhandled
|
|
115
|
+
* rejections. report() works either way.
|
|
109
116
|
* @param {boolean} [options.sourceContext=true] - Include the source lines around the app's own
|
|
110
117
|
* stack frames in exception records
|
|
111
118
|
* @param {boolean} [options.sourceMaps=true] - Map stack frames of compiled code (TypeScript
|
|
@@ -114,11 +121,12 @@ class PeekInside {
|
|
|
114
121
|
* queries, outgoing calls, console lines and exceptions
|
|
115
122
|
* @param {boolean} [options.captureQueries=true] - pg, mysql2, MongoDB (and Mongoose), Prisma
|
|
116
123
|
* @param {boolean} [options.captureOutgoing=true] - node:http/https requests and fetch
|
|
117
|
-
* @param {boolean} [options.captureLogs=true] - Log lines (the console, pino, winston
|
|
118
|
-
* timelines and the Logs page
|
|
124
|
+
* @param {boolean} [options.captureLogs=true] - Log lines (the console, pino, winston, NestJS's
|
|
125
|
+
* Logger): on request timelines and the Logs page
|
|
119
126
|
* @param {'debug'|'info'|'warn'|'error'} [options.logLevel='info'] - The lowest level the Logs
|
|
120
127
|
* page records
|
|
121
|
-
* @param {boolean} [options.captureJobs=true] - Runs of node-cron, cron
|
|
128
|
+
* @param {boolean} [options.captureJobs=true] - Runs of node-cron, cron, BullMQ and
|
|
129
|
+
* @nestjs/schedule jobs
|
|
122
130
|
* @param {boolean} [options.queryParams=true] - Keep the (masked) values sent with queries
|
|
123
131
|
* @param {number} [options.slowQueryMs=100] - Queries this slow or slower are flagged
|
|
124
132
|
* @param {number} [options.nPlusOneThreshold=5] - A statement run this many times in one request
|
|
@@ -162,9 +170,10 @@ class PeekInside {
|
|
|
162
170
|
this._sampler = null;
|
|
163
171
|
// The requests this instance serves, counted for its process samples.
|
|
164
172
|
this._requests = null;
|
|
165
|
-
// Log lines for the Logs page, and
|
|
173
|
+
// Log lines for the Logs page, job runs, and dumps (local mode only).
|
|
166
174
|
this._logs = null;
|
|
167
175
|
this._jobs = null;
|
|
176
|
+
this._dumps = null;
|
|
168
177
|
this._servers = new ServerBinder(this, { onListening: (port) => this._onAppListening(port) });
|
|
169
178
|
|
|
170
179
|
const environment = this.options.environment || settings.environment();
|
|
@@ -237,6 +246,9 @@ class PeekInside {
|
|
|
237
246
|
if (logs) {
|
|
238
247
|
this._logs = new LogCapture({ level: LogCapture.level(this.options.logLevel), sink: () => this._transport, isEnabled: () => this.isEnabled });
|
|
239
248
|
}
|
|
249
|
+
if (this._local) {
|
|
250
|
+
this._dumps = new DumpCapture({ capture: this._exceptions, sink: () => this._transport, isEnabled: () => this.isEnabled });
|
|
251
|
+
}
|
|
240
252
|
this._jobs = new JobRunner({
|
|
241
253
|
capture: this._exceptions,
|
|
242
254
|
sink: () => this._transport,
|
|
@@ -364,6 +376,48 @@ class PeekInside {
|
|
|
364
376
|
return instance ? instance.measure(name, fn) : fn();
|
|
365
377
|
}
|
|
366
378
|
|
|
379
|
+
/**
|
|
380
|
+
* Shows a value in the local dashboard instead of your terminal: on the Dumps page and under the
|
|
381
|
+
* request (or job run) it was dumped in, as a tree that opens level by level, with the line that
|
|
382
|
+
* dumped it. Returns the value, so it can wrap an expression in place.
|
|
383
|
+
*
|
|
384
|
+
* Dumps are for development: they go to the local dashboard only. With PeekInside Cloud, or when
|
|
385
|
+
* nothing is recorded, dump() just returns the value.
|
|
386
|
+
*
|
|
387
|
+
* @template T
|
|
388
|
+
* @param {T} value - Anything: objects, arrays, Maps, class instances, errors...
|
|
389
|
+
* @param {string} [label] - Shown with the dump, such as "cart before checkout"
|
|
390
|
+
* @returns {T} The same value
|
|
391
|
+
*
|
|
392
|
+
* @example
|
|
393
|
+
* const order = PeekInside.dump(await buildOrder(cart), 'order');
|
|
394
|
+
*/
|
|
395
|
+
dump(value, label) {
|
|
396
|
+
if (this._dumps) this._dumps.record(value, label, PeekInside.prototype.dump);
|
|
397
|
+
return value;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* PeekInside#dump() with the instance that tracks the current request, or else the most recently
|
|
402
|
+
* created instance that records.
|
|
403
|
+
*
|
|
404
|
+
* @template T
|
|
405
|
+
* @param {T} value
|
|
406
|
+
* @param {string} [label]
|
|
407
|
+
* @returns {T} The same value
|
|
408
|
+
*/
|
|
409
|
+
static dump(value, label) {
|
|
410
|
+
try {
|
|
411
|
+
const context = RequestContext.current();
|
|
412
|
+
const capture = context && context.capture && context.capture.recording ? context.capture : ExceptionCapture.latest();
|
|
413
|
+
const owner = capture ? capture.owner : null;
|
|
414
|
+
if (owner && owner._dumps) owner._dumps.record(value, label, PeekInside.dump);
|
|
415
|
+
} catch (error) {
|
|
416
|
+
// The value comes back all the same.
|
|
417
|
+
}
|
|
418
|
+
return value;
|
|
419
|
+
}
|
|
420
|
+
|
|
367
421
|
/**
|
|
368
422
|
* The id of the request the calling code runs for: the id its record has in the dashboard, to
|
|
369
423
|
* put in the app's own logs.
|
|
@@ -559,7 +613,9 @@ class PeekInside {
|
|
|
559
613
|
/**
|
|
560
614
|
* Attach to an application and record its traffic
|
|
561
615
|
*
|
|
562
|
-
* Detects Fastify, Koa, Express-like apps
|
|
616
|
+
* Detects NestJS apps (and application contexts), Hono apps, Fastify, Koa, Express-like apps
|
|
617
|
+
* (Express, Connect) and native `http.Server`s. Attach a NestJS app before app.listen(), a Hono
|
|
618
|
+
* app before serve(), and a Fastify app before fastify.listen().
|
|
563
619
|
*
|
|
564
620
|
* @param {Object} app - Application or server instance
|
|
565
621
|
* @param {Object} [options] - `maxBodySize` and `ignorePaths` to merge into the configuration
|
|
@@ -573,7 +629,7 @@ class PeekInside {
|
|
|
573
629
|
if (Adapter) {
|
|
574
630
|
this._adapter(Adapter).attach(app);
|
|
575
631
|
} else {
|
|
576
|
-
console.warn(`${LOG_PREFIX} Could not detect the framework.
|
|
632
|
+
console.warn(`${LOG_PREFIX} Could not detect the framework. Pass an Express, Koa, Fastify, NestJS or Hono app, or use expressMiddleware(), koaMiddleware(), fastifyHook() or wrapHttpServer().`);
|
|
577
633
|
}
|
|
578
634
|
return this;
|
|
579
635
|
}
|
|
@@ -581,7 +637,7 @@ class PeekInside {
|
|
|
581
637
|
/**
|
|
582
638
|
* One-line setup: record every request an app handles
|
|
583
639
|
*
|
|
584
|
-
* @param {Object} app - Express, Koa or
|
|
640
|
+
* @param {Object} app - Express, Koa, Fastify, NestJS or Hono app, or an http.Server
|
|
585
641
|
* @param {Object} [options] - Configuration options (see constructor)
|
|
586
642
|
* @returns {PeekInside} PeekInside instance
|
|
587
643
|
*
|
|
@@ -11,7 +11,7 @@ import { randomUUID } from 'crypto';
|
|
|
11
11
|
import http from 'http';
|
|
12
12
|
import { performance } from 'perf_hooks';
|
|
13
13
|
import { BodyDecoder, EMPTY_BODY } from './body-decoder.js';
|
|
14
|
-
import { hasBody, parseQuery } from './http-message.js';
|
|
14
|
+
import { hasBody, parseQuery, stringParams } from './http-message.js';
|
|
15
15
|
import { RequestBodyTap } from './request-body-tap.js';
|
|
16
16
|
import { ResponseTap } from './response-tap.js';
|
|
17
17
|
|
|
@@ -41,6 +41,10 @@ const kBodySource = Symbol('peekInside.bodySource');
|
|
|
41
41
|
* @property {Array<{ id: string, name: string, message: string }>} [exceptions] - The first of them
|
|
42
42
|
* @property {Object} [stats] - Timeline counters (queries, outgoing calls, logs), added by PeekInside
|
|
43
43
|
* @property {Object} [timeline] - The request's timeline, when something was recorded on it
|
|
44
|
+
* @property {string} [route] - The route pattern it matched ("/users/:id"), when the framework says
|
|
45
|
+
* @property {string} [handler] - The code that handled it ("UsersController.findOne"), when known
|
|
46
|
+
* @property {Array<{ id: string, label: string|null, type: string, preview: string }>} [dumps] - The
|
|
47
|
+
* dumps made during it (local dashboard only)
|
|
44
48
|
*/
|
|
45
49
|
|
|
46
50
|
/**
|
|
@@ -250,14 +254,3 @@ export class ExchangeRecorder {
|
|
|
250
254
|
return decoder.decodeValue(parsed, headers);
|
|
251
255
|
}
|
|
252
256
|
}
|
|
253
|
-
|
|
254
|
-
// Route parameters as strings (Express keeps them as strings; other routers may not).
|
|
255
|
-
function stringParams(params) {
|
|
256
|
-
const result = {};
|
|
257
|
-
if (!params || typeof params !== 'object') return result;
|
|
258
|
-
for (const key of Object.keys(params)) {
|
|
259
|
-
const value = params[key];
|
|
260
|
-
if (value !== undefined && value !== null && typeof value !== 'object') result[key] = String(value);
|
|
261
|
-
}
|
|
262
|
-
return result;
|
|
263
|
-
}
|
|
@@ -136,3 +136,20 @@ export function hasBody(headers) {
|
|
|
136
136
|
export function setOwn(target, key, value) {
|
|
137
137
|
Object.defineProperty(target, key, { value, enumerable: true, writable: true, configurable: true });
|
|
138
138
|
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Route parameters as strings, as records keep them: Express keeps them as strings, other routers
|
|
142
|
+
* (Fastify, Hono) may not. Values that are objects are left out.
|
|
143
|
+
*
|
|
144
|
+
* @param {*} params
|
|
145
|
+
* @returns {Object<string, string>}
|
|
146
|
+
*/
|
|
147
|
+
export function stringParams(params) {
|
|
148
|
+
const result = {};
|
|
149
|
+
if (!params || typeof params !== 'object') return result;
|
|
150
|
+
for (const key of Object.keys(params)) {
|
|
151
|
+
const value = params[key];
|
|
152
|
+
if (value !== undefined && value !== null && typeof value !== 'object') setOwn(result, key, String(value));
|
|
153
|
+
}
|
|
154
|
+
return result;
|
|
155
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The dumps of one PeekInside instance (PeekInside.dump()): each value is read into a tree (see
|
|
3
|
+
* DumpValue), with the line of the app that dumped it and the request or job run running then,
|
|
4
|
+
* and goes to the local dashboard. A dump during a request is listed on the request's record and
|
|
5
|
+
* placed on its timeline.
|
|
6
|
+
*
|
|
7
|
+
* Dumps are rate limited per process (50 a second, bursts of 200), like log lines. Nothing here
|
|
8
|
+
* throws into the app: dump() always hands the value back.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { randomUUID } from 'crypto';
|
|
12
|
+
import { truncate } from '../exceptions/limits.js';
|
|
13
|
+
import { TokenBucket } from '../exceptions/rate-limiter.js';
|
|
14
|
+
import { RequestContext } from '../exceptions/request-context.js';
|
|
15
|
+
import { DumpRecord } from './dump-record.js';
|
|
16
|
+
import { DumpValue } from './dump-value.js';
|
|
17
|
+
|
|
18
|
+
const MAX_LABEL = 120;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Records dumps for one PeekInside instance.
|
|
22
|
+
*/
|
|
23
|
+
export class DumpCapture {
|
|
24
|
+
/**
|
|
25
|
+
* @param {Object} options
|
|
26
|
+
* @param {import('../exceptions/exception-capture.js').ExceptionCapture} options.capture - The
|
|
27
|
+
* instance's exceptions, whose stack frames (mapped to source files) locate a dump
|
|
28
|
+
* @param {() => Object|null} options.sink - The instance's transport
|
|
29
|
+
* @param {() => boolean} options.isEnabled - Whether the instance records right now
|
|
30
|
+
* @param {TokenBucket} [options.limiter]
|
|
31
|
+
*/
|
|
32
|
+
constructor({ capture, sink, isEnabled, limiter = new TokenBucket({ rate: 50, burst: 200 }) }) {
|
|
33
|
+
this.capture = capture;
|
|
34
|
+
this._sink = sink;
|
|
35
|
+
this._isEnabled = isEnabled;
|
|
36
|
+
this.limiter = limiter;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @type {boolean}
|
|
41
|
+
*/
|
|
42
|
+
get recording() {
|
|
43
|
+
const sink = this._sink();
|
|
44
|
+
return Boolean(sink && !sink.disabled && this._isEnabled());
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Records a dump. Never throws.
|
|
49
|
+
*
|
|
50
|
+
* @param {*} value
|
|
51
|
+
* @param {*} label - A string names the dump; anything else is ignored
|
|
52
|
+
* @param {Function} entry - The function the app called (PeekInside.dump): the stack is read
|
|
53
|
+
* from its caller
|
|
54
|
+
* @returns {string|null} The dump's id, or null when nothing was recorded
|
|
55
|
+
*/
|
|
56
|
+
record(value, label, entry) {
|
|
57
|
+
try {
|
|
58
|
+
if (!this.recording) return null;
|
|
59
|
+
const context = RequestContext.current();
|
|
60
|
+
// Requests local mode doesn't record (server traffic) take their dumps with them.
|
|
61
|
+
if (context && context.blocked) return null;
|
|
62
|
+
if (!this.limiter.take()) return null;
|
|
63
|
+
const own = context && context.capture === this.capture ? context : null;
|
|
64
|
+
const { tree, type, preview, truncated } = DumpValue.capture(value);
|
|
65
|
+
const sink = this._sink();
|
|
66
|
+
const record = new DumpRecord({
|
|
67
|
+
id: randomUUID(),
|
|
68
|
+
occurredAt: Date.now(),
|
|
69
|
+
environment: sink.environment,
|
|
70
|
+
release: sink.release || null,
|
|
71
|
+
requestId: own ? own.requestId : null,
|
|
72
|
+
jobRunId: own ? own.jobRunId || null : null,
|
|
73
|
+
label: typeof label === 'string' && label.trim() ? truncate(label.trim(), MAX_LABEL) : null,
|
|
74
|
+
location: this._callSite(entry),
|
|
75
|
+
summary: { type, preview, truncated },
|
|
76
|
+
detail: { value: tree }
|
|
77
|
+
});
|
|
78
|
+
if (own) own.noteDump(record);
|
|
79
|
+
sink.enqueueRecord(record);
|
|
80
|
+
return record.id;
|
|
81
|
+
} catch (error) {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// The app's line that called `entry`, mapped through source maps like an exception's frames.
|
|
87
|
+
_callSite(entry) {
|
|
88
|
+
const holder = {};
|
|
89
|
+
const limit = Error.stackTraceLimit;
|
|
90
|
+
try {
|
|
91
|
+
Error.stackTraceLimit = 1;
|
|
92
|
+
Error.captureStackTrace(holder, entry);
|
|
93
|
+
} finally {
|
|
94
|
+
Error.stackTraceLimit = limit;
|
|
95
|
+
}
|
|
96
|
+
const [frame] = this.capture.builder.frames({ stack: String(holder.stack || ''), fullMessage: '', isError: true }, 1);
|
|
97
|
+
if (!frame || !frame.file) return null;
|
|
98
|
+
return { file: frame.file, relativeFile: frame.relativeFile, line: frame.line, column: frame.column, function: frame.function };
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One dump, as the local dashboard's Dumps page lists it: when, the label it was given, the line
|
|
3
|
+
* that dumped it, the request or job run it belongs to, and the value. The list shows the value's
|
|
4
|
+
* type and a one-line preview; the tree itself is the dump's detail, loaded when it is opened.
|
|
5
|
+
*
|
|
6
|
+
* Dumps are for development and go to the local dashboard only, so a record is never sealed.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* A recorded dump.
|
|
11
|
+
*/
|
|
12
|
+
export class DumpRecord {
|
|
13
|
+
/**
|
|
14
|
+
* @param {Object} fields
|
|
15
|
+
* @param {string} fields.id - UUID
|
|
16
|
+
* @param {number} fields.occurredAt - ms since the epoch
|
|
17
|
+
* @param {string} fields.environment
|
|
18
|
+
* @param {string|null} [fields.release=null]
|
|
19
|
+
* @param {string|null} [fields.requestId=null] - The request it was dumped during
|
|
20
|
+
* @param {string|null} [fields.jobRunId=null] - The job run it was dumped during
|
|
21
|
+
* @param {string|null} [fields.label=null] - What the app called it
|
|
22
|
+
* @param {Object|null} [fields.location=null] - The line that dumped it: file (absolute),
|
|
23
|
+
* relativeFile, line, column, function
|
|
24
|
+
* @param {{ type: string, preview: string, truncated: boolean }} fields.summary
|
|
25
|
+
* @param {{ value: Object }} fields.detail - The value's tree (see DumpValue)
|
|
26
|
+
*/
|
|
27
|
+
constructor({ id, occurredAt, environment, release = null, requestId = null, jobRunId = null, label = null, location = null, summary, detail }) {
|
|
28
|
+
this.id = id;
|
|
29
|
+
this.occurredAt = occurredAt;
|
|
30
|
+
this.environment = environment;
|
|
31
|
+
this.release = release;
|
|
32
|
+
this.requestId = requestId;
|
|
33
|
+
this.jobRunId = jobRunId;
|
|
34
|
+
this.label = label;
|
|
35
|
+
this.location = location;
|
|
36
|
+
this.summary = summary;
|
|
37
|
+
this.detail = detail;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* What a request's record lists of the dumps made while it was handled.
|
|
42
|
+
*
|
|
43
|
+
* @returns {{ id: string, label: string|null, type: string, preview: string }}
|
|
44
|
+
*/
|
|
45
|
+
link() {
|
|
46
|
+
return { id: this.id, label: this.label, type: this.summary.type, preview: this.summary.preview };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The plain form, as the local hub stores it.
|
|
51
|
+
*
|
|
52
|
+
* @returns {Object}
|
|
53
|
+
*/
|
|
54
|
+
toJSON() {
|
|
55
|
+
return { ...this };
|
|
56
|
+
}
|
|
57
|
+
}
|