peekinside 1.2.4 → 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 +13 -2
- package/index.d.ts +44 -13
- package/package.json +10 -1
- package/src/PeekInside.js +93 -17
- package/src/alerts/rule-matcher.js +158 -0
- package/src/capture/exchange-recorder.js +5 -12
- package/src/capture/http-message.js +17 -0
- package/src/crypto.js +26 -2
- 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/exception-record.js +11 -1
- package/src/exceptions/request-context.js +118 -10
- package/src/hub/ingest-payload.js +89 -8
- package/src/hub/project-log.js +71 -4
- package/src/hub/server.js +19 -8
- 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 +114 -6
- 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/runtime/request-counter.js +40 -0
- package/src/runtime/sampler.js +120 -6
- package/src/timeline/timeline.js +24 -3
- package/src/transport/batch.js +4 -1
- package/src/transport/cloud.js +34 -3
- package/src/transport/local.js +7 -5
- package/src/user.js +18 -7
package/README.md
CHANGED
|
@@ -5,6 +5,7 @@ Debug Node.js from the inside: every error with the request that set it off, the
|
|
|
5
5
|
[](https://www.npmjs.com/package/peekinside)
|
|
6
6
|

|
|
7
7
|

|
|
8
|
+
[](https://buymeacoffee.com/thejesbin)
|
|
8
9
|
|
|
9
10
|
## Install
|
|
10
11
|
|
|
@@ -20,11 +21,13 @@ const app = express();
|
|
|
20
21
|
PeekInside.attach(app);
|
|
21
22
|
```
|
|
22
23
|
|
|
23
|
-
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.
|
|
24
25
|
|
|
25
26
|
Check the setup before anything breaks on its own: `npx peekinside test` sends a test error from your project.
|
|
26
27
|
|
|
27
|
-
|
|
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.
|
|
28
31
|
|
|
29
32
|
## Documentation
|
|
30
33
|
|
|
@@ -37,10 +40,18 @@ Everything else is in the docs: **[peekinside.dev/docs](https://peekinside.dev/d
|
|
|
37
40
|
- [Timeline and queries](https://peekinside.dev/docs/timeline)
|
|
38
41
|
- [Logs](https://peekinside.dev/docs/logs)
|
|
39
42
|
- [Jobs and schedules](https://peekinside.dev/docs/jobs)
|
|
43
|
+
- [Dumps](https://peekinside.dev/docs/dumps)
|
|
40
44
|
- [Runtime](https://peekinside.dev/docs/runtime)
|
|
45
|
+
- [Alerts](https://peekinside.dev/docs/alerts)
|
|
41
46
|
- [Configuration](https://peekinside.dev/docs/configuration)
|
|
42
47
|
- [API](https://peekinside.dev/docs/api)
|
|
43
48
|
|
|
49
|
+
## Support
|
|
50
|
+
|
|
51
|
+
If PeekInside saves you a debugging session, you can buy me a coffee:
|
|
52
|
+
|
|
53
|
+
[](https://buymeacoffee.com/thejesbin)
|
|
54
|
+
|
|
44
55
|
## License
|
|
45
56
|
|
|
46
57
|
MIT
|
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;
|
|
@@ -208,6 +209,8 @@ export interface PeekInsideUser {
|
|
|
208
209
|
id?: string | number;
|
|
209
210
|
email?: string;
|
|
210
211
|
name?: string;
|
|
212
|
+
/** As your app writes it, such as "+91 98765 43210". */
|
|
213
|
+
phone?: string | number;
|
|
211
214
|
}
|
|
212
215
|
|
|
213
216
|
/** Options that can still change after the instance is created (see `attachTo`). */
|
|
@@ -215,9 +218,14 @@ export type CaptureOptions = Pick<PeekInsideOptions, "maxBodySize" | "ignorePath
|
|
|
215
218
|
|
|
216
219
|
/**
|
|
217
220
|
* What `PeekInside.attach()` understands: an Express or Connect app, a Koa app, a Fastify
|
|
218
|
-
* 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.
|
|
219
222
|
*/
|
|
220
|
-
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 };
|
|
221
229
|
|
|
222
230
|
/** Express and Connect style middleware. */
|
|
223
231
|
export type NodeMiddleware = (req: IncomingMessage, res: ServerResponse, next: (error?: unknown) => void) => void;
|
|
@@ -234,7 +242,7 @@ export type KoaMiddleware = (
|
|
|
234
242
|
* @example
|
|
235
243
|
* import PeekInside from "peekinside";
|
|
236
244
|
*
|
|
237
|
-
* PeekInside.attach(app); // Express, Koa, Fastify or an http.Server
|
|
245
|
+
* PeekInside.attach(app); // Express, Koa, Fastify, NestJS, Hono or an http.Server
|
|
238
246
|
*/
|
|
239
247
|
declare class PeekInside {
|
|
240
248
|
/**
|
|
@@ -253,9 +261,11 @@ declare class PeekInside {
|
|
|
253
261
|
readonly release: string | null;
|
|
254
262
|
|
|
255
263
|
/**
|
|
256
|
-
* 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()`.
|
|
257
266
|
*
|
|
258
|
-
* @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
|
|
259
269
|
*/
|
|
260
270
|
static attach(app: AttachTarget, options?: PeekInsideOptions): PeekInside;
|
|
261
271
|
|
|
@@ -299,6 +309,27 @@ declare class PeekInside {
|
|
|
299
309
|
*/
|
|
300
310
|
static measure<T>(name: string, fn: () => T): T;
|
|
301
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
|
+
|
|
302
333
|
/**
|
|
303
334
|
* The id of the request the calling code runs for, as the dashboard knows it: put it in your
|
|
304
335
|
* own logs to find the request from a log line. `null` outside a recorded request.
|
|
@@ -315,7 +346,7 @@ declare class PeekInside {
|
|
|
315
346
|
*
|
|
316
347
|
* @example
|
|
317
348
|
* app.use((req, res, next) => {
|
|
318
|
-
* if (req.user) PeekInside.setUser({ id: req.user.id, email: req.user.email });
|
|
349
|
+
* if (req.user) PeekInside.setUser({ id: req.user.id, email: req.user.email, name: req.user.name, phone: req.user.phone });
|
|
319
350
|
* next();
|
|
320
351
|
* });
|
|
321
352
|
*/
|
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';
|
|
@@ -42,6 +48,7 @@ import { JobRunner } from './jobs/job-runner.js';
|
|
|
42
48
|
import { LogCapture } from './logs/log-capture.js';
|
|
43
49
|
import { LoopMonitor } from './runtime/loop-monitor.js';
|
|
44
50
|
import { RuntimeSampler } from './runtime/sampler.js';
|
|
51
|
+
import { RequestCounter } from './runtime/request-counter.js';
|
|
45
52
|
import { UserInfo } from './user.js';
|
|
46
53
|
import { CloudTransport } from './transport/cloud.js';
|
|
47
54
|
import { LocalTransport } from './transport/local.js';
|
|
@@ -59,9 +66,10 @@ const DEFAULT_OPTIONS = {
|
|
|
59
66
|
...TIMELINE_DEFAULTS
|
|
60
67
|
};
|
|
61
68
|
|
|
62
|
-
// Tried in this order by attachTo():
|
|
63
|
-
//
|
|
64
|
-
|
|
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];
|
|
65
73
|
|
|
66
74
|
// Marks requests already being recorded, so a request that passes through several integrations
|
|
67
75
|
// (or PeekInside instances) is recorded once.
|
|
@@ -72,7 +80,7 @@ const kTracked = Symbol('peekInside.tracked');
|
|
|
72
80
|
*
|
|
73
81
|
* @class PeekInside
|
|
74
82
|
* @example
|
|
75
|
-
* // Express, Koa, Fastify or an http.Server
|
|
83
|
+
* // Express, Koa, Fastify, NestJS, Hono or an http.Server
|
|
76
84
|
* PeekInside.attach(app);
|
|
77
85
|
*
|
|
78
86
|
* @example
|
|
@@ -103,8 +111,8 @@ class PeekInside {
|
|
|
103
111
|
* "v2.4.0". Defaults to PEEKINSIDE_RELEASE, then the commit your host deployed (Vercel, Render,
|
|
104
112
|
* Railway, Heroku and others set it), then the commit checked out in the project folder.
|
|
105
113
|
* @param {boolean} [options.captureExceptions=true] - Record exceptions automatically: errors that
|
|
106
|
-
* reach Express, Koa or
|
|
107
|
-
* report() works either way.
|
|
114
|
+
* reach Express, Koa, Fastify, NestJS or Hono error handling, uncaught exceptions and unhandled
|
|
115
|
+
* rejections. report() works either way.
|
|
108
116
|
* @param {boolean} [options.sourceContext=true] - Include the source lines around the app's own
|
|
109
117
|
* stack frames in exception records
|
|
110
118
|
* @param {boolean} [options.sourceMaps=true] - Map stack frames of compiled code (TypeScript
|
|
@@ -113,11 +121,12 @@ class PeekInside {
|
|
|
113
121
|
* queries, outgoing calls, console lines and exceptions
|
|
114
122
|
* @param {boolean} [options.captureQueries=true] - pg, mysql2, MongoDB (and Mongoose), Prisma
|
|
115
123
|
* @param {boolean} [options.captureOutgoing=true] - node:http/https requests and fetch
|
|
116
|
-
* @param {boolean} [options.captureLogs=true] - Log lines (the console, pino, winston
|
|
117
|
-
* 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
|
|
118
126
|
* @param {'debug'|'info'|'warn'|'error'} [options.logLevel='info'] - The lowest level the Logs
|
|
119
127
|
* page records
|
|
120
|
-
* @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
|
|
121
130
|
* @param {boolean} [options.queryParams=true] - Keep the (masked) values sent with queries
|
|
122
131
|
* @param {number} [options.slowQueryMs=100] - Queries this slow or slower are flagged
|
|
123
132
|
* @param {number} [options.nPlusOneThreshold=5] - A statement run this many times in one request
|
|
@@ -159,9 +168,12 @@ class PeekInside {
|
|
|
159
168
|
// Whether this instance uses the event-loop monitor, and its process sampler.
|
|
160
169
|
this._loopMonitor = false;
|
|
161
170
|
this._sampler = null;
|
|
162
|
-
//
|
|
171
|
+
// The requests this instance serves, counted for its process samples.
|
|
172
|
+
this._requests = null;
|
|
173
|
+
// Log lines for the Logs page, job runs, and dumps (local mode only).
|
|
163
174
|
this._logs = null;
|
|
164
175
|
this._jobs = null;
|
|
176
|
+
this._dumps = null;
|
|
165
177
|
this._servers = new ServerBinder(this, { onListening: (port) => this._onAppListening(port) });
|
|
166
178
|
|
|
167
179
|
const environment = this.options.environment || settings.environment();
|
|
@@ -218,9 +230,11 @@ class PeekInside {
|
|
|
218
230
|
}
|
|
219
231
|
if (this.options.runtime !== false) {
|
|
220
232
|
const transport = this._transport;
|
|
233
|
+
this._requests = new RequestCounter();
|
|
221
234
|
this._sampler = new RuntimeSampler({
|
|
222
235
|
intervalMs: this._cloud ? 60_000 : 10_000,
|
|
223
236
|
monitor: LoopMonitor.shared,
|
|
237
|
+
requests: this._requests,
|
|
224
238
|
onSample: (sample) => transport.addSample(sample)
|
|
225
239
|
}).start();
|
|
226
240
|
}
|
|
@@ -232,6 +246,9 @@ class PeekInside {
|
|
|
232
246
|
if (logs) {
|
|
233
247
|
this._logs = new LogCapture({ level: LogCapture.level(this.options.logLevel), sink: () => this._transport, isEnabled: () => this.isEnabled });
|
|
234
248
|
}
|
|
249
|
+
if (this._local) {
|
|
250
|
+
this._dumps = new DumpCapture({ capture: this._exceptions, sink: () => this._transport, isEnabled: () => this.isEnabled });
|
|
251
|
+
}
|
|
235
252
|
this._jobs = new JobRunner({
|
|
236
253
|
capture: this._exceptions,
|
|
237
254
|
sink: () => this._transport,
|
|
@@ -359,6 +376,48 @@ class PeekInside {
|
|
|
359
376
|
return instance ? instance.measure(name, fn) : fn();
|
|
360
377
|
}
|
|
361
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
|
+
|
|
362
421
|
/**
|
|
363
422
|
* The id of the request the calling code runs for: the id its record has in the dashboard, to
|
|
364
423
|
* put in the app's own logs.
|
|
@@ -427,13 +486,14 @@ class PeekInside {
|
|
|
427
486
|
* customer ran into. In PeekInside Cloud the user is encrypted with the rest of the request.
|
|
428
487
|
* Call it once the user is known, such as in your authentication middleware.
|
|
429
488
|
*
|
|
430
|
-
* @param {{ id?: string|number, email?: string, name?: string }|string|number|null} user -
|
|
431
|
-
* user, or just their id; null forgets it
|
|
489
|
+
* @param {{ id?: string|number, email?: string, name?: string, phone?: string|number }|string|number|null} user -
|
|
490
|
+
* The user, or just their id; null forgets it. The phone is kept as written (phoneNumber,
|
|
491
|
+
* phone_number and mobile work too).
|
|
432
492
|
* @returns {boolean} Whether a request was running to attach it to
|
|
433
493
|
*
|
|
434
494
|
* @example
|
|
435
495
|
* app.use((req, res, next) => {
|
|
436
|
-
* if (req.user) PeekInside.setUser({ id: req.user.id, email: req.user.email });
|
|
496
|
+
* if (req.user) PeekInside.setUser({ id: req.user.id, email: req.user.email, name: req.user.name, phone: req.user.phone });
|
|
437
497
|
* next();
|
|
438
498
|
* });
|
|
439
499
|
*/
|
|
@@ -553,7 +613,9 @@ class PeekInside {
|
|
|
553
613
|
/**
|
|
554
614
|
* Attach to an application and record its traffic
|
|
555
615
|
*
|
|
556
|
-
* 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().
|
|
557
619
|
*
|
|
558
620
|
* @param {Object} app - Application or server instance
|
|
559
621
|
* @param {Object} [options] - `maxBodySize` and `ignorePaths` to merge into the configuration
|
|
@@ -567,7 +629,7 @@ class PeekInside {
|
|
|
567
629
|
if (Adapter) {
|
|
568
630
|
this._adapter(Adapter).attach(app);
|
|
569
631
|
} else {
|
|
570
|
-
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().`);
|
|
571
633
|
}
|
|
572
634
|
return this;
|
|
573
635
|
}
|
|
@@ -575,7 +637,7 @@ class PeekInside {
|
|
|
575
637
|
/**
|
|
576
638
|
* One-line setup: record every request an app handles
|
|
577
639
|
*
|
|
578
|
-
* @param {Object} app - Express, Koa or
|
|
640
|
+
* @param {Object} app - Express, Koa, Fastify, NestJS or Hono app, or an http.Server
|
|
579
641
|
* @param {Object} [options] - Configuration options (see constructor)
|
|
580
642
|
* @returns {PeekInside} PeekInside instance
|
|
581
643
|
*
|
|
@@ -683,6 +745,7 @@ class PeekInside {
|
|
|
683
745
|
|
|
684
746
|
_track(req, res, target, integration) {
|
|
685
747
|
let context = null;
|
|
748
|
+
const requests = this._requests;
|
|
686
749
|
const recorder = new ExchangeRecorder(req, res, target, {
|
|
687
750
|
maxBodySize: () => this.options.maxBodySize,
|
|
688
751
|
onExchange: (exchange) => {
|
|
@@ -698,6 +761,19 @@ class PeekInside {
|
|
|
698
761
|
});
|
|
699
762
|
context = new RequestContext({ capture: this._exceptions, req, recorder, target, integration });
|
|
700
763
|
recorder.start();
|
|
764
|
+
if (requests) {
|
|
765
|
+
// Counted off the response itself, so a request ends once whatever happens to its record.
|
|
766
|
+
requests.started();
|
|
767
|
+
let open = true;
|
|
768
|
+
const end = () => {
|
|
769
|
+
if (open) {
|
|
770
|
+
open = false;
|
|
771
|
+
requests.ended();
|
|
772
|
+
}
|
|
773
|
+
};
|
|
774
|
+
res.once('finish', end);
|
|
775
|
+
res.once('close', end);
|
|
776
|
+
}
|
|
701
777
|
if (this._timeline) {
|
|
702
778
|
context.timeline = new Timeline(this._timeline, recorder.perfStart);
|
|
703
779
|
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The parts of a project's alert rules that only the app's server can check: conditions on a
|
|
3
|
+
* request's path, user and response, and on an exception's or a log line's message, which reach
|
|
4
|
+
* PeekInside Cloud sealed. The matcher fetches them now and then, checks each record before it is
|
|
5
|
+
* sealed, and the transport sends which records met which rule with the batch: never what they
|
|
6
|
+
* hold. Everything else about a rule (a status, a duration, an environment) is checked by
|
|
7
|
+
* PeekInside Cloud itself.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { RequestContext } from '../exceptions/request-context.js';
|
|
11
|
+
|
|
12
|
+
// Fetched again this often, the first time this long after the transport first delivered.
|
|
13
|
+
const REFRESH_MS = 60_000;
|
|
14
|
+
const FIRST_FETCH_MS = 1000;
|
|
15
|
+
// The most of a response body checked for a "contains" condition.
|
|
16
|
+
const MAX_BODY = 64 * 1024;
|
|
17
|
+
const RULE_ID = /^rl[0-9a-z]{12}$/;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @typedef {Object} SealedRule
|
|
21
|
+
* @property {string} id - The rule's id
|
|
22
|
+
* @property {'requests'|'exceptions'|'logs'} source
|
|
23
|
+
* @property {{ field: string, op: 'is'|'is-not'|'contains'|'matches', value: string }[]} conditions
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
export class RuleMatcher {
|
|
27
|
+
/**
|
|
28
|
+
* @param {Object} options
|
|
29
|
+
* @param {() => Promise<{ rules: SealedRule[] }>} options.fetchRules - Asks PeekInside Cloud for them
|
|
30
|
+
* @param {number} [options.refreshMs]
|
|
31
|
+
*/
|
|
32
|
+
constructor({ fetchRules, refreshMs = REFRESH_MS }) {
|
|
33
|
+
this.fetchRules = fetchRules;
|
|
34
|
+
this.refreshMs = refreshMs;
|
|
35
|
+
/** @type {SealedRule[]} */
|
|
36
|
+
this.rules = [];
|
|
37
|
+
this.timer = null;
|
|
38
|
+
this.first = null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Fetches the rules shortly, then keeps them current. Its timers never keep the process alive:
|
|
43
|
+
* a script that ends right away never waits for them.
|
|
44
|
+
*
|
|
45
|
+
* @returns {void}
|
|
46
|
+
*/
|
|
47
|
+
start() {
|
|
48
|
+
if (this.timer || this.first) return;
|
|
49
|
+
this.first = RequestContext.outside(() =>
|
|
50
|
+
setTimeout(() => {
|
|
51
|
+
void this.refresh();
|
|
52
|
+
this.timer = RequestContext.outside(() => setInterval(() => void this.refresh(), this.refreshMs));
|
|
53
|
+
if (typeof this.timer.unref === 'function') this.timer.unref();
|
|
54
|
+
}, FIRST_FETCH_MS)
|
|
55
|
+
);
|
|
56
|
+
if (typeof this.first.unref === 'function') this.first.unref();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
stop() {
|
|
60
|
+
clearTimeout(this.first);
|
|
61
|
+
clearInterval(this.timer);
|
|
62
|
+
this.first = null;
|
|
63
|
+
this.timer = null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* @returns {Promise<void>} Never rejects: without rules, nothing is reported
|
|
68
|
+
*/
|
|
69
|
+
async refresh() {
|
|
70
|
+
try {
|
|
71
|
+
const answer = await RequestContext.outside(() => this.fetchRules());
|
|
72
|
+
this.rules = Array.isArray(answer && answer.rules) ? answer.rules.filter(valid) : [];
|
|
73
|
+
} catch (error) {
|
|
74
|
+
// Kept as they were: PeekInside Cloud may be briefly unreachable.
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* The rules a record meets on its sealed fields.
|
|
80
|
+
*
|
|
81
|
+
* @param {Object} record - A RequestRecord, ExceptionRecord or LogRecord, before it is sealed
|
|
82
|
+
* @returns {{ rule: string, id: string }[]}
|
|
83
|
+
*/
|
|
84
|
+
matches(record) {
|
|
85
|
+
if (!this.rules.length || !record || typeof record.id !== 'string') return [];
|
|
86
|
+
const source = sourceOf(record);
|
|
87
|
+
if (!source) return [];
|
|
88
|
+
const found = [];
|
|
89
|
+
for (const rule of this.rules) {
|
|
90
|
+
if (rule.source !== source) continue;
|
|
91
|
+
if (rule.conditions.every((condition) => meets(condition, valuesOf(record, condition.field)))) found.push({ rule: rule.id, id: record.id });
|
|
92
|
+
}
|
|
93
|
+
return found;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function valid(rule) {
|
|
98
|
+
return (
|
|
99
|
+
rule &&
|
|
100
|
+
RULE_ID.test(rule.id) &&
|
|
101
|
+
['requests', 'exceptions', 'logs'].includes(rule.source) &&
|
|
102
|
+
Array.isArray(rule.conditions) &&
|
|
103
|
+
rule.conditions.length > 0 &&
|
|
104
|
+
rule.conditions.every((condition) => condition && typeof condition.field === 'string' && typeof condition.value === 'string')
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function sourceOf(record) {
|
|
109
|
+
const name = record.constructor && record.constructor.name;
|
|
110
|
+
if (name === 'RequestRecord') return 'requests';
|
|
111
|
+
if (name === 'ExceptionRecord') return 'exceptions';
|
|
112
|
+
if (name === 'LogRecord') return 'logs';
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// The values a condition looks at (a user has several: its id, email and phone).
|
|
117
|
+
function valuesOf(record, field) {
|
|
118
|
+
const summary = record.summary || {};
|
|
119
|
+
switch (field) {
|
|
120
|
+
case 'path':
|
|
121
|
+
return [summary.path];
|
|
122
|
+
case 'user': {
|
|
123
|
+
const user = summary.user || {};
|
|
124
|
+
return [user.id, user.email, user.phone].filter((value) => value !== undefined && value !== null).map(String);
|
|
125
|
+
}
|
|
126
|
+
case 'response': {
|
|
127
|
+
const body = record.detail && record.detail.response ? record.detail.response.body : null;
|
|
128
|
+
const text = typeof body === 'string' ? body : body && typeof body.text === 'string' ? body.text : '';
|
|
129
|
+
return [text.slice(0, MAX_BODY)];
|
|
130
|
+
}
|
|
131
|
+
case 'name':
|
|
132
|
+
return [summary.name];
|
|
133
|
+
case 'message':
|
|
134
|
+
return [summary.message];
|
|
135
|
+
default:
|
|
136
|
+
return [];
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Text compares without case; "matches" takes * as anything. A condition is met by any of the values.
|
|
141
|
+
function meets(condition, values) {
|
|
142
|
+
const wanted = String(condition.value).toLowerCase();
|
|
143
|
+
const pattern = condition.op === 'matches' ? new RegExp(`^${wanted.split('*').map(escape).join('.*')}$`) : null;
|
|
144
|
+
const met = values.some((value) => {
|
|
145
|
+
if (typeof value !== 'string') return false;
|
|
146
|
+
const text = value.toLowerCase();
|
|
147
|
+
if (condition.op === 'is') return text === wanted;
|
|
148
|
+
if (condition.op === 'is-not') return text !== wanted;
|
|
149
|
+
if (condition.op === 'contains') return text.includes(wanted);
|
|
150
|
+
if (condition.op === 'matches') return pattern.test(text);
|
|
151
|
+
return false;
|
|
152
|
+
});
|
|
153
|
+
return condition.op === 'is-not' ? values.length === 0 || met : met;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function escape(text) {
|
|
157
|
+
return text.replace(/[.+?^${}()|[\]\\]/g, '\\$&');
|
|
158
|
+
}
|
|
@@ -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
|
+
}
|