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.
@@ -0,0 +1,277 @@
1
+ /**
2
+ * NestJS integration (Nest 10 and later, on Express or Fastify).
3
+ *
4
+ * Nest answers errors itself: its exception filters run before Express or Fastify see an error,
5
+ * and its default filter answers 500 and logs it. So PeekInside hooks into Nest's own pieces,
6
+ * reached through the app object, which makes the copy of Nest the app runs the one that is
7
+ * patched, whichever module system loaded it:
8
+ *
9
+ * - Requests are recorded from the HTTP server Nest created, before Express or Fastify runs, so
10
+ * bodies are captured as they arrive and middleware order doesn't matter.
11
+ * - Each route handler Nest builds (RouterExplorer#createCallbackProxy) is wrapped to note the
12
+ * controller method and the route pattern on the request, before guards, pipes and
13
+ * interceptors run.
14
+ * - Every error of an HTTP request passes through ExceptionsHandler#next. One that an exception
15
+ * filter of the app catches is handled, unless the filter hands it to Nest's default answer
16
+ * (BaseExceptionFilter#catch, what `super.catch()` calls); an error that gets the default answer
17
+ * is unhandled. Errors with a 4xx status (NotFoundException, BadRequestException...) are how an
18
+ * app answers bad requests, and aren't recorded.
19
+ * - Nest's Logger lines and @nestjs/schedule's jobs are recorded too (see
20
+ * instrumentation/loggers.js and instrumentation/jobs.js).
21
+ *
22
+ * Nest registers routes and scheduled methods when the app initializes, so attach before
23
+ * app.listen() (or app.init()). A worker made with NestFactory.createApplicationContext() is
24
+ * initialized by then: for its scheduled methods, PeekInside must exist before the worker does
25
+ * (instrumentation/index.js patches @nestjs/schedule when PeekInside is created). Every step is
26
+ * feature-detected: a Nest version that doesn't fit is left alone.
27
+ */
28
+
29
+ import { LOG_PREFIX } from '../config.js';
30
+ import { RequestContext } from '../exceptions/request-context.js';
31
+ import { JobInstrumentation } from '../instrumentation/jobs.js';
32
+ import { LoggerInstrumentation } from '../instrumentation/loggers.js';
33
+ import { FrameworkAdapter } from './framework-adapter.js';
34
+
35
+ const kRoutes = Symbol('peekInside.nestRoutes');
36
+ const kErrors = Symbol('peekInside.nestErrors');
37
+
38
+ /**
39
+ * Records a Nest app's requests from its HTTP server, and hooks into Nest for the rest.
40
+ */
41
+ export class NestAdapter extends FrameworkAdapter {
42
+ static framework = 'nest';
43
+
44
+ /**
45
+ * @param {*} app
46
+ * @returns {boolean} Whether `app` looks like a Nest application (NestFactory.create()) or
47
+ * application context (NestFactory.createApplicationContext(), such as a worker)
48
+ */
49
+ static matches(app) {
50
+ return Boolean(app) && typeof app.select === 'function' && typeof app.useLogger === 'function' && typeof app.resolve === 'function';
51
+ }
52
+
53
+ /**
54
+ * Binds the app's HTTP server (an application context has none) and installs the hooks. Logs and
55
+ * jobs are only hooked for an instance that records.
56
+ *
57
+ * @param {Object} app - What NestFactory.create() returned
58
+ * @returns {void}
59
+ */
60
+ attach(app) {
61
+ const { options } = this.peekInside;
62
+ const records = Boolean(this.peekInside._transport);
63
+ const installed = NestHooks.install(app, {
64
+ exceptions: options.captureExceptions !== false,
65
+ logs: records && options.captureLogs !== false,
66
+ jobs: records && options.captureJobs !== false
67
+ });
68
+ if (installed.lateJobs) {
69
+ console.warn(`${LOG_PREFIX} This NestJS app set up its scheduled jobs before PeekInside existed, so their failures and their @Interval and @Timeout runs aren't recorded. Create PeekInside first (new PeekInside()), then attach it with peekInside.attachTo(app).`);
70
+ }
71
+ const server = httpServerOf(app);
72
+ if (server) this.servers.bind(server, { interceptRequests: true, integration: 'nest' });
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Installs the hooks into a Nest app.
78
+ */
79
+ export class NestHooks {
80
+ /**
81
+ * @param {Object} app
82
+ * @param {Object} [options]
83
+ * @param {boolean} [options.exceptions=true] - Record the errors Nest answers
84
+ * @param {boolean} [options.logs=true] - Record Nest's Logger lines (ConsoleLogger)
85
+ * @param {boolean} [options.jobs=true] - Record @nestjs/schedule's jobs
86
+ * @returns {{ routes: boolean, errors: boolean, logs: boolean, jobs: boolean, lateJobs: boolean }}
87
+ * What was hooked, and whether scheduled methods were registered before the scheduler was
88
+ */
89
+ static install(app, { exceptions = true, logs = true, jobs = true } = {}) {
90
+ const installed = { routes: false, errors: false, logs: false, jobs: false, lateJobs: false };
91
+ try {
92
+ const resolver = read(app, 'routesResolver');
93
+ installed.routes = patchRoutes(read(resolver, 'routerExplorer'));
94
+ if (exceptions) installed.errors = patchErrors(read(resolver, 'routerExceptionsFilter'));
95
+ if (logs) installed.logs = LoggerInstrumentation.nest(activeLogger(app));
96
+ const explorer = jobs ? scheduleExplorer(app) : null;
97
+ if (explorer) {
98
+ installed.jobs = JobInstrumentation.nestSchedule(explorer);
99
+ installed.lateJobs = registeredBefore(explorer);
100
+ }
101
+ } catch (error) {
102
+ // Whatever was hooked stays; the rest is left alone.
103
+ }
104
+ return installed;
105
+ }
106
+ }
107
+
108
+ // Wraps the handler Nest builds for each route: it notes the controller method and the route the
109
+ // request matched before Nest runs guards, pipes, interceptors and the method.
110
+ function patchRoutes(explorer) {
111
+ const proto = explorer ? Object.getPrototypeOf(explorer) : null;
112
+ if (!proto || typeof proto.createCallbackProxy !== 'function') return false;
113
+ if (proto[kRoutes]) return true;
114
+ const original = proto.createCallbackProxy;
115
+ proto.createCallbackProxy = function (instance, callback, methodName) {
116
+ const proxy = original.apply(this, arguments);
117
+ if (typeof proxy !== 'function') return proxy;
118
+ const handler = handlerName(instance, methodName);
119
+ return function (req, res, next) {
120
+ noteRoute(req, handler);
121
+ return proxy.apply(this, arguments);
122
+ };
123
+ };
124
+ Object.defineProperty(proto, kRoutes, { value: true });
125
+ return true;
126
+ }
127
+
128
+ // Observes ExceptionsHandler#next and BaseExceptionFilter#catch. ExceptionsHandler isn't exported
129
+ // by every Nest version: the app's RouterExceptionFilters creates one, which gives its prototype.
130
+ function patchErrors(filters) {
131
+ let sample = null;
132
+ try {
133
+ sample = filters && typeof filters.create === 'function' ? filters.create({}, function handler() {}, undefined) : null;
134
+ } catch (error) {
135
+ sample = null;
136
+ }
137
+ const proto = sample ? Object.getPrototypeOf(sample) : null;
138
+ if (!proto || typeof proto.next !== 'function') return false;
139
+ if (proto[kErrors]) return true;
140
+
141
+ const originalNext = proto.next;
142
+ proto.next = function (exception, host) {
143
+ try {
144
+ if (catchesIt(this.filters, exception)) noteError(host, exception, true);
145
+ } catch (failure) {
146
+ // Never in the way of the app's error handling.
147
+ }
148
+ return originalNext.apply(this, arguments);
149
+ };
150
+ Object.defineProperty(proto, kErrors, { value: true });
151
+
152
+ const base = Object.getPrototypeOf(proto);
153
+ if (base && base !== Object.prototype && typeof base.catch === 'function' && !base[kErrors]) {
154
+ const originalCatch = base.catch;
155
+ base.catch = function (exception, host) {
156
+ noteError(host, exception, false);
157
+ return originalCatch.apply(this, arguments);
158
+ };
159
+ Object.defineProperty(base, kErrors, { value: true });
160
+ }
161
+ return true;
162
+ }
163
+
164
+ // Whether one of the app's exception filters catches the error, as Nest picks them: a filter
165
+ // with no exception types catches everything.
166
+ function catchesIt(filters, exception) {
167
+ if (!Array.isArray(filters) || !filters.length) return false;
168
+ return filters.some((filter) => {
169
+ const types = filter && Array.isArray(filter.exceptionMetatypes) ? filter.exceptionMetatypes : [];
170
+ return !types.length || types.some((type) => typeof type === 'function' && exception instanceof type);
171
+ });
172
+ }
173
+
174
+ // An error a filter catches stays open: the filter may still hand it to Nest's default answer.
175
+ function noteError(host, exception, handled) {
176
+ try {
177
+ const req = host && typeof host.getArgByIndex === 'function' ? host.getArgByIndex(0) : null;
178
+ const context = RequestContext.of(nodeRequest(req)) || RequestContext.current();
179
+ if (context && context.capture) {
180
+ context.capture.frameworkError(context, exception, { mechanism: 'nest', handled, open: handled });
181
+ }
182
+ } catch (failure) {
183
+ // Recording must never change the error the app sees.
184
+ }
185
+ }
186
+
187
+ function noteRoute(req, handler) {
188
+ try {
189
+ const raw = nodeRequest(req);
190
+ const context = RequestContext.of(raw);
191
+ if (!context) return;
192
+ // Express keeps the route parameters on the Node.js request; Fastify on its own request.
193
+ context.noteRoute({ route: routeOf(req), handler, ...(raw !== req ? { params: () => req.params } : {}) });
194
+ } catch (error) {
195
+ // The request is recorded without them.
196
+ }
197
+ }
198
+
199
+ // The route pattern the platform matched: Express's Route, Fastify's route options (routerPath
200
+ // before Fastify 4.10).
201
+ function routeOf(req) {
202
+ if (!req) return null;
203
+ if (req.route && typeof req.route.path === 'string') return req.route.path;
204
+ const options = req.routeOptions;
205
+ if (options && typeof options === 'object') return typeof options.url === 'string' ? options.url : null;
206
+ return typeof req.routerPath === 'string' ? req.routerPath : null;
207
+ }
208
+
209
+ // "OrdersController.create"
210
+ function handlerName(instance, methodName) {
211
+ const owner = instance && instance.constructor && typeof instance.constructor.name === 'string' ? instance.constructor.name : '';
212
+ const method = typeof methodName === 'string' ? methodName : '';
213
+ return owner && method ? `${owner}.${method}` : method || null;
214
+ }
215
+
216
+ // The Node.js request behind what Nest passes around: Express's request is the Node.js one,
217
+ // Fastify's keeps it in `raw`.
218
+ function nodeRequest(req) {
219
+ return req && typeof req === 'object' && req.raw && typeof req.raw === 'object' ? req.raw : req;
220
+ }
221
+
222
+ // The logger Nest's Logger writes through: its ConsoleLogger, unless the app set another one.
223
+ function activeLogger(app) {
224
+ const logger = read(app, 'logger');
225
+ const Logger = logger && logger.constructor;
226
+ return Logger && typeof Logger === 'function' ? Logger.staticInstanceRef || null : null;
227
+ }
228
+
229
+ // @nestjs/schedule's ScheduleExplorer, one of the app's providers.
230
+ function scheduleExplorer(app) {
231
+ const container = read(app, 'container');
232
+ const modules = container && typeof container.getModules === 'function' ? container.getModules() : null;
233
+ if (!modules || typeof modules.values !== 'function') return null;
234
+ for (const module of modules.values()) {
235
+ const providers = module && module.providers;
236
+ if (!providers || typeof providers.values !== 'function') continue;
237
+ for (const wrapper of providers.values()) {
238
+ let instance = null;
239
+ try {
240
+ instance = wrapper ? wrapper.instance : null;
241
+ } catch (error) {
242
+ instance = null;
243
+ }
244
+ if (instance && typeof instance.wrapFunctionInTryCatchBlocks === 'function' && typeof instance.lookupSchedulers === 'function') return instance;
245
+ }
246
+ }
247
+ return null;
248
+ }
249
+
250
+ // Whether the scheduler registered methods before it was patched: their errors are swallowed
251
+ // unrecorded, and intervals and timeouts aren't job runs.
252
+ function registeredBefore(explorer) {
253
+ const orchestrator = read(explorer, 'schedulerOrchestrator');
254
+ return ['cronJobs', 'intervals', 'timeouts'].some((key) => {
255
+ const registered = read(orchestrator, key);
256
+ return Boolean(registered) && Object.values(registered).some((entry) => entry && typeof entry.target === 'function' && !JobInstrumentation.isRun(entry.target));
257
+ });
258
+ }
259
+
260
+ function httpServerOf(app) {
261
+ try {
262
+ const server = typeof app.getHttpServer === 'function' ? app.getHttpServer() : null;
263
+ return server && typeof server.emit === 'function' ? server : null;
264
+ } catch (error) {
265
+ return null;
266
+ }
267
+ }
268
+
269
+ // An object property of Nest's internals, or null.
270
+ function read(target, key) {
271
+ try {
272
+ const value = target ? target[key] : null;
273
+ return value && typeof value === 'object' ? value : null;
274
+ } catch (error) {
275
+ return null;
276
+ }
277
+ }
@@ -3,9 +3,10 @@
3
3
  * it, and registers the app as soon as it is known) and, for plain http servers, to see every
4
4
  * request.
5
5
  *
6
- * Servers are found three ways: passed directly (http.Server, Fastify), returned by a patched
7
- * app.listen() (Express, Koa), or discovered from the socket of a request the app handled, which
8
- * catches servers created with http.createServer(app).
6
+ * Servers are found three ways: passed directly (http.Server, Fastify, the server a NestJS app
7
+ * created), returned by a patched app.listen() (Express, Koa), or discovered from the socket of a
8
+ * request the app handled, which catches servers created with http.createServer(app) and Hono's
9
+ * serve().
9
10
  */
10
11
 
11
12
  /**
@@ -73,10 +74,12 @@ export class ServerBinder {
73
74
  * @param {import('http').Server} server
74
75
  * @param {Object} [options]
75
76
  * @param {boolean} [options.interceptRequests=false] - Also record every request the server
76
- * emits, for plain http servers where no middleware runs
77
+ * emits, for plain http servers where no middleware runs, and for frameworks recorded before
78
+ * their own code runs (NestJS)
79
+ * @param {string} [options.integration='http'] - The integration recording those requests
77
80
  * @returns {void}
78
81
  */
79
- bind(server, { interceptRequests = false } = {}) {
82
+ bind(server, { interceptRequests = false, integration = 'http' } = {}) {
80
83
  if (!server || typeof server.emit !== 'function' || this._bound.has(server)) {
81
84
  return;
82
85
  }
@@ -91,7 +94,7 @@ export class ServerBinder {
91
94
  server.emit = function (event, req, res) {
92
95
  if (event === 'request' && req && res) {
93
96
  const args = arguments;
94
- return peekInside._intercept(req, res, () => originalEmit.apply(this, args), 'http');
97
+ return peekInside._intercept(req, res, () => originalEmit.apply(this, args), integration);
95
98
  }
96
99
  return originalEmit.apply(this, arguments);
97
100
  };
@@ -1,7 +1,7 @@
1
1
  /**
2
- * Log lines for the Logs page: what the app writes with the console, pino or winston, inside a
3
- * request, inside a job run, or anywhere else (startup, timers), each linked to its request or job
4
- * run when there is one.
2
+ * Log lines for the Logs page: what the app writes with the console, pino, winston or NestJS's
3
+ * Logger, inside a request, inside a job run, or anywhere else (startup, timers), each linked to
4
+ * its request or job run when there is one.
5
5
  *
6
6
  * Lines are rate limited per process (100 a second, bursts of 500) and below the `logLevel` option
7
7
  * are left out, so an app that logs heavily costs a bounded amount. PeekInside's own lines are
@@ -67,8 +67,8 @@ export class LogCapture {
67
67
  * @param {'debug'|'info'|'warn'|'error'} level
68
68
  * @param {string} message - As written (formatted)
69
69
  * @param {Object} [options]
70
- * @param {'console'|'pino'|'winston'} [options.source='console']
71
- * @param {*} [options.fields] - Structured data written with the line (pino, winston)
70
+ * @param {'console'|'pino'|'winston'|'nest'} [options.source='console']
71
+ * @param {*} [options.fields] - Structured data written with the line (pino, winston, nest)
72
72
  * @returns {string|null} The record's id, or null when nothing was recorded
73
73
  */
74
74
  static record(level, message, { source = 'console', fields } = {}) {
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * One log line, as the Logs page lists it (see the data contract): when, its level, where it came
3
- * from (console, pino, winston), the request or job run it belongs to, and the line itself.
3
+ * from (console, pino, winston, nest), the request or job run it belongs to, and the line itself.
4
4
  *
5
5
  * PeekInside Cloud receives the line sealed: only the time, level, source and links are readable.
6
6
  */
@@ -18,7 +18,7 @@ export class LogRecord {
18
18
  * @param {string} fields.environment
19
19
  * @param {string|null} [fields.release=null]
20
20
  * @param {'debug'|'info'|'warn'|'error'} fields.level
21
- * @param {'console'|'pino'|'winston'} fields.source
21
+ * @param {'console'|'pino'|'winston'|'nest'} fields.source
22
22
  * @param {string|null} [fields.requestId=null] - The request it was written during
23
23
  * @param {string|null} [fields.jobRunId=null] - The job run it was written during
24
24
  * @param {{ message: string, fields?: Object|null }} fields.summary - The line (sealed in Cloud)
package/src/record.js CHANGED
@@ -32,8 +32,9 @@ export class RequestRecord {
32
32
  * lists the first of them)
33
33
  * @param {Object} [fields.stats] - Timeline counters: queries, queryTime, slowQueries, nPlusOne,
34
34
  * duplicates, http, httpTime, logs (detail.timeline holds the spans); absent when the timeline is off
35
- * @param {{ url: string, path: string, contentType: string, user?: Object }} fields.summary - Shown in
36
- * the request list (with the user, when the app set one)
35
+ * @param {{ url: string, path: string, contentType: string, route?: string, handler?: string, user?: Object }} fields.summary -
36
+ * Shown in the request list: with the route pattern and the handler when the framework says
37
+ * (NestJS, Hono, Fastify), and the user when the app set one
37
38
  * @param {Object} fields.detail - Everything else: headers, bodies, query, client details
38
39
  */
39
40
  constructor({ id, occurredAt, environment, release = null, method, status, duration, requestSize, responseSize, aborted, exceptionCount = 0, stats, summary, detail }) {
@@ -83,6 +84,8 @@ export class RequestRecord {
83
84
  url: exchange.url,
84
85
  path: exchange.path,
85
86
  contentType: headerValue(response.headers, 'content-type'),
87
+ ...(exchange.route ? { route: exchange.route } : {}),
88
+ ...(exchange.handler ? { handler: exchange.handler } : {}),
86
89
  ...(exchange.user ? { user: exchange.user } : {})
87
90
  },
88
91
  detail: {
@@ -106,6 +109,7 @@ export class RequestRecord {
106
109
  bodyEncoding: response.bodyEncoding
107
110
  },
108
111
  exceptions: Array.isArray(exchange.exceptions) ? exchange.exceptions.slice(0, MAX_EXCEPTION_LINKS) : [],
112
+ ...(Array.isArray(exchange.dumps) && exchange.dumps.length ? { dumps: exchange.dumps } : {}),
109
113
  ...(exchange.timeline ? { timeline: exchange.timeline } : {})
110
114
  }
111
115
  });
@@ -37,7 +37,7 @@ export const TIMELINE_DEFAULTS = Object.freeze({
37
37
  });
38
38
 
39
39
  // Spans listed per request, by type.
40
- const SPAN_CAPS = Object.freeze({ query: 500, http: 100, log: 200, measure: 100, exception: 10, block: 20 });
40
+ const SPAN_CAPS = Object.freeze({ query: 500, http: 100, log: 200, measure: 100, exception: 10, block: 20, dump: 50 });
41
41
  const MAX_STATEMENTS = 200;
42
42
  // Executions per statement whose parameters are kept.
43
43
  const PARAMS_PER_STATEMENT = 20;
@@ -121,7 +121,7 @@ export class Timeline {
121
121
  this.origin = origin;
122
122
  this.closed = false;
123
123
  this.spans = [];
124
- this.listed = { query: 0, http: 0, log: 0, measure: 0, exception: 0, block: 0 };
124
+ this.listed = { query: 0, http: 0, log: 0, measure: 0, exception: 0, block: 0, dump: 0 };
125
125
  this.dropped = 0;
126
126
  this.statements = [];
127
127
  this.byFingerprint = new Map();
@@ -263,7 +263,7 @@ export class Timeline {
263
263
  *
264
264
  * @param {'debug'|'info'|'warn'|'error'} level
265
265
  * @param {string} message - Formatted as it was written
266
- * @param {'console'|'pino'|'winston'} [source='console']
266
+ * @param {'console'|'pino'|'winston'|'nest'} [source='console']
267
267
  * @returns {void}
268
268
  */
269
269
  log(level, message, source = 'console') {
@@ -282,6 +282,27 @@ export class Timeline {
282
282
  }
283
283
  }
284
284
 
285
+ /**
286
+ * Records a dump made during the request (local dashboard only): its label and a preview of the
287
+ * value; the Dumps page has the value itself.
288
+ *
289
+ * @param {{ id: string, label: string|null, summary: { preview: string } }} record - A DumpRecord
290
+ * @returns {void}
291
+ */
292
+ dump(record) {
293
+ if (this.closed) return;
294
+ try {
295
+ if (this.listed.dump < SPAN_CAPS.dump) {
296
+ this.listed.dump++;
297
+ this.spans.push({ type: 'dump', start: this._offset(performance.now()), id: record.id, label: record.label, preview: cut(record.summary.preview, MAX_LOG) });
298
+ } else {
299
+ this.dropped++;
300
+ }
301
+ } catch (error) {
302
+ // The dump is still on the Dumps page.
303
+ }
304
+ }
305
+
285
306
  /**
286
307
  * Records an exception recorded during the request.
287
308
  *
@@ -13,6 +13,7 @@
13
13
  import { LOG_PREFIX } from '../config.js';
14
14
  import { ExceptionRecord } from '../exceptions/exception-record.js';
15
15
  import { RequestContext } from '../exceptions/request-context.js';
16
+ import { DumpRecord } from '../dumps/dump-record.js';
16
17
  import { JobRunRecord } from '../jobs/job-run-record.js';
17
18
  import { LogRecord } from '../logs/log-record.js';
18
19
  import { RequestRecord } from '../record.js';
@@ -170,13 +171,15 @@ export class BatchTransport {
170
171
  const exceptions = [];
171
172
  const jobs = [];
172
173
  const logs = [];
174
+ const dumps = [];
173
175
  for (const record of records) {
174
176
  if (record instanceof ExceptionRecord) exceptions.push(record);
175
177
  else if (record instanceof JobRunRecord) jobs.push(record);
176
178
  else if (record instanceof LogRecord) logs.push(record);
179
+ else if (record instanceof DumpRecord) dumps.push(record);
177
180
  else requests.push(record);
178
181
  }
179
- return { requests, exceptions, jobs, logs };
182
+ return { requests, exceptions, jobs, logs, dumps };
180
183
  }
181
184
 
182
185
  /**
@@ -16,6 +16,7 @@ import { settings } from '../config.js';
16
16
  import { ApiKey, Sealer, SyncSealer } from '../crypto.js';
17
17
  import { RequestContext } from '../exceptions/request-context.js';
18
18
  import { ExceptionRecord } from '../exceptions/exception-record.js';
19
+ import { DumpRecord } from '../dumps/dump-record.js';
19
20
  import { JobRunRecord } from '../jobs/job-run-record.js';
20
21
  import { LogRecord } from '../logs/log-record.js';
21
22
  import { RequestRecord } from '../record.js';
@@ -130,6 +131,17 @@ export class CloudTransport extends BatchTransport {
130
131
  this._scheduleResend(SPOOL_RESEND_DELAY_MS);
131
132
  }
132
133
 
134
+ /**
135
+ * Queues a record for sending. Dumps are for the local dashboard: PeekInside Cloud never gets them.
136
+ *
137
+ * @param {Object} record
138
+ * @returns {void}
139
+ */
140
+ enqueueRecord(record) {
141
+ if (record instanceof DumpRecord) return;
142
+ super.enqueueRecord(record);
143
+ }
144
+
133
145
  /**
134
146
  * Stops the background timer and the flush at exit. Sending itself stays possible: drain()
135
147
  * still sends what is queued.
@@ -182,14 +182,15 @@ export class LocalTransport extends BatchTransport {
182
182
  const records = fatal ? [...this.sending, ...this.queue.splice(0)] : [];
183
183
  for (const exchange of exchanges) records.push(RequestRecord.fromExchange(exchange, this.environment, this.release));
184
184
  records.push(...exceptions);
185
- const { requests, exceptions: errors, jobs, logs } = BatchTransport.split(records);
185
+ const { requests, exceptions: errors, jobs, logs, dumps } = BatchTransport.split(records);
186
186
  Spool.local(settings.dataDir(), this.project.id).write({
187
187
  project: this.project,
188
188
  instance: this.instance,
189
189
  requests: requests.slice(-MAX_SPOOLED_REQUESTS),
190
190
  exceptions: errors.slice(-MAX_SPOOLED_EXCEPTIONS),
191
191
  jobs: jobs.slice(-MAX_SPOOLED_EXCEPTIONS),
192
- logs: logs.slice(-MAX_SPOOLED_REQUESTS)
192
+ logs: logs.slice(-MAX_SPOOLED_REQUESTS),
193
+ dumps: dumps.slice(-MAX_SPOOLED_EXCEPTIONS)
193
194
  });
194
195
  } catch (error) {
195
196
  // A data folder that can't be written loses the records, never the app.
@@ -207,13 +208,14 @@ export class LocalTransport extends BatchTransport {
207
208
  }
208
209
  const blocked = this.blocked.take();
209
210
  const samples = this.takeSamples();
210
- const { requests, exceptions, jobs, logs } = BatchTransport.split(batch);
211
+ const { requests, exceptions, jobs, logs, dumps } = BatchTransport.split(batch);
211
212
  try {
212
213
  const answer = await this.hub.request('POST', '/api/ingest', {
213
- body: { project: this.project, instance: this.instance, requests, exceptions, jobs, logs, samples, blocked: blocked.map(([, entry]) => entry) }
214
+ body: { project: this.project, instance: this.instance, requests, exceptions, jobs, logs, dumps, samples, blocked: blocked.map(([, entry]) => entry) }
214
215
  });
215
216
  if (exceptions.length && answer.acceptedExceptions === undefined) this._noteOutdatedHub();
216
217
  else if ((jobs.length || logs.length) && answer.acceptedJobs === undefined) this._noteOutdatedHub();
218
+ else if (dumps.length && answer.acceptedDumps === undefined) this._noteOutdatedHub();
217
219
  return true;
218
220
  } catch (error) {
219
221
  if (error.code === 'NOT_HUB') {
@@ -238,7 +240,7 @@ export class LocalTransport extends BatchTransport {
238
240
  }
239
241
 
240
242
  // A hub started before this version keeps running until it is restarted, and ignores what it
241
- // doesn't know (exceptions, job runs, log lines).
243
+ // doesn't know (exceptions, job runs, log lines, dumps).
242
244
  _noteOutdatedHub() {
243
245
  if (this.outdatedHubNoted) return;
244
246
  this.outdatedHubNoted = true;