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,218 @@
1
+ /**
2
+ * node-postgres (pg 8) queries on the request's timeline. Knex, Sequelize, TypeORM, Drizzle,
3
+ * Kysely and Prisma's pg adapter all send their queries through it.
4
+ *
5
+ * - Client.prototype.query starts the span, for every calling form: text, a config object, a
6
+ * Submittable (pg.Query, pg-cursor, pg-query-stream), with a callback or a promise. It returns
7
+ * exactly what pg returns: the promise pg created, the Submittable, or undefined.
8
+ * - The span ends when the query does, in Query#handleReadyForQuery / Query#handleError: the
9
+ * methods pg's client calls on every query, whatever form it was sent in. Nothing is added to the
10
+ * promise the app gets, and no listener to the query.
11
+ * - To know which span a query belongs to, Query.prototype gets a `callback` accessor: the Query
12
+ * constructor assigns `this.callback`, inside client.query(), and the accessor turns that into
13
+ * the same own property a plain assignment would create, while noting the span.
14
+ * - A pool hands a released client to the next waiting connect() from inside another request's
15
+ * handling; queued callbacks are bound to the context they were queued in, so each query lands
16
+ * on its own request's timeline. pool.query() also reads the caller, which a pooled query run
17
+ * from a callback can no longer find on the stack.
18
+ */
19
+
20
+ import { AsyncResource } from 'async_hooks';
21
+ import { Caller } from '../timeline/caller.js';
22
+ import { Timeline } from '../timeline/timeline.js';
23
+ import { Operation } from './operation.js';
24
+ import { ownerOf, patchMethod } from './patch.js';
25
+
26
+ const kSpan = Symbol('peekInside.querySpan');
27
+ const kAccessor = Symbol('peekInside.callbackAccessor');
28
+ // The span of the query being created inside client.query(), for the callback accessor.
29
+ let pending = null;
30
+
31
+ /**
32
+ * Patches a pg module.
33
+ */
34
+ export class PgInstrumentation {
35
+ /**
36
+ * @param {*} pg - What require('pg') returns
37
+ * @returns {boolean} Whether it was recognized and patched
38
+ */
39
+ static patch(pg) {
40
+ try {
41
+ const Client = pg && pg.Client;
42
+ if (typeof Client !== 'function' || !Client.prototype || typeof Client.prototype.query !== 'function') return false;
43
+ const Query = pg.Query || Client.Query;
44
+ if (typeof Query === 'function') patchQuery(Query.prototype);
45
+ patchClient(ownerOf(Client.prototype, 'query'), Query);
46
+ if (typeof pg.Pool === 'function') patchPool(pg.Pool.prototype);
47
+ return true;
48
+ } catch (error) {
49
+ return false;
50
+ }
51
+ }
52
+ }
53
+
54
+ function patchClient(owner, Query) {
55
+ patchMethod(owner, 'query', (original) => function query(config, values, callback) {
56
+ const timeline = Timeline.current();
57
+ if (!timeline || config === null || config === undefined || !timeline.settings.captureQueries) {
58
+ return original.apply(this, arguments);
59
+ }
60
+
61
+ let text;
62
+ let params;
63
+ try {
64
+ if (typeof config === 'string') {
65
+ text = config;
66
+ params = Array.isArray(values) ? values : undefined;
67
+ } else if (typeof config === 'object') {
68
+ // pg-query-stream keeps its statement on the cursor it wraps.
69
+ const source = typeof config.text !== 'string' && config.cursor && typeof config.cursor.text === 'string' ? config.cursor : config;
70
+ text = source.text;
71
+ params = Array.isArray(source.values) ? source.values : Array.isArray(values) ? values : undefined;
72
+ }
73
+ } catch (error) {
74
+ text = undefined;
75
+ }
76
+ if (typeof text !== 'string') return original.apply(this, arguments);
77
+
78
+ const span = timeline.query({ system: 'postgresql', client: 'pg', text, params, operation: Operation.noteQuery() });
79
+ if (!span) return original.apply(this, arguments);
80
+
81
+ const submittable = typeof config === 'object' && typeof config.submit === 'function';
82
+ const previous = pending;
83
+ pending = submittable ? null : span;
84
+ let returned = false;
85
+ try {
86
+ const result = original.apply(this, arguments);
87
+ returned = true;
88
+ return result;
89
+ } finally {
90
+ // The accessor took the span if a Query was created; otherwise it is still here.
91
+ const untaken = pending === span;
92
+ pending = previous;
93
+ if (!returned) {
94
+ span.cancel();
95
+ } else if (submittable) {
96
+ attach(config, span, Query);
97
+ } else if (untaken) {
98
+ // A pg version whose Query doesn't assign callback in its constructor: no way to see the
99
+ // end, so the span stays open (its duration is null).
100
+ }
101
+ }
102
+ });
103
+ }
104
+
105
+ // Query.prototype: the end of every query, and the accessor that tags new queries with their span.
106
+ function patchQuery(proto) {
107
+ if (!proto || proto[kAccessor]) return;
108
+ const existing = Object.getOwnPropertyDescriptor(proto, 'callback');
109
+ if (!existing) {
110
+ Object.defineProperty(proto, 'callback', {
111
+ configurable: true,
112
+ enumerable: false,
113
+ get() {
114
+ return undefined;
115
+ },
116
+ set(value) {
117
+ Object.defineProperty(this, 'callback', { value, writable: true, enumerable: true, configurable: true });
118
+ if (pending && !this[kSpan]) {
119
+ Object.defineProperty(this, kSpan, { value: pending, writable: true, configurable: true });
120
+ pending = null;
121
+ }
122
+ }
123
+ });
124
+ }
125
+ Object.defineProperty(proto, kAccessor, { value: true });
126
+
127
+ patchMethod(proto, 'handleReadyForQuery', (original) => function handleReadyForQuery(con) {
128
+ const span = this[kSpan];
129
+ // A query canceled by an error ends in handleError, which this calls.
130
+ if (span && !this._canceledDueToError) {
131
+ this[kSpan] = null;
132
+ span.end({ rows: rowsOf(this._results) });
133
+ }
134
+ return original.apply(this, arguments);
135
+ });
136
+ patchMethod(proto, 'handleError', (original) => function handleError(err, connection) {
137
+ const span = this[kSpan];
138
+ if (span) {
139
+ this[kSpan] = null;
140
+ span.end({ error: this._canceledDueToError || err });
141
+ }
142
+ return original.apply(this, arguments);
143
+ });
144
+ }
145
+
146
+ // A Submittable the app created: pg.Query instances end through the patched prototype; other
147
+ // classes (pg-cursor, pg-query-stream) through their own handlers, wrapped on the instance.
148
+ function attach(submittable, span, Query) {
149
+ try {
150
+ Object.defineProperty(submittable, kSpan, { value: span, writable: true, configurable: true });
151
+ if (typeof Query === 'function' && submittable instanceof Query) return;
152
+ for (const name of ['handleReadyForQuery', 'handleError']) {
153
+ const handler = submittable[name];
154
+ if (typeof handler !== 'function') continue;
155
+ submittable[name] = function () {
156
+ const current = this[kSpan];
157
+ if (current) {
158
+ this[kSpan] = null;
159
+ // A cursor reads in batches: its last result holds only the last batch, so the row
160
+ // count isn't known.
161
+ current.end(name === 'handleError' ? { error: arguments[0] } : {});
162
+ }
163
+ return handler.apply(this, arguments);
164
+ };
165
+ }
166
+ } catch (error) {
167
+ span.end();
168
+ }
169
+ }
170
+
171
+ // pg-pool: queued callbacks run in the context they were queued in; pool.query() reads the caller.
172
+ function patchPool(proto) {
173
+ // Bound whatever the caller's context: otherwise a callback queued outside any request (or by a
174
+ // request that has already answered) runs in the context of the request that freed the client.
175
+ patchMethod(ownerOf(proto, 'connect'), 'connect', (original) => function connect(cb) {
176
+ if (typeof cb === 'function') {
177
+ return original.call(this, AsyncResource.bind(cb));
178
+ }
179
+ return original.apply(this, arguments);
180
+ });
181
+ patchMethod(ownerOf(proto, 'query'), 'query', (original) => function query(text, values, cb) {
182
+ const timeline = Timeline.current();
183
+ if (!timeline || !timeline.settings.captureQueries) return original.apply(this, arguments);
184
+ let sql;
185
+ try {
186
+ sql = typeof text === 'string' ? text : text && typeof text === 'object' ? text.text : undefined;
187
+ } catch (error) {
188
+ sql = undefined;
189
+ }
190
+ if (typeof sql !== 'string') return original.apply(this, arguments);
191
+ const key = Timeline.sqlFingerprint(sql, 'postgresql');
192
+ // Inside a Prisma operation, the caller is the line that made the Prisma call, when known.
193
+ if (!timeline.needsCaller(key) || Operation.caller()) return original.apply(this, arguments);
194
+ const caller = timeline.settings.caller.capture();
195
+ const args = arguments;
196
+ return Caller.withHint({ key, caller }, () => original.apply(this, args));
197
+ });
198
+ }
199
+
200
+ // Rows returned or affected: one Result, or one per statement of a multi-statement query.
201
+ function rowsOf(results) {
202
+ try {
203
+ const list = Array.isArray(results) ? results : [results];
204
+ let total = 0;
205
+ let known = false;
206
+ for (const result of list) {
207
+ if (!result) continue;
208
+ const count = Number.isInteger(result.rowCount) ? result.rowCount : Array.isArray(result.rows) ? result.rows.length : null;
209
+ if (count !== null) {
210
+ total += count;
211
+ known = true;
212
+ }
213
+ }
214
+ return known ? total : null;
215
+ } catch (error) {
216
+ return null;
217
+ }
218
+ }
@@ -0,0 +1,181 @@
1
+ /**
2
+ * Prisma Client operations on the request's timeline.
3
+ *
4
+ * Every Prisma Client call (prisma.user.findMany(), $queryRaw, ...) goes through the client's
5
+ * _request(), which gets the model, the action, the arguments and the call site Prisma captured
6
+ * where the app made the call.
7
+ *
8
+ * - With a query engine (Prisma 5 and 6 without driver adapters), the database work happens
9
+ * inside the engine, out of reach: each operation is one span, such as
10
+ * `User.findMany({ where: { id: ? }, include: { posts: ? } })`, with the arguments, masked, as
11
+ * its parameters. Raw queries ($queryRaw, $executeRaw) are recorded as their SQL and values.
12
+ * - With a driver adapter (pg, mysql2 underneath), the driver's queries are recorded by the pg and
13
+ * mysql2 integrations, with the operation that ran them ("User.findMany") and its caller (also
14
+ * for the queries Prisma runs on its own, an include's); the operation itself isn't recorded, so
15
+ * nothing is counted twice. Prisma 6 runs adapters from its WASM engine, outside the request's
16
+ * context, where the driver queries can't be told apart: an operation none of whose queries
17
+ * were seen is recorded as a whole, as with the query engine.
18
+ *
19
+ * The caller comes from Prisma's call site: Prisma runs the operation lazily, when its promise is
20
+ * awaited, by which time the app's frame is no longer on the stack. Without a call site
21
+ * (errorFormat "minimal"), the caller is unknown.
22
+ */
23
+
24
+ import { performance } from 'perf_hooks';
25
+ import { Shape } from '../timeline/shape.js';
26
+ import { Timeline } from '../timeline/timeline.js';
27
+ import { Operation } from './operation.js';
28
+ import { ownerOf, patchMethod } from './patch.js';
29
+
30
+ const SYSTEMS = { postgresql: 'postgresql', postgres: 'postgresql', cockroachdb: 'postgresql', mysql: 'mysql', sqlite: 'sqlite', sqlserver: 'sqlserver', mongodb: 'mongodb' };
31
+ const RAW = new Set(['queryRaw', 'executeRaw']);
32
+ const SINGLE = /^(findUnique|findFirst|findUniqueOrThrow|findFirstOrThrow|create|update|upsert|delete)$/;
33
+
34
+ /**
35
+ * Patches Prisma Client.
36
+ */
37
+ export class PrismaInstrumentation {
38
+ /**
39
+ * @param {*} target - A PrismaClient class, a PrismaClient instance, or the @prisma/client module
40
+ * @returns {boolean} Whether it was recognized and patched
41
+ */
42
+ static patch(target) {
43
+ try {
44
+ const proto = prototypeOf(target);
45
+ if (!proto) return false;
46
+ patchMethod(ownerOf(proto, '_request'), '_request', (original) => function _request(params) {
47
+ const timeline = Timeline.current();
48
+ if (!timeline || !timeline.settings.captureQueries || !params || typeof params !== 'object') {
49
+ return original.apply(this, arguments);
50
+ }
51
+ const action = String(params.action || '');
52
+ const name = params.model ? `${params.model}.${action}` : String(params.clientMethod || action || 'query');
53
+ const args = arguments;
54
+
55
+ // The span's description, or null if the call can't be described (it runs unrecorded).
56
+ const describe = (startedAt) => {
57
+ try {
58
+ const statement = RAW.has(action) ? rawStatement(params.args, this._activeProvider) : null;
59
+ return {
60
+ system: SYSTEMS[this._activeProvider] || 'other',
61
+ client: 'prisma',
62
+ text: statement ? statement.text : `${name}(${Shape.of(params.args)})`,
63
+ sql: Boolean(statement),
64
+ params: statement ? statement.values : params.args,
65
+ caller: () => callerOf(timeline.settings.caller, params.callsite),
66
+ startedAt
67
+ };
68
+ } catch (error) {
69
+ return null;
70
+ }
71
+ };
72
+
73
+ // A driver adapter: the driver integrations record the queries, labeled with the operation
74
+ // and with its caller.
75
+ if (usesAdapter(this)) {
76
+ const operation = { name, queries: 0, caller: params.callsite ? () => callerOf(timeline.settings.caller, params.callsite) : null };
77
+ const startedAt = performance.now();
78
+ const result = Operation.run(operation, () => original.apply(this, args));
79
+ if (!result || typeof result.then !== 'function') return result;
80
+ // None of its queries was seen: record the operation itself, from its start.
81
+ const settle = (outcome) => {
82
+ if (operation.queries > 0) return;
83
+ const description = describe(startedAt);
84
+ const span = description && timeline.query(description);
85
+ if (span) span.end(outcome);
86
+ };
87
+ return result.then(
88
+ (value) => {
89
+ settle({ rows: rowsOf(action, value) });
90
+ return value;
91
+ },
92
+ (error) => {
93
+ settle({ error });
94
+ throw error;
95
+ }
96
+ );
97
+ }
98
+
99
+ const description = describe(performance.now());
100
+ const span = description && timeline.query(description);
101
+ if (!span) return original.apply(this, arguments);
102
+
103
+ let result;
104
+ let returned = false;
105
+ try {
106
+ result = original.apply(this, arguments);
107
+ returned = true;
108
+ } finally {
109
+ if (!returned) span.cancel();
110
+ }
111
+ // Prisma's own promise, awaited by its PrismaPromise: not the one the app holds.
112
+ if (result && typeof result.then === 'function') {
113
+ return result.then(
114
+ (value) => {
115
+ span.end({ rows: rowsOf(action, value) });
116
+ return value;
117
+ },
118
+ (error) => {
119
+ span.end({ error });
120
+ throw error;
121
+ }
122
+ );
123
+ }
124
+ span.end();
125
+ return result;
126
+ });
127
+ return true;
128
+ } catch (error) {
129
+ return false;
130
+ }
131
+ }
132
+ }
133
+
134
+ // The class's prototype, from the class itself, an instance, or the @prisma/client module.
135
+ function prototypeOf(target) {
136
+ if (!target) return null;
137
+ if (typeof target === 'function' && target.prototype && typeof target.prototype._request === 'function') return target.prototype;
138
+ if (typeof target === 'object' && typeof target._request === 'function') return Object.getPrototypeOf(target);
139
+ if (typeof target === 'object' && typeof target.PrismaClient === 'function') return prototypeOf(target.PrismaClient);
140
+ return null;
141
+ }
142
+
143
+ function usesAdapter(client) {
144
+ try {
145
+ return Boolean(client._engineConfig && client._engineConfig.adapter);
146
+ } catch (error) {
147
+ return false;
148
+ }
149
+ }
150
+
151
+ // $queryRaw`...` passes a Sql object (text with $1 placeholders, sql with ?); $queryRawUnsafe
152
+ // passes [query, ...values].
153
+ function rawStatement(args, provider) {
154
+ if (Array.isArray(args)) return { text: String(args[0]), values: args.slice(1) };
155
+ if (!args || typeof args !== 'object') return null;
156
+ const pg = SYSTEMS[provider] === 'postgresql';
157
+ const text = pg ? args.text ?? args.sql : args.sql ?? args.text;
158
+ if (typeof text !== 'string') return null;
159
+ return { text, values: Array.isArray(args.values) ? args.values : undefined };
160
+ }
161
+
162
+ // The app's frame from the stack Prisma captured at the call, or from Prisma's own location.
163
+ function callerOf(caller, callsite) {
164
+ if (!callsite || typeof callsite !== 'object') return null;
165
+ const error = callsite._error;
166
+ if (error && typeof error.stack === 'string') return caller.fromStack(error.stack);
167
+ if (typeof callsite.getLocation === 'function') return caller.fromLocation(callsite.getLocation());
168
+ return null;
169
+ }
170
+
171
+ function rowsOf(action, value) {
172
+ try {
173
+ if (Array.isArray(value)) return value.length;
174
+ if (value && typeof value === 'object' && Number.isInteger(value.count) && /Many$/.test(action)) return value.count;
175
+ if (action === 'executeRaw' && Number.isInteger(value)) return value;
176
+ if (SINGLE.test(action)) return value ? 1 : 0;
177
+ } catch (error) {
178
+ // A result that can't be read: the row count is unknown.
179
+ }
180
+ return null;
181
+ }
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Records the errors an Express app handles, and the ones it leaves to Express, without changing
3
+ * how either is answered.
4
+ *
5
+ * Express routes an error (thrown by a handler, passed to next(err), or a rejected promise in
6
+ * Express 5) through the router's stack to the error-handling middleware, the ones with four
7
+ * parameters. Two places see where it ends up:
8
+ *
9
+ * - The router's Layer runs every handler. A four-parameter handler receiving the error means the
10
+ * app handles it (handled: true). Layer is reached through `router.stack[i].constructor`, and
11
+ * its error method is `handle_error` in Express 4 and `handleError` in Express 5 (the "router"
12
+ * package). The patch is on the prototype, so routers created with express.Router() are covered
13
+ * too, and it only observes: the original runs with the same arguments.
14
+ * - When no handler is left, the router calls the `done` callback that app.handle() gave it:
15
+ * Express's final handler, which answers 500 (handled: false). app.handle() is wrapped to tell
16
+ * when it uses the final handler (no callback: the app isn't mounted inside another one), and
17
+ * router.handle() to wrap that callback.
18
+ *
19
+ * An error middleware may log an error and pass it on with next(err): the record stays open until
20
+ * the error reaches the final handler (unhandled) or the request ends (handled). Every step is
21
+ * feature-detected; an Express version that doesn't fit is left alone.
22
+ */
23
+
24
+ import { RequestContext } from '../exceptions/request-context.js';
25
+
26
+ const kInstalled = Symbol('peekInside.expressErrors');
27
+ const kLayerPatched = Symbol('peekInside.layerPatched');
28
+
29
+ /**
30
+ * Installs the error hooks on Express apps (4 and 5).
31
+ */
32
+ export class ExpressErrorHooks {
33
+ /**
34
+ * Hooks into an app's router, once per app. Does nothing for apps without an Express router
35
+ * (Connect) or before the app has one.
36
+ *
37
+ * @param {Function} app - Express app
38
+ * @returns {boolean} Whether the hooks are installed
39
+ */
40
+ static install(app) {
41
+ try {
42
+ if (!app || (typeof app !== 'function' && typeof app !== 'object')) return false;
43
+ if (app[kInstalled]) return true;
44
+ const router = routerOf(app);
45
+ if (!router || typeof router.handle !== 'function' || typeof app.handle !== 'function') return false;
46
+
47
+ Object.defineProperty(app, kInstalled, { value: true, configurable: true });
48
+ wrapFinalHandler(app, router);
49
+ patchLayers(router);
50
+ return true;
51
+ } catch (error) {
52
+ return false;
53
+ }
54
+ }
55
+ }
56
+
57
+ // Express 4 keeps the router in app._router (app.router throws there); Express 5 creates it on
58
+ // first access of app.router.
59
+ function routerOf(app) {
60
+ if (app._router) return app._router;
61
+ try {
62
+ return app.router || null;
63
+ } catch (error) {
64
+ return null;
65
+ }
66
+ }
67
+
68
+ function wrapFinalHandler(app, router) {
69
+ // The request app.handle() is dispatching with Express's final handler. router.handle() runs
70
+ // synchronously inside app.handle(), so one variable is enough.
71
+ let finalDispatch = null;
72
+
73
+ const originalAppHandle = app.handle;
74
+ app.handle = function handle(req, res, callback) {
75
+ if (typeof callback === 'function') return originalAppHandle.apply(this, arguments);
76
+ const previous = finalDispatch;
77
+ finalDispatch = req;
78
+ try {
79
+ return originalAppHandle.apply(this, arguments);
80
+ } finally {
81
+ finalDispatch = previous;
82
+ }
83
+ };
84
+
85
+ const originalRouterHandle = router.handle;
86
+ router.handle = function handle(req, res, out) {
87
+ if (finalDispatch !== req || typeof out !== 'function') {
88
+ return originalRouterHandle.apply(this, arguments);
89
+ }
90
+ const finalHandler = out;
91
+ return originalRouterHandle.call(this, req, res, function done(error) {
92
+ if (error && error !== 'route' && error !== 'router') noteError(req, error, false);
93
+ return finalHandler.apply(this, arguments);
94
+ });
95
+ };
96
+ }
97
+
98
+ function patchLayers(router) {
99
+ const stack = Array.isArray(router.stack) ? router.stack : [];
100
+ for (const layer of stack) {
101
+ const proto = layer && layer.constructor && layer.constructor.prototype;
102
+ if (proto && patchLayer(proto)) return;
103
+ }
104
+ }
105
+
106
+ // Observes four-parameter handlers receiving an error.
107
+ function patchLayer(proto) {
108
+ if (proto[kLayerPatched]) return true;
109
+ const method = typeof proto.handle_error === 'function' ? 'handle_error' : typeof proto.handleError === 'function' ? 'handleError' : null;
110
+ if (!method) return false;
111
+
112
+ const original = proto[method];
113
+ const patched = {
114
+ [method](error, req, res, next) {
115
+ try {
116
+ if (error && typeof this.handle === 'function' && this.handle.length === 4) noteError(req, error, true);
117
+ } catch (failure) {
118
+ // Never in the way of the app's error handling.
119
+ }
120
+ return original.apply(this, arguments);
121
+ }
122
+ }[method];
123
+
124
+ Object.defineProperty(proto, kLayerPatched, { value: true, configurable: true });
125
+ proto[method] = patched;
126
+ return true;
127
+ }
128
+
129
+ // Handled errors stay open (an error middleware may still pass them on); unhandled ones are final.
130
+ function noteError(req, error, handled) {
131
+ const context = RequestContext.of(req);
132
+ if (context && context.capture) {
133
+ context.capture.frameworkError(context, error, { mechanism: 'express', handled, open: handled });
134
+ }
135
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Express integration, which also covers Connect and other apps built on use() middleware.
3
+ */
4
+
5
+ import { ExpressErrorHooks } from './express-errors.js';
6
+ import { FrameworkAdapter } from './framework-adapter.js';
7
+
8
+ /**
9
+ * Records requests through a middleware, and the errors Express routes (see express-errors.js).
10
+ */
11
+ export class ExpressAdapter extends FrameworkAdapter {
12
+ static framework = 'express';
13
+
14
+ /**
15
+ * @param {*} app
16
+ * @returns {boolean} Whether `app` takes middleware through use()
17
+ */
18
+ static matches(app) {
19
+ return Boolean(app) && typeof app.use === 'function';
20
+ }
21
+
22
+ /**
23
+ * Middleware that records each exchange. Its place in the stack doesn't matter: request bodies
24
+ * are captured as the app reads them, or taken from req.body when a parser ran first. The rest
25
+ * of the request runs inside its context, so exceptions are linked to it.
26
+ *
27
+ * Used on its own (expressMiddleware()), it installs the error hooks on the app of the first
28
+ * request it sees; errors that reach Express's final handler are recorded from the next request
29
+ * on.
30
+ *
31
+ * @returns {Function} Middleware `(req, res, next)`
32
+ */
33
+ middleware() {
34
+ return (req, res, next) => {
35
+ if (this.peekInside.options.captureExceptions !== false && req && req.app) {
36
+ ExpressErrorHooks.install(req.app);
37
+ }
38
+ return this.peekInside._intercept(req, res, next, 'express');
39
+ };
40
+ }
41
+
42
+ /**
43
+ * Adds the middleware, hooks into the app's error handling, and watches app.listen() for the
44
+ * app's server.
45
+ *
46
+ * @param {Object} app - Express or Connect app
47
+ * @returns {void}
48
+ */
49
+ attach(app) {
50
+ app.use(this.middleware());
51
+ // app.use() has created the router the hooks need.
52
+ if (this.peekInside.options.captureExceptions !== false) {
53
+ ExpressErrorHooks.install(app);
54
+ }
55
+ this.servers.patchListen(app);
56
+ }
57
+ }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Fastify integration.
3
+ */
4
+
5
+ import { ExchangeRecorder } from '../capture/exchange-recorder.js';
6
+ import { RequestContext } from '../exceptions/request-context.js';
7
+ import { FrameworkAdapter } from './framework-adapter.js';
8
+
9
+ /**
10
+ * Records requests from a Fastify onRequest hook, and errors from an onError hook.
11
+ *
12
+ * onRequest runs before Fastify reads the body, so bodies are captured as Fastify parses them.
13
+ * Hooks must be added before the app is ready, so attach before fastify.listen().
14
+ */
15
+ export class FastifyAdapter extends FrameworkAdapter {
16
+ static framework = 'fastify';
17
+
18
+ /**
19
+ * @param {*} app
20
+ * @returns {boolean} Whether `app` looks like a Fastify instance
21
+ */
22
+ static matches(app) {
23
+ return Boolean(app) && typeof app.addHook === 'function' && typeof app.decorate === 'function';
24
+ }
25
+
26
+ /**
27
+ * Adds the hooks, and binds the Fastify server to learn the app's port.
28
+ *
29
+ * The rest of each request (Fastify's done()) runs inside the request's context. onError runs
30
+ * before Fastify's error handler and can't change the response: an error counts as handled when
31
+ * the app set its own error handler (setErrorHandler()).
32
+ *
33
+ * @param {Object} fastify - Fastify instance
34
+ * @returns {void}
35
+ */
36
+ attach(fastify) {
37
+ fastify.addHook('onRequest', (request, reply, done) => {
38
+ ExchangeRecorder.setBodySource(request.raw, () => request.body);
39
+ this.peekInside._intercept(request.raw, reply.raw, done, 'fastify');
40
+ });
41
+ if (this.peekInside.options.captureExceptions !== false) {
42
+ fastify.addHook('onError', (request, reply, error, done) => {
43
+ FastifyAdapter._noteError(request, error);
44
+ done();
45
+ });
46
+ }
47
+ if (fastify.server) {
48
+ this.servers.bind(fastify.server);
49
+ }
50
+ }
51
+
52
+ static _noteError(request, error) {
53
+ try {
54
+ const context = RequestContext.of(request.raw);
55
+ if (context && context.capture) {
56
+ context.capture.frameworkError(context, error, { mechanism: 'fastify', handled: FastifyAdapter._hasErrorHandler(request) });
57
+ }
58
+ } catch (failure) {
59
+ // Recording must never change the error the app sees.
60
+ }
61
+ }
62
+
63
+ // Fastify's own handler is named defaultErrorHandler; anything else was set by the app.
64
+ static _hasErrorHandler(request) {
65
+ try {
66
+ const handler = request.server && request.server.errorHandler;
67
+ return typeof handler === 'function' && handler.name !== 'defaultErrorHandler';
68
+ } catch (error) {
69
+ return false;
70
+ }
71
+ }
72
+ }