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,282 @@
1
+ /**
2
+ * An append-only log of records in one folder, kept in segment files:
3
+ *
4
+ * 00000001.jsonl details, one JSON object per line
5
+ * 00000001.idx one summary per line, with the offset and length of its detail line
6
+ *
7
+ * Summaries are kept in memory, so lists never touch the disk; details are read on demand. The log
8
+ * keeps its newest `maxEntries` records: files rotate in segments, and the oldest segment is
9
+ * deleted once the rest still holds `maxEntries`. Deleting whole files keeps retention cheap:
10
+ * nothing is ever rewritten.
11
+ *
12
+ * Every record gets a sequence number. Numbers keep increasing after the log is cleared (the
13
+ * caller stores `seqBase`), so dashboards that poll with `after` stay in step.
14
+ *
15
+ * A project's requests and its exceptions are two such logs (see ProjectLog).
16
+ */
17
+
18
+ import fs from 'fs';
19
+ import path from 'path';
20
+
21
+ const SEGMENT_INDEX = /^(\d{8})\.idx$/;
22
+
23
+ /**
24
+ * One log of records with summaries and details.
25
+ */
26
+ export class SegmentLog {
27
+ /**
28
+ * Use SegmentLog.load() for a log that may have files already.
29
+ *
30
+ * @param {string} dir - The log's folder; created on the first append
31
+ * @param {Object} options
32
+ * @param {number} options.maxEntries - Records kept
33
+ * @param {number} options.segmentSize - Records per segment file
34
+ * @param {number} options.seqBase - Lowest sequence number the next record may get
35
+ * @param {(source: Object) => Object} options.pick - The summary of a record or index entry:
36
+ * only known fields, in a fixed order (index lines and API answers never carry anything else)
37
+ */
38
+ constructor(dir, { maxEntries, segmentSize, seqBase, pick }) {
39
+ this.dir = dir;
40
+ this.maxEntries = maxEntries;
41
+ this.segmentSize = segmentSize;
42
+ this.pick = pick;
43
+ // Summaries with their place on disk (seg, off, len), oldest first, sorted by seq.
44
+ this.entries = [];
45
+ this.byId = new Map();
46
+ // Numbers of the segment files on disk, oldest first, and the one being written.
47
+ this.segments = [];
48
+ this.segment = { no: 1, count: 0, size: 0 };
49
+ this.seqBase = seqBase;
50
+ this.nextSeq = seqBase;
51
+ }
52
+
53
+ /**
54
+ * Reads a log from its folder. Index lines cut short by a crash are skipped; a missing folder is
55
+ * an empty log.
56
+ *
57
+ * @param {string} dir
58
+ * @param {Object} options - See the constructor
59
+ * @returns {SegmentLog}
60
+ */
61
+ static load(dir, options) {
62
+ const log = new SegmentLog(dir, options);
63
+ let names;
64
+ try {
65
+ names = fs.readdirSync(dir);
66
+ } catch (error) {
67
+ if (error.code === 'ENOENT') return log;
68
+ throw error;
69
+ }
70
+
71
+ log.segments = names
72
+ .map((name) => SEGMENT_INDEX.exec(name))
73
+ .filter(Boolean)
74
+ .map((match) => Number(match[1]))
75
+ .sort((a, b) => a - b);
76
+
77
+ let maxSeq = 0;
78
+ for (const no of log.segments) {
79
+ const lines = fs.readFileSync(log._segmentFile(no, 'idx'), 'utf8').split('\n');
80
+ for (const line of lines) {
81
+ if (!line) continue;
82
+ let entry;
83
+ try {
84
+ entry = JSON.parse(line);
85
+ } catch (error) {
86
+ continue; // a line cut short by a crash
87
+ }
88
+ if (!entry || typeof entry.id !== 'string' || !Number.isInteger(entry.seq) || log.byId.has(entry.id)) continue;
89
+ entry.seg = no;
90
+ log.entries.push(entry);
91
+ log.byId.set(entry.id, entry);
92
+ maxSeq = Math.max(maxSeq, entry.seq);
93
+ }
94
+ }
95
+ log.entries.sort((a, b) => a.seq - b.seq);
96
+ log.nextSeq = Math.max(log.seqBase, maxSeq + 1);
97
+
98
+ const last = log.segments[log.segments.length - 1];
99
+ if (last) {
100
+ let size = 0;
101
+ try {
102
+ size = fs.statSync(log._segmentFile(last, 'jsonl')).size;
103
+ } catch (error) {
104
+ size = 0;
105
+ }
106
+ log.segment = { no: last, count: log.entries.filter((entry) => entry.seg === last).length, size };
107
+ }
108
+ return log;
109
+ }
110
+
111
+ /**
112
+ * Number of records kept.
113
+ *
114
+ * @type {number}
115
+ */
116
+ get size() {
117
+ return this.entries.length;
118
+ }
119
+
120
+ /**
121
+ * The newest record's summary entry, or null.
122
+ *
123
+ * @type {Object|null}
124
+ */
125
+ get newest() {
126
+ return this.entries[this.entries.length - 1] || null;
127
+ }
128
+
129
+ /**
130
+ * Stores new records. Records already stored (same id) are skipped, so retried batches are
131
+ * harmless.
132
+ *
133
+ * @param {Object[]} records - Validated records: summary fields plus `detail`
134
+ * @returns {Object[]} Summaries of the stored records
135
+ */
136
+ append(records) {
137
+ const added = [];
138
+ let data = [];
139
+ let index = [];
140
+ // One write per file and segment, rather than one per record.
141
+ const write = () => {
142
+ if (!data.length) return;
143
+ fs.mkdirSync(this.dir, { recursive: true, mode: 0o700 });
144
+ fs.appendFileSync(this._segmentFile(this.segment.no, 'jsonl'), data.join(''), { mode: 0o600 });
145
+ fs.appendFileSync(this._segmentFile(this.segment.no, 'idx'), index.join(''), { mode: 0o600 });
146
+ if (!this.segments.includes(this.segment.no)) this.segments.push(this.segment.no);
147
+ data = [];
148
+ index = [];
149
+ };
150
+
151
+ for (const record of records) {
152
+ if (this.byId.has(record.id)) continue;
153
+ if (this.segment.count >= this.segmentSize) {
154
+ write();
155
+ this.segment = { no: this.segment.no + 1, count: 0, size: 0 };
156
+ }
157
+
158
+ const line = JSON.stringify(record.detail ?? {});
159
+ const length = Buffer.byteLength(line);
160
+ const entry = {
161
+ ...this.pick({ ...record, seq: this.nextSeq++ }),
162
+ seg: this.segment.no,
163
+ off: this.segment.size,
164
+ len: length
165
+ };
166
+ this.segment.size += length + 1;
167
+ this.segment.count++;
168
+ data.push(`${line}\n`);
169
+ index.push(`${JSON.stringify(entry)}\n`);
170
+ this.entries.push(entry);
171
+ this.byId.set(entry.id, entry);
172
+ added.push(this.pick(entry));
173
+ }
174
+
175
+ write();
176
+ this.enforceRetention();
177
+ return added;
178
+ }
179
+
180
+ /**
181
+ * Summaries, newest first; oldest first when `after` is given (for polling new records).
182
+ *
183
+ * @param {{ after?: number, before?: number, limit: number }} query - Sequence numbers to page by
184
+ * @returns {{ entries: Object[], hasMore: boolean }}
185
+ */
186
+ list({ after, before, limit }) {
187
+ const entries = this.entries;
188
+
189
+ if (after !== undefined) {
190
+ const start = firstIndex(entries, (entry) => entry.seq > after);
191
+ return {
192
+ entries: entries.slice(start, start + limit).map(this.pick),
193
+ hasMore: start + limit < entries.length
194
+ };
195
+ }
196
+
197
+ const end = before === undefined ? entries.length : firstIndex(entries, (entry) => entry.seq >= before);
198
+ const start = Math.max(0, end - limit);
199
+ return {
200
+ entries: entries.slice(start, end).reverse().map(this.pick),
201
+ hasMore: start > 0
202
+ };
203
+ }
204
+
205
+ /**
206
+ * The detail of one record, read from disk.
207
+ *
208
+ * @param {string} id
209
+ * @returns {Object|null} null when it is no longer stored
210
+ */
211
+ detail(id) {
212
+ const entry = this.byId.get(id);
213
+ if (!entry) return null;
214
+
215
+ let fd;
216
+ try {
217
+ fd = fs.openSync(this._segmentFile(entry.seg, 'jsonl'), 'r');
218
+ } catch (error) {
219
+ if (error.code === 'ENOENT') return null;
220
+ throw error;
221
+ }
222
+ try {
223
+ const buffer = Buffer.alloc(entry.len);
224
+ const read = fs.readSync(fd, buffer, 0, entry.len, entry.off);
225
+ return JSON.parse(buffer.subarray(0, read).toString('utf8'));
226
+ } finally {
227
+ fs.closeSync(fd);
228
+ }
229
+ }
230
+
231
+ /**
232
+ * Deletes all records. Sequence numbers continue from where they were: the caller stores the
233
+ * new `seqBase`.
234
+ *
235
+ * @returns {void}
236
+ */
237
+ clear() {
238
+ for (const no of this.segments) this._deleteSegment(no);
239
+ this.segments = [];
240
+ this.entries = [];
241
+ this.byId.clear();
242
+ this.segment = { no: this.segment.no + 1, count: 0, size: 0 };
243
+ this.seqBase = this.nextSeq;
244
+ }
245
+
246
+ /**
247
+ * Deletes the oldest segments while the newer ones still hold `maxEntries` records.
248
+ *
249
+ * @returns {void}
250
+ */
251
+ enforceRetention() {
252
+ while (this.segments.length > 1) {
253
+ const oldest = this.segments[0];
254
+ const inOldest = firstIndex(this.entries, (entry) => entry.seg !== oldest);
255
+ if (this.entries.length - inOldest < this.maxEntries) break;
256
+ for (const entry of this.entries.splice(0, inOldest)) this.byId.delete(entry.id);
257
+ this._deleteSegment(oldest);
258
+ this.segments.shift();
259
+ }
260
+ }
261
+
262
+ _deleteSegment(no) {
263
+ fs.rmSync(this._segmentFile(no, 'jsonl'), { force: true });
264
+ fs.rmSync(this._segmentFile(no, 'idx'), { force: true });
265
+ }
266
+
267
+ _segmentFile(no, extension) {
268
+ return path.join(this.dir, `${String(no).padStart(8, '0')}.${extension}`);
269
+ }
270
+ }
271
+
272
+ // Binary search over entries sorted by seq: the first index where `predicate` holds.
273
+ function firstIndex(entries, predicate) {
274
+ let low = 0;
275
+ let high = entries.length;
276
+ while (low < high) {
277
+ const middle = (low + high) >>> 1;
278
+ if (predicate(entries[middle])) high = middle;
279
+ else low = middle + 1;
280
+ }
281
+ return low;
282
+ }