@saptools/cf-live-trace 0.1.6 → 0.1.8
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 +26 -7
- package/dist/cli.js +1216 -80
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +139 -7
- package/dist/index.js +1070 -67
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
// src/runtime-source.ts
|
|
2
2
|
var CF_LIVE_TRACE_GLOBAL_NAME = "__SAPTOOLS_CF_LIVE_TRACE__";
|
|
3
|
-
var CF_LIVE_TRACE_RUNTIME_VERSION =
|
|
3
|
+
var CF_LIVE_TRACE_RUNTIME_VERSION = 3;
|
|
4
4
|
var CF_LIVE_TRACE_RUNTIME_SOURCE = `
|
|
5
5
|
(() => {
|
|
6
6
|
const name = '${CF_LIVE_TRACE_GLOBAL_NAME}';
|
|
7
7
|
const runtimeVersion = ${String(CF_LIVE_TRACE_RUNTIME_VERSION)};
|
|
8
8
|
const existing = globalThis[name];
|
|
9
9
|
if (existing && typeof existing.version === 'number' && existing.version >= runtimeVersion) return existing;
|
|
10
|
+
let staleCleanup = null;
|
|
10
11
|
if (existing && typeof existing.uninstall === 'function') {
|
|
11
12
|
try {
|
|
12
|
-
existing.uninstall();
|
|
13
|
+
const cleanup = existing.uninstall();
|
|
14
|
+
if (cleanup && typeof cleanup.then === 'function') {
|
|
15
|
+
staleCleanup = Promise.resolve(cleanup).catch(() => undefined);
|
|
16
|
+
}
|
|
13
17
|
} catch {}
|
|
14
18
|
}
|
|
15
19
|
let BufferCtor = globalThis.Buffer;
|
|
@@ -22,7 +26,9 @@ var CF_LIVE_TRACE_RUNTIME_SOURCE = `
|
|
|
22
26
|
droppedCount: 0,
|
|
23
27
|
originals: {},
|
|
24
28
|
seen: new WeakSet(),
|
|
25
|
-
nextId: 1
|
|
29
|
+
nextId: 1,
|
|
30
|
+
nextDrainId: 1,
|
|
31
|
+
pendingDrain: null
|
|
26
32
|
};
|
|
27
33
|
const loadRequire = () => {
|
|
28
34
|
if (typeof require === 'function') return require;
|
|
@@ -55,13 +61,62 @@ var CF_LIVE_TRACE_RUNTIME_SOURCE = `
|
|
|
55
61
|
if (chunk instanceof Uint8Array && BufferCtor) return BufferCtor.from(chunk).toString('utf8');
|
|
56
62
|
return '';
|
|
57
63
|
};
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
if (
|
|
64
|
+
const chunkBuffer = (chunk) => {
|
|
65
|
+
if (!BufferCtor || chunk === undefined || chunk === null) return null;
|
|
66
|
+
if (BufferCtor.isBuffer(chunk)) return chunk;
|
|
67
|
+
if (typeof chunk === 'string' || chunk instanceof Uint8Array) return BufferCtor.from(chunk);
|
|
68
|
+
return null;
|
|
69
|
+
};
|
|
70
|
+
const textByteLength = (text) => BufferCtor ? BufferCtor.byteLength(text) : text.length;
|
|
71
|
+
const chunkByteLength = (chunk) => {
|
|
72
|
+
if (chunk === undefined || chunk === null) return 0;
|
|
73
|
+
if (BufferCtor && BufferCtor.isBuffer(chunk)) return chunk.length;
|
|
74
|
+
if (chunk instanceof Uint8Array) return chunk.byteLength;
|
|
75
|
+
return textByteLength(typeof chunk === 'string' ? chunk : '');
|
|
76
|
+
};
|
|
77
|
+
const truncateTextToBytes = (text, maxBytes) => {
|
|
78
|
+
if (!BufferCtor) return text.slice(0, maxBytes);
|
|
79
|
+
const encoded = BufferCtor.from(text);
|
|
80
|
+
if (encoded.length <= maxBytes) return text;
|
|
81
|
+
let boundary = Math.min(maxBytes, encoded.length);
|
|
82
|
+
while (boundary > 0 && encoded[boundary] !== undefined && (encoded[boundary] & 0xc0) === 0x80) {
|
|
83
|
+
boundary -= 1;
|
|
84
|
+
}
|
|
85
|
+
return encoded.subarray(0, boundary).toString('utf8');
|
|
86
|
+
};
|
|
87
|
+
const appendPreview = (chunks, currentBytes, chunk, enabled) => {
|
|
88
|
+
if (!enabled) return currentBytes;
|
|
89
|
+
const maxBytes = Math.floor(Number(state.options.maxBodyBytes) || 0);
|
|
90
|
+
if (maxBytes <= 0) return currentBytes;
|
|
91
|
+
const remaining = maxBytes - currentBytes;
|
|
92
|
+
if (remaining <= 0) return currentBytes;
|
|
93
|
+
const buffer = chunkBuffer(chunk);
|
|
94
|
+
if (buffer) {
|
|
95
|
+
const addition = buffer.subarray(0, remaining);
|
|
96
|
+
chunks.push(addition);
|
|
97
|
+
return currentBytes + addition.length;
|
|
98
|
+
}
|
|
61
99
|
const text = chunkText(chunk);
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
return
|
|
100
|
+
const addition = truncateTextToBytes(text, remaining);
|
|
101
|
+
chunks.push(addition);
|
|
102
|
+
return currentBytes + textByteLength(addition);
|
|
103
|
+
};
|
|
104
|
+
const completeUtf8Length = (buffer) => {
|
|
105
|
+
if (!buffer || buffer.length === 0) return 0;
|
|
106
|
+
let start = buffer.length - 1;
|
|
107
|
+
while (start > 0 && (buffer[start] & 0xc0) === 0x80) start -= 1;
|
|
108
|
+
const lead = buffer[start];
|
|
109
|
+
const expected = lead <= 0x7f ? 1 : lead <= 0xdf ? 2 : lead <= 0xef ? 3 : lead <= 0xf7 ? 4 : 1;
|
|
110
|
+
return buffer.length - start < expected ? start : buffer.length;
|
|
111
|
+
};
|
|
112
|
+
const previewText = (chunks, retainedBytes) => {
|
|
113
|
+
if (!BufferCtor) {
|
|
114
|
+
const text = chunks.join('');
|
|
115
|
+
return { text, bytes: textByteLength(text) };
|
|
116
|
+
}
|
|
117
|
+
const buffer = BufferCtor.concat(chunks, retainedBytes);
|
|
118
|
+
const completeBytes = completeUtf8Length(buffer);
|
|
119
|
+
return { text: buffer.subarray(0, completeBytes).toString('utf8'), bytes: completeBytes };
|
|
65
120
|
};
|
|
66
121
|
const enqueue = (event) => {
|
|
67
122
|
if (state.queue.length >= state.options.maxEvents) {
|
|
@@ -78,31 +133,30 @@ var CF_LIVE_TRACE_RUNTIME_SOURCE = `
|
|
|
78
133
|
const traceId = String(state.nextId++);
|
|
79
134
|
let requestBytes = 0;
|
|
80
135
|
let responseBytes = 0;
|
|
81
|
-
|
|
82
|
-
|
|
136
|
+
const requestPreviewChunks = [];
|
|
137
|
+
const responsePreviewChunks = [];
|
|
138
|
+
let requestPreviewBytes = 0;
|
|
139
|
+
let responsePreviewBytes = 0;
|
|
83
140
|
let finished = false;
|
|
84
141
|
const originalReqEmit = req.emit;
|
|
85
142
|
const originalWrite = res.write;
|
|
86
143
|
const originalEnd = res.end;
|
|
87
144
|
req.emit = function patchedReqEmit(eventName, ...args) {
|
|
88
145
|
if (eventName === 'data' && args[0] !== undefined) {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
requestPreview = appendPreview(requestPreview, args[0], state.options.captureRequestBody);
|
|
146
|
+
requestBytes += chunkByteLength(args[0]);
|
|
147
|
+
requestPreviewBytes = appendPreview(requestPreviewChunks, requestPreviewBytes, args[0], state.options.captureRequestBody);
|
|
92
148
|
}
|
|
93
149
|
return originalReqEmit.apply(this, [eventName, ...args]);
|
|
94
150
|
};
|
|
95
151
|
res.write = function patchedWrite(chunk, ...args) {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
responsePreview = appendPreview(responsePreview, chunk, state.options.captureResponseBody);
|
|
152
|
+
responseBytes += chunkByteLength(chunk);
|
|
153
|
+
responsePreviewBytes = appendPreview(responsePreviewChunks, responsePreviewBytes, chunk, state.options.captureResponseBody);
|
|
99
154
|
return originalWrite.apply(this, [chunk, ...args]);
|
|
100
155
|
};
|
|
101
156
|
res.end = function patchedEnd(chunk, ...args) {
|
|
102
157
|
if (chunk !== undefined) {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
responsePreview = appendPreview(responsePreview, chunk, state.options.captureResponseBody);
|
|
158
|
+
responseBytes += chunkByteLength(chunk);
|
|
159
|
+
responsePreviewBytes = appendPreview(responsePreviewChunks, responsePreviewBytes, chunk, state.options.captureResponseBody);
|
|
106
160
|
}
|
|
107
161
|
return originalEnd.apply(this, [chunk, ...args]);
|
|
108
162
|
};
|
|
@@ -113,6 +167,8 @@ var CF_LIVE_TRACE_RUNTIME_SOURCE = `
|
|
|
113
167
|
res.write = originalWrite;
|
|
114
168
|
res.end = originalEnd;
|
|
115
169
|
const rawUrl = initialUrl || String(req.url || '');
|
|
170
|
+
const requestPreview = previewText(requestPreviewChunks, requestPreviewBytes);
|
|
171
|
+
const responsePreview = previewText(responsePreviewChunks, responsePreviewBytes);
|
|
116
172
|
enqueue({
|
|
117
173
|
id: traceId,
|
|
118
174
|
timestamp: new Date().toISOString(),
|
|
@@ -127,10 +183,10 @@ var CF_LIVE_TRACE_RUNTIME_SOURCE = `
|
|
|
127
183
|
responseBytes,
|
|
128
184
|
requestHeaders: toHeaderRecord(req.headers),
|
|
129
185
|
responseHeaders: toHeaderRecord(typeof res.getHeaders === 'function' ? res.getHeaders() : {}),
|
|
130
|
-
requestBodyPreview: requestPreview,
|
|
131
|
-
responseBodyPreview: responsePreview,
|
|
132
|
-
requestBodyTruncated: state.options.maxBodyBytes > 0 &&
|
|
133
|
-
responseBodyTruncated: state.options.maxBodyBytes > 0 &&
|
|
186
|
+
requestBodyPreview: requestPreview.text,
|
|
187
|
+
responseBodyPreview: responsePreview.text,
|
|
188
|
+
requestBodyTruncated: state.options.captureRequestBody && state.options.maxBodyBytes > 0 && requestBytes > requestPreview.bytes,
|
|
189
|
+
responseBodyTruncated: state.options.captureResponseBody && state.options.maxBodyBytes > 0 && responseBytes > responsePreview.bytes,
|
|
134
190
|
droppedBeforeEvent: state.droppedCount,
|
|
135
191
|
traceId,
|
|
136
192
|
correlationId: req.headers && typeof req.headers['x-saptools-trace-id'] === 'string' ? req.headers['x-saptools-trace-id'] : null
|
|
@@ -154,10 +210,10 @@ var CF_LIVE_TRACE_RUNTIME_SOURCE = `
|
|
|
154
210
|
const numeric = Number(value);
|
|
155
211
|
return Number.isFinite(numeric) && numeric > 0 ? Math.floor(numeric) : 0;
|
|
156
212
|
};
|
|
157
|
-
const limitPreview = (event, previewKey, truncatedKey,
|
|
213
|
+
const limitPreview = (event, previewKey, truncatedKey, maxBytes) => {
|
|
158
214
|
const preview = event[previewKey];
|
|
159
|
-
if (
|
|
160
|
-
return { ...event, [previewKey]: preview
|
|
215
|
+
if (maxBytes <= 0 || typeof preview !== 'string' || textByteLength(preview) <= maxBytes) return event;
|
|
216
|
+
return { ...event, [previewKey]: truncateTextToBytes(preview, maxBytes), [truncatedKey]: true };
|
|
161
217
|
};
|
|
162
218
|
const eventForDrain = (event, maxChars) => {
|
|
163
219
|
if (!event || typeof event !== 'object') return event;
|
|
@@ -166,10 +222,44 @@ var CF_LIVE_TRACE_RUNTIME_SOURCE = `
|
|
|
166
222
|
output = limitPreview(output, 'responseBodyPreview', 'responseBodyTruncated', maxChars);
|
|
167
223
|
return output;
|
|
168
224
|
};
|
|
225
|
+
const acknowledgeDrain = (drainId) => {
|
|
226
|
+
const pending = state.pendingDrain;
|
|
227
|
+
if (!pending || drainId !== pending.id) return;
|
|
228
|
+
const drainedEvents = new Set(pending.sourceEvents);
|
|
229
|
+
state.queue = state.queue.filter((event) => !drainedEvents.has(event));
|
|
230
|
+
state.pendingDrain = null;
|
|
231
|
+
};
|
|
232
|
+
const drainResult = () => {
|
|
233
|
+
const pending = state.pendingDrain;
|
|
234
|
+
if (!pending) {
|
|
235
|
+
return { drainId: null, events: [], droppedCount: state.droppedCount, queueSize: state.queue.length };
|
|
236
|
+
}
|
|
237
|
+
return {
|
|
238
|
+
drainId: pending.id,
|
|
239
|
+
events: pending.events,
|
|
240
|
+
droppedCount: state.droppedCount,
|
|
241
|
+
queueSize: state.queue.length
|
|
242
|
+
};
|
|
243
|
+
};
|
|
169
244
|
const api = {
|
|
170
245
|
version: runtimeVersion,
|
|
171
246
|
async install(options) {
|
|
172
|
-
|
|
247
|
+
if (staleCleanup) {
|
|
248
|
+
await staleCleanup;
|
|
249
|
+
staleCleanup = null;
|
|
250
|
+
}
|
|
251
|
+
state.options = {
|
|
252
|
+
...state.options,
|
|
253
|
+
...options,
|
|
254
|
+
maxBodyBytes: toTransportLimit(options && options.maxBodyBytes),
|
|
255
|
+
maxEvents: Math.max(1, Math.floor(Number(options && options.maxEvents) || 1))
|
|
256
|
+
};
|
|
257
|
+
state.queue = [];
|
|
258
|
+
state.pendingDrain = null;
|
|
259
|
+
state.droppedCount = 0;
|
|
260
|
+
state.seen = new WeakSet();
|
|
261
|
+
state.nextId = 1;
|
|
262
|
+
state.nextDrainId = 1;
|
|
173
263
|
if (!state.installed) {
|
|
174
264
|
if (!BufferCtor) {
|
|
175
265
|
const bufferModule = await loadModule('buffer');
|
|
@@ -188,11 +278,19 @@ var CF_LIVE_TRACE_RUNTIME_SOURCE = `
|
|
|
188
278
|
state.enabled = false;
|
|
189
279
|
return api.status();
|
|
190
280
|
},
|
|
191
|
-
drainEvents(maxCount, maxTransportBodyBytes) {
|
|
281
|
+
drainEvents(maxCount, maxTransportBodyBytes, acknowledgedDrainId) {
|
|
282
|
+
acknowledgeDrain(acknowledgedDrainId);
|
|
283
|
+
if (state.pendingDrain) return drainResult();
|
|
192
284
|
const count = Math.max(0, Math.min(Number(maxCount) || 0, state.queue.length));
|
|
193
285
|
const transportLimit = toTransportLimit(maxTransportBodyBytes);
|
|
194
|
-
|
|
195
|
-
|
|
286
|
+
if (count === 0) return drainResult();
|
|
287
|
+
const sourceEvents = state.queue.slice(0, count);
|
|
288
|
+
state.pendingDrain = {
|
|
289
|
+
id: 'd' + String(state.nextDrainId++),
|
|
290
|
+
sourceEvents,
|
|
291
|
+
events: sourceEvents.map((event) => eventForDrain(event, transportLimit))
|
|
292
|
+
};
|
|
293
|
+
return drainResult();
|
|
196
294
|
},
|
|
197
295
|
status() {
|
|
198
296
|
return { installed: state.installed, enabled: state.enabled, queueSize: state.queue.length, droppedCount: state.droppedCount, maxEvents: state.options.maxEvents };
|
|
@@ -204,6 +302,8 @@ var CF_LIVE_TRACE_RUNTIME_SOURCE = `
|
|
|
204
302
|
if (state.originals.httpServerEmit && http && http.Server) http.Server.prototype.emit = state.originals.httpServerEmit;
|
|
205
303
|
if (state.originals.httpsServerEmit && https && https.Server) https.Server.prototype.emit = state.originals.httpsServerEmit;
|
|
206
304
|
state.installed = false;
|
|
305
|
+
state.queue = [];
|
|
306
|
+
state.pendingDrain = null;
|
|
207
307
|
return api.status();
|
|
208
308
|
}
|
|
209
309
|
};
|
|
@@ -214,37 +314,57 @@ var CF_LIVE_TRACE_RUNTIME_SOURCE = `
|
|
|
214
314
|
function buildInstallExpression(options) {
|
|
215
315
|
return `${CF_LIVE_TRACE_RUNTIME_SOURCE}.install(${JSON.stringify(options)})`;
|
|
216
316
|
}
|
|
217
|
-
function buildDrainExpression(maxCount, maxTransportBodyBytes) {
|
|
218
|
-
|
|
317
|
+
function buildDrainExpression(maxCount, maxTransportBodyBytes, acknowledgedDrainId = null) {
|
|
318
|
+
const acknowledgement = JSON.stringify(acknowledgedDrainId);
|
|
319
|
+
return `globalThis.${CF_LIVE_TRACE_GLOBAL_NAME}?.drainEvents(${String(maxCount)}, ${String(maxTransportBodyBytes)}, ${acknowledgement}) ?? { drainId: null, events: [], droppedCount: 0, queueSize: 0 }`;
|
|
219
320
|
}
|
|
220
321
|
function buildStopExpression(options) {
|
|
221
322
|
return options.uninstallRuntimeHook ? `globalThis.${CF_LIVE_TRACE_GLOBAL_NAME}?.uninstall() ?? { installed: false, enabled: false }` : `globalThis.${CF_LIVE_TRACE_GLOBAL_NAME}?.disable() ?? { installed: false, enabled: false }`;
|
|
222
323
|
}
|
|
223
324
|
|
|
224
325
|
// src/preview.ts
|
|
225
|
-
|
|
226
|
-
|
|
326
|
+
import { Buffer as Buffer2 } from "buffer";
|
|
327
|
+
function truncatePreview(preview, maxBytes) {
|
|
328
|
+
if (maxBytes <= 0) {
|
|
227
329
|
return { preview, truncated: false };
|
|
228
330
|
}
|
|
229
|
-
|
|
331
|
+
const encoded = Buffer2.from(preview);
|
|
332
|
+
if (encoded.length <= maxBytes) {
|
|
230
333
|
return { preview, truncated: false };
|
|
231
334
|
}
|
|
232
|
-
return {
|
|
335
|
+
return {
|
|
336
|
+
preview: encoded.subarray(0, utf8Boundary(encoded, maxBytes)).toString("utf8"),
|
|
337
|
+
truncated: true
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
function utf8Boundary(encoded, maxBytes) {
|
|
341
|
+
let boundary = Math.min(maxBytes, encoded.length);
|
|
342
|
+
while (boundary > 0 && isContinuationByte(encoded[boundary])) {
|
|
343
|
+
boundary -= 1;
|
|
344
|
+
}
|
|
345
|
+
return boundary;
|
|
346
|
+
}
|
|
347
|
+
function isContinuationByte(value) {
|
|
348
|
+
return value !== void 0 && (value & 192) === 128;
|
|
233
349
|
}
|
|
234
350
|
|
|
235
351
|
// src/payload.ts
|
|
236
352
|
var fallbackEventId = 0;
|
|
237
353
|
function parseDrainResult(payload, options) {
|
|
238
354
|
if (!isRecord(payload)) {
|
|
239
|
-
return { events: [], droppedCount: 0, queueSize: 0 };
|
|
355
|
+
return { drainId: null, events: [], droppedCount: 0, queueSize: 0 };
|
|
240
356
|
}
|
|
241
357
|
const rawEvents = Array.isArray(payload["events"]) ? payload["events"] : [];
|
|
242
358
|
return {
|
|
359
|
+
drainId: readDrainId(payload["drainId"]),
|
|
243
360
|
events: rawEvents.map((event) => parseRuntimeEvent(event, options)).filter((event) => event !== null),
|
|
244
361
|
droppedCount: readNonNegativeNumber(payload["droppedCount"]),
|
|
245
362
|
queueSize: readNonNegativeNumber(payload["queueSize"])
|
|
246
363
|
};
|
|
247
364
|
}
|
|
365
|
+
function readDrainId(value) {
|
|
366
|
+
return typeof value === "string" && /^d\d+$/.test(value) ? value : null;
|
|
367
|
+
}
|
|
248
368
|
function parseRuntimeEvent(payload, options) {
|
|
249
369
|
if (!isRecord(payload)) {
|
|
250
370
|
return null;
|
|
@@ -415,12 +535,815 @@ function toStatusBucket(status) {
|
|
|
415
535
|
return "unknown";
|
|
416
536
|
}
|
|
417
537
|
|
|
538
|
+
// src/trace-compact.ts
|
|
539
|
+
var COMPACT_BODY_PREVIEW_CHARS = 128;
|
|
540
|
+
var SENSITIVE_QUERY_KEYS = new Set([
|
|
541
|
+
"access_token",
|
|
542
|
+
"api_key",
|
|
543
|
+
"auth",
|
|
544
|
+
"authorization",
|
|
545
|
+
"client_secret",
|
|
546
|
+
"oauth_code",
|
|
547
|
+
"passwd",
|
|
548
|
+
"password",
|
|
549
|
+
"refresh_token",
|
|
550
|
+
"sig",
|
|
551
|
+
"signature",
|
|
552
|
+
"token"
|
|
553
|
+
].map(normalizeQueryKey));
|
|
554
|
+
function detectBodyFormat(body, headers) {
|
|
555
|
+
if (body.length === 0) {
|
|
556
|
+
return "empty";
|
|
557
|
+
}
|
|
558
|
+
const contentType = headerValue(headers, "content-type").toLowerCase();
|
|
559
|
+
if (contentType.includes("json")) {
|
|
560
|
+
return "json";
|
|
561
|
+
}
|
|
562
|
+
if (contentType.includes("html")) {
|
|
563
|
+
return "html";
|
|
564
|
+
}
|
|
565
|
+
if (contentType.includes("xml")) {
|
|
566
|
+
return "xml";
|
|
567
|
+
}
|
|
568
|
+
if (contentType.includes("x-www-form-urlencoded")) {
|
|
569
|
+
return "form";
|
|
570
|
+
}
|
|
571
|
+
if (contentType.includes("octet-stream")) {
|
|
572
|
+
return "binary";
|
|
573
|
+
}
|
|
574
|
+
return detectBodyFormatFromText(body, contentType);
|
|
575
|
+
}
|
|
576
|
+
function compactTraceEvent(record) {
|
|
577
|
+
const event = record.event;
|
|
578
|
+
const requestBody = compactBody(event.requestBodyPreview);
|
|
579
|
+
const responseBody = compactBody(event.responseBodyPreview);
|
|
580
|
+
return {
|
|
581
|
+
id: event.id,
|
|
582
|
+
sessionId: record.sessionId,
|
|
583
|
+
requestId: record.requestId,
|
|
584
|
+
timestamp: event.timestamp,
|
|
585
|
+
instance: event.instance,
|
|
586
|
+
method: event.method,
|
|
587
|
+
path: event.path,
|
|
588
|
+
url: redactSensitiveQueryValues(event.url),
|
|
589
|
+
normalizedUrl: redactSensitiveQueryValues(event.normalizedUrl),
|
|
590
|
+
status: event.status,
|
|
591
|
+
durationMs: event.durationMs,
|
|
592
|
+
requestBytes: event.requestBytes,
|
|
593
|
+
responseBytes: event.responseBytes,
|
|
594
|
+
requestBodyFormat: record.requestBodyFormat,
|
|
595
|
+
responseBodyFormat: record.responseBodyFormat,
|
|
596
|
+
requestBodyPreview: requestBody.preview,
|
|
597
|
+
requestBodyPreviewRemainingChars: requestBody.remainingChars,
|
|
598
|
+
responseBodyPreview: responseBody.preview,
|
|
599
|
+
responseBodyPreviewRemainingChars: responseBody.remainingChars,
|
|
600
|
+
requestBodyTruncated: event.requestBodyTruncated,
|
|
601
|
+
responseBodyTruncated: event.responseBodyTruncated,
|
|
602
|
+
droppedBeforeEvent: event.droppedBeforeEvent,
|
|
603
|
+
source: event.source,
|
|
604
|
+
traceId: event.traceId,
|
|
605
|
+
correlationId: event.correlationId
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
function headerValue(headers, name) {
|
|
609
|
+
const lowerName = name.toLowerCase();
|
|
610
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
611
|
+
if (key.toLowerCase() === lowerName) {
|
|
612
|
+
return value;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
return "";
|
|
616
|
+
}
|
|
617
|
+
function detectBodyFormatFromText(body, contentType) {
|
|
618
|
+
const trimmed = body.trim();
|
|
619
|
+
if (isJsonText(trimmed)) {
|
|
620
|
+
return "json";
|
|
621
|
+
}
|
|
622
|
+
if (/^<!doctype\s+html/i.test(trimmed) || /^<html(?:\s|>)/i.test(trimmed)) {
|
|
623
|
+
return "html";
|
|
624
|
+
}
|
|
625
|
+
if (/^<\?xml(?:\s|>)/i.test(trimmed) || /^<[A-Za-z_][\w:.-]*(?:\s|>|\/>)/.test(trimmed)) {
|
|
626
|
+
return "xml";
|
|
627
|
+
}
|
|
628
|
+
if (contentType.startsWith("text/")) {
|
|
629
|
+
return "text";
|
|
630
|
+
}
|
|
631
|
+
if (hasBinaryControlChars(body)) {
|
|
632
|
+
return "binary";
|
|
633
|
+
}
|
|
634
|
+
return "text";
|
|
635
|
+
}
|
|
636
|
+
function isJsonText(text) {
|
|
637
|
+
if (!text.startsWith("{") && !text.startsWith("[")) {
|
|
638
|
+
return false;
|
|
639
|
+
}
|
|
640
|
+
try {
|
|
641
|
+
JSON.parse(text);
|
|
642
|
+
return true;
|
|
643
|
+
} catch {
|
|
644
|
+
return false;
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
function hasBinaryControlChars(text) {
|
|
648
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
649
|
+
const code = text.charCodeAt(index);
|
|
650
|
+
if (code <= 8 || code === 11 || code === 12 || code >= 14 && code <= 31) {
|
|
651
|
+
return true;
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
return false;
|
|
655
|
+
}
|
|
656
|
+
function compactBody(body) {
|
|
657
|
+
let preview = "";
|
|
658
|
+
let length = 0;
|
|
659
|
+
for (const character of body) {
|
|
660
|
+
if (length < COMPACT_BODY_PREVIEW_CHARS) {
|
|
661
|
+
preview += character;
|
|
662
|
+
}
|
|
663
|
+
length += 1;
|
|
664
|
+
}
|
|
665
|
+
return {
|
|
666
|
+
preview,
|
|
667
|
+
remainingChars: Math.max(0, length - COMPACT_BODY_PREVIEW_CHARS)
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
function redactSensitiveQueryValues(rawUrl) {
|
|
671
|
+
const queryIndex = rawUrl.indexOf("?");
|
|
672
|
+
if (queryIndex < 0) {
|
|
673
|
+
return rawUrl;
|
|
674
|
+
}
|
|
675
|
+
const fragmentIndex = rawUrl.indexOf("#", queryIndex);
|
|
676
|
+
const queryEnd = fragmentIndex < 0 ? rawUrl.length : fragmentIndex;
|
|
677
|
+
const query = rawUrl.slice(queryIndex + 1, queryEnd);
|
|
678
|
+
const redacted = query.split("&").map(redactQueryPart).join("&");
|
|
679
|
+
return `${rawUrl.slice(0, queryIndex + 1)}${redacted}${rawUrl.slice(queryEnd)}`;
|
|
680
|
+
}
|
|
681
|
+
function redactQueryPart(part) {
|
|
682
|
+
const separatorIndex = part.indexOf("=");
|
|
683
|
+
const rawKey = separatorIndex < 0 ? part : part.slice(0, separatorIndex);
|
|
684
|
+
if (!SENSITIVE_QUERY_KEYS.has(normalizeQueryKey(rawKey))) {
|
|
685
|
+
return part;
|
|
686
|
+
}
|
|
687
|
+
return `${rawKey}=redacted`;
|
|
688
|
+
}
|
|
689
|
+
function normalizeQueryKey(rawKey) {
|
|
690
|
+
try {
|
|
691
|
+
return decodeURIComponent(rawKey).toLowerCase().replaceAll(/[^a-z0-9]/g, "");
|
|
692
|
+
} catch {
|
|
693
|
+
return rawKey.toLowerCase().replaceAll(/[^a-z0-9]/g, "");
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
// src/trace-inspect.ts
|
|
698
|
+
var DEFAULT_BODY_LIMIT = 4e3;
|
|
699
|
+
function inspectTraceBody(record, options) {
|
|
700
|
+
return inspectTraceBodyResult(record, options).rows;
|
|
701
|
+
}
|
|
702
|
+
function inspectTraceBodyResult(record, options) {
|
|
703
|
+
const limit = positive("limit", options.limit ?? DEFAULT_BODY_LIMIT);
|
|
704
|
+
const maxRows = positive("max rows", options.maxRows ?? Number.MAX_SAFE_INTEGER);
|
|
705
|
+
const pointer = options.path ?? "";
|
|
706
|
+
const parsed = parseJsonBody(bodyText(record, options.body));
|
|
707
|
+
const selected = resolvePointer(parsed, pointer);
|
|
708
|
+
const rows = inspectionRows(selected, pointer, limit, maxRows);
|
|
709
|
+
return {
|
|
710
|
+
rows: rows.values,
|
|
711
|
+
totalRows: rows.total,
|
|
712
|
+
rowsTruncated: rows.total > rows.values.length
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
function inspectionRows(selected, pointer, valueLimit, maxRows) {
|
|
716
|
+
if (Array.isArray(selected)) {
|
|
717
|
+
const values = selected.slice(0, maxRows).map(
|
|
718
|
+
(item, index) => inspectionRow(`${pointer}/${String(index)}`, item, valueLimit)
|
|
719
|
+
);
|
|
720
|
+
return { values, total: selected.length };
|
|
721
|
+
}
|
|
722
|
+
if (!isRecord2(selected)) {
|
|
723
|
+
return { values: [inspectionRow(pointer, selected, valueLimit)], total: 1 };
|
|
724
|
+
}
|
|
725
|
+
return objectInspectionRows(selected, pointer, valueLimit, maxRows);
|
|
726
|
+
}
|
|
727
|
+
function objectInspectionRows(selected, pointer, valueLimit, maxRows) {
|
|
728
|
+
const values = [];
|
|
729
|
+
let total = 0;
|
|
730
|
+
for (const key in selected) {
|
|
731
|
+
if (!Object.hasOwn(selected, key)) {
|
|
732
|
+
continue;
|
|
733
|
+
}
|
|
734
|
+
if (values.length < maxRows) {
|
|
735
|
+
values.push(inspectionRow(`${pointer}/${escapePointerToken(key)}`, selected[key], valueLimit));
|
|
736
|
+
}
|
|
737
|
+
total += 1;
|
|
738
|
+
}
|
|
739
|
+
return { values, total };
|
|
740
|
+
}
|
|
741
|
+
function searchTraceRecords(records, searchTerm, options) {
|
|
742
|
+
const term = searchTerm.trim().toLowerCase();
|
|
743
|
+
if (term.length === 0) {
|
|
744
|
+
throw new Error("search text must not be empty");
|
|
745
|
+
}
|
|
746
|
+
const limit = positive("limit", options.limit);
|
|
747
|
+
const previewLength = positive("preview length", options.previewLength ?? 128);
|
|
748
|
+
const matches = [];
|
|
749
|
+
for (const record of records) {
|
|
750
|
+
for (const side of selectedSides(options.body)) {
|
|
751
|
+
const remaining = limit - matches.length;
|
|
752
|
+
matches.push(...searchBody(record, side, term, remaining, previewLength));
|
|
753
|
+
if (matches.length >= limit) {
|
|
754
|
+
return matches;
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
return matches;
|
|
759
|
+
}
|
|
760
|
+
function searchBody(record, side, term, limit, previewLength) {
|
|
761
|
+
if (limit <= 0) {
|
|
762
|
+
return [];
|
|
763
|
+
}
|
|
764
|
+
const body = bodyText(record, side);
|
|
765
|
+
const parsed = tryParseJson(body);
|
|
766
|
+
return parsed === void 0 ? plainTextMatches(record, side, body, term, limit, previewLength) : jsonSearchMatches(record, side, parsed.value, term, limit, previewLength);
|
|
767
|
+
}
|
|
768
|
+
function plainTextMatches(record, side, text, term, limit, previewLength) {
|
|
769
|
+
const matches = [];
|
|
770
|
+
const lowerText = text.toLowerCase();
|
|
771
|
+
let offset = lowerText.indexOf(term);
|
|
772
|
+
while (offset >= 0 && matches.length < limit) {
|
|
773
|
+
const start = Math.max(0, offset - 32);
|
|
774
|
+
matches.push(toSearchMatch(record, side, "", text.slice(start, start + previewLength).replaceAll(/[\r\n\t]/g, " "), offset));
|
|
775
|
+
offset = lowerText.indexOf(term, offset + Math.max(1, term.length));
|
|
776
|
+
}
|
|
777
|
+
return matches;
|
|
778
|
+
}
|
|
779
|
+
function jsonSearchMatches(record, side, root, term, limit, previewLength) {
|
|
780
|
+
const matches = [];
|
|
781
|
+
const stack = [{ path: "", value: root }];
|
|
782
|
+
while (stack.length > 0 && matches.length < limit) {
|
|
783
|
+
const entry = stack.pop();
|
|
784
|
+
if (entry === void 0) {
|
|
785
|
+
break;
|
|
786
|
+
}
|
|
787
|
+
inspectJsonSearchEntry(record, side, entry, term, limit, previewLength, matches, stack);
|
|
788
|
+
}
|
|
789
|
+
return matches.slice(0, limit);
|
|
790
|
+
}
|
|
791
|
+
function inspectJsonSearchEntry(record, side, entry, term, limit, previewLength, matches, stack) {
|
|
792
|
+
if (typeof entry.value === "object" && entry.value !== null) {
|
|
793
|
+
pushJsonChildren(record, side, entry, term, limit, previewLength, matches, stack);
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
const text = jsonScalarSearchText(entry.value);
|
|
797
|
+
if (text.toLowerCase().includes(term)) {
|
|
798
|
+
matches.push(toSearchMatch(record, side, entry.path, text.slice(0, previewLength)));
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
function pushJsonChildren(record, side, entry, term, limit, previewLength, matches, stack) {
|
|
802
|
+
if (!Array.isArray(entry.value) && !isRecord2(entry.value)) {
|
|
803
|
+
return;
|
|
804
|
+
}
|
|
805
|
+
const entries = Array.isArray(entry.value) ? entry.value.map((item, index) => [String(index), item]) : Object.entries(entry.value);
|
|
806
|
+
for (const [key, value] of entries.reverse()) {
|
|
807
|
+
if (matches.length >= limit) {
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
810
|
+
const path = `${entry.path}/${escapePointerToken(key)}`;
|
|
811
|
+
if (key.toLowerCase().includes(term)) {
|
|
812
|
+
matches.push(toSearchMatch(record, side, path, jsonValueText(value, previewLength)));
|
|
813
|
+
}
|
|
814
|
+
stack.push({ path, value });
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
function toSearchMatch(record, side, path, preview, offset) {
|
|
818
|
+
return {
|
|
819
|
+
sessionId: record.sessionId,
|
|
820
|
+
requestId: record.requestId,
|
|
821
|
+
timestamp: record.event.timestamp,
|
|
822
|
+
method: record.event.method,
|
|
823
|
+
normalizedUrl: record.event.normalizedUrl,
|
|
824
|
+
status: record.event.status,
|
|
825
|
+
body: side,
|
|
826
|
+
path,
|
|
827
|
+
...offset === void 0 ? {} : { offset },
|
|
828
|
+
preview
|
|
829
|
+
};
|
|
830
|
+
}
|
|
831
|
+
function selectedSides(body) {
|
|
832
|
+
return body === "both" ? ["request", "response"] : [body];
|
|
833
|
+
}
|
|
834
|
+
function bodyText(record, body) {
|
|
835
|
+
return body === "request" ? record.event.requestBodyPreview : record.event.responseBodyPreview;
|
|
836
|
+
}
|
|
837
|
+
function parseJsonBody(body) {
|
|
838
|
+
try {
|
|
839
|
+
return JSON.parse(body);
|
|
840
|
+
} catch (error) {
|
|
841
|
+
throw new Error("Saved trace body does not contain valid JSON", { cause: error });
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
function tryParseJson(body) {
|
|
845
|
+
try {
|
|
846
|
+
return { value: JSON.parse(body) };
|
|
847
|
+
} catch {
|
|
848
|
+
return void 0;
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
function decodePointerToken(token) {
|
|
852
|
+
if (/~(?:[^01]|$)/.test(token)) {
|
|
853
|
+
throw new Error("Invalid JSON Pointer escape");
|
|
854
|
+
}
|
|
855
|
+
return token.replaceAll("~1", "/").replaceAll("~0", "~");
|
|
856
|
+
}
|
|
857
|
+
function pointerTokens(pointer) {
|
|
858
|
+
if (pointer === "") {
|
|
859
|
+
return [];
|
|
860
|
+
}
|
|
861
|
+
if (!pointer.startsWith("/")) {
|
|
862
|
+
throw new Error("JSON Pointer must be empty or start with /");
|
|
863
|
+
}
|
|
864
|
+
return pointer.slice(1).split("/").map(decodePointerToken);
|
|
865
|
+
}
|
|
866
|
+
function resolvePointer(root, pointer) {
|
|
867
|
+
let current = root;
|
|
868
|
+
for (const token of pointerTokens(pointer)) {
|
|
869
|
+
current = resolvePointerToken(current, token, pointer);
|
|
870
|
+
}
|
|
871
|
+
return current;
|
|
872
|
+
}
|
|
873
|
+
function resolvePointerToken(current, token, pointer) {
|
|
874
|
+
if (Array.isArray(current)) {
|
|
875
|
+
const index = Number(token);
|
|
876
|
+
if (!Number.isSafeInteger(index) || index < 0 || index >= current.length) {
|
|
877
|
+
throw new Error(`JSON Pointer path "${pointer}" not found`);
|
|
878
|
+
}
|
|
879
|
+
return current[index];
|
|
880
|
+
}
|
|
881
|
+
if (isRecord2(current) && Object.hasOwn(current, token)) {
|
|
882
|
+
return current[token];
|
|
883
|
+
}
|
|
884
|
+
throw new Error(`JSON Pointer path "${pointer}" not found`);
|
|
885
|
+
}
|
|
886
|
+
function escapePointerToken(token) {
|
|
887
|
+
return token.replaceAll("~", "~0").replaceAll("/", "~1");
|
|
888
|
+
}
|
|
889
|
+
function inspectionRow(path, value, limit) {
|
|
890
|
+
return { path, type: jsonType(value), value: jsonValueText(value, limit) };
|
|
891
|
+
}
|
|
892
|
+
function jsonType(value) {
|
|
893
|
+
if (value === null) {
|
|
894
|
+
return "null";
|
|
895
|
+
}
|
|
896
|
+
if (Array.isArray(value)) {
|
|
897
|
+
return "array";
|
|
898
|
+
}
|
|
899
|
+
return typeof value;
|
|
900
|
+
}
|
|
901
|
+
function jsonValueText(value, limit) {
|
|
902
|
+
if (Array.isArray(value)) {
|
|
903
|
+
return `items=${String(value.length)}`;
|
|
904
|
+
}
|
|
905
|
+
if (typeof value === "object" && value !== null) {
|
|
906
|
+
return `keys=${String(Object.keys(value).length)}`;
|
|
907
|
+
}
|
|
908
|
+
if (typeof value === "string") {
|
|
909
|
+
return value.slice(0, limit);
|
|
910
|
+
}
|
|
911
|
+
if (value === null) {
|
|
912
|
+
return "null";
|
|
913
|
+
}
|
|
914
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
915
|
+
return String(value);
|
|
916
|
+
}
|
|
917
|
+
return "";
|
|
918
|
+
}
|
|
919
|
+
function jsonScalarSearchText(value) {
|
|
920
|
+
if (typeof value === "string") {
|
|
921
|
+
return value;
|
|
922
|
+
}
|
|
923
|
+
if (value === null) {
|
|
924
|
+
return "null";
|
|
925
|
+
}
|
|
926
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
927
|
+
return String(value);
|
|
928
|
+
}
|
|
929
|
+
return "";
|
|
930
|
+
}
|
|
931
|
+
function positive(name, value) {
|
|
932
|
+
if (!Number.isSafeInteger(value) || value <= 0) {
|
|
933
|
+
throw new Error(`${name} must be a positive safe integer`);
|
|
934
|
+
}
|
|
935
|
+
return value;
|
|
936
|
+
}
|
|
937
|
+
function isRecord2(value) {
|
|
938
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
// src/trace-store.ts
|
|
942
|
+
import { createHash, randomBytes } from "crypto";
|
|
943
|
+
import { access, mkdir, readFile, readdir, rename, rm, writeFile } from "fs/promises";
|
|
944
|
+
import { homedir } from "os";
|
|
945
|
+
import { join } from "path";
|
|
946
|
+
var SAPTOOLS_DIR_NAME = ".saptools";
|
|
947
|
+
var CF_LIVE_TRACE_DIR_NAME = "cf-live-trace";
|
|
948
|
+
var SESSIONS_DIR_NAME = "sessions";
|
|
949
|
+
var EVENTS_DIR_NAME = "events";
|
|
950
|
+
var MANIFEST_FILE_NAME = "manifest.json";
|
|
951
|
+
var TRACE_TTL_MS = 2 * 60 * 60 * 1e3;
|
|
952
|
+
var MAX_TARGET_SLUG_BYTES = 160;
|
|
953
|
+
var SESSION_ID_PATTERN = /^s(?:[0-9a-f]{8}|[0-9a-f]{16})$/;
|
|
954
|
+
var REQUEST_ID_PATTERN = /^r(?:[0-9a-f]{8}|[0-9a-f]{16})$/;
|
|
955
|
+
function traceSessionsRoot(saptoolsRoot) {
|
|
956
|
+
return join(saptoolsRoot ?? join(homedir(), SAPTOOLS_DIR_NAME), CF_LIVE_TRACE_DIR_NAME, SESSIONS_DIR_NAME);
|
|
957
|
+
}
|
|
958
|
+
async function createTraceSession(input, options = {}) {
|
|
959
|
+
await pruneTraceSessions(options);
|
|
960
|
+
const sessionId = resolveSessionId(options.sessionId);
|
|
961
|
+
const createdAt = (options.now?.() ?? /* @__PURE__ */ new Date()).toISOString();
|
|
962
|
+
const target = toTargetIdentity(input.target);
|
|
963
|
+
const directory = sessionDirectory(sessionId, options.saptoolsRoot);
|
|
964
|
+
const eventsDirectory2 = join(directory, EVENTS_DIR_NAME);
|
|
965
|
+
const manifestPath2 = join(directory, MANIFEST_FILE_NAME);
|
|
966
|
+
await mkdir(eventsDirectory2, { recursive: true, mode: 448 });
|
|
967
|
+
await writeJsonFile(manifestPath2, createManifest(sessionId, createdAt, target));
|
|
968
|
+
return { sessionId, createdAt, target, directory, eventsDirectory: eventsDirectory2, manifestPath: manifestPath2 };
|
|
969
|
+
}
|
|
970
|
+
async function writeTraceEvent(session, event, options = {}) {
|
|
971
|
+
const now = options.now?.() ?? /* @__PURE__ */ new Date();
|
|
972
|
+
const requestId = resolveRequestId(options.requestId?.());
|
|
973
|
+
await ensureSessionManifest(session);
|
|
974
|
+
const record = {
|
|
975
|
+
version: 1,
|
|
976
|
+
sessionId: session.sessionId,
|
|
977
|
+
requestId,
|
|
978
|
+
createdAt: now.toISOString(),
|
|
979
|
+
expiresAt: new Date(now.getTime() + TRACE_TTL_MS).toISOString(),
|
|
980
|
+
target: session.target,
|
|
981
|
+
requestBodyFormat: detectBodyFormat(event.requestBodyPreview, event.requestHeaders),
|
|
982
|
+
responseBodyFormat: detectBodyFormat(event.responseBodyPreview, event.responseHeaders),
|
|
983
|
+
event
|
|
984
|
+
};
|
|
985
|
+
const backupPath = join(session.eventsDirectory, eventFileName(record, now));
|
|
986
|
+
await mkdir(session.eventsDirectory, { recursive: true, mode: 448 });
|
|
987
|
+
await writeJsonFile(backupPath, record);
|
|
988
|
+
return { ...record, backupPath };
|
|
989
|
+
}
|
|
990
|
+
async function readTraceEvent(sessionId, requestId, options = {}) {
|
|
991
|
+
const resolvedSessionId = resolveSessionId(sessionId);
|
|
992
|
+
const resolvedRequestId = resolveRequestId(requestId);
|
|
993
|
+
const entries = await listEventFilePaths(resolvedSessionId, options.saptoolsRoot);
|
|
994
|
+
const candidates = entries.filter((path) => path.includes(`-${resolvedRequestId}-`));
|
|
995
|
+
for (const path of candidates) {
|
|
996
|
+
const record = await readStoredTraceEvent(path, resolvedSessionId);
|
|
997
|
+
if (record === void 0 || isExpired(record, options)) {
|
|
998
|
+
await rm(path, { force: true });
|
|
999
|
+
continue;
|
|
1000
|
+
}
|
|
1001
|
+
if (record.requestId === resolvedRequestId) {
|
|
1002
|
+
return record;
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
throw new Error("Saved trace request not found or expired");
|
|
1006
|
+
}
|
|
1007
|
+
async function listTraceEvents(sessionId, options = {}) {
|
|
1008
|
+
const records = [];
|
|
1009
|
+
await visitTraceEvents(sessionId, (record) => {
|
|
1010
|
+
records.push(record);
|
|
1011
|
+
return true;
|
|
1012
|
+
}, options);
|
|
1013
|
+
return records;
|
|
1014
|
+
}
|
|
1015
|
+
async function visitTraceEvents(sessionId, visitor, options = {}) {
|
|
1016
|
+
const resolvedSessionId = resolveSessionId(sessionId);
|
|
1017
|
+
const entries = await listEventFilePaths(resolvedSessionId, options.saptoolsRoot);
|
|
1018
|
+
for (const path of entries) {
|
|
1019
|
+
const record = await readStoredTraceEvent(path, resolvedSessionId);
|
|
1020
|
+
if (record === void 0 || isExpired(record, options)) {
|
|
1021
|
+
await rm(path, { force: true });
|
|
1022
|
+
continue;
|
|
1023
|
+
}
|
|
1024
|
+
if (!await visitor(record)) {
|
|
1025
|
+
return;
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
async function listTraceSessions(options = {}) {
|
|
1030
|
+
await pruneTraceSessions(options);
|
|
1031
|
+
const sessionIds = await listSessionIds(options.saptoolsRoot);
|
|
1032
|
+
const summaries = await Promise.all(sessionIds.map(async (sessionId) => await readSessionSummary(sessionId, options)));
|
|
1033
|
+
return summaries.filter((summary) => summary !== void 0).sort((left, right) => left.createdAt.localeCompare(right.createdAt));
|
|
1034
|
+
}
|
|
1035
|
+
async function pruneTraceSessions(options = {}) {
|
|
1036
|
+
const sessionIds = await listSessionIds(options.saptoolsRoot);
|
|
1037
|
+
const now = (options.now?.() ?? /* @__PURE__ */ new Date()).getTime();
|
|
1038
|
+
let removed = 0;
|
|
1039
|
+
for (const sessionId of sessionIds) {
|
|
1040
|
+
removed += await pruneSession(sessionId, now, options.saptoolsRoot);
|
|
1041
|
+
}
|
|
1042
|
+
return removed;
|
|
1043
|
+
}
|
|
1044
|
+
function toTargetIdentity(target) {
|
|
1045
|
+
return {
|
|
1046
|
+
...target.region === void 0 ? {} : { region: target.region },
|
|
1047
|
+
...target.apiEndpoint === void 0 ? {} : { apiEndpoint: target.apiEndpoint },
|
|
1048
|
+
org: target.org,
|
|
1049
|
+
space: target.space,
|
|
1050
|
+
app: target.app,
|
|
1051
|
+
instance: String(target.instanceIndex ?? 0)
|
|
1052
|
+
};
|
|
1053
|
+
}
|
|
1054
|
+
function createManifest(sessionId, createdAt, target) {
|
|
1055
|
+
return { version: 1, sessionId, createdAt, target };
|
|
1056
|
+
}
|
|
1057
|
+
async function ensureSessionManifest(session) {
|
|
1058
|
+
try {
|
|
1059
|
+
await access(session.manifestPath);
|
|
1060
|
+
return;
|
|
1061
|
+
} catch (error) {
|
|
1062
|
+
if (!isNodeError(error, "ENOENT")) {
|
|
1063
|
+
throw error;
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
await mkdir(session.eventsDirectory, { recursive: true, mode: 448 });
|
|
1067
|
+
await writeJsonFile(
|
|
1068
|
+
session.manifestPath,
|
|
1069
|
+
createManifest(session.sessionId, session.createdAt, session.target)
|
|
1070
|
+
);
|
|
1071
|
+
}
|
|
1072
|
+
function isExpired(record, options) {
|
|
1073
|
+
const now = (options.now?.() ?? /* @__PURE__ */ new Date()).getTime();
|
|
1074
|
+
return Date.parse(record.expiresAt) <= now;
|
|
1075
|
+
}
|
|
1076
|
+
async function readSessionSummary(sessionId, options) {
|
|
1077
|
+
const manifest = await readStoredManifest(manifestPath(sessionId, options.saptoolsRoot));
|
|
1078
|
+
if (manifest?.sessionId !== sessionId) {
|
|
1079
|
+
return void 0;
|
|
1080
|
+
}
|
|
1081
|
+
const eventCount = (await listEventFilePaths(sessionId, options.saptoolsRoot)).length;
|
|
1082
|
+
return {
|
|
1083
|
+
sessionId,
|
|
1084
|
+
createdAt: manifest.createdAt,
|
|
1085
|
+
target: manifest.target,
|
|
1086
|
+
eventCount,
|
|
1087
|
+
directory: sessionDirectory(sessionId, options.saptoolsRoot)
|
|
1088
|
+
};
|
|
1089
|
+
}
|
|
1090
|
+
async function pruneSession(sessionId, now, saptoolsRoot) {
|
|
1091
|
+
const manifest = await readStoredManifest(manifestPath(sessionId, saptoolsRoot));
|
|
1092
|
+
const eventPaths = await listEventFilePaths(sessionId, saptoolsRoot);
|
|
1093
|
+
let removed = 0;
|
|
1094
|
+
let remaining = 0;
|
|
1095
|
+
for (const path of eventPaths) {
|
|
1096
|
+
const expiresAt = eventPathExpiresAt(path) ?? await storedEventExpiresAt(path);
|
|
1097
|
+
if (expiresAt === void 0 || expiresAt <= now) {
|
|
1098
|
+
await rm(path, { force: true });
|
|
1099
|
+
removed += 1;
|
|
1100
|
+
} else {
|
|
1101
|
+
remaining += 1;
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
if (remaining === 0 && isManifestExpiredOrMissing(manifest, now)) {
|
|
1105
|
+
await rm(sessionDirectory(sessionId, saptoolsRoot), { recursive: true, force: true });
|
|
1106
|
+
}
|
|
1107
|
+
return removed;
|
|
1108
|
+
}
|
|
1109
|
+
async function storedEventExpiresAt(path) {
|
|
1110
|
+
const record = await readStoredTraceEvent(path);
|
|
1111
|
+
return record === void 0 ? void 0 : Date.parse(record.expiresAt);
|
|
1112
|
+
}
|
|
1113
|
+
function isManifestExpiredOrMissing(manifest, now) {
|
|
1114
|
+
if (manifest === void 0) {
|
|
1115
|
+
return true;
|
|
1116
|
+
}
|
|
1117
|
+
return Date.parse(manifest.createdAt) + TRACE_TTL_MS <= now;
|
|
1118
|
+
}
|
|
1119
|
+
async function listSessionIds(saptoolsRoot) {
|
|
1120
|
+
try {
|
|
1121
|
+
const entries = await readdir(traceSessionsRoot(saptoolsRoot), { withFileTypes: true });
|
|
1122
|
+
return entries.filter((entry) => entry.isDirectory() && SESSION_ID_PATTERN.test(entry.name)).map((entry) => entry.name);
|
|
1123
|
+
} catch (error) {
|
|
1124
|
+
if (isNodeError(error, "ENOENT")) {
|
|
1125
|
+
return [];
|
|
1126
|
+
}
|
|
1127
|
+
throw error;
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
async function listEventFilePaths(sessionId, saptoolsRoot) {
|
|
1131
|
+
try {
|
|
1132
|
+
const directory = eventsDirectory(sessionId, saptoolsRoot);
|
|
1133
|
+
const entries = await readdir(directory, { withFileTypes: true });
|
|
1134
|
+
return entries.filter((entry) => entry.isFile() && entry.name.endsWith(".json")).map((entry) => join(directory, entry.name)).sort(compareEventPaths);
|
|
1135
|
+
} catch (error) {
|
|
1136
|
+
if (isNodeError(error, "ENOENT")) {
|
|
1137
|
+
return [];
|
|
1138
|
+
}
|
|
1139
|
+
throw error;
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
function compareEventPaths(left, right) {
|
|
1143
|
+
const leftTimestamp = eventPathTimestamp(left);
|
|
1144
|
+
const rightTimestamp = eventPathTimestamp(right);
|
|
1145
|
+
return leftTimestamp.localeCompare(rightTimestamp) || left.localeCompare(right);
|
|
1146
|
+
}
|
|
1147
|
+
function eventPathTimestamp(path) {
|
|
1148
|
+
return /-(\d{8}T\d{9}Z)\.json$/.exec(path)?.[1] ?? "";
|
|
1149
|
+
}
|
|
1150
|
+
function eventPathExpiresAt(path) {
|
|
1151
|
+
const compact = eventPathTimestamp(path);
|
|
1152
|
+
if (compact.length === 0) {
|
|
1153
|
+
return void 0;
|
|
1154
|
+
}
|
|
1155
|
+
const iso = compact.replace(
|
|
1156
|
+
/^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})(\d{3})Z$/,
|
|
1157
|
+
"$1-$2-$3T$4:$5:$6.$7Z"
|
|
1158
|
+
);
|
|
1159
|
+
const createdAt = Date.parse(iso);
|
|
1160
|
+
return Number.isNaN(createdAt) ? void 0 : createdAt + TRACE_TTL_MS;
|
|
1161
|
+
}
|
|
1162
|
+
async function readStoredManifest(path) {
|
|
1163
|
+
const parsed = await readJsonFile(path);
|
|
1164
|
+
return parsed !== void 0 && isStoredManifest(parsed) ? parsed : void 0;
|
|
1165
|
+
}
|
|
1166
|
+
async function readStoredTraceEvent(path, expectedSessionId) {
|
|
1167
|
+
const parsed = await readJsonFile(path);
|
|
1168
|
+
if (parsed === void 0 || !isStoredTraceEvent(parsed)) {
|
|
1169
|
+
return void 0;
|
|
1170
|
+
}
|
|
1171
|
+
if (expectedSessionId !== void 0 && parsed.sessionId !== expectedSessionId) {
|
|
1172
|
+
return void 0;
|
|
1173
|
+
}
|
|
1174
|
+
return { ...parsed, backupPath: path };
|
|
1175
|
+
}
|
|
1176
|
+
async function readJsonFile(path) {
|
|
1177
|
+
let raw;
|
|
1178
|
+
try {
|
|
1179
|
+
raw = await readFile(path, "utf8");
|
|
1180
|
+
} catch (error) {
|
|
1181
|
+
if (isNodeError(error, "ENOENT")) {
|
|
1182
|
+
return void 0;
|
|
1183
|
+
}
|
|
1184
|
+
throw error;
|
|
1185
|
+
}
|
|
1186
|
+
try {
|
|
1187
|
+
return JSON.parse(raw);
|
|
1188
|
+
} catch (error) {
|
|
1189
|
+
if (!(error instanceof SyntaxError)) {
|
|
1190
|
+
throw error;
|
|
1191
|
+
}
|
|
1192
|
+
return void 0;
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
async function writeJsonFile(path, value) {
|
|
1196
|
+
const temporaryPath = `${path}.tmp-${process.pid.toString()}-${randomHex(4)}`;
|
|
1197
|
+
let renamed = false;
|
|
1198
|
+
try {
|
|
1199
|
+
await writeFile(temporaryPath, `${JSON.stringify(value, null, 2)}
|
|
1200
|
+
`, {
|
|
1201
|
+
encoding: "utf8",
|
|
1202
|
+
mode: 384
|
|
1203
|
+
});
|
|
1204
|
+
await rename(temporaryPath, path);
|
|
1205
|
+
renamed = true;
|
|
1206
|
+
} finally {
|
|
1207
|
+
if (!renamed) {
|
|
1208
|
+
await rm(temporaryPath, { force: true });
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
function eventFileName(record, now) {
|
|
1213
|
+
const parts = [
|
|
1214
|
+
targetSlug(record.target),
|
|
1215
|
+
record.sessionId,
|
|
1216
|
+
record.requestId,
|
|
1217
|
+
fileTimestamp(now)
|
|
1218
|
+
].join("-");
|
|
1219
|
+
return `${parts}.json`;
|
|
1220
|
+
}
|
|
1221
|
+
function targetSlug(target) {
|
|
1222
|
+
const regionOrApi = target.region ?? target.apiEndpoint ?? "api";
|
|
1223
|
+
const fullSlug = [regionOrApi, target.org, target.space, target.app].map(sanitizePathPart).join("-");
|
|
1224
|
+
if (Buffer.byteLength(fullSlug) <= MAX_TARGET_SLUG_BYTES) {
|
|
1225
|
+
return fullSlug;
|
|
1226
|
+
}
|
|
1227
|
+
const hash = createHash("sha256").update(fullSlug).digest("hex").slice(0, 12);
|
|
1228
|
+
const prefix = fullSlug.slice(0, MAX_TARGET_SLUG_BYTES - hash.length - 1).replace(/-+$/, "");
|
|
1229
|
+
return `${prefix}-${hash}`;
|
|
1230
|
+
}
|
|
1231
|
+
function sanitizePathPart(value) {
|
|
1232
|
+
const sanitized = value.trim().toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
1233
|
+
return sanitized.length === 0 ? "unknown" : sanitized;
|
|
1234
|
+
}
|
|
1235
|
+
function fileTimestamp(now) {
|
|
1236
|
+
return now.toISOString().replaceAll("-", "").replaceAll(":", "").replace(".", "");
|
|
1237
|
+
}
|
|
1238
|
+
function sessionDirectory(sessionId, saptoolsRoot) {
|
|
1239
|
+
return join(traceSessionsRoot(saptoolsRoot), sessionId);
|
|
1240
|
+
}
|
|
1241
|
+
function eventsDirectory(sessionId, saptoolsRoot) {
|
|
1242
|
+
return join(sessionDirectory(sessionId, saptoolsRoot), EVENTS_DIR_NAME);
|
|
1243
|
+
}
|
|
1244
|
+
function manifestPath(sessionId, saptoolsRoot) {
|
|
1245
|
+
return join(sessionDirectory(sessionId, saptoolsRoot), MANIFEST_FILE_NAME);
|
|
1246
|
+
}
|
|
1247
|
+
function resolveSessionId(value) {
|
|
1248
|
+
const sessionId = value ?? `s${randomHex(8)}`;
|
|
1249
|
+
if (!SESSION_ID_PATTERN.test(sessionId)) {
|
|
1250
|
+
throw new Error("Invalid trace session id");
|
|
1251
|
+
}
|
|
1252
|
+
return sessionId;
|
|
1253
|
+
}
|
|
1254
|
+
function resolveRequestId(value) {
|
|
1255
|
+
const requestId = value ?? `r${randomHex(8)}`;
|
|
1256
|
+
if (!REQUEST_ID_PATTERN.test(requestId)) {
|
|
1257
|
+
throw new Error("Invalid trace request id");
|
|
1258
|
+
}
|
|
1259
|
+
return requestId;
|
|
1260
|
+
}
|
|
1261
|
+
function randomHex(bytes) {
|
|
1262
|
+
return randomBytes(bytes).toString("hex");
|
|
1263
|
+
}
|
|
1264
|
+
function isStoredManifest(value) {
|
|
1265
|
+
return isRecord3(value) && value["version"] === 1 && isSessionId(value["sessionId"]) && isIsoTimestamp(value["createdAt"]) && isTraceTargetIdentity(value["target"]);
|
|
1266
|
+
}
|
|
1267
|
+
function isStoredTraceEvent(value) {
|
|
1268
|
+
return isRecord3(value) && value["version"] === 1 && isStoredEventMetadata(value) && isTraceTargetIdentity(value["target"]) && isTraceBodyFormat(value["requestBodyFormat"]) && isTraceBodyFormat(value["responseBodyFormat"]) && isLiveTraceEvent(value["event"]);
|
|
1269
|
+
}
|
|
1270
|
+
function isStoredEventMetadata(value) {
|
|
1271
|
+
return isSessionId(value["sessionId"]) && isRequestId(value["requestId"]) && isIsoTimestamp(value["createdAt"]) && isIsoTimestamp(value["expiresAt"]);
|
|
1272
|
+
}
|
|
1273
|
+
function isTraceTargetIdentity(value) {
|
|
1274
|
+
return isRecord3(value) && optionalString(value["region"]) && optionalString(value["apiEndpoint"]) && typeof value["org"] === "string" && typeof value["space"] === "string" && typeof value["app"] === "string" && typeof value["instance"] === "string";
|
|
1275
|
+
}
|
|
1276
|
+
function isLiveTraceEvent(value) {
|
|
1277
|
+
return isRecord3(value) && hasLiveTraceStrings(value) && hasLiveTraceMetrics(value) && isStringRecord(value["requestHeaders"]) && isStringRecord(value["responseHeaders"]) && typeof value["requestBodyTruncated"] === "boolean" && typeof value["responseBodyTruncated"] === "boolean" && value["source"] === "runtime-http" && optionalNullableString(value["correlationId"]);
|
|
1278
|
+
}
|
|
1279
|
+
function hasLiveTraceStrings(value) {
|
|
1280
|
+
const fields = [
|
|
1281
|
+
"id",
|
|
1282
|
+
"appId",
|
|
1283
|
+
"instance",
|
|
1284
|
+
"method",
|
|
1285
|
+
"path",
|
|
1286
|
+
"url",
|
|
1287
|
+
"normalizedUrl",
|
|
1288
|
+
"requestBodyPreview",
|
|
1289
|
+
"responseBodyPreview",
|
|
1290
|
+
"traceId"
|
|
1291
|
+
];
|
|
1292
|
+
return isIsoTimestamp(value["timestamp"]) && fields.every((field) => typeof value[field] === "string");
|
|
1293
|
+
}
|
|
1294
|
+
function hasLiveTraceMetrics(value) {
|
|
1295
|
+
return isNullableFiniteNumber(value["status"]) && isNullableNonNegativeNumber(value["durationMs"]) && isNonNegativeFiniteNumber(value["requestBytes"]) && isNonNegativeFiniteNumber(value["responseBytes"]) && isNonNegativeFiniteNumber(value["droppedBeforeEvent"]);
|
|
1296
|
+
}
|
|
1297
|
+
function isTraceBodyFormat(value) {
|
|
1298
|
+
return value === "empty" || value === "json" || value === "xml" || value === "html" || value === "form" || value === "text" || value === "binary" || value === "unknown";
|
|
1299
|
+
}
|
|
1300
|
+
function optionalString(value) {
|
|
1301
|
+
return value === void 0 || typeof value === "string";
|
|
1302
|
+
}
|
|
1303
|
+
function optionalNullableString(value) {
|
|
1304
|
+
return value === null || typeof value === "string";
|
|
1305
|
+
}
|
|
1306
|
+
function isStringRecord(value) {
|
|
1307
|
+
return isRecord3(value) && Object.values(value).every((entry) => typeof entry === "string");
|
|
1308
|
+
}
|
|
1309
|
+
function isSessionId(value) {
|
|
1310
|
+
return typeof value === "string" && SESSION_ID_PATTERN.test(value);
|
|
1311
|
+
}
|
|
1312
|
+
function isRequestId(value) {
|
|
1313
|
+
return typeof value === "string" && REQUEST_ID_PATTERN.test(value);
|
|
1314
|
+
}
|
|
1315
|
+
function isIsoTimestamp(value) {
|
|
1316
|
+
if (typeof value !== "string") {
|
|
1317
|
+
return false;
|
|
1318
|
+
}
|
|
1319
|
+
const parsed = new Date(value);
|
|
1320
|
+
return !Number.isNaN(parsed.getTime()) && parsed.toISOString() === value;
|
|
1321
|
+
}
|
|
1322
|
+
function isNullableFiniteNumber(value) {
|
|
1323
|
+
return value === null || isFiniteNumber(value);
|
|
1324
|
+
}
|
|
1325
|
+
function isNullableNonNegativeNumber(value) {
|
|
1326
|
+
return value === null || isNonNegativeFiniteNumber(value);
|
|
1327
|
+
}
|
|
1328
|
+
function isNonNegativeFiniteNumber(value) {
|
|
1329
|
+
return isFiniteNumber(value) && value >= 0;
|
|
1330
|
+
}
|
|
1331
|
+
function isFiniteNumber(value) {
|
|
1332
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
1333
|
+
}
|
|
1334
|
+
function isRecord3(value) {
|
|
1335
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1336
|
+
}
|
|
1337
|
+
function isNodeError(error, code) {
|
|
1338
|
+
return isRecord3(error) && error["code"] === code;
|
|
1339
|
+
}
|
|
1340
|
+
|
|
418
1341
|
// src/cf.ts
|
|
419
1342
|
import { execFile, spawn } from "child_process";
|
|
420
|
-
import { mkdtemp, rm } from "fs/promises";
|
|
1343
|
+
import { mkdtemp, rm as rm2 } from "fs/promises";
|
|
421
1344
|
import { connect as netConnect, createServer } from "net";
|
|
422
1345
|
import { tmpdir } from "os";
|
|
423
|
-
import { join } from "path";
|
|
1346
|
+
import { join as join2 } from "path";
|
|
424
1347
|
import { promisify } from "util";
|
|
425
1348
|
var execFileAsync = promisify(execFile);
|
|
426
1349
|
var CF_MAX_BUFFER_BYTES = 8 * 1024 * 1024;
|
|
@@ -654,7 +1577,7 @@ function formatCfError(args, error, redactor) {
|
|
|
654
1577
|
return redactor?.(message) ?? message;
|
|
655
1578
|
}
|
|
656
1579
|
function extractErrorDetail(error) {
|
|
657
|
-
if (!
|
|
1580
|
+
if (!isRecord4(error)) {
|
|
658
1581
|
return "";
|
|
659
1582
|
}
|
|
660
1583
|
const stderr = typeof error["stderr"] === "string" ? error["stderr"].trim() : "";
|
|
@@ -757,7 +1680,7 @@ function retryPortProbe(socket, deadline, attempt, finish, setRetryTimer) {
|
|
|
757
1680
|
}
|
|
758
1681
|
setRetryTimer(setTimeout(attempt, TUNNEL_READY_POLL_MS));
|
|
759
1682
|
}
|
|
760
|
-
function
|
|
1683
|
+
function isRecord4(value) {
|
|
761
1684
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
762
1685
|
}
|
|
763
1686
|
var INSPECTOR_SIGNAL_COMMAND = [
|
|
@@ -866,7 +1789,8 @@ var RUNTIME_QUEUE_SIZE = 1e3;
|
|
|
866
1789
|
var CONTROL_EVALUATE_TIMEOUT_MS = 5e3;
|
|
867
1790
|
var DRAIN_EVALUATE_TIMEOUT_MS = 1e4;
|
|
868
1791
|
var DRAIN_TIMEOUT_RETRY_LIMIT = 3;
|
|
869
|
-
var
|
|
1792
|
+
var MAX_DRAIN_EVALUATE_TIMEOUT_MS = 6e4;
|
|
1793
|
+
var DRAIN_BODY_BUDGET_BYTES = 8 * 1024 * 1024;
|
|
870
1794
|
var defaultDependencies = {
|
|
871
1795
|
prepareCfSession,
|
|
872
1796
|
ensureSshEnabled,
|
|
@@ -883,11 +1807,12 @@ var LiveTraceSession = class {
|
|
|
883
1807
|
}
|
|
884
1808
|
options;
|
|
885
1809
|
dependencies;
|
|
886
|
-
|
|
1810
|
+
summaryEvents = [];
|
|
887
1811
|
consecutiveDrainTimeouts = 0;
|
|
888
|
-
|
|
1812
|
+
drainTask;
|
|
889
1813
|
inspectorClient;
|
|
890
1814
|
pollTimer;
|
|
1815
|
+
pendingDrainAcknowledgement = null;
|
|
891
1816
|
state = "idle";
|
|
892
1817
|
stopRequested = false;
|
|
893
1818
|
tunnelHandle;
|
|
@@ -896,6 +1821,8 @@ var LiveTraceSession = class {
|
|
|
896
1821
|
return;
|
|
897
1822
|
}
|
|
898
1823
|
this.stopRequested = false;
|
|
1824
|
+
this.pendingDrainAcknowledgement = null;
|
|
1825
|
+
this.summaryEvents.splice(0);
|
|
899
1826
|
await this.startRuntimeTrace(resolveStartOptions(options));
|
|
900
1827
|
}
|
|
901
1828
|
async stop(options) {
|
|
@@ -981,36 +1908,65 @@ var LiveTraceSession = class {
|
|
|
981
1908
|
this.stopPolling();
|
|
982
1909
|
this.consecutiveDrainTimeouts = 0;
|
|
983
1910
|
this.pollTimer = this.dependencies.setInterval(() => {
|
|
984
|
-
|
|
1911
|
+
this.startDrain(maxBodyBytes);
|
|
985
1912
|
}, DRAIN_INTERVAL_MS);
|
|
986
1913
|
}
|
|
987
|
-
|
|
988
|
-
if (this.
|
|
1914
|
+
startDrain(maxBodyBytes) {
|
|
1915
|
+
if (this.drainTask !== void 0 || this.inspectorClient === void 0 || this.state !== "streaming") {
|
|
989
1916
|
return;
|
|
990
1917
|
}
|
|
991
|
-
|
|
1918
|
+
const task = this.drainTraceEvents(maxBodyBytes);
|
|
1919
|
+
this.drainTask = task;
|
|
1920
|
+
void task.then(
|
|
1921
|
+
() => {
|
|
1922
|
+
this.clearDrainTask(task);
|
|
1923
|
+
},
|
|
1924
|
+
() => {
|
|
1925
|
+
this.clearDrainTask(task);
|
|
1926
|
+
}
|
|
1927
|
+
);
|
|
1928
|
+
}
|
|
1929
|
+
clearDrainTask(task) {
|
|
1930
|
+
if (this.drainTask === task) {
|
|
1931
|
+
this.drainTask = void 0;
|
|
1932
|
+
}
|
|
1933
|
+
}
|
|
1934
|
+
async drainTraceEvents(maxBodyBytes) {
|
|
1935
|
+
const batchSize = resolveDrainBatchSize(maxBodyBytes);
|
|
992
1936
|
try {
|
|
993
|
-
const payload = await this.
|
|
994
|
-
buildDrainExpression(
|
|
995
|
-
|
|
1937
|
+
const payload = await this.requireInspector().evaluate(
|
|
1938
|
+
buildDrainExpression(
|
|
1939
|
+
batchSize,
|
|
1940
|
+
resolveDrainTransportBodyLimit(maxBodyBytes),
|
|
1941
|
+
this.pendingDrainAcknowledgement
|
|
1942
|
+
),
|
|
1943
|
+
resolveDrainEvaluateTimeout(maxBodyBytes, batchSize)
|
|
996
1944
|
);
|
|
997
1945
|
this.consecutiveDrainTimeouts = 0;
|
|
998
|
-
|
|
1946
|
+
try {
|
|
1947
|
+
await this.publishDrainedEvents(payload, maxBodyBytes);
|
|
1948
|
+
} catch (error) {
|
|
1949
|
+
await this.handleEventHandlerFailure(error);
|
|
1950
|
+
}
|
|
999
1951
|
} catch (error) {
|
|
1000
1952
|
await this.handleDrainFailure(error);
|
|
1001
|
-
} finally {
|
|
1002
|
-
this.drainInFlight = false;
|
|
1003
1953
|
}
|
|
1004
1954
|
}
|
|
1005
|
-
publishDrainedEvents(payload, maxBodyBytes) {
|
|
1955
|
+
async publishDrainedEvents(payload, maxBodyBytes) {
|
|
1006
1956
|
const drained = parseDrainResult(payload, { appId: this.options.target.app, maxBodyBytes });
|
|
1007
|
-
if (drained.events.length
|
|
1957
|
+
if (drained.events.length > 0) {
|
|
1958
|
+
await this.options.onEvents?.(drained.events);
|
|
1959
|
+
this.publishSummary(drained.events);
|
|
1960
|
+
}
|
|
1961
|
+
this.pendingDrainAcknowledgement = drained.drainId;
|
|
1962
|
+
}
|
|
1963
|
+
publishSummary(events) {
|
|
1964
|
+
if (this.options.onSummary === void 0) {
|
|
1008
1965
|
return;
|
|
1009
1966
|
}
|
|
1010
|
-
this.
|
|
1011
|
-
this.
|
|
1012
|
-
this.options.
|
|
1013
|
-
this.options.onSummary?.(buildUrlSummaries(this.events));
|
|
1967
|
+
this.summaryEvents.push(...events.map(toSummaryEvent));
|
|
1968
|
+
this.summaryEvents.splice(0, Math.max(0, this.summaryEvents.length - RUNTIME_QUEUE_SIZE));
|
|
1969
|
+
this.options.onSummary(buildUrlSummaries(this.summaryEvents));
|
|
1014
1970
|
}
|
|
1015
1971
|
async handleDrainFailure(error) {
|
|
1016
1972
|
if (isEvaluateTimeout(error)) {
|
|
@@ -1021,11 +1977,19 @@ var LiveTraceSession = class {
|
|
|
1021
1977
|
}
|
|
1022
1978
|
}
|
|
1023
1979
|
this.log(`Live Trace stream failed for ${this.options.target.app}: ${formatError(error)}`);
|
|
1024
|
-
await this.stopRuntimeTrace(false);
|
|
1980
|
+
await this.stopRuntimeTrace(false, false);
|
|
1025
1981
|
this.postState("error", "Runtime HTTP trace connection was lost.", false, true);
|
|
1026
1982
|
}
|
|
1027
|
-
async
|
|
1983
|
+
async handleEventHandlerFailure(error) {
|
|
1984
|
+
this.log(`Live Trace event handler failed for ${this.options.target.app}: ${formatError(error)}`);
|
|
1985
|
+
await this.stopRuntimeTrace(false, false);
|
|
1986
|
+
this.postState("error", "Trace event handler failed.", false, true);
|
|
1987
|
+
}
|
|
1988
|
+
async stopRuntimeTrace(uninstallRuntimeHook, waitForDrain = true) {
|
|
1028
1989
|
this.stopPolling();
|
|
1990
|
+
if (waitForDrain) {
|
|
1991
|
+
await this.drainTask;
|
|
1992
|
+
}
|
|
1029
1993
|
this.consecutiveDrainTimeouts = 0;
|
|
1030
1994
|
const uninstalled = await this.stopInspectorHook(uninstallRuntimeHook);
|
|
1031
1995
|
await this.closeInspectorClient();
|
|
@@ -1109,12 +2073,20 @@ var LiveTraceStartupError = class extends Error {
|
|
|
1109
2073
|
stateMessage;
|
|
1110
2074
|
};
|
|
1111
2075
|
function resolveStartOptions(options) {
|
|
2076
|
+
const maxBodyBytes = options.maxBodyBytes ?? 4096;
|
|
2077
|
+
if (!Number.isSafeInteger(maxBodyBytes) || maxBodyBytes <= 0) {
|
|
2078
|
+
throw new Error("maxBodyBytes must be a positive safe integer.");
|
|
2079
|
+
}
|
|
2080
|
+
const runtimeQueueSize = options.runtimeQueueSize ?? RUNTIME_QUEUE_SIZE;
|
|
2081
|
+
if (!Number.isSafeInteger(runtimeQueueSize) || runtimeQueueSize <= 0) {
|
|
2082
|
+
throw new Error("runtimeQueueSize must be a positive safe integer.");
|
|
2083
|
+
}
|
|
1112
2084
|
return {
|
|
1113
2085
|
captureHeaders: options.captureHeaders ?? true,
|
|
1114
2086
|
captureRequestBody: options.captureRequestBody ?? true,
|
|
1115
2087
|
captureResponseBody: options.captureResponseBody ?? true,
|
|
1116
|
-
maxBodyBytes
|
|
1117
|
-
runtimeQueueSize
|
|
2088
|
+
maxBodyBytes,
|
|
2089
|
+
runtimeQueueSize
|
|
1118
2090
|
};
|
|
1119
2091
|
}
|
|
1120
2092
|
function stopLateTunnel(tunnel) {
|
|
@@ -1123,7 +2095,16 @@ function stopLateTunnel(tunnel) {
|
|
|
1123
2095
|
}
|
|
1124
2096
|
}
|
|
1125
2097
|
function resolveDrainTransportBodyLimit(maxBodyBytes) {
|
|
1126
|
-
return maxBodyBytes > 0 ?
|
|
2098
|
+
return maxBodyBytes > 0 ? maxBodyBytes : 1;
|
|
2099
|
+
}
|
|
2100
|
+
function resolveDrainBatchSize(maxBodyBytes) {
|
|
2101
|
+
const perEventBytes = Math.max(1, maxBodyBytes * 2);
|
|
2102
|
+
return Math.max(1, Math.min(DRAIN_BATCH_SIZE, Math.floor(DRAIN_BODY_BUDGET_BYTES / perEventBytes)));
|
|
2103
|
+
}
|
|
2104
|
+
function resolveDrainEvaluateTimeout(maxBodyBytes, batchSize) {
|
|
2105
|
+
const estimatedBytes = Math.min(Number.MAX_SAFE_INTEGER, maxBodyBytes * 2 * batchSize);
|
|
2106
|
+
const extraMegabytes = Math.ceil(Math.max(0, estimatedBytes - 1e6) / 1e6);
|
|
2107
|
+
return Math.min(MAX_DRAIN_EVALUATE_TIMEOUT_MS, DRAIN_EVALUATE_TIMEOUT_MS + extraMegabytes * 5e3);
|
|
1127
2108
|
}
|
|
1128
2109
|
function isEvaluateTimeout(error) {
|
|
1129
2110
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -1146,6 +2127,15 @@ function formatError(error) {
|
|
|
1146
2127
|
const message = error instanceof Error ? error.message : String(error);
|
|
1147
2128
|
return message.trim().length > 0 ? message.trim() : "Unknown error";
|
|
1148
2129
|
}
|
|
2130
|
+
function toSummaryEvent(event) {
|
|
2131
|
+
return {
|
|
2132
|
+
...event,
|
|
2133
|
+
requestHeaders: {},
|
|
2134
|
+
responseHeaders: {},
|
|
2135
|
+
requestBodyPreview: "",
|
|
2136
|
+
responseBodyPreview: ""
|
|
2137
|
+
};
|
|
2138
|
+
}
|
|
1149
2139
|
export {
|
|
1150
2140
|
CF_LIVE_TRACE_GLOBAL_NAME,
|
|
1151
2141
|
CF_LIVE_TRACE_RUNTIME_SOURCE,
|
|
@@ -1157,13 +2147,26 @@ export {
|
|
|
1157
2147
|
buildInstallExpression,
|
|
1158
2148
|
buildStopExpression,
|
|
1159
2149
|
buildUrlSummaries,
|
|
2150
|
+
compactTraceEvent,
|
|
1160
2151
|
createSecretRedactor,
|
|
2152
|
+
createTraceSession,
|
|
2153
|
+
detectBodyFormat,
|
|
2154
|
+
inspectTraceBody,
|
|
2155
|
+
inspectTraceBodyResult,
|
|
2156
|
+
listTraceEvents,
|
|
2157
|
+
listTraceSessions,
|
|
1161
2158
|
normalizeEventUrl,
|
|
1162
2159
|
openInspectorTunnel,
|
|
1163
2160
|
parseDrainResult,
|
|
1164
2161
|
prepareCfSession,
|
|
2162
|
+
pruneTraceSessions,
|
|
2163
|
+
readTraceEvent,
|
|
2164
|
+
searchTraceRecords,
|
|
1165
2165
|
startNodeInspector,
|
|
2166
|
+
traceSessionsRoot,
|
|
1166
2167
|
truncatePreview,
|
|
1167
|
-
tryStartNodeInspector
|
|
2168
|
+
tryStartNodeInspector,
|
|
2169
|
+
visitTraceEvents,
|
|
2170
|
+
writeTraceEvent
|
|
1168
2171
|
};
|
|
1169
2172
|
//# sourceMappingURL=index.js.map
|