peekinside 0.0.0-stage → 1.2.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.
Files changed (83) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +45 -2
  3. package/bin/peekinside.js +8 -0
  4. package/index.d.ts +389 -0
  5. package/index.js +3 -0
  6. package/package.json +77 -4
  7. package/src/PeekInside.js +731 -0
  8. package/src/capture/body-capture.js +86 -0
  9. package/src/capture/body-decoder.js +188 -0
  10. package/src/capture/exchange-recorder.js +263 -0
  11. package/src/capture/http-message.js +138 -0
  12. package/src/capture/ignored-paths.js +49 -0
  13. package/src/capture/request-body-tap.js +40 -0
  14. package/src/capture/response-tap.js +83 -0
  15. package/src/cli.js +254 -0
  16. package/src/config.js +183 -0
  17. package/src/crypto.js +189 -0
  18. package/src/deployment.js +183 -0
  19. package/src/exceptions/exception-builder.js +339 -0
  20. package/src/exceptions/exception-capture.js +407 -0
  21. package/src/exceptions/exception-record.js +118 -0
  22. package/src/exceptions/fingerprint.js +60 -0
  23. package/src/exceptions/frame-classifier.js +114 -0
  24. package/src/exceptions/limits.js +95 -0
  25. package/src/exceptions/process-monitor.js +192 -0
  26. package/src/exceptions/rate-limiter.js +50 -0
  27. package/src/exceptions/request-context.js +337 -0
  28. package/src/exceptions/safe-json.js +186 -0
  29. package/src/exceptions/source-context.js +144 -0
  30. package/src/exceptions/source-maps.js +313 -0
  31. package/src/exceptions/stack-parser.js +179 -0
  32. package/src/hub/brand/peekinside-span-core-mark-dark.svg +7 -0
  33. package/src/hub/brand/peekinside-span-core-mark-light.svg +7 -0
  34. package/src/hub/client.js +184 -0
  35. package/src/hub/entry.js +4 -0
  36. package/src/hub/exception-state.js +97 -0
  37. package/src/hub/ingest-payload.js +362 -0
  38. package/src/hub/process.js +175 -0
  39. package/src/hub/project-log.js +559 -0
  40. package/src/hub/segment-log.js +282 -0
  41. package/src/hub/server.js +538 -0
  42. package/src/hub/spool-ingester.js +110 -0
  43. package/src/hub/store.js +404 -0
  44. package/src/instrumentation/console.js +83 -0
  45. package/src/instrumentation/http.js +146 -0
  46. package/src/instrumentation/index.js +234 -0
  47. package/src/instrumentation/jobs.js +186 -0
  48. package/src/instrumentation/loggers.js +135 -0
  49. package/src/instrumentation/mongodb.js +221 -0
  50. package/src/instrumentation/mysql2.js +161 -0
  51. package/src/instrumentation/operation.js +70 -0
  52. package/src/instrumentation/patch.js +72 -0
  53. package/src/instrumentation/pg.js +218 -0
  54. package/src/instrumentation/prisma.js +181 -0
  55. package/src/integrations/express-errors.js +135 -0
  56. package/src/integrations/express.js +57 -0
  57. package/src/integrations/fastify.js +72 -0
  58. package/src/integrations/framework-adapter.js +53 -0
  59. package/src/integrations/http-server.js +32 -0
  60. package/src/integrations/koa.js +72 -0
  61. package/src/integrations/server-binder.js +120 -0
  62. package/src/jobs/job-context.js +40 -0
  63. package/src/jobs/job-run-record.js +104 -0
  64. package/src/jobs/job-runner.js +173 -0
  65. package/src/logs/log-capture.js +143 -0
  66. package/src/logs/log-record.js +74 -0
  67. package/src/project.js +76 -0
  68. package/src/record.js +181 -0
  69. package/src/release.js +157 -0
  70. package/src/runtime/loop-monitor.js +162 -0
  71. package/src/runtime/sampler.js +93 -0
  72. package/src/spool.js +195 -0
  73. package/src/timeline/caller.js +129 -0
  74. package/src/timeline/execution-hash.js +110 -0
  75. package/src/timeline/lru.js +50 -0
  76. package/src/timeline/masking.js +266 -0
  77. package/src/timeline/shape.js +77 -0
  78. package/src/timeline/sql.js +396 -0
  79. package/src/timeline/timeline.js +642 -0
  80. package/src/transport/batch.js +324 -0
  81. package/src/transport/cloud.js +363 -0
  82. package/src/transport/local.js +354 -0
  83. package/src/user.js +83 -0
@@ -0,0 +1,731 @@
1
+ /**
2
+ * PeekInside - Real-time HTTP request inspector for Node.js applications
3
+ *
4
+ * Records every request and response your app handles. During development they go to the free
5
+ * local dashboard, which is shared by all your projects and keeps its data on this machine.
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.
8
+ *
9
+ * Exceptions are recorded too: errors that reach the framework's error handling, crashes
10
+ * (uncaught exceptions and unhandled rejections), and errors the app reports. Each is linked to the
11
+ * request it happened in.
12
+ *
13
+ * Each request also gets a timeline: the database queries, outgoing HTTP calls, console lines and
14
+ * exceptions that happened while it was handled (see src/timeline and src/instrumentation).
15
+ *
16
+ * This class is the public API. It decides where requests go and which requests are recorded,
17
+ * and leaves the work to the capture (src/capture), exception (src/exceptions), framework
18
+ * (src/integrations) and transport (src/transport) classes.
19
+ *
20
+ * @license MIT
21
+ */
22
+
23
+ import { ExchangeRecorder } from './capture/exchange-recorder.js';
24
+ import { splitUrl } from './capture/http-message.js';
25
+ import { isIgnoredPath } from './capture/ignored-paths.js';
26
+ import { LOG_PREFIX, VERSION, settings } from './config.js';
27
+ import { TrafficClassifier } from './deployment.js';
28
+ import { ExceptionCapture } from './exceptions/exception-capture.js';
29
+ import { ProcessMonitor } from './exceptions/process-monitor.js';
30
+ import { RequestContext } from './exceptions/request-context.js';
31
+ import { Instrumentation } from './instrumentation/index.js';
32
+ import { isNativePromise } from './instrumentation/patch.js';
33
+ import { TIMELINE_DEFAULTS, Timeline, TimelineSettings } from './timeline/timeline.js';
34
+ import { ExpressAdapter } from './integrations/express.js';
35
+ import { FastifyAdapter } from './integrations/fastify.js';
36
+ import { HttpServerAdapter } from './integrations/http-server.js';
37
+ import { KoaAdapter } from './integrations/koa.js';
38
+ import { ServerBinder } from './integrations/server-binder.js';
39
+ import { ProjectIdentity } from './project.js';
40
+ import { Release } from './release.js';
41
+ import { JobRunner } from './jobs/job-runner.js';
42
+ import { LogCapture } from './logs/log-capture.js';
43
+ import { LoopMonitor } from './runtime/loop-monitor.js';
44
+ import { RuntimeSampler } from './runtime/sampler.js';
45
+ import { UserInfo } from './user.js';
46
+ import { CloudTransport } from './transport/cloud.js';
47
+ import { LocalTransport } from './transport/local.js';
48
+
49
+ const DEFAULT_OPTIONS = {
50
+ maxBodySize: 64 * 1024,
51
+ ignorePaths: [],
52
+ captureExceptions: true,
53
+ sourceContext: true,
54
+ sourceMaps: true,
55
+ runtime: true,
56
+ captureJobs: true,
57
+ logLevel: 'info',
58
+ timeline: true,
59
+ ...TIMELINE_DEFAULTS
60
+ };
61
+
62
+ // Tried in this order by attachTo(): Fastify and Koa apps also have use(), and anything with
63
+ // use() is handled as middleware before falling back to a plain server.
64
+ const ADAPTERS = [FastifyAdapter, KoaAdapter, ExpressAdapter, HttpServerAdapter];
65
+
66
+ // Marks requests already being recorded, so a request that passes through several integrations
67
+ // (or PeekInside instances) is recorded once.
68
+ const kTracked = Symbol('peekInside.tracked');
69
+
70
+ /**
71
+ * PeekInside Class
72
+ *
73
+ * @class PeekInside
74
+ * @example
75
+ * // Express, Koa, Fastify or an http.Server
76
+ * PeekInside.attach(app);
77
+ *
78
+ * @example
79
+ * // Recording through middleware only
80
+ * const peekInside = new PeekInside();
81
+ * app.use(peekInside.expressMiddleware());
82
+ */
83
+ class PeekInside {
84
+ /**
85
+ * Create a PeekInside instance
86
+ *
87
+ * @param {Object} [options] - Configuration options
88
+ * @param {number} [options.maxBodySize=65536] - Maximum bytes captured per request or response body
89
+ * @param {Array<string|RegExp|Function>} [options.ignorePaths=[]] - Paths that are never recorded:
90
+ * string prefixes, regular expressions, or predicates receiving the pathname
91
+ * @param {string} [options.project] - Name in the local dashboard. Defaults to PEEKINSIDE_PROJECT,
92
+ * then the name in your package.json.
93
+ * @param {string} [options.projectRoot] - The project's folder, where its package.json is. By
94
+ * default it is found from the entry script; its own files are told apart from node_modules
95
+ * by it.
96
+ * @param {boolean} [options.local] - Turn the free local dashboard on or off. By default it is on,
97
+ * except when NODE_ENV is "production" or "test", or in CI. PEEKINSIDE_LOCAL=1 or 0 does the same.
98
+ * @param {string} [options.apiKey=process.env.PEEKINSIDE_API_KEY] - PeekInside Cloud API key. When set,
99
+ * requests are encrypted and sent to PeekInside Cloud instead of the local dashboard.
100
+ * @param {string} [options.environment] - Environment label, such as "staging". Defaults to
101
+ * PEEKINSIDE_ENV, then NODE_ENV.
102
+ * @param {string} [options.release] - The deploy that is running, such as a commit hash or
103
+ * "v2.4.0". Defaults to PEEKINSIDE_RELEASE, then the commit your host deployed (Vercel, Render,
104
+ * Railway, Heroku and others set it), then the commit checked out in the project folder.
105
+ * @param {boolean} [options.captureExceptions=true] - Record exceptions automatically: errors that
106
+ * reach Express, Koa or Fastify error handling, uncaught exceptions and unhandled rejections.
107
+ * report() works either way.
108
+ * @param {boolean} [options.sourceContext=true] - Include the source lines around the app's own
109
+ * stack frames in exception records
110
+ * @param {boolean} [options.sourceMaps=true] - Map stack frames of compiled code (TypeScript
111
+ * output, bundles) back to the source files, with the source maps the build wrote
112
+ * @param {boolean} [options.timeline=true] - Record a timeline for each request: its database
113
+ * queries, outgoing calls, console lines and exceptions
114
+ * @param {boolean} [options.captureQueries=true] - pg, mysql2, MongoDB (and Mongoose), Prisma
115
+ * @param {boolean} [options.captureOutgoing=true] - node:http/https requests and fetch
116
+ * @param {boolean} [options.captureLogs=true] - Log lines (the console, pino, winston): on request
117
+ * timelines and the Logs page
118
+ * @param {'debug'|'info'|'warn'|'error'} [options.logLevel='info'] - The lowest level the Logs
119
+ * page records
120
+ * @param {boolean} [options.captureJobs=true] - Runs of node-cron, cron and BullMQ jobs
121
+ * @param {boolean} [options.queryParams=true] - Keep the (masked) values sent with queries
122
+ * @param {number} [options.slowQueryMs=100] - Queries this slow or slower are flagged
123
+ * @param {number} [options.nPlusOneThreshold=5] - A statement run this many times in one request
124
+ * is flagged as an N+1
125
+ * @param {number|false} [options.loopBlockMs=100] - Event-loop blocks this long or longer are
126
+ * recorded on the timelines of the requests in flight; false turns detection off
127
+ * @param {boolean} [options.runtime=true] - Sample the process's memory, CPU and event-loop delay
128
+ * (every 10 s for the local dashboard, every minute for PeekInside Cloud)
129
+ *
130
+ * @example
131
+ * const peekInside = new PeekInside({
132
+ * project: 'orders-api',
133
+ * maxBodySize: 128 * 1024,
134
+ * ignorePaths: ['/health', /^\/metrics/]
135
+ * });
136
+ */
137
+ constructor(options = {}) {
138
+ /**
139
+ * Effective configuration
140
+ * @type {Object}
141
+ */
142
+ this.options = { ...DEFAULT_OPTIONS, ...definedOnly(options) };
143
+
144
+ /**
145
+ * Whether new exchanges are recorded
146
+ * @type {boolean}
147
+ */
148
+ this.isEnabled = true;
149
+
150
+ // At most one transport is set: cloud mode wins over the local dashboard.
151
+ this._cloud = null;
152
+ this._local = null;
153
+ // Records this instance's exceptions; null when nothing is recorded at all.
154
+ this._exceptions = null;
155
+ // What each request's timeline records; null when timelines are off.
156
+ this._timeline = null;
157
+ // Local mode only: requests that reached a server are counted instead of recorded.
158
+ this._classifier = null;
159
+ // Whether this instance uses the event-loop monitor, and its process sampler.
160
+ this._loopMonitor = false;
161
+ this._sampler = null;
162
+ // Log lines for the Logs page, and job runs.
163
+ this._logs = null;
164
+ this._jobs = null;
165
+ this._servers = new ServerBinder(this, { onListening: (port) => this._onAppListening(port) });
166
+
167
+ const environment = this.options.environment || settings.environment();
168
+ const apiKey = this.options.apiKey || settings.apiKey();
169
+ let project = null;
170
+ const identify = () => project || (project = ProjectIdentity.detect(this.options.project || settings.projectName(), this.options.projectRoot));
171
+
172
+ /**
173
+ * The running release, stored with every request and exception; null when none was found
174
+ * @type {string|null}
175
+ */
176
+ this.release = Release.detect({ explicit: this.options.release, root: identify().root });
177
+
178
+ if (apiKey) {
179
+ try {
180
+ this._cloud = new CloudTransport({ apiKey, environment, release: this.release, version: VERSION });
181
+ console.log(`${LOG_PREFIX} Sending encrypted requests to PeekInside Cloud (${this._cloud.environment})`);
182
+ } catch (error) {
183
+ console.error(`${LOG_PREFIX} ${error.message}. Nothing will be sent to PeekInside Cloud.`);
184
+ }
185
+ }
186
+
187
+ if (!this._cloud) {
188
+ const local = typeof this.options.local === 'boolean' ? this.options.local : settings.localMode();
189
+ if (local) {
190
+ this._local = new LocalTransport({ project: identify(), environment, release: this.release });
191
+ this._classifier = new TrafficClassifier();
192
+ } else {
193
+ this.isEnabled = false;
194
+ if (settings.isProduction()) {
195
+ console.warn(`${LOG_PREFIX} The free local dashboard is for development, so it is off when NODE_ENV=production. Set PEEKINSIDE_API_KEY to use PeekInside Cloud.`);
196
+ }
197
+ }
198
+ }
199
+
200
+ if (this._transport) {
201
+ // In both modes, the project folder tells the app's own stack frames from its dependencies'.
202
+ this._exceptions = new ExceptionCapture({
203
+ root: identify().root,
204
+ automatic: this.options.captureExceptions !== false,
205
+ sourceContext: this.options.sourceContext !== false,
206
+ sourceMaps: this.options.sourceMaps !== false,
207
+ isEnabled: () => this.isEnabled,
208
+ sink: () => this._transport,
209
+ owner: this
210
+ });
211
+ ExceptionCapture.register(this._exceptions);
212
+ ProcessMonitor.update();
213
+
214
+ const blocks = this.options.timeline !== false && this.options.loopBlockMs !== false && this.options.loopBlockMs !== 0;
215
+ if (blocks || this.options.runtime !== false) {
216
+ LoopMonitor.acquire();
217
+ this._loopMonitor = true;
218
+ }
219
+ if (this.options.runtime !== false) {
220
+ const transport = this._transport;
221
+ this._sampler = new RuntimeSampler({
222
+ intervalMs: this._cloud ? 60_000 : 10_000,
223
+ monitor: LoopMonitor.shared,
224
+ onSample: (sample) => transport.addSample(sample)
225
+ }).start();
226
+ }
227
+
228
+ if (this.options.timeline !== false) {
229
+ this._timeline = new TimelineSettings(this.options, identify().root);
230
+ }
231
+ const logs = this.options.captureLogs !== false;
232
+ if (logs) {
233
+ this._logs = new LogCapture({ level: LogCapture.level(this.options.logLevel), sink: () => this._transport, isEnabled: () => this.isEnabled });
234
+ }
235
+ this._jobs = new JobRunner({
236
+ capture: this._exceptions,
237
+ sink: () => this._transport,
238
+ timeline: () => this._timeline,
239
+ isEnabled: () => this.isEnabled
240
+ });
241
+ Instrumentation.install({
242
+ root: identify().root,
243
+ queries: Boolean(this._timeline && this._timeline.captureQueries),
244
+ outgoing: Boolean(this._timeline && this._timeline.captureOutgoing),
245
+ logs,
246
+ jobs: this.options.captureJobs !== false
247
+ });
248
+ }
249
+ }
250
+
251
+ /**
252
+ * Record a request/response exchange
253
+ *
254
+ * Works with the native `req`/`res` objects of any Node.js HTTP framework. Request bodies are
255
+ * captured as the application reads them, so middleware order does not matter. The exchange is
256
+ * recorded once the response has been sent, or when the connection closes early (as aborted).
257
+ * Paths matching `ignorePaths` are skipped. In local mode, requests that reached the app on a
258
+ * server (a public domain or client address) are not recorded: servers need PeekInside Cloud.
259
+ *
260
+ * @param {http.IncomingMessage} req - The HTTP request
261
+ * @param {http.ServerResponse} res - The HTTP response
262
+ * @param {Function} [next] - Called once tracking is set up (middleware style)
263
+ * @returns {*} The return value of `next`, if given
264
+ *
265
+ * @example
266
+ * const server = http.createServer((req, res) => {
267
+ * peekInside.intercept(req, res);
268
+ * res.end('Hello World!');
269
+ * });
270
+ */
271
+ intercept(req, res, next = null) {
272
+ return this._intercept(req, res, next, 'custom');
273
+ }
274
+
275
+ /**
276
+ * Records an error, with the request it relates to: the request running now (found through
277
+ * AsyncLocalStorage), or `options.request`.
278
+ *
279
+ * @param {*} error - An Error, or any thrown value
280
+ * @param {Object} [options]
281
+ * @param {http.IncomingMessage} [options.request] - The request the error relates to
282
+ * @param {Object<string, string>} [options.tags] - Up to 32 short labels, such as `{ plan: 'pro' }`
283
+ * @param {*} [options.extra] - Any JSON-safe data; past 16 KB only a preview is kept
284
+ * @param {boolean} [options.handled=true] - Whether the app handled the error
285
+ * @returns {string|null} The exception's id, or null when nothing was recorded (recording is
286
+ * off or paused, the request isn't recorded, or too many exceptions were recorded this second)
287
+ *
288
+ * @example
289
+ * try {
290
+ * await chargeCard(order);
291
+ * } catch (error) {
292
+ * peekInside.report(error, { tags: { provider: 'stripe' } });
293
+ * res.status(502).json({ error: 'payment_failed' });
294
+ * }
295
+ */
296
+ report(error, options = {}) {
297
+ return this._exceptions ? this._exceptions.report(error, options || {}) : null;
298
+ }
299
+
300
+ /**
301
+ * Runs `fn` and records how long it took on the request's timeline, as a measure span. Async
302
+ * functions are measured until their promise settles. Outside a request (or with timelines off),
303
+ * just runs `fn`.
304
+ *
305
+ * @param {string} name - Shown on the timeline, such as "render invoice"
306
+ * @param {Function} fn - Sync or async
307
+ * @returns {*} What `fn` returns (for an async `fn`, a promise of the same outcome)
308
+ *
309
+ * @example
310
+ * const pdf = await peekInside.measure('render invoice', () => renderPdf(order));
311
+ */
312
+ measure(name, fn) {
313
+ const timeline = this._timeline ? Timeline.current() : null;
314
+ const span = timeline && timeline.settings === this._timeline ? timeline.measure(name) : null;
315
+ if (!span) return fn();
316
+
317
+ let result;
318
+ try {
319
+ result = fn();
320
+ } catch (error) {
321
+ span.end({ error });
322
+ throw error;
323
+ }
324
+ // A native promise is followed to its outcome; the returned promise settles the same way, so a
325
+ // rejection nobody handles is still reported. Lazy thenables (query builders) are left alone.
326
+ if (isNativePromise(result)) {
327
+ return result.then(
328
+ (value) => {
329
+ span.end();
330
+ return value;
331
+ },
332
+ (error) => {
333
+ span.end({ error });
334
+ throw error;
335
+ }
336
+ );
337
+ }
338
+ span.end();
339
+ return result;
340
+ }
341
+
342
+ /**
343
+ * PeekInside#measure() with the instance that tracks the current request, or else the most
344
+ * recently created instance that records. Without one, just runs `fn`.
345
+ *
346
+ * @param {string} name
347
+ * @param {Function} fn
348
+ * @returns {*} What `fn` returns
349
+ */
350
+ static measure(name, fn) {
351
+ let instance = null;
352
+ try {
353
+ const context = RequestContext.current();
354
+ const capture = context && context.capture && context.capture.recording ? context.capture : ExceptionCapture.latest();
355
+ instance = capture ? capture.owner : null;
356
+ } catch (error) {
357
+ instance = null;
358
+ }
359
+ return instance ? instance.measure(name, fn) : fn();
360
+ }
361
+
362
+ /**
363
+ * The id of the request the calling code runs for: the id its record has in the dashboard, to
364
+ * put in the app's own logs.
365
+ *
366
+ * @returns {string|null} null outside a recorded request
367
+ *
368
+ * @example
369
+ * logger.info({ requestId: PeekInside.requestId() }, 'Charging card');
370
+ */
371
+ static requestId() {
372
+ try {
373
+ const context = RequestContext.current();
374
+ return context ? context.requestId : null;
375
+ } catch (error) {
376
+ return null;
377
+ }
378
+ }
379
+
380
+ /**
381
+ * Runs a background job as a recorded job run: the Jobs page lists it with how long it took and
382
+ * how it ended, and its queries, calls, log lines and exceptions link to it. A job that throws
383
+ * (or rejects) is recorded as failed, and its error as an exception, grouped into issues like
384
+ * the others. `fn` runs exactly as it would without PeekInside.
385
+ *
386
+ * @param {string} name - Such as "send-invoices"
387
+ * @param {Function} fn - Sync or async
388
+ * @param {Object} [options]
389
+ * @param {string} [options.schedule] - Its cron expression ("0 * * * *"), when it runs on one:
390
+ * the Jobs page then shows when it misses a run, and PeekInside Cloud can alert you
391
+ * @param {string} [options.timezone] - The schedule's time zone
392
+ * @param {string} [options.queue] - The queue it came from
393
+ * @param {number} [options.attempt=1]
394
+ * @param {*} [options.data] - The job's input, shown with the run (first 8 KB)
395
+ * @returns {*} What fn returns (for an async fn, a promise of the same outcome)
396
+ *
397
+ * @example
398
+ * cron.schedule('0 * * * *', () => peekInside.job('send-invoices', sendInvoices, { schedule: '0 * * * *' }));
399
+ */
400
+ job(name, fn, options = {}) {
401
+ return this._jobs ? this._jobs.run(name, fn, options || {}) : fn();
402
+ }
403
+
404
+ /**
405
+ * job() with the instance that tracks the current request, or else the most recently created
406
+ * instance that records. Without one, just runs `fn`.
407
+ *
408
+ * @param {string} name
409
+ * @param {Function} fn
410
+ * @param {Object} [options] - See job()
411
+ * @returns {*} What fn returns
412
+ */
413
+ static job(name, fn, options = {}) {
414
+ try {
415
+ const context = RequestContext.current();
416
+ const owner = context && context.capture && context.capture.recording ? context.capture.owner : null;
417
+ if (owner) return owner.job(name, fn, options);
418
+ } catch (error) {
419
+ // Fall back to the latest instance.
420
+ }
421
+ return JobRunner.run(name, fn, options || {});
422
+ }
423
+
424
+ /**
425
+ * Tells PeekInside who the current request is for. The request and every exception it runs into
426
+ * carry the user, so the dashboard counts the users an issue affects and lists everything one
427
+ * customer ran into. In PeekInside Cloud the user is encrypted with the rest of the request.
428
+ * Call it once the user is known, such as in your authentication middleware.
429
+ *
430
+ * @param {{ id?: string|number, email?: string, name?: string }|string|number|null} user - The
431
+ * user, or just their id; null forgets it
432
+ * @returns {boolean} Whether a request was running to attach it to
433
+ *
434
+ * @example
435
+ * app.use((req, res, next) => {
436
+ * if (req.user) PeekInside.setUser({ id: req.user.id, email: req.user.email });
437
+ * next();
438
+ * });
439
+ */
440
+ static setUser(user) {
441
+ try {
442
+ const context = RequestContext.current();
443
+ if (!context) return false;
444
+ context.user = UserInfo.normalize(user);
445
+ return true;
446
+ } catch (error) {
447
+ return false;
448
+ }
449
+ }
450
+
451
+ /**
452
+ * Same as PeekInside.setUser(): the user belongs to the request running now.
453
+ *
454
+ * @param {{ id?: string|number, email?: string, name?: string }|string|number|null} user
455
+ * @returns {boolean}
456
+ */
457
+ setUser(user) {
458
+ return PeekInside.setUser(user);
459
+ }
460
+
461
+ /**
462
+ * Instruments database modules that automatic patching can't reach: bundled apps, or a
463
+ * monorepo package with its own copy of a driver. Accepts pg, mysql2, mysql2/promise, mongodb,
464
+ * mongoose, a PrismaClient class or instance, and the @prisma/client module; anything else is
465
+ * ignored.
466
+ *
467
+ * @param {...*} modules
468
+ * @returns {PeekInside} This instance, for chaining
469
+ *
470
+ * @example
471
+ * import pg from 'pg';
472
+ * peekInside.instrument(pg);
473
+ */
474
+ instrument(...modules) {
475
+ Instrumentation.instrument(modules);
476
+ return this;
477
+ }
478
+
479
+ /**
480
+ * Records an error with the instance that tracks the current request, or else the most recently
481
+ * created instance that records. See PeekInside#report().
482
+ *
483
+ * @param {*} error
484
+ * @param {Object} [options] - See PeekInside#report()
485
+ * @returns {string|null} The exception's id, or null when nothing was recorded
486
+ *
487
+ * @example
488
+ * PeekInside.report(new Error('Cache unavailable'), { extra: { key } });
489
+ */
490
+ static report(error, options = {}) {
491
+ try {
492
+ const request = options && options.request;
493
+ const context = request
494
+ ? RequestContext.of(request) || RequestContext.of(request.raw) || RequestContext.of(request.req)
495
+ : RequestContext.current();
496
+ const owner = context && context.capture && context.capture.recording ? context.capture : null;
497
+ const capture = owner || ExceptionCapture.latest();
498
+ return capture ? capture.report(error, options || {}) : null;
499
+ } catch (failure) {
500
+ return null;
501
+ }
502
+ }
503
+
504
+ /**
505
+ * Enable or disable recording
506
+ *
507
+ * @param {boolean} enabled - True to record, false to pause recording
508
+ * @returns {void}
509
+ */
510
+ setEnabled(enabled) {
511
+ this.isEnabled = Boolean(enabled) && Boolean(this._transport);
512
+ }
513
+
514
+ /**
515
+ * Express/Connect middleware that records exchanges
516
+ *
517
+ * @returns {Function} Middleware `(req, res, next)`
518
+ */
519
+ expressMiddleware() {
520
+ return this._adapter(ExpressAdapter).middleware();
521
+ }
522
+
523
+ /**
524
+ * Koa middleware that records exchanges
525
+ *
526
+ * @returns {Function} Koa middleware `(ctx, next)`
527
+ */
528
+ koaMiddleware() {
529
+ return this._adapter(KoaAdapter).middleware();
530
+ }
531
+
532
+ /**
533
+ * Register a Fastify `onRequest` hook that records exchanges
534
+ *
535
+ * @param {Object} fastify - Fastify instance
536
+ * @returns {void}
537
+ */
538
+ fastifyHook(fastify) {
539
+ this._adapter(FastifyAdapter).attach(fastify);
540
+ }
541
+
542
+ /**
543
+ * Record every request handled by a native HTTP server
544
+ *
545
+ * @param {http.Server} server - HTTP server
546
+ * @returns {http.Server} The same server
547
+ */
548
+ wrapHttpServer(server) {
549
+ this._adapter(HttpServerAdapter).attach(server);
550
+ return server;
551
+ }
552
+
553
+ /**
554
+ * Attach to an application and record its traffic
555
+ *
556
+ * Detects Fastify, Koa, Express-like apps (Express, Connect) and native `http.Server`s.
557
+ *
558
+ * @param {Object} app - Application or server instance
559
+ * @param {Object} [options] - `maxBodySize` and `ignorePaths` to merge into the configuration
560
+ * @returns {PeekInside} This instance, for chaining
561
+ */
562
+ attachTo(app, options = {}) {
563
+ const { maxBodySize, ignorePaths } = options;
564
+ Object.assign(this.options, definedOnly({ maxBodySize, ignorePaths }));
565
+
566
+ const Adapter = ADAPTERS.find((candidate) => candidate.matches(app));
567
+ if (Adapter) {
568
+ this._adapter(Adapter).attach(app);
569
+ } else {
570
+ console.warn(`${LOG_PREFIX} Could not detect the framework. Use expressMiddleware(), koaMiddleware(), fastifyHook() or wrapHttpServer().`);
571
+ }
572
+ return this;
573
+ }
574
+
575
+ /**
576
+ * One-line setup: record every request an app handles
577
+ *
578
+ * @param {Object} app - Express, Koa or Fastify app, or an http.Server
579
+ * @param {Object} [options] - Configuration options (see constructor)
580
+ * @returns {PeekInside} PeekInside instance
581
+ *
582
+ * @example
583
+ * PeekInside.attach(app);
584
+ */
585
+ static attach(app, options = {}) {
586
+ const peekInside = new PeekInside(options);
587
+ if (app) {
588
+ peekInside.attachTo(app);
589
+ }
590
+ return peekInside;
591
+ }
592
+
593
+ /**
594
+ * Send everything still queued
595
+ *
596
+ * Useful before a short-lived process exits (scripts, serverless functions, tests).
597
+ *
598
+ * @returns {Promise<void>}
599
+ */
600
+ async flush() {
601
+ const transport = this._transport;
602
+ if (transport) {
603
+ await transport.drain();
604
+ }
605
+ }
606
+
607
+ /**
608
+ * Stop recording and sending
609
+ *
610
+ * @returns {void}
611
+ */
612
+ stop() {
613
+ this.isEnabled = false;
614
+ if (this._cloud) {
615
+ this._cloud.stop();
616
+ }
617
+ if (this._local) {
618
+ this._local.stop();
619
+ }
620
+ if (this._exceptions) {
621
+ ExceptionCapture.unregister(this._exceptions);
622
+ ProcessMonitor.update();
623
+ }
624
+ if (this._sampler) {
625
+ this._sampler.stop();
626
+ this._sampler = null;
627
+ }
628
+ if (this._loopMonitor) {
629
+ this._loopMonitor = false;
630
+ LoopMonitor.release();
631
+ }
632
+ }
633
+
634
+ // Where recorded exchanges go, or null when recording is off.
635
+ get _transport() {
636
+ return this._cloud || this._local;
637
+ }
638
+
639
+ // An adapter for one integration. The first integration used names the app's framework in
640
+ // the local dashboard.
641
+ _adapter(Adapter) {
642
+ if (this._local) {
643
+ this._local.setFramework(Adapter.framework);
644
+ }
645
+ return new Adapter(this, this._servers);
646
+ }
647
+
648
+ // Tracks one request and runs the rest of its handling (`next`) inside its context, so that
649
+ // exceptions during it are linked to it. Integrations pass their name, which says how an error
650
+ // escaping the request was captured.
651
+ _intercept(req, res, next, integration) {
652
+ const proceed = typeof next === 'function' ? next : noop;
653
+
654
+ if (!this.isEnabled || !req || !res || req[kTracked]) {
655
+ return proceed();
656
+ }
657
+
658
+ const url = req.originalUrl || req.url || '/';
659
+ const { pathname, search } = splitUrl(url);
660
+ if (isIgnoredPath(pathname, this.options.ignorePaths)) {
661
+ return proceed();
662
+ }
663
+
664
+ req[kTracked] = true;
665
+ let context = null;
666
+ try {
667
+ this._servers.discover(req);
668
+ const target = { url, pathname, search };
669
+ // The free local dashboard is for development: requests that reached a server are not
670
+ // recorded, and neither are their exceptions.
671
+ const signal = this._classifier ? this._classifier.serverSignal(req) : null;
672
+ if (signal) {
673
+ this._local.noteServerRequest(signal);
674
+ context = new RequestContext({ capture: this._exceptions, req, target, blocked: true, integration });
675
+ } else {
676
+ context = this._track(req, res, target, integration);
677
+ }
678
+ } catch (error) {
679
+ // Monitoring must never break the host application.
680
+ }
681
+ return context ? context.run(proceed) : proceed();
682
+ }
683
+
684
+ _track(req, res, target, integration) {
685
+ let context = null;
686
+ const recorder = new ExchangeRecorder(req, res, target, {
687
+ maxBodySize: () => this.options.maxBodySize,
688
+ onExchange: (exchange) => {
689
+ // Exceptions and timeline first: the record is built from the completed exchange.
690
+ if (context) {
691
+ context.complete(exchange);
692
+ }
693
+ const transport = this._transport;
694
+ if (transport) {
695
+ transport.enqueue(exchange);
696
+ }
697
+ }
698
+ });
699
+ context = new RequestContext({ capture: this._exceptions, req, recorder, target, integration });
700
+ recorder.start();
701
+ if (this._timeline) {
702
+ context.timeline = new Timeline(this._timeline, recorder.perfStart);
703
+ }
704
+ // Listeners of the request's and response's events run for this request too.
705
+ context.bind(req);
706
+ context.bind(res);
707
+ return context;
708
+ }
709
+
710
+ // Registers the app with the local dashboard as soon as its port is known.
711
+ _onAppListening(port) {
712
+ if (this._local && this.isEnabled) {
713
+ this._local.start(port);
714
+ }
715
+ }
716
+ }
717
+
718
+ function noop() {}
719
+
720
+ // Options left undefined must not override the defaults.
721
+ function definedOnly(options) {
722
+ const defined = {};
723
+ for (const key of Object.keys(options || {})) {
724
+ if (options[key] !== undefined) {
725
+ defined[key] = options[key];
726
+ }
727
+ }
728
+ return defined;
729
+ }
730
+
731
+ export default PeekInside;