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 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
- Also recorded: background jobs (node-cron, cron, BullMQ, or `PeekInside.job()`, with an email when a scheduled job misses its run), log lines from the console, pino and winston, 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
+ 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 objects fit
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 Fastify error handling,
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 winston: on the timeline of the request (or job
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 BullMQ jobs automatically (see `job()` for other jobs).
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 = Server | { use(...args: any[]): unknown } | { addHook(...args: any[]): unknown };
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 http.Server
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.2.5",
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(): Fastify and Koa apps also have use(), and anything with
64
- // use() is handled as middleware before falling back to a plain server.
65
- const ADAPTERS = [FastifyAdapter, KoaAdapter, ExpressAdapter, HttpServerAdapter];
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 Fastify error handling, uncaught exceptions and unhandled rejections.
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): on request
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 and BullMQ jobs
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 job runs.
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 (Express, Connect) and native `http.Server`s.
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. Use expressMiddleware(), koaMiddleware(), fastifyHook() or wrapHttpServer().`);
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 Fastify app, or an http.Server
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
+ }