peekinside 1.2.5 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -2
- package/index.d.ts +41 -12
- package/package.json +10 -1
- package/src/PeekInside.js +70 -14
- package/src/capture/exchange-recorder.js +5 -12
- package/src/capture/http-message.js +17 -0
- package/src/dumps/dump-capture.js +100 -0
- package/src/dumps/dump-record.js +57 -0
- package/src/dumps/dump-value.js +428 -0
- package/src/exceptions/exception-capture.js +5 -4
- package/src/exceptions/request-context.js +118 -10
- package/src/hub/ingest-payload.js +66 -6
- package/src/hub/project-log.js +71 -4
- package/src/hub/server.js +18 -7
- package/src/hub/spool-ingester.js +1 -0
- package/src/hub/store.js +44 -1
- package/src/instrumentation/index.js +48 -3
- package/src/instrumentation/jobs.js +93 -2
- package/src/instrumentation/loggers.js +78 -4
- package/src/integrations/fastify.js +18 -1
- package/src/integrations/hono.js +209 -0
- package/src/integrations/nest.js +277 -0
- package/src/integrations/server-binder.js +9 -6
- package/src/logs/log-capture.js +5 -5
- package/src/logs/log-record.js +2 -2
- package/src/record.js +6 -2
- package/src/timeline/timeline.js +24 -3
- package/src/transport/batch.js +4 -1
- package/src/transport/cloud.js +12 -0
- package/src/transport/local.js +7 -5
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The value a dump shows: a typed tree of what the app passed to dump(), read without running the
|
|
3
|
+
* app's code.
|
|
4
|
+
*
|
|
5
|
+
* console.log turns a value into text; a dump keeps its shape, so the dashboard can show it as a
|
|
6
|
+
* tree that opens level by level, with the type of each node: a Map stays a Map, a Date a date, a
|
|
7
|
+
* class instance says its class. Reading is careful: getters aren't called (they could do
|
|
8
|
+
* anything), proxies aren't looked into, and a cycle becomes a reference to the ancestor it loops
|
|
9
|
+
* back to. The tree stops at bounds on depth, items per object, string length and total size, and
|
|
10
|
+
* says what it left out.
|
|
11
|
+
*
|
|
12
|
+
* Nodes, by `t`:
|
|
13
|
+
*
|
|
14
|
+
* str { v, len? } a string; `len` is its full length when it was cut
|
|
15
|
+
* num { v } big { v } numbers and BigInts, as text (NaN, Infinity and -0 survive)
|
|
16
|
+
* bool { v } null undef hole hole: an empty slot of a sparse array
|
|
17
|
+
* sym { v } a symbol, as text
|
|
18
|
+
* fn { name, kind? } a function; kind is "class", "async" or "generator"
|
|
19
|
+
* date { v } re { v } a date (ISO, or "Invalid Date") and a regular expression
|
|
20
|
+
* err { cls, msg, stack?, props } an error, with its own properties (code, status...)
|
|
21
|
+
* arr { cls?, len, items } an array (cls for subclasses and typed arrays)
|
|
22
|
+
* obj { cls, len, props } an object: cls is its class, null for a plain object
|
|
23
|
+
* map { len, entries } set { len, items }
|
|
24
|
+
* buf { cls, len, hex } a Buffer or ArrayBuffer: its size and first bytes
|
|
25
|
+
* box { cls, v } a boxed primitive (new String("x"))
|
|
26
|
+
* get { set? } an own accessor property, not called
|
|
27
|
+
* ref { path } a cycle: the ancestor it points back to ("$.owner")
|
|
28
|
+
* cut { cls?, len? } not read: past the depth or size bounds
|
|
29
|
+
* proxy promise weak { cls } kinds whose contents aren't read
|
|
30
|
+
*
|
|
31
|
+
* `len` of a container counts all its items, even when only the first ones are in the tree.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import { types } from 'util';
|
|
35
|
+
|
|
36
|
+
export const DUMP_LIMITS = Object.freeze({
|
|
37
|
+
depth: 10,
|
|
38
|
+
items: 100,
|
|
39
|
+
string: 4000,
|
|
40
|
+
stack: 4000,
|
|
41
|
+
nodes: 5000,
|
|
42
|
+
// Characters of strings in one tree, all together.
|
|
43
|
+
text: 256 * 1024,
|
|
44
|
+
bytes: 48,
|
|
45
|
+
preview: 160
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Reads values into dump trees.
|
|
50
|
+
*/
|
|
51
|
+
export class DumpValue {
|
|
52
|
+
/**
|
|
53
|
+
* @param {*} value
|
|
54
|
+
* @returns {{ tree: Object, type: string, preview: string, truncated: boolean }} The tree, a short
|
|
55
|
+
* name of its type ("User", "array", "string"), a one-line preview, and whether bounds cut it
|
|
56
|
+
*/
|
|
57
|
+
static capture(value) {
|
|
58
|
+
const reader = new Reader();
|
|
59
|
+
let tree;
|
|
60
|
+
try {
|
|
61
|
+
tree = reader.node(value, 0, '$');
|
|
62
|
+
} catch (error) {
|
|
63
|
+
// Every read is guarded; this is the last line of defense.
|
|
64
|
+
tree = { t: 'cut' };
|
|
65
|
+
reader.truncated = true;
|
|
66
|
+
}
|
|
67
|
+
return { tree, type: DumpValue.typeOf(tree), preview: DumpValue.preview(tree, DUMP_LIMITS.preview), truncated: reader.truncated };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* A short name of a node's type: its class for objects, else its kind.
|
|
72
|
+
*
|
|
73
|
+
* @param {Object} node
|
|
74
|
+
* @returns {string}
|
|
75
|
+
*/
|
|
76
|
+
static typeOf(node) {
|
|
77
|
+
switch (node.t) {
|
|
78
|
+
case 'str':
|
|
79
|
+
return 'string';
|
|
80
|
+
case 'num':
|
|
81
|
+
return 'number';
|
|
82
|
+
case 'big':
|
|
83
|
+
return 'bigint';
|
|
84
|
+
case 'bool':
|
|
85
|
+
return 'boolean';
|
|
86
|
+
case 'undef':
|
|
87
|
+
return 'undefined';
|
|
88
|
+
case 'sym':
|
|
89
|
+
return 'symbol';
|
|
90
|
+
case 'fn':
|
|
91
|
+
return node.kind === 'class' ? 'class' : 'function';
|
|
92
|
+
case 're':
|
|
93
|
+
return 'RegExp';
|
|
94
|
+
case 'date':
|
|
95
|
+
return 'Date';
|
|
96
|
+
case 'arr':
|
|
97
|
+
return node.cls || 'array';
|
|
98
|
+
case 'map':
|
|
99
|
+
return 'Map';
|
|
100
|
+
case 'set':
|
|
101
|
+
return 'Set';
|
|
102
|
+
case 'err':
|
|
103
|
+
case 'obj':
|
|
104
|
+
case 'buf':
|
|
105
|
+
case 'box':
|
|
106
|
+
return node.cls || 'object';
|
|
107
|
+
default:
|
|
108
|
+
return node.t;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* The node on one line, as a list shows it: `User { id: 1, name: "Ada", … }`.
|
|
114
|
+
*
|
|
115
|
+
* @param {Object} node
|
|
116
|
+
* @param {number} [max=160] - Characters at most
|
|
117
|
+
* @returns {string}
|
|
118
|
+
*/
|
|
119
|
+
static preview(node, max = DUMP_LIMITS.preview) {
|
|
120
|
+
const text = line(node, 2);
|
|
121
|
+
return text.length > max ? `${text.slice(0, max - 1)}…` : text;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
class Reader {
|
|
126
|
+
constructor() {
|
|
127
|
+
this.nodes = 0;
|
|
128
|
+
this.text = 0;
|
|
129
|
+
this.truncated = false;
|
|
130
|
+
// The objects being read, from the root down, to their paths: a cycle points back to one.
|
|
131
|
+
this.ancestors = new Map();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
node(value, depth, path) {
|
|
135
|
+
this.nodes++;
|
|
136
|
+
switch (typeof value) {
|
|
137
|
+
case 'string':
|
|
138
|
+
return this.string(value, DUMP_LIMITS.string);
|
|
139
|
+
case 'number':
|
|
140
|
+
return { t: 'num', v: Object.is(value, -0) ? '-0' : String(value) };
|
|
141
|
+
case 'bigint':
|
|
142
|
+
return { t: 'big', v: value.toString() };
|
|
143
|
+
case 'boolean':
|
|
144
|
+
return { t: 'bool', v: value };
|
|
145
|
+
case 'undefined':
|
|
146
|
+
return { t: 'undef' };
|
|
147
|
+
case 'symbol':
|
|
148
|
+
return { t: 'sym', v: guard(() => value.toString(), 'Symbol()') };
|
|
149
|
+
case 'function':
|
|
150
|
+
return fn(value);
|
|
151
|
+
default:
|
|
152
|
+
return value === null ? { t: 'null' } : this.object(value, depth, path);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
string(value, max) {
|
|
157
|
+
const room = Math.max(0, Math.min(max, DUMP_LIMITS.text - this.text));
|
|
158
|
+
if (value.length <= room) {
|
|
159
|
+
this.text += value.length;
|
|
160
|
+
return { t: 'str', v: value };
|
|
161
|
+
}
|
|
162
|
+
this.truncated = true;
|
|
163
|
+
const cut = value.slice(0, room);
|
|
164
|
+
this.text += cut.length;
|
|
165
|
+
return { t: 'str', v: cut, len: value.length };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
object(value, depth, path) {
|
|
169
|
+
if (types.isProxy(value)) return { t: 'proxy' };
|
|
170
|
+
const ancestor = this.ancestors.get(value);
|
|
171
|
+
if (ancestor) return { t: 'ref', path: ancestor };
|
|
172
|
+
const cls = className(value);
|
|
173
|
+
if (types.isDate(value)) return { t: 'date', v: guard(() => (Number.isNaN(value.getTime()) ? 'Invalid Date' : value.toISOString()), 'Invalid Date') };
|
|
174
|
+
if (types.isRegExp(value)) return { t: 're', v: guard(() => RegExp.prototype.toString.call(value), '/(?:)/') };
|
|
175
|
+
if (types.isPromise(value)) return { t: 'promise' };
|
|
176
|
+
if (types.isWeakMap(value) || types.isWeakSet(value)) return { t: 'weak', cls: cls || 'Weak' };
|
|
177
|
+
if (Buffer.isBuffer(value)) return { t: 'buf', cls: 'Buffer', len: value.length, hex: value.subarray(0, DUMP_LIMITS.bytes).toString('hex') };
|
|
178
|
+
if (types.isAnyArrayBuffer(value)) {
|
|
179
|
+
return { t: 'buf', cls: cls || 'ArrayBuffer', len: value.byteLength, hex: Buffer.from(value, 0, Math.min(DUMP_LIMITS.bytes, value.byteLength)).toString('hex') };
|
|
180
|
+
}
|
|
181
|
+
if (types.isBoxedPrimitive(value)) return { t: 'box', cls: cls || 'Object', v: this.node(guard(() => value.valueOf(), undefined), depth + 1, path) };
|
|
182
|
+
if (depth >= DUMP_LIMITS.depth || this.nodes > DUMP_LIMITS.nodes || this.text >= DUMP_LIMITS.text) {
|
|
183
|
+
this.truncated = true;
|
|
184
|
+
return { t: 'cut', ...(cls ? { cls } : {}), ...sizeOf(value) };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
this.ancestors.set(value, path);
|
|
188
|
+
try {
|
|
189
|
+
if (types.isNativeError(value) || value instanceof Error) return this.error(value, cls, depth, path);
|
|
190
|
+
if (Array.isArray(value)) return this.array(value, cls, depth, path);
|
|
191
|
+
if (types.isTypedArray(value)) return this.typedArray(value, cls);
|
|
192
|
+
if (types.isMap(value)) return this.map(value, depth, path);
|
|
193
|
+
if (types.isSet(value)) return this.set(value, depth, path);
|
|
194
|
+
return { t: 'obj', cls, ...this.props(value, depth, path, null) };
|
|
195
|
+
} finally {
|
|
196
|
+
this.ancestors.delete(value);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Own enumerable properties, string keys then symbols; `skip` leaves some out.
|
|
201
|
+
props(value, depth, path, skip) {
|
|
202
|
+
const keys = guard(() => Reflect.ownKeys(value), []);
|
|
203
|
+
const props = [];
|
|
204
|
+
let len = 0;
|
|
205
|
+
for (const key of keys) {
|
|
206
|
+
if (skip && skip.has(key)) continue;
|
|
207
|
+
const descriptor = guard(() => Object.getOwnPropertyDescriptor(value, key), undefined);
|
|
208
|
+
if (!descriptor || !descriptor.enumerable) continue;
|
|
209
|
+
len++;
|
|
210
|
+
if (props.length >= DUMP_LIMITS.items || this.nodes > DUMP_LIMITS.nodes) {
|
|
211
|
+
this.truncated = true;
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
const name = typeof key === 'symbol' ? `[${guard(() => key.toString(), 'Symbol()')}]` : key;
|
|
215
|
+
props.push([name, this.property(descriptor, depth, childPath(path, name))]);
|
|
216
|
+
}
|
|
217
|
+
return { len, props };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
property(descriptor, depth, path) {
|
|
221
|
+
if ('value' in descriptor) return this.node(descriptor.value, depth + 1, path);
|
|
222
|
+
this.nodes++;
|
|
223
|
+
return descriptor.set ? { t: 'get', set: true } : { t: 'get' };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
array(value, cls, depth, path) {
|
|
227
|
+
const len = value.length;
|
|
228
|
+
const count = Math.min(len, DUMP_LIMITS.items);
|
|
229
|
+
const items = [];
|
|
230
|
+
for (let index = 0; index < count && this.nodes <= DUMP_LIMITS.nodes; index++) {
|
|
231
|
+
const descriptor = guard(() => Object.getOwnPropertyDescriptor(value, index), undefined);
|
|
232
|
+
items.push(descriptor ? this.property(descriptor, depth, `${path}[${index}]`) : (this.nodes++, { t: 'hole' }));
|
|
233
|
+
}
|
|
234
|
+
if (items.length < len) this.truncated = true;
|
|
235
|
+
return { t: 'arr', ...(cls && cls !== 'Array' ? { cls } : {}), len, items };
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
typedArray(value, cls) {
|
|
239
|
+
const len = value.length;
|
|
240
|
+
const count = Math.min(len, DUMP_LIMITS.items);
|
|
241
|
+
const items = [];
|
|
242
|
+
for (let index = 0; index < count; index++) {
|
|
243
|
+
this.nodes++;
|
|
244
|
+
const item = value[index];
|
|
245
|
+
items.push(typeof item === 'bigint' ? { t: 'big', v: item.toString() } : { t: 'num', v: Object.is(item, -0) ? '-0' : String(item) });
|
|
246
|
+
}
|
|
247
|
+
if (count < len) this.truncated = true;
|
|
248
|
+
return { t: 'arr', cls: cls || 'TypedArray', len, items };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
map(value, depth, path) {
|
|
252
|
+
const len = guard(() => MAP_SIZE.call(value), 0);
|
|
253
|
+
const entries = [];
|
|
254
|
+
for (const [key, item] of guard(() => Map.prototype.entries.call(value), [])) {
|
|
255
|
+
if (entries.length >= DUMP_LIMITS.items || this.nodes > DUMP_LIMITS.nodes) break;
|
|
256
|
+
const keyNode = this.node(key, depth + 1, `${path}.keys()[${entries.length}]`);
|
|
257
|
+
entries.push([keyNode, this.node(item, depth + 1, `${path}.get(${keyText(keyNode)})`)]);
|
|
258
|
+
}
|
|
259
|
+
if (entries.length < len) this.truncated = true;
|
|
260
|
+
return { t: 'map', len, entries };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
set(value, depth, path) {
|
|
264
|
+
const len = guard(() => SET_SIZE.call(value), 0);
|
|
265
|
+
const items = [];
|
|
266
|
+
for (const item of guard(() => Set.prototype.values.call(value), [])) {
|
|
267
|
+
if (items.length >= DUMP_LIMITS.items || this.nodes > DUMP_LIMITS.nodes) break;
|
|
268
|
+
items.push(this.node(item, depth + 1, `${path}[${items.length}]`));
|
|
269
|
+
}
|
|
270
|
+
if (items.length < len) this.truncated = true;
|
|
271
|
+
return { t: 'set', len, items };
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
error(value, cls, depth, path) {
|
|
275
|
+
const name = ownString(value, 'name') || cls || 'Error';
|
|
276
|
+
const message = ownString(value, 'message');
|
|
277
|
+
// V8 keeps a native error's stack behind its own accessor; reading it runs no app code.
|
|
278
|
+
const stack = types.isNativeError(value) ? guard(() => (typeof value.stack === 'string' ? value.stack : ''), '') : ownString(value, 'stack');
|
|
279
|
+
const { props } = this.props(value, depth, path, ERROR_OWN);
|
|
280
|
+
const cause = guard(() => Object.getOwnPropertyDescriptor(value, 'cause'), undefined);
|
|
281
|
+
if (cause && 'value' in cause) props.push(['cause', this.node(cause.value, depth + 1, `${path}.cause`)]);
|
|
282
|
+
return {
|
|
283
|
+
t: 'err',
|
|
284
|
+
cls: name,
|
|
285
|
+
msg: this.string(message, DUMP_LIMITS.string).v,
|
|
286
|
+
...(stack ? { stack: this.string(stack, DUMP_LIMITS.stack).v } : {}),
|
|
287
|
+
props
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const ERROR_OWN = new Set(['name', 'message', 'stack', 'cause']);
|
|
293
|
+
// Map's and Set's own size getters, so an object's own `size` can't stand in.
|
|
294
|
+
const MAP_SIZE = Object.getOwnPropertyDescriptor(Map.prototype, 'size').get;
|
|
295
|
+
const SET_SIZE = Object.getOwnPropertyDescriptor(Set.prototype, 'size').get;
|
|
296
|
+
|
|
297
|
+
function fn(value) {
|
|
298
|
+
if (types.isProxy(value)) return { t: 'proxy' };
|
|
299
|
+
const name = guard(() => {
|
|
300
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, 'name');
|
|
301
|
+
return descriptor && typeof descriptor.value === 'string' ? descriptor.value : '';
|
|
302
|
+
}, '');
|
|
303
|
+
let kind = null;
|
|
304
|
+
if (types.isAsyncFunction(value)) kind = 'async';
|
|
305
|
+
else if (types.isGeneratorFunction(value)) kind = 'generator';
|
|
306
|
+
else if (guard(() => /^class\b/.test(Function.prototype.toString.call(value)), false)) kind = 'class';
|
|
307
|
+
return { t: 'fn', name, ...(kind ? { kind } : {}) };
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// The class of an object, read without getters: null for plain objects (and those without a
|
|
311
|
+
// prototype).
|
|
312
|
+
function className(value) {
|
|
313
|
+
return guard(() => {
|
|
314
|
+
const proto = Object.getPrototypeOf(value);
|
|
315
|
+
if (proto === null || proto === Object.prototype) return null;
|
|
316
|
+
const constructor = Object.getOwnPropertyDescriptor(proto, 'constructor');
|
|
317
|
+
const name = constructor && typeof constructor.value === 'function' ? Object.getOwnPropertyDescriptor(constructor.value, 'name') : null;
|
|
318
|
+
return name && typeof name.value === 'string' && name.value ? name.value : 'Object';
|
|
319
|
+
}, null);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// How big a container left unread is, when that is cheap to know.
|
|
323
|
+
function sizeOf(value) {
|
|
324
|
+
if (Array.isArray(value)) return { len: value.length };
|
|
325
|
+
if (types.isMap(value)) return { len: guard(() => MAP_SIZE.call(value), 0) };
|
|
326
|
+
if (types.isSet(value)) return { len: guard(() => SET_SIZE.call(value), 0) };
|
|
327
|
+
return {};
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function ownString(value, key) {
|
|
331
|
+
const descriptor = guard(() => Object.getOwnPropertyDescriptor(value, key), undefined);
|
|
332
|
+
if (descriptor && typeof descriptor.value === 'string') return descriptor.value;
|
|
333
|
+
// name and message usually live on the prototype (Error.prototype.name); read as data only.
|
|
334
|
+
for (let proto = guard(() => Object.getPrototypeOf(value), null); proto; proto = guard(() => Object.getPrototypeOf(proto), null)) {
|
|
335
|
+
const inherited = guard(() => Object.getOwnPropertyDescriptor(proto, key), undefined);
|
|
336
|
+
if (inherited) return typeof inherited.value === 'string' ? inherited.value : '';
|
|
337
|
+
}
|
|
338
|
+
return '';
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function childPath(path, key) {
|
|
342
|
+
return /^[A-Za-z_$][\w$]*$/.test(key) ? `${path}.${key}` : `${path}[${JSON.stringify(key)}]`;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function keyText(node) {
|
|
346
|
+
return node.t === 'str' ? JSON.stringify(node.v) : node.t === 'num' || node.t === 'big' || node.t === 'bool' ? String(node.v) : '…';
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function guard(read, fallback) {
|
|
350
|
+
try {
|
|
351
|
+
return read();
|
|
352
|
+
} catch (error) {
|
|
353
|
+
return fallback;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// One node as text, `depth` levels down.
|
|
358
|
+
function line(node, depth) {
|
|
359
|
+
switch (node.t) {
|
|
360
|
+
case 'str':
|
|
361
|
+
return JSON.stringify(node.v.length > 60 ? `${node.v.slice(0, 60)}…` : node.v);
|
|
362
|
+
case 'num':
|
|
363
|
+
case 'sym':
|
|
364
|
+
case 'date':
|
|
365
|
+
case 're':
|
|
366
|
+
return node.v;
|
|
367
|
+
case 'big':
|
|
368
|
+
return `${node.v}n`;
|
|
369
|
+
case 'bool':
|
|
370
|
+
return String(node.v);
|
|
371
|
+
case 'null':
|
|
372
|
+
return 'null';
|
|
373
|
+
case 'undef':
|
|
374
|
+
return 'undefined';
|
|
375
|
+
case 'hole':
|
|
376
|
+
return '<empty>';
|
|
377
|
+
case 'fn':
|
|
378
|
+
return node.kind === 'class' ? `[class ${node.name || '(anonymous)'}]` : `[${node.kind === 'async' ? 'AsyncFunction' : 'Function'}${node.name ? ` ${node.name}` : ''}]`;
|
|
379
|
+
case 'err':
|
|
380
|
+
return node.msg ? `${node.cls}: ${node.msg}` : node.cls;
|
|
381
|
+
case 'buf':
|
|
382
|
+
return `<${node.cls} ${(node.hex.match(/../g) || []).slice(0, 8).join(' ')}${node.len > 8 ? ' …' : ''}>`;
|
|
383
|
+
case 'box':
|
|
384
|
+
return `[${node.cls}: ${line(node.v, 0)}]`;
|
|
385
|
+
case 'get':
|
|
386
|
+
return node.set ? '[Getter/Setter]' : '[Getter]';
|
|
387
|
+
case 'ref':
|
|
388
|
+
return `[Circular ${node.path}]`;
|
|
389
|
+
case 'proxy':
|
|
390
|
+
return '[Proxy]';
|
|
391
|
+
case 'promise':
|
|
392
|
+
return 'Promise';
|
|
393
|
+
case 'weak':
|
|
394
|
+
return node.cls;
|
|
395
|
+
case 'cut':
|
|
396
|
+
return `${node.cls ? `${node.cls} ` : ''}{…}`;
|
|
397
|
+
case 'arr': {
|
|
398
|
+
const head = node.cls ? `${node.cls}(${node.len}) ` : '';
|
|
399
|
+
if (!node.len) return `${head}[]`;
|
|
400
|
+
if (depth <= 0) return `${head}[…]`;
|
|
401
|
+
return `${head}[${list(node.items.map((item) => line(item, depth - 1)), node.len)}]`;
|
|
402
|
+
}
|
|
403
|
+
case 'map': {
|
|
404
|
+
if (!node.len) return 'Map(0) {}';
|
|
405
|
+
if (depth <= 0) return `Map(${node.len}) {…}`;
|
|
406
|
+
return `Map(${node.len}) { ${list(node.entries.map(([key, value]) => `${line(key, 0)} => ${line(value, depth - 1)}`), node.len)} }`;
|
|
407
|
+
}
|
|
408
|
+
case 'set': {
|
|
409
|
+
if (!node.len) return 'Set(0) {}';
|
|
410
|
+
if (depth <= 0) return `Set(${node.len}) {…}`;
|
|
411
|
+
return `Set(${node.len}) { ${list(node.items.map((item) => line(item, depth - 1)), node.len)} }`;
|
|
412
|
+
}
|
|
413
|
+
case 'obj': {
|
|
414
|
+
const head = node.cls ? `${node.cls} ` : '';
|
|
415
|
+
if (!node.len) return `${head}{}`;
|
|
416
|
+
if (depth <= 0) return `${head}{…}`;
|
|
417
|
+
return `${head}{ ${list(node.props.map(([key, value]) => `${/^[A-Za-z_$][\w$]*$/.test(key) ? key : JSON.stringify(key)}: ${line(value, depth - 1)}`), node.len)} }`;
|
|
418
|
+
}
|
|
419
|
+
default:
|
|
420
|
+
return '…';
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// The first few parts, and an ellipsis for the rest.
|
|
425
|
+
function list(parts, total) {
|
|
426
|
+
const shown = parts.slice(0, 4);
|
|
427
|
+
return total > shown.length ? `${shown.join(', ')}, …` : shown.join(', ');
|
|
428
|
+
}
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
* - Whether an Express error was handled is only known later: an error middleware that receives
|
|
14
14
|
* it may still pass it on to Express's final handler. Such records stay open on the request and
|
|
15
15
|
* are settled when the error reaches the final handler (unhandled) or the request's record is
|
|
16
|
-
* built (handled).
|
|
16
|
+
* built (handled). NestJS exception filters and Hono error handlers work the same way: one that
|
|
17
|
+
* takes an error may still hand it to the framework's default answer.
|
|
17
18
|
*
|
|
18
19
|
* Nothing here ever throws into the app: a failure to record is a lost exception, never a new one.
|
|
19
20
|
*/
|
|
@@ -134,12 +135,12 @@ export class ExceptionCapture {
|
|
|
134
135
|
}
|
|
135
136
|
|
|
136
137
|
/**
|
|
137
|
-
* An error reached a framework during a request: Express, Koa or
|
|
138
|
+
* An error reached a framework during a request: Express, Koa, Fastify, NestJS or Hono.
|
|
138
139
|
*
|
|
139
140
|
* @param {RequestContext|null} context - The request's context
|
|
140
141
|
* @param {*} error
|
|
141
142
|
* @param {Object} options
|
|
142
|
-
* @param {'express'|'koa'|'fastify'} options.mechanism
|
|
143
|
+
* @param {'express'|'koa'|'fastify'|'nest'|'hono'} options.mechanism
|
|
143
144
|
* @param {boolean} options.handled - Whether the app handled it
|
|
144
145
|
* @param {boolean} [options.open=false] - The outcome isn't final yet (see the module comment)
|
|
145
146
|
* @returns {string|null} The exception's id, or null when nothing was recorded
|
|
@@ -290,7 +291,7 @@ export class ExceptionCapture {
|
|
|
290
291
|
exchange.exceptionCount = own.exceptionCount;
|
|
291
292
|
exchange.exceptions = own.links.slice();
|
|
292
293
|
own.completeTimeline(exchange);
|
|
293
|
-
|
|
294
|
+
own.describe(exchange);
|
|
294
295
|
exchanges.push(exchange);
|
|
295
296
|
}
|
|
296
297
|
}
|
|
@@ -14,15 +14,24 @@
|
|
|
14
14
|
* The context also keeps the request's exception bookkeeping: which errors were already recorded
|
|
15
15
|
* (each error object is recorded once per request), the records whose outcome is still open (see
|
|
16
16
|
* ExceptionCapture), and the count and links that go into the request's record.
|
|
17
|
+
*
|
|
18
|
+
* Integrations that know what the framework matched the request to note it here (noteRoute()):
|
|
19
|
+
* the route pattern, the code that handles it and the route parameters. The request's record and
|
|
20
|
+
* its exceptions carry them.
|
|
17
21
|
*/
|
|
18
22
|
|
|
19
23
|
import { AsyncLocalStorage } from 'async_hooks';
|
|
20
24
|
import { BodyDecoder } from '../capture/body-decoder.js';
|
|
21
|
-
import { parseQuery, splitUrl } from '../capture/http-message.js';
|
|
22
|
-
import { LIMITS } from './limits.js';
|
|
25
|
+
import { parseQuery, splitUrl, stringParams } from '../capture/http-message.js';
|
|
26
|
+
import { LIMITS, truncate } from './limits.js';
|
|
23
27
|
|
|
24
28
|
const storage = new AsyncLocalStorage();
|
|
25
29
|
const kContext = Symbol('peekInside.context');
|
|
30
|
+
// What a route pattern and a handler name may take up.
|
|
31
|
+
const MAX_ROUTE = 512;
|
|
32
|
+
const MAX_HANDLER = 256;
|
|
33
|
+
// Dumps listed in a request's record (the Dumps page has them all).
|
|
34
|
+
const MAX_DUMP_LINKS = 20;
|
|
26
35
|
// The request an exception last escaped from, until the current job ends.
|
|
27
36
|
let escaped = null;
|
|
28
37
|
|
|
@@ -83,7 +92,7 @@ export class RequestContext {
|
|
|
83
92
|
* @param {boolean} [fields.blocked=false] - The request reached a server in local mode: neither
|
|
84
93
|
* it nor its exceptions are recorded
|
|
85
94
|
* @param {string} [fields.integration='custom'] - Where tracking started: "express", "koa",
|
|
86
|
-
* "fastify", "http" (a server's emit()) or "custom" (intercept())
|
|
95
|
+
* "fastify", "nest", "hono", "http" (a server's emit()) or "custom" (intercept())
|
|
87
96
|
*/
|
|
88
97
|
constructor({ capture, req, recorder = null, target, blocked = false, integration = 'custom' }) {
|
|
89
98
|
this.capture = capture;
|
|
@@ -119,8 +128,26 @@ export class RequestContext {
|
|
|
119
128
|
* @type {import('../user.js').User|null}
|
|
120
129
|
*/
|
|
121
130
|
this.user = null;
|
|
131
|
+
/**
|
|
132
|
+
* The route pattern the framework matched ("/users/:id"), once an integration noted it
|
|
133
|
+
* @type {string|null}
|
|
134
|
+
*/
|
|
135
|
+
this.route = null;
|
|
136
|
+
/**
|
|
137
|
+
* The code that handles the request ("UsersController.findOne"), once an integration noted it
|
|
138
|
+
* @type {string|null}
|
|
139
|
+
*/
|
|
140
|
+
this.handler = null;
|
|
141
|
+
/**
|
|
142
|
+
* Dumps made during the request (PeekInside.dump()), for its record
|
|
143
|
+
* @type {{ id: string, label: string|null, type: string, preview: string }[]}
|
|
144
|
+
*/
|
|
145
|
+
this.dumps = [];
|
|
122
146
|
|
|
123
147
|
this._req = req;
|
|
148
|
+
// Route parameters an integration noted (or a function returning them), for frameworks that
|
|
149
|
+
// don't put them on the Node.js request.
|
|
150
|
+
this._params = undefined;
|
|
124
151
|
// What remains for exceptions after the request finished (see release()).
|
|
125
152
|
this._after = null;
|
|
126
153
|
|
|
@@ -169,12 +196,81 @@ export class RequestContext {
|
|
|
169
196
|
}
|
|
170
197
|
|
|
171
198
|
/**
|
|
172
|
-
*
|
|
199
|
+
* Notes what the framework matched the request to, as an integration learns it. Each part that is
|
|
200
|
+
* given replaces the one noted before; null forgets it, and parts left out stay as they are.
|
|
201
|
+
*
|
|
202
|
+
* @param {Object} match
|
|
203
|
+
* @param {string|null} [match.route] - The route pattern, such as "/users/:id"
|
|
204
|
+
* @param {string|null} [match.handler] - The code that handles it, such as "UsersController.findOne"
|
|
205
|
+
* @param {Object|(() => Object)|null} [match.params] - The route parameters, or a function that
|
|
206
|
+
* returns them when they are needed, for frameworks that keep them off the Node.js request
|
|
207
|
+
* @returns {void}
|
|
208
|
+
*/
|
|
209
|
+
noteRoute({ route, handler, params } = {}) {
|
|
210
|
+
if (route !== undefined) this.route = typeof route === 'string' && route ? truncate(route, MAX_ROUTE) : null;
|
|
211
|
+
if (handler !== undefined) this.handler = typeof handler === 'string' && handler ? truncate(handler, MAX_HANDLER) : null;
|
|
212
|
+
if (params !== undefined) this._params = params;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* The route parameters an integration noted, as strings.
|
|
217
|
+
*
|
|
218
|
+
* @returns {Object<string, string>|null} null when none were noted
|
|
219
|
+
*/
|
|
220
|
+
routeParams() {
|
|
221
|
+
let params = this._params;
|
|
222
|
+
if (typeof params === 'function') {
|
|
223
|
+
try {
|
|
224
|
+
params = params();
|
|
225
|
+
} catch (error) {
|
|
226
|
+
params = null;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return params && typeof params === 'object' ? stringParams(params) : null;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* For the exception list: the request's method and path, and its route and handler when known.
|
|
173
234
|
*
|
|
174
|
-
* @returns {{ method: string, path: string }}
|
|
235
|
+
* @returns {{ method: string, path: string, route?: string, handler?: string }}
|
|
175
236
|
*/
|
|
176
237
|
requestLine() {
|
|
177
|
-
return {
|
|
238
|
+
return {
|
|
239
|
+
method: this.method,
|
|
240
|
+
path: this.path,
|
|
241
|
+
...(this.route ? { route: this.route } : {}),
|
|
242
|
+
...(this.handler ? { handler: this.handler } : {})
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Counts a dump made during the request: listed on its record, and placed on its timeline.
|
|
248
|
+
*
|
|
249
|
+
* @param {import('../dumps/dump-record.js').DumpRecord} record
|
|
250
|
+
* @returns {void}
|
|
251
|
+
*/
|
|
252
|
+
noteDump(record) {
|
|
253
|
+
if (this.dumps.length < MAX_DUMP_LINKS) this.dumps.push(record.link());
|
|
254
|
+
if (this.timeline) this.timeline.dump(record);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Adds what the request's record shows of the request beyond the exchange itself: who it was for,
|
|
259
|
+
* its route and handler, the dumps made during it, and route parameters the Node.js request
|
|
260
|
+
* didn't carry.
|
|
261
|
+
*
|
|
262
|
+
* @param {import('../capture/exchange-recorder.js').Exchange} exchange - Changed in place
|
|
263
|
+
* @returns {void}
|
|
264
|
+
*/
|
|
265
|
+
describe(exchange) {
|
|
266
|
+
if (this.user) exchange.user = this.user;
|
|
267
|
+
if (this.route) exchange.route = this.route;
|
|
268
|
+
if (this.handler) exchange.handler = this.handler;
|
|
269
|
+
if (this.dumps.length) exchange.dumps = this.dumps.slice();
|
|
270
|
+
if (this._params !== undefined && (!exchange.params || !Object.keys(exchange.params).length)) {
|
|
271
|
+
const params = this.routeParams();
|
|
272
|
+
if (params) exchange.params = params;
|
|
273
|
+
}
|
|
178
274
|
}
|
|
179
275
|
|
|
180
276
|
/**
|
|
@@ -185,9 +281,11 @@ export class RequestContext {
|
|
|
185
281
|
*/
|
|
186
282
|
snapshot() {
|
|
187
283
|
if (this.recorder && !this.recorder.recorded) {
|
|
188
|
-
|
|
284
|
+
const snapshot = { id: this.requestId, ...this.recorder.snapshot(LIMITS.requestBody) };
|
|
285
|
+
if (this._params !== undefined && !Object.keys(snapshot.params).length) snapshot.params = this.routeParams() || {};
|
|
286
|
+
return snapshot;
|
|
189
287
|
}
|
|
190
|
-
const after = this._after ||
|
|
288
|
+
const after = this._after || this._parts();
|
|
191
289
|
return {
|
|
192
290
|
id: this.requestId,
|
|
193
291
|
method: this.method,
|
|
@@ -223,7 +321,7 @@ export class RequestContext {
|
|
|
223
321
|
complete(exchange) {
|
|
224
322
|
if (this.capture) this.capture.finish(this, exchange);
|
|
225
323
|
this.completeTimeline(exchange);
|
|
226
|
-
|
|
324
|
+
this.describe(exchange);
|
|
227
325
|
this.release();
|
|
228
326
|
}
|
|
229
327
|
|
|
@@ -253,11 +351,21 @@ export class RequestContext {
|
|
|
253
351
|
*/
|
|
254
352
|
release() {
|
|
255
353
|
this.finished = true;
|
|
256
|
-
this._after =
|
|
354
|
+
this._after = this._parts();
|
|
355
|
+
// A function returning the parameters may hold the framework's request.
|
|
356
|
+
if (typeof this._params === 'function') this._params = this.routeParams();
|
|
257
357
|
this._req = null;
|
|
258
358
|
this.recorder = null;
|
|
259
359
|
this.timeline = null;
|
|
260
360
|
}
|
|
361
|
+
|
|
362
|
+
// The small parts of the request a snapshot keeps, with the parameters an integration noted when
|
|
363
|
+
// the request has none of its own.
|
|
364
|
+
_parts() {
|
|
365
|
+
const parts = RequestSnapshot.partsOf(this._req);
|
|
366
|
+
if (this._params !== undefined && !Object.keys(parts.params).length) parts.params = this.routeParams() || {};
|
|
367
|
+
return parts;
|
|
368
|
+
}
|
|
261
369
|
}
|
|
262
370
|
|
|
263
371
|
/**
|