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,221 @@
1
+ /**
2
+ * MongoDB driver (5.x and 6.x) operations on the request's timeline, and so Mongoose's (7 and 8).
3
+ *
4
+ * The hook is the driver's internal Connection#command, which every operation goes through
5
+ * (finds, aggregations, writes, cursor batches), whatever API the app used. Commands the driver
6
+ * sends for itself (handshakes, authentication, session cleanup) are left out.
7
+ *
8
+ * Each command becomes a statement such as `orders.find({ customerId: ?, status: { $in: ? } })`:
9
+ * the collection, the command and the shape of its main argument, without values. The values
10
+ * themselves (filter, update, documents, pipeline...) are the span's parameters, masked by key.
11
+ *
12
+ * In driver 6 the command is async and the request's context follows its promise chain. In driver
13
+ * 5 it takes a callback, and the pool's wait queue hands connections out from other requests'
14
+ * callbacks: queued checkOut callbacks are bound to the context they were queued in.
15
+ */
16
+
17
+ import { AsyncResource } from 'async_hooks';
18
+ import { Shape } from '../timeline/shape.js';
19
+ import { Timeline } from '../timeline/timeline.js';
20
+ import { Operation } from './operation.js';
21
+ import { ownerOf, patchMethod } from './patch.js';
22
+
23
+ // Commands the driver runs for itself, never for the app's queries.
24
+ const INTERNAL = new Set([
25
+ 'hello', 'ismaster', 'saslstart', 'saslcontinue', 'authenticate', 'getnonce', 'logout', 'endsessions',
26
+ 'killcursors', 'buildinfo', 'ping', 'getparameter', 'serverstatus', 'connectionstatus', 'getlasterror'
27
+ ]);
28
+ // The fields of each command that carry the app's values.
29
+ const VALUE_FIELDS = ['filter', 'query', 'update', 'updates', 'deletes', 'documents', 'pipeline', 'key', 'sort', 'projection', 'limit', 'skip'];
30
+
31
+ /**
32
+ * Patches a MongoDB driver.
33
+ */
34
+ export class MongoInstrumentation {
35
+ /**
36
+ * @param {{ Connection: Function, ConnectionPool?: Function }} internals - The driver's internal
37
+ * classes (lib/cmap/connection.js, lib/cmap/connection_pool.js)
38
+ * @returns {boolean} Whether they were recognized and patched
39
+ */
40
+ static patch({ Connection, ConnectionPool }) {
41
+ try {
42
+ if (typeof Connection !== 'function' || typeof Connection.prototype.command !== 'function') return false;
43
+ patchCommand(ownerOf(Connection.prototype, 'command'));
44
+ if (typeof ConnectionPool === 'function') patchPool(ConnectionPool.prototype);
45
+ return true;
46
+ } catch (error) {
47
+ return false;
48
+ }
49
+ }
50
+
51
+ /**
52
+ * The statement text and parameters of a command, or null for the driver's own commands.
53
+ *
54
+ * @param {{ db?: string, collection?: string }} ns - The command's namespace
55
+ * @param {Object} command - The command document, such as { find: 'orders', filter: {...} }
56
+ * @returns {{ text: string, params: Object|undefined }|null}
57
+ */
58
+ static describe(ns, command) {
59
+ if (!command || typeof command !== 'object') return null;
60
+ const name = Object.keys(command)[0];
61
+ if (!name || INTERNAL.has(name.toLowerCase())) return null;
62
+ const target = command[name];
63
+ // Database commands go to the "$cmd" collection: they are named after the database.
64
+ const nsCollection = ns && ns.collection && ns.collection !== '$cmd' ? ns.collection : null;
65
+ const collection = typeof target === 'string' ? target
66
+ : typeof command.collection === 'string' ? command.collection
67
+ : nsCollection || (ns && ns.db) || 'db';
68
+
69
+ let shape = '';
70
+ switch (name) {
71
+ case 'find':
72
+ shape = Shape.of(command.filter || {});
73
+ break;
74
+ case 'aggregate':
75
+ shape = Shape.of(command.pipeline || []);
76
+ break;
77
+ case 'count':
78
+ case 'distinct':
79
+ shape = Shape.of(command.query || {});
80
+ break;
81
+ case 'insert':
82
+ shape = Shape.of(Array.isArray(command.documents) && command.documents.length ? command.documents[0] : {});
83
+ break;
84
+ case 'update': {
85
+ const first = Array.isArray(command.updates) ? command.updates[0] : null;
86
+ shape = Shape.of({ q: (first && first.q) || {}, u: (first && first.u) || {} });
87
+ break;
88
+ }
89
+ case 'delete': {
90
+ const first = Array.isArray(command.deletes) ? command.deletes[0] : null;
91
+ shape = Shape.of((first && first.q) || {});
92
+ break;
93
+ }
94
+ case 'findAndModify':
95
+ shape = Shape.of({ query: command.query || {}, update: command.update || {} });
96
+ break;
97
+ default:
98
+ break;
99
+ }
100
+
101
+ let params;
102
+ for (const field of VALUE_FIELDS) {
103
+ // { update: 'orders', updates: [...] }: the command's own key names the collection.
104
+ if (field === name || command[field] === undefined) continue;
105
+ params = params || {};
106
+ params[field] = command[field];
107
+ }
108
+ return { text: `${collection}.${name}(${shape})`, params };
109
+ }
110
+ }
111
+
112
+ function patchCommand(owner) {
113
+ patchMethod(owner, 'command', (original) => {
114
+ // Driver 6's command is async and its last parameter is a response class (a function too);
115
+ // driver 5's takes a callback. The method's kind says which, not the argument's type.
116
+ const callbackStyle = original.constructor.name !== 'AsyncFunction';
117
+ return commandWrapper(original, callbackStyle);
118
+ });
119
+ }
120
+
121
+ function commandWrapper(original, callbackStyle) {
122
+ return function command(ns, cmd) {
123
+ const timeline = Timeline.current();
124
+ if (!timeline || !timeline.settings.captureQueries) return original.apply(this, arguments);
125
+ let described;
126
+ try {
127
+ described = MongoInstrumentation.describe(ns, cmd);
128
+ } catch (error) {
129
+ described = null;
130
+ }
131
+ if (!described) return original.apply(this, arguments);
132
+
133
+ const span = timeline.query({ system: 'mongodb', client: 'mongodb', text: described.text, sql: false, params: described.params, operation: Operation.noteQuery() });
134
+ if (!span) return original.apply(this, arguments);
135
+
136
+ // Driver 5: command(ns, cmd, options, callback)
137
+ const last = callbackStyle ? arguments[arguments.length - 1] : undefined;
138
+ let args = arguments;
139
+ if (typeof last === 'function') {
140
+ args = Array.prototype.slice.call(arguments);
141
+ args[args.length - 1] = function (err, reply) {
142
+ span.end(err ? { error: err } : outcomeOf(reply));
143
+ return last.apply(this, arguments);
144
+ };
145
+ }
146
+
147
+ let result;
148
+ let returned = false;
149
+ try {
150
+ result = original.apply(this, args);
151
+ returned = true;
152
+ } finally {
153
+ if (!returned) span.cancel();
154
+ }
155
+ // Driver 6: an internal promise the driver awaits; the app never sees it.
156
+ if (result && typeof result.then === 'function' && typeof last !== 'function') {
157
+ return result.then(
158
+ (reply) => {
159
+ span.end(outcomeOf(reply));
160
+ return reply;
161
+ },
162
+ (error) => {
163
+ span.end({ error });
164
+ throw error;
165
+ }
166
+ );
167
+ }
168
+ return result;
169
+ };
170
+ }
171
+
172
+ // Driver 5's pool queues checkOut callbacks; bind them to the context they were queued in.
173
+ function patchPool(proto) {
174
+ const owner = ownerOf(proto, 'checkOut');
175
+ if (!owner) return;
176
+ patchMethod(owner, 'checkOut', (original) => function checkOut(callback) {
177
+ if (typeof callback === 'function') {
178
+ const args = Array.prototype.slice.call(arguments);
179
+ args[0] = AsyncResource.bind(callback);
180
+ return original.apply(this, args);
181
+ }
182
+ return original.apply(this, arguments);
183
+ });
184
+ }
185
+
186
+ // What a reply says: documents returned (a cursor's batch) or matched / written (n), and the
187
+ // first write error. A write that hit a duplicate key answers ok: 1 with writeErrors.
188
+ function outcomeOf(reply) {
189
+ try {
190
+ if (!reply || typeof reply !== 'object') return {};
191
+ // Driver 6 cursor replies are read lazily; the batch size is cheap.
192
+ if (typeof reply.batchSize === 'number') return { rows: reply.batchSize };
193
+ // Other driver 6 replies are lazy BSON too; write replies are small.
194
+ const object = typeof reply.toObject === 'function' ? reply.toObject() : reply;
195
+ const failure = firstWriteError(object);
196
+ const cursor = object.cursor;
197
+ let rows = null;
198
+ if (cursor && typeof cursor === 'object') {
199
+ const batch = cursor.firstBatch || cursor.nextBatch;
200
+ if (Array.isArray(batch)) rows = batch.length;
201
+ } else if (Number.isInteger(object.n)) {
202
+ rows = object.n;
203
+ } else if (Array.isArray(object.values)) {
204
+ rows = object.values.length;
205
+ } else if ('value' in object) {
206
+ rows = object.value ? 1 : 0;
207
+ }
208
+ return failure ? { rows, error: failure } : { rows };
209
+ } catch (error) {
210
+ return {};
211
+ }
212
+ }
213
+
214
+ function firstWriteError(reply) {
215
+ const writeError = Array.isArray(reply.writeErrors) && reply.writeErrors[0];
216
+ const concernError = reply.writeConcernError;
217
+ const error = writeError || concernError;
218
+ if (!error || typeof error !== 'object') return null;
219
+ const message = String(error.errmsg || error.message || 'write error');
220
+ return error.code !== undefined ? `${error.code}: ${message}` : message;
221
+ }
@@ -0,0 +1,161 @@
1
+ /**
2
+ * mysql2 (3.x) queries on the request's timeline, including mysql2/promise, which runs on the same
3
+ * core connections. Knex, Sequelize, TypeORM, Drizzle, Kysely and Prisma's MariaDB/MySQL adapters
4
+ * send their queries through it.
5
+ *
6
+ * - Connection.prototype.query and .execute start the span. The statement is recorded with its
7
+ * placeholders and values (mysql2 formats query() values into the SQL client side; the timeline
8
+ * keeps them apart, so values can be masked by column).
9
+ * - The span ends with the callback (mysql2/promise always passes one), or, in streaming mode, with
10
+ * the command's 'end' or 'error' event, seen by wrapping the command's emit(): no listener is
11
+ * added, so an unhandled 'error' still behaves as it would.
12
+ * - Pool.getConnection callbacks run in the context they were queued in, and pool.query() /
13
+ * pool.execute() read the caller (the pooled query runs from a callback, on a later tick).
14
+ */
15
+
16
+ import { AsyncResource } from 'async_hooks';
17
+ import { Caller } from '../timeline/caller.js';
18
+ import { Timeline } from '../timeline/timeline.js';
19
+ import { Operation } from './operation.js';
20
+ import { ownerOf, patchMethod } from './patch.js';
21
+
22
+ /**
23
+ * Patches a mysql2 module.
24
+ */
25
+ export class Mysql2Instrumentation {
26
+ /**
27
+ * @param {*} mysql2 - What require('mysql2') returns
28
+ * @returns {boolean} Whether it was recognized and patched
29
+ */
30
+ static patch(mysql2) {
31
+ try {
32
+ const Connection = mysql2 && mysql2.Connection;
33
+ // The core Connection queues commands; mysql2/promise's Connection only wraps one.
34
+ if (typeof Connection !== 'function' || !Connection.prototype || typeof Connection.prototype.execute !== 'function' || typeof Connection.prototype.addCommand !== 'function') return false;
35
+ for (const method of ['query', 'execute']) {
36
+ patchMethod(ownerOf(Connection.prototype, method), method, (original) => function (sql, values, cb) {
37
+ return run(this, original, arguments);
38
+ });
39
+ }
40
+ if (typeof mysql2.Pool === 'function') patchPool(mysql2.Pool.prototype);
41
+ return true;
42
+ } catch (error) {
43
+ return false;
44
+ }
45
+ }
46
+ }
47
+
48
+ function run(connection, original, args) {
49
+ const timeline = Timeline.current();
50
+ if (!timeline || !timeline.settings.captureQueries) return original.apply(connection, args);
51
+
52
+ const sql = args[0];
53
+ let text;
54
+ let params;
55
+ try {
56
+ if (sql && typeof sql === 'object') {
57
+ text = sql.sql;
58
+ params = sql.values !== undefined ? sql.values : typeof args[1] !== 'function' ? args[1] : undefined;
59
+ } else {
60
+ text = sql;
61
+ params = typeof args[1] === 'function' ? undefined : args[1];
62
+ }
63
+ } catch (error) {
64
+ text = undefined;
65
+ }
66
+ if (typeof text !== 'string') return original.apply(connection, args);
67
+
68
+ const span = timeline.query({ system: 'mysql', client: 'mysql2', text, params, operation: Operation.noteQuery() });
69
+ if (!span) return original.apply(connection, args);
70
+
71
+ const finish = (err, rows) => span.end(err ? { error: err } : { rows: rowsOf(rows) });
72
+ const callArgs = Array.prototype.slice.call(args);
73
+ let observed = false;
74
+ if (sql && typeof sql === 'object' && typeof sql.onResult === 'function') {
75
+ // A command built elsewhere (pool.query builds it, then hands it to a connection).
76
+ const onResult = sql.onResult;
77
+ sql.onResult = function (err, rows, fields) {
78
+ finish(err, rows);
79
+ return onResult.apply(this, arguments);
80
+ };
81
+ observed = true;
82
+ } else {
83
+ for (let i = 1; i < 3 && i < callArgs.length; i++) {
84
+ if (typeof callArgs[i] !== 'function') continue;
85
+ const callback = callArgs[i];
86
+ callArgs[i] = function (err, rows, fields) {
87
+ finish(err, rows);
88
+ return callback.apply(this, arguments);
89
+ };
90
+ observed = true;
91
+ break;
92
+ }
93
+ }
94
+
95
+ let command;
96
+ let returned = false;
97
+ try {
98
+ command = original.apply(connection, callArgs);
99
+ returned = true;
100
+ } finally {
101
+ if (!returned) span.cancel();
102
+ }
103
+ if (!observed) observeEvents(command, span);
104
+ return command;
105
+ }
106
+
107
+ // Streaming mode: the command emits 'result' per row, then 'end' (or 'error').
108
+ function observeEvents(command, span) {
109
+ if (!command || typeof command.emit !== 'function') {
110
+ span.end();
111
+ return;
112
+ }
113
+ const emit = command.emit;
114
+ let rows = 0;
115
+ command.emit = function (event, value) {
116
+ if (event === 'result') rows++;
117
+ else if (event === 'end') span.end({ rows });
118
+ else if (event === 'error') span.end({ error: value });
119
+ return emit.apply(this, arguments);
120
+ };
121
+ }
122
+
123
+ function patchPool(proto) {
124
+ // Bound whatever the caller's context (see the pg integration).
125
+ patchMethod(ownerOf(proto, 'getConnection'), 'getConnection', (original) => function getConnection(cb) {
126
+ if (typeof cb === 'function') {
127
+ return original.call(this, AsyncResource.bind(cb));
128
+ }
129
+ return original.apply(this, arguments);
130
+ });
131
+ for (const method of ['query', 'execute']) {
132
+ patchMethod(ownerOf(proto, method), method, (original) => function (sql, values, cb) {
133
+ const timeline = Timeline.current();
134
+ if (!timeline || !timeline.settings.captureQueries) return original.apply(this, arguments);
135
+ let text;
136
+ try {
137
+ text = sql && typeof sql === 'object' ? sql.sql : sql;
138
+ } catch (error) {
139
+ text = undefined;
140
+ }
141
+ if (typeof text !== 'string') return original.apply(this, arguments);
142
+ const key = Timeline.sqlFingerprint(text, 'mysql');
143
+ // Inside a Prisma operation, the caller is the line that made the Prisma call, when known.
144
+ if (!timeline.needsCaller(key) || Operation.caller()) return original.apply(this, arguments);
145
+ const caller = timeline.settings.caller.capture();
146
+ const args = arguments;
147
+ return Caller.withHint({ key, caller }, () => original.apply(this, args));
148
+ });
149
+ }
150
+ }
151
+
152
+ // Rows returned (a SELECT's rows) or affected (a ResultSetHeader's affectedRows).
153
+ function rowsOf(rows) {
154
+ try {
155
+ if (Array.isArray(rows)) return rows.length;
156
+ if (rows && Number.isInteger(rows.affectedRows)) return rows.affectedRows;
157
+ } catch (error) {
158
+ // Unreadable: unknown.
159
+ }
160
+ return null;
161
+ }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * The ORM operation the code running now belongs to ("User.findMany"), so the database queries a
3
+ * Prisma call runs through a driver adapter (pg, mysql2) are recorded with the call that ran them,
4
+ * and with the app's line that made it: Prisma runs some of them on its own (an include's), where
5
+ * the app's frame isn't on the stack.
6
+ */
7
+
8
+ import { AsyncLocalStorage } from 'async_hooks';
9
+
10
+ const operations = new AsyncLocalStorage();
11
+
12
+ /**
13
+ * Carries the current ORM operation through the async work it causes, and counts the driver
14
+ * queries recorded for it: an operation whose queries can't be seen (Prisma 6's engine runs its
15
+ * adapter outside the request's context) is recorded as a whole instead.
16
+ */
17
+ export class Operation {
18
+ /**
19
+ * Runs `fn` as part of `operation`.
20
+ *
21
+ * @param {{ name: string, queries: number, caller?: Function|null }} operation - Such as
22
+ * { name: "User.findMany", queries: 0 }; `caller`, when the ORM captured its call site,
23
+ * returns the app's frame that made the call (a CallerInfo, or null)
24
+ * @param {Function} fn
25
+ * @returns {*} What fn returns
26
+ */
27
+ static run(operation, fn) {
28
+ return operations.run(operation, fn);
29
+ }
30
+
31
+ /**
32
+ * @returns {{ name: string, queries: number }|undefined} The operation the code running now
33
+ * belongs to
34
+ */
35
+ static current() {
36
+ return operations.getStore();
37
+ }
38
+
39
+ /**
40
+ * The app's frame that made the current operation's call, when the ORM captured it (read once
41
+ * per operation): the queries the operation runs take their caller from it.
42
+ *
43
+ * @returns {import('../timeline/caller.js').CallerInfo|null}
44
+ */
45
+ static caller() {
46
+ const operation = operations.getStore();
47
+ if (!operation || typeof operation.caller !== 'function') return null;
48
+ if (operation.found === undefined) {
49
+ try {
50
+ operation.found = operation.caller() || null;
51
+ } catch (error) {
52
+ operation.found = null;
53
+ }
54
+ }
55
+ return operation.found;
56
+ }
57
+
58
+ /**
59
+ * Notes a driver query recorded for the current operation, and returns the operation's name
60
+ * for the query's span.
61
+ *
62
+ * @returns {string|undefined}
63
+ */
64
+ static noteQuery() {
65
+ const operation = operations.getStore();
66
+ if (!operation) return undefined;
67
+ operation.queries++;
68
+ return operation.name;
69
+ }
70
+ }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Patching library methods without the app noticing.
3
+ *
4
+ * A wrapper keeps the original's name and length (some libraries inspect a function's length to
5
+ * pick a calling convention), is installed once per method and module instance, and calls the
6
+ * original with the same `this` and arguments.
7
+ */
8
+
9
+ // Owner object -> names of its methods this copy of PeekInside has patched.
10
+ const patched = new WeakMap();
11
+
12
+ /**
13
+ * Replaces `owner[name]` with `makeWrapper(original)`, once.
14
+ *
15
+ * @param {Object} owner - The object that holds the method as an own property (a prototype)
16
+ * @param {string} name
17
+ * @param {(original: Function) => Function} makeWrapper
18
+ * @returns {boolean} Whether the method is patched now (also when it already was)
19
+ */
20
+ export function patchMethod(owner, name, makeWrapper) {
21
+ if (!owner || typeof owner[name] !== 'function') return false;
22
+ let names = patched.get(owner);
23
+ if (!names) {
24
+ names = new Set();
25
+ patched.set(owner, names);
26
+ }
27
+ if (names.has(name)) return true;
28
+
29
+ const original = owner[name];
30
+ const wrapper = makeWrapper(original);
31
+ try {
32
+ Object.defineProperty(wrapper, 'length', { value: original.length, configurable: true });
33
+ Object.defineProperty(wrapper, 'name', { value: original.name, configurable: true });
34
+ } catch (error) {
35
+ // Cosmetic only.
36
+ }
37
+ const descriptor = Object.getOwnPropertyDescriptor(owner, name);
38
+ Object.defineProperty(owner, name, {
39
+ value: wrapper,
40
+ writable: descriptor ? descriptor.writable !== false : true,
41
+ enumerable: descriptor ? descriptor.enumerable : false,
42
+ configurable: true
43
+ });
44
+ names.add(name);
45
+ return true;
46
+ }
47
+
48
+ /**
49
+ * The object in `target`'s prototype chain that holds `name` as an own property: where a method
50
+ * has to be patched so every instance (and subclass) sees the wrapper.
51
+ *
52
+ * @param {Object} target - Usually a class's prototype
53
+ * @param {string} name
54
+ * @returns {Object|null}
55
+ */
56
+ export function ownerOf(target, name) {
57
+ for (let current = target; current && current !== Object.prototype; current = Object.getPrototypeOf(current)) {
58
+ if (Object.prototype.hasOwnProperty.call(current, name)) return current;
59
+ }
60
+ return null;
61
+ }
62
+
63
+ /**
64
+ * Whether a value is a native promise (not merely a thenable: lazy thenables, such as Prisma's
65
+ * and Mongoose's queries, run when then() is called).
66
+ *
67
+ * @param {*} value
68
+ * @returns {boolean}
69
+ */
70
+ export function isNativePromise(value) {
71
+ return value instanceof Promise;
72
+ }