peekinside 0.0.0-stage → 1.2.1

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 +561 -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,339 @@
1
+ /**
2
+ * Turns whatever was thrown into an ExceptionRecord: an Error, an error from another realm, or any
3
+ * other value (`throw 'oops'`, `Promise.reject({ code: 1 })`).
4
+ *
5
+ * Thrown values come from the app and its dependencies, so nothing about them is trusted: a getter
6
+ * on an error can throw, a proxy can throw on every access, a message can be megabytes long.
7
+ * Every read is guarded and every field is bounded (see limits.js); building a record never
8
+ * throws for a strange value.
9
+ */
10
+
11
+ import { randomUUID } from 'crypto';
12
+ import os from 'os';
13
+ import { types } from 'util';
14
+ import { setOwn } from '../capture/http-message.js';
15
+ import { ExceptionRecord } from './exception-record.js';
16
+ import { Fingerprint } from './fingerprint.js';
17
+ import { FrameClassifier } from './frame-classifier.js';
18
+ import { LIMITS, truncate, truncateBytes, truncateStack } from './limits.js';
19
+ import { SafeJson } from './safe-json.js';
20
+ import { SourceContext } from './source-context.js';
21
+ import { SourceMaps } from './source-maps.js';
22
+ import { StackParser } from './stack-parser.js';
23
+
24
+ // The name of thrown values that aren't errors.
25
+ export const THROWN_VALUE = 'ThrownValue';
26
+
27
+ /**
28
+ * @typedef {Object} BuildOptions
29
+ * @property {'express'|'koa'|'fastify'|'http'|'uncaught'|'unhandledRejection'|'report'|'job'} mechanism
30
+ * @property {boolean} handled
31
+ * @property {boolean} fatal
32
+ * @property {string} environment - Normalized environment label
33
+ * @property {string|null} [release] - The running release
34
+ * @property {string|null} [requestId]
35
+ * @property {Object|null} [request] - Request snapshot (see RequestSnapshot)
36
+ * @property {{ method: string, path: string }|null} [requestLine] - For the exception list
37
+ * @property {import('../user.js').User|null} [user] - Who it happened to
38
+ * @property {string|null} [jobRunId] - The job run it happened in
39
+ * @property {{ name: string }|null} [job] - The job, for the exception list
40
+ * @property {Object<string, string>} [tags]
41
+ * @property {*} [extra]
42
+ */
43
+
44
+ /**
45
+ * Builds exception records for one project.
46
+ */
47
+ export class ExceptionBuilder {
48
+ /**
49
+ * @param {Object} options
50
+ * @param {string} options.root - The project folder: frames inside it are in-app
51
+ * @param {boolean} [options.sourceContext=true] - Read source lines around in-app frames
52
+ * @param {boolean} [options.sourceMaps=true] - Map frames of compiled code to their sources
53
+ */
54
+ constructor({ root, sourceContext = true, sourceMaps = true }) {
55
+ this.classifier = new FrameClassifier(root);
56
+ this.sources = sourceContext ? new SourceContext(root) : null;
57
+ this.sourceMaps = sourceMaps ? new SourceMaps(this.classifier) : null;
58
+ }
59
+
60
+ /**
61
+ * @param {*} value - What was thrown (or rejected, or reported)
62
+ * @param {BuildOptions} options
63
+ * @returns {ExceptionRecord}
64
+ */
65
+ build(value, { mechanism, handled, fatal, environment, release = null, requestId = null, request = null, requestLine = null, user = null, jobRunId = null, job = null, tags, extra }) {
66
+ const thrown = ExceptionBuilder.describe(value);
67
+ const frames = this.frames(thrown, LIMITS.frames);
68
+ if (this.sources) this.sources.addTo(frames, LIMITS.contextFrames, this.sourceMaps && ((frame) => this.sourceMaps.linesOf(frame)));
69
+
70
+ const summary = {
71
+ name: thrown.name,
72
+ message: thrown.message,
73
+ code: thrown.code,
74
+ fingerprint: Fingerprint.of({ name: thrown.name, code: thrown.code, message: thrown.message, frames }),
75
+ mechanism,
76
+ location: locate(frames),
77
+ request: requestLine,
78
+ ...(job ? { job } : {}),
79
+ ...(user ? { user } : {})
80
+ };
81
+ const detail = {
82
+ stack: truncateStack(thrown.stack, LIMITS.stack),
83
+ frames,
84
+ causes: this.causes(value),
85
+ request,
86
+ process: processInfo(),
87
+ tags: cleanTags(tags),
88
+ extra: cleanExtra(extra)
89
+ };
90
+
91
+ return new ExceptionRecord({
92
+ id: randomUUID(),
93
+ occurredAt: Date.now(),
94
+ environment,
95
+ release,
96
+ handled: Boolean(handled),
97
+ fatal: Boolean(fatal),
98
+ requestId: requestId || null,
99
+ jobRunId: jobRunId || null,
100
+ summary,
101
+ detail
102
+ });
103
+ }
104
+
105
+ /**
106
+ * Frames of a described value, top frame first, mapped to their sources, without source lines.
107
+ *
108
+ * @param {{ stack: string, fullMessage: string, isError: boolean }} thrown - From describe()
109
+ * @param {number} limit
110
+ * @returns {import('./frame-classifier.js').Frame[]}
111
+ */
112
+ frames(thrown, limit) {
113
+ if (!thrown.isError) return [];
114
+ const frames = StackParser.parse(thrown.stack, { message: thrown.fullMessage, limit }).map((frame) => this.classifier.classify(frame));
115
+ return this.sourceMaps ? this.sourceMaps.apply(frames) : frames;
116
+ }
117
+
118
+ /**
119
+ * The error.cause chain, then the errors of an AggregateError.
120
+ *
121
+ * @param {*} value
122
+ * @returns {Object[]}
123
+ */
124
+ causes(value) {
125
+ const causes = [];
126
+ const seen = new Set([value]);
127
+
128
+ let current = value;
129
+ for (let depth = 0; depth < LIMITS.causeDepth && isObject(current); depth++) {
130
+ const cause = safeGet(current, 'cause');
131
+ if (cause === undefined || cause === null || seen.has(cause)) break;
132
+ seen.add(cause);
133
+ causes.push(this.cause(cause, 'cause'));
134
+ current = cause;
135
+ }
136
+
137
+ const errors = isObject(value) ? safeGet(value, 'errors') : undefined;
138
+ if (Array.isArray(errors)) {
139
+ for (const error of errors.slice(0, LIMITS.aggregateErrors)) {
140
+ causes.push(this.cause(error, 'aggregate'));
141
+ }
142
+ }
143
+ return causes;
144
+ }
145
+
146
+ /**
147
+ * One entry of `causes`: like the exception itself, with a shorter stack and no source lines.
148
+ *
149
+ * @param {*} value
150
+ * @param {'cause'|'aggregate'} relation
151
+ * @returns {Object}
152
+ */
153
+ cause(value, relation) {
154
+ const thrown = ExceptionBuilder.describe(value);
155
+ return {
156
+ relation,
157
+ name: thrown.name,
158
+ message: thrown.message,
159
+ code: thrown.code,
160
+ stack: truncateStack(thrown.stack, LIMITS.causeStack),
161
+ frames: this.frames(thrown, LIMITS.causeFrames)
162
+ };
163
+ }
164
+
165
+ /**
166
+ * The name, message, code and stack of a thrown value, bounded.
167
+ *
168
+ * Errors are recognized across realms (vm contexts, worker messages) and by shape, since
169
+ * libraries sometimes throw error-like objects that don't inherit from Error.
170
+ *
171
+ * @param {*} value
172
+ * @returns {{ name: string, message: string, fullMessage: string, code: string|null, stack: string,
173
+ * isError: boolean }} `fullMessage` is the message before it was cut, to find where the
174
+ * stack's frames start
175
+ */
176
+ static describe(value) {
177
+ if (isErrorLike(value)) {
178
+ const name = truncate(errorName(value), LIMITS.name);
179
+ const rawMessage = safeGet(value, 'message');
180
+ const fullMessage = typeof rawMessage === 'string' ? rawMessage : safeString(rawMessage);
181
+ const message = truncate(fullMessage, LIMITS.message);
182
+ const rawStack = safeGet(value, 'stack');
183
+ const stack = typeof rawStack === 'string' && rawStack !== '' ? rawStack : (message ? `${name}: ${message}` : name);
184
+ return { name, message, fullMessage, code: errorCode(safeGet(value, 'code')), stack, isError: true };
185
+ }
186
+
187
+ const message = preview(value);
188
+ let stack;
189
+ try {
190
+ stack = String(value);
191
+ } catch (error) {
192
+ stack = message;
193
+ }
194
+ return { name: THROWN_VALUE, message, fullMessage: message, code: null, stack: truncateBytes(stack, LIMITS.stack), isError: false };
195
+ }
196
+ }
197
+
198
+ /**
199
+ * The process an exception happened in.
200
+ *
201
+ * @returns {Object} pid, node, platform, arch, hostname, uptime (seconds), memory (bytes)
202
+ */
203
+ export function processInfo() {
204
+ let memory = { rss: 0, heapUsed: 0, heapTotal: 0 };
205
+ try {
206
+ const usage = process.memoryUsage();
207
+ memory = { rss: usage.rss, heapUsed: usage.heapUsed, heapTotal: usage.heapTotal };
208
+ } catch (error) {
209
+ // Some sandboxes refuse memory statistics; the rest still helps.
210
+ }
211
+ let hostname = '';
212
+ try {
213
+ hostname = os.hostname();
214
+ } catch (error) {
215
+ hostname = '';
216
+ }
217
+ return {
218
+ pid: process.pid,
219
+ node: process.version,
220
+ platform: process.platform,
221
+ arch: process.arch,
222
+ hostname,
223
+ uptime: Math.round(process.uptime() * 10) / 10,
224
+ memory
225
+ };
226
+ }
227
+
228
+ // Top in-app frame, else the top frame that has a file; null without frames.
229
+ function locate(frames) {
230
+ const frame = frames.find((item) => item.inApp) || frames.find((item) => item.file);
231
+ if (!frame) return null;
232
+ return { file: frame.relativeFile || frame.file, line: frame.line, column: frame.column, function: frame.function };
233
+ }
234
+
235
+ function isObject(value) {
236
+ return value !== null && (typeof value === 'object' || typeof value === 'function');
237
+ }
238
+
239
+ function isErrorLike(value) {
240
+ if (value === null || typeof value !== 'object') return false;
241
+ try {
242
+ if (types.isNativeError(value) || value instanceof Error) return true;
243
+ return typeof value.message === 'string' && typeof value.stack === 'string';
244
+ } catch (error) {
245
+ return false;
246
+ }
247
+ }
248
+
249
+ // error.name, unless it is the generic "Error" inherited by a subclass that didn't set its own
250
+ // name (class ValidationError extends Error {}): the class name says more.
251
+ function errorName(error) {
252
+ const name = safeGet(error, 'name');
253
+ const constructorName = safeConstructorName(error);
254
+ if (typeof name === 'string' && name !== '' && !(name === 'Error' && constructorName && constructorName !== 'Error' && constructorName !== 'Object')) {
255
+ return name;
256
+ }
257
+ return constructorName && constructorName !== 'Object' ? constructorName : (typeof name === 'string' && name) || 'Error';
258
+ }
259
+
260
+ function safeConstructorName(value) {
261
+ try {
262
+ const name = value.constructor && value.constructor.name;
263
+ return typeof name === 'string' ? name : '';
264
+ } catch (error) {
265
+ return '';
266
+ }
267
+ }
268
+
269
+ function errorCode(code) {
270
+ if (typeof code === 'string' && code !== '') return truncate(code, LIMITS.code);
271
+ if (typeof code === 'number' && Number.isFinite(code)) return String(code);
272
+ return null;
273
+ }
274
+
275
+ // A short, readable form of a thrown value that isn't an error.
276
+ function preview(value) {
277
+ if (typeof value === 'string') return truncate(value, LIMITS.valuePreview);
278
+ if (typeof value === 'function') return `[Function${value.name ? `: ${value.name}` : ''}]`;
279
+ if (value === null || typeof value !== 'object') return truncate(safeString(value), LIMITS.valuePreview);
280
+ const { json } = SafeJson.stringify(value, LIMITS.valuePreview);
281
+ return truncate(json === undefined ? safeString(value) : json, LIMITS.valuePreview);
282
+ }
283
+
284
+ function safeString(value) {
285
+ try {
286
+ return String(value);
287
+ } catch (error) {
288
+ try {
289
+ return Object.prototype.toString.call(value);
290
+ } catch (nested) {
291
+ return '[unprintable value]';
292
+ }
293
+ }
294
+ }
295
+
296
+ function safeGet(target, key) {
297
+ try {
298
+ return target[key];
299
+ } catch (error) {
300
+ return undefined;
301
+ }
302
+ }
303
+
304
+ // String tags only: at most 32, with bounded keys and values. Numbers and booleans are kept as
305
+ // text; objects are dropped (they belong in `extra`).
306
+ function cleanTags(tags) {
307
+ const clean = {};
308
+ if (!tags || typeof tags !== 'object') return clean;
309
+ let keys;
310
+ try {
311
+ keys = Object.keys(tags);
312
+ } catch (error) {
313
+ return clean;
314
+ }
315
+ let count = 0;
316
+ for (const key of keys) {
317
+ if (count >= LIMITS.tags) break;
318
+ const value = safeGet(tags, key);
319
+ if (typeof value !== 'string' && typeof value !== 'number' && typeof value !== 'boolean' && typeof value !== 'bigint') continue;
320
+ setOwn(clean, truncate(key, LIMITS.tagKey), truncate(String(value), LIMITS.tagValue));
321
+ count++;
322
+ }
323
+ return clean;
324
+ }
325
+
326
+ // JSON-safe `extra`; past 16 KB, only a preview of its JSON.
327
+ function cleanExtra(extra) {
328
+ if (extra === undefined) return null;
329
+ const { json, truncated } = SafeJson.stringify(extra, LIMITS.extra + 1);
330
+ if (json === undefined) return null;
331
+ if (truncated || Buffer.byteLength(json) > LIMITS.extra) {
332
+ return { truncated: true, preview: truncateBytes(json, LIMITS.extraPreview) };
333
+ }
334
+ try {
335
+ return JSON.parse(json);
336
+ } catch (error) {
337
+ return null;
338
+ }
339
+ }