@saptools/cf-live-trace 0.1.5 → 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 +1267 -92
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +139 -7
- package/dist/index.js +1085 -76
- 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,25 +278,32 @@ 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 };
|
|
199
297
|
},
|
|
200
|
-
uninstall() {
|
|
298
|
+
async uninstall() {
|
|
201
299
|
state.enabled = false;
|
|
202
|
-
const
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
if (state.originals.httpServerEmit && http && http.Server) http.Server.prototype.emit = state.originals.httpServerEmit;
|
|
207
|
-
if (state.originals.httpsServerEmit && https && https.Server) https.Server.prototype.emit = state.originals.httpsServerEmit;
|
|
208
|
-
}
|
|
300
|
+
const http = await loadModule('http');
|
|
301
|
+
const https = await loadModule('https');
|
|
302
|
+
if (state.originals.httpServerEmit && http && http.Server) http.Server.prototype.emit = state.originals.httpServerEmit;
|
|
303
|
+
if (state.originals.httpsServerEmit && https && https.Server) https.Server.prototype.emit = state.originals.httpsServerEmit;
|
|
209
304
|
state.installed = false;
|
|
305
|
+
state.queue = [];
|
|
306
|
+
state.pendingDrain = null;
|
|
210
307
|
return api.status();
|
|
211
308
|
}
|
|
212
309
|
};
|
|
@@ -217,37 +314,57 @@ var CF_LIVE_TRACE_RUNTIME_SOURCE = `
|
|
|
217
314
|
function buildInstallExpression(options) {
|
|
218
315
|
return `${CF_LIVE_TRACE_RUNTIME_SOURCE}.install(${JSON.stringify(options)})`;
|
|
219
316
|
}
|
|
220
|
-
function buildDrainExpression(maxCount, maxTransportBodyBytes) {
|
|
221
|
-
|
|
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 }`;
|
|
222
320
|
}
|
|
223
321
|
function buildStopExpression(options) {
|
|
224
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 }`;
|
|
225
323
|
}
|
|
226
324
|
|
|
227
325
|
// src/preview.ts
|
|
228
|
-
|
|
229
|
-
|
|
326
|
+
import { Buffer as Buffer2 } from "buffer";
|
|
327
|
+
function truncatePreview(preview, maxBytes) {
|
|
328
|
+
if (maxBytes <= 0) {
|
|
230
329
|
return { preview, truncated: false };
|
|
231
330
|
}
|
|
232
|
-
|
|
331
|
+
const encoded = Buffer2.from(preview);
|
|
332
|
+
if (encoded.length <= maxBytes) {
|
|
233
333
|
return { preview, truncated: false };
|
|
234
334
|
}
|
|
235
|
-
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;
|
|
236
349
|
}
|
|
237
350
|
|
|
238
351
|
// src/payload.ts
|
|
239
352
|
var fallbackEventId = 0;
|
|
240
353
|
function parseDrainResult(payload, options) {
|
|
241
354
|
if (!isRecord(payload)) {
|
|
242
|
-
return { events: [], droppedCount: 0, queueSize: 0 };
|
|
355
|
+
return { drainId: null, events: [], droppedCount: 0, queueSize: 0 };
|
|
243
356
|
}
|
|
244
357
|
const rawEvents = Array.isArray(payload["events"]) ? payload["events"] : [];
|
|
245
358
|
return {
|
|
359
|
+
drainId: readDrainId(payload["drainId"]),
|
|
246
360
|
events: rawEvents.map((event) => parseRuntimeEvent(event, options)).filter((event) => event !== null),
|
|
247
361
|
droppedCount: readNonNegativeNumber(payload["droppedCount"]),
|
|
248
362
|
queueSize: readNonNegativeNumber(payload["queueSize"])
|
|
249
363
|
};
|
|
250
364
|
}
|
|
365
|
+
function readDrainId(value) {
|
|
366
|
+
return typeof value === "string" && /^d\d+$/.test(value) ? value : null;
|
|
367
|
+
}
|
|
251
368
|
function parseRuntimeEvent(payload, options) {
|
|
252
369
|
if (!isRecord(payload)) {
|
|
253
370
|
return null;
|
|
@@ -418,12 +535,815 @@ function toStatusBucket(status) {
|
|
|
418
535
|
return "unknown";
|
|
419
536
|
}
|
|
420
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
|
+
|
|
421
1341
|
// src/cf.ts
|
|
422
1342
|
import { execFile, spawn } from "child_process";
|
|
423
|
-
import { mkdtemp, rm } from "fs/promises";
|
|
1343
|
+
import { mkdtemp, rm as rm2 } from "fs/promises";
|
|
424
1344
|
import { connect as netConnect, createServer } from "net";
|
|
425
1345
|
import { tmpdir } from "os";
|
|
426
|
-
import { join } from "path";
|
|
1346
|
+
import { join as join2 } from "path";
|
|
427
1347
|
import { promisify } from "util";
|
|
428
1348
|
var execFileAsync = promisify(execFile);
|
|
429
1349
|
var CF_MAX_BUFFER_BYTES = 8 * 1024 * 1024;
|
|
@@ -521,6 +1441,8 @@ function spawnPortForward(params) {
|
|
|
521
1441
|
env: buildCfEnv(params.cfHomeDir),
|
|
522
1442
|
stdio: ["ignore", "pipe", "pipe"]
|
|
523
1443
|
});
|
|
1444
|
+
child.stdout.resume();
|
|
1445
|
+
child.stderr.resume();
|
|
524
1446
|
return {
|
|
525
1447
|
process: child,
|
|
526
1448
|
localPort: params.localPort,
|
|
@@ -655,14 +1577,18 @@ function formatCfError(args, error, redactor) {
|
|
|
655
1577
|
return redactor?.(message) ?? message;
|
|
656
1578
|
}
|
|
657
1579
|
function extractErrorDetail(error) {
|
|
658
|
-
if (!
|
|
1580
|
+
if (!isRecord4(error)) {
|
|
659
1581
|
return "";
|
|
660
1582
|
}
|
|
661
1583
|
const stderr = typeof error["stderr"] === "string" ? error["stderr"].trim() : "";
|
|
662
1584
|
if (stderr.length > 0) {
|
|
663
1585
|
return stderr;
|
|
664
1586
|
}
|
|
665
|
-
|
|
1587
|
+
const message = error["message"];
|
|
1588
|
+
if (typeof message === "string") {
|
|
1589
|
+
return message.trim();
|
|
1590
|
+
}
|
|
1591
|
+
return message instanceof Error ? message.message.trim() : "";
|
|
666
1592
|
}
|
|
667
1593
|
function formatErrorMessage(error) {
|
|
668
1594
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -754,7 +1680,7 @@ function retryPortProbe(socket, deadline, attempt, finish, setRetryTimer) {
|
|
|
754
1680
|
}
|
|
755
1681
|
setRetryTimer(setTimeout(attempt, TUNNEL_READY_POLL_MS));
|
|
756
1682
|
}
|
|
757
|
-
function
|
|
1683
|
+
function isRecord4(value) {
|
|
758
1684
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
759
1685
|
}
|
|
760
1686
|
var INSPECTOR_SIGNAL_COMMAND = [
|
|
@@ -863,7 +1789,8 @@ var RUNTIME_QUEUE_SIZE = 1e3;
|
|
|
863
1789
|
var CONTROL_EVALUATE_TIMEOUT_MS = 5e3;
|
|
864
1790
|
var DRAIN_EVALUATE_TIMEOUT_MS = 1e4;
|
|
865
1791
|
var DRAIN_TIMEOUT_RETRY_LIMIT = 3;
|
|
866
|
-
var
|
|
1792
|
+
var MAX_DRAIN_EVALUATE_TIMEOUT_MS = 6e4;
|
|
1793
|
+
var DRAIN_BODY_BUDGET_BYTES = 8 * 1024 * 1024;
|
|
867
1794
|
var defaultDependencies = {
|
|
868
1795
|
prepareCfSession,
|
|
869
1796
|
ensureSshEnabled,
|
|
@@ -880,11 +1807,12 @@ var LiveTraceSession = class {
|
|
|
880
1807
|
}
|
|
881
1808
|
options;
|
|
882
1809
|
dependencies;
|
|
883
|
-
|
|
1810
|
+
summaryEvents = [];
|
|
884
1811
|
consecutiveDrainTimeouts = 0;
|
|
885
|
-
|
|
1812
|
+
drainTask;
|
|
886
1813
|
inspectorClient;
|
|
887
1814
|
pollTimer;
|
|
1815
|
+
pendingDrainAcknowledgement = null;
|
|
888
1816
|
state = "idle";
|
|
889
1817
|
stopRequested = false;
|
|
890
1818
|
tunnelHandle;
|
|
@@ -893,11 +1821,16 @@ var LiveTraceSession = class {
|
|
|
893
1821
|
return;
|
|
894
1822
|
}
|
|
895
1823
|
this.stopRequested = false;
|
|
1824
|
+
this.pendingDrainAcknowledgement = null;
|
|
1825
|
+
this.summaryEvents.splice(0);
|
|
896
1826
|
await this.startRuntimeTrace(resolveStartOptions(options));
|
|
897
1827
|
}
|
|
898
1828
|
async stop(options) {
|
|
899
1829
|
this.stopRequested = true;
|
|
900
1830
|
if (!this.isRunning()) {
|
|
1831
|
+
if (this.state === "error" && options.reason === "error") {
|
|
1832
|
+
return;
|
|
1833
|
+
}
|
|
901
1834
|
this.postState("stopped", `Trace stopped (${options.reason}).`, false, false);
|
|
902
1835
|
return;
|
|
903
1836
|
}
|
|
@@ -975,36 +1908,65 @@ var LiveTraceSession = class {
|
|
|
975
1908
|
this.stopPolling();
|
|
976
1909
|
this.consecutiveDrainTimeouts = 0;
|
|
977
1910
|
this.pollTimer = this.dependencies.setInterval(() => {
|
|
978
|
-
|
|
1911
|
+
this.startDrain(maxBodyBytes);
|
|
979
1912
|
}, DRAIN_INTERVAL_MS);
|
|
980
1913
|
}
|
|
981
|
-
|
|
982
|
-
if (this.
|
|
1914
|
+
startDrain(maxBodyBytes) {
|
|
1915
|
+
if (this.drainTask !== void 0 || this.inspectorClient === void 0 || this.state !== "streaming") {
|
|
983
1916
|
return;
|
|
984
1917
|
}
|
|
985
|
-
|
|
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);
|
|
986
1936
|
try {
|
|
987
|
-
const payload = await this.
|
|
988
|
-
buildDrainExpression(
|
|
989
|
-
|
|
1937
|
+
const payload = await this.requireInspector().evaluate(
|
|
1938
|
+
buildDrainExpression(
|
|
1939
|
+
batchSize,
|
|
1940
|
+
resolveDrainTransportBodyLimit(maxBodyBytes),
|
|
1941
|
+
this.pendingDrainAcknowledgement
|
|
1942
|
+
),
|
|
1943
|
+
resolveDrainEvaluateTimeout(maxBodyBytes, batchSize)
|
|
990
1944
|
);
|
|
991
1945
|
this.consecutiveDrainTimeouts = 0;
|
|
992
|
-
|
|
1946
|
+
try {
|
|
1947
|
+
await this.publishDrainedEvents(payload, maxBodyBytes);
|
|
1948
|
+
} catch (error) {
|
|
1949
|
+
await this.handleEventHandlerFailure(error);
|
|
1950
|
+
}
|
|
993
1951
|
} catch (error) {
|
|
994
1952
|
await this.handleDrainFailure(error);
|
|
995
|
-
} finally {
|
|
996
|
-
this.drainInFlight = false;
|
|
997
1953
|
}
|
|
998
1954
|
}
|
|
999
|
-
publishDrainedEvents(payload, maxBodyBytes) {
|
|
1955
|
+
async publishDrainedEvents(payload, maxBodyBytes) {
|
|
1000
1956
|
const drained = parseDrainResult(payload, { appId: this.options.target.app, maxBodyBytes });
|
|
1001
|
-
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) {
|
|
1002
1965
|
return;
|
|
1003
1966
|
}
|
|
1004
|
-
this.
|
|
1005
|
-
this.
|
|
1006
|
-
this.options.
|
|
1007
|
-
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));
|
|
1008
1970
|
}
|
|
1009
1971
|
async handleDrainFailure(error) {
|
|
1010
1972
|
if (isEvaluateTimeout(error)) {
|
|
@@ -1015,11 +1977,19 @@ var LiveTraceSession = class {
|
|
|
1015
1977
|
}
|
|
1016
1978
|
}
|
|
1017
1979
|
this.log(`Live Trace stream failed for ${this.options.target.app}: ${formatError(error)}`);
|
|
1018
|
-
await this.stopRuntimeTrace(false);
|
|
1980
|
+
await this.stopRuntimeTrace(false, false);
|
|
1019
1981
|
this.postState("error", "Runtime HTTP trace connection was lost.", false, true);
|
|
1020
1982
|
}
|
|
1021
|
-
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) {
|
|
1022
1989
|
this.stopPolling();
|
|
1990
|
+
if (waitForDrain) {
|
|
1991
|
+
await this.drainTask;
|
|
1992
|
+
}
|
|
1023
1993
|
this.consecutiveDrainTimeouts = 0;
|
|
1024
1994
|
const uninstalled = await this.stopInspectorHook(uninstallRuntimeHook);
|
|
1025
1995
|
await this.closeInspectorClient();
|
|
@@ -1103,12 +2073,20 @@ var LiveTraceStartupError = class extends Error {
|
|
|
1103
2073
|
stateMessage;
|
|
1104
2074
|
};
|
|
1105
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
|
+
}
|
|
1106
2084
|
return {
|
|
1107
2085
|
captureHeaders: options.captureHeaders ?? true,
|
|
1108
2086
|
captureRequestBody: options.captureRequestBody ?? true,
|
|
1109
2087
|
captureResponseBody: options.captureResponseBody ?? true,
|
|
1110
|
-
maxBodyBytes
|
|
1111
|
-
runtimeQueueSize
|
|
2088
|
+
maxBodyBytes,
|
|
2089
|
+
runtimeQueueSize
|
|
1112
2090
|
};
|
|
1113
2091
|
}
|
|
1114
2092
|
function stopLateTunnel(tunnel) {
|
|
@@ -1117,7 +2095,16 @@ function stopLateTunnel(tunnel) {
|
|
|
1117
2095
|
}
|
|
1118
2096
|
}
|
|
1119
2097
|
function resolveDrainTransportBodyLimit(maxBodyBytes) {
|
|
1120
|
-
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);
|
|
1121
2108
|
}
|
|
1122
2109
|
function isEvaluateTimeout(error) {
|
|
1123
2110
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -1140,6 +2127,15 @@ function formatError(error) {
|
|
|
1140
2127
|
const message = error instanceof Error ? error.message : String(error);
|
|
1141
2128
|
return message.trim().length > 0 ? message.trim() : "Unknown error";
|
|
1142
2129
|
}
|
|
2130
|
+
function toSummaryEvent(event) {
|
|
2131
|
+
return {
|
|
2132
|
+
...event,
|
|
2133
|
+
requestHeaders: {},
|
|
2134
|
+
responseHeaders: {},
|
|
2135
|
+
requestBodyPreview: "",
|
|
2136
|
+
responseBodyPreview: ""
|
|
2137
|
+
};
|
|
2138
|
+
}
|
|
1143
2139
|
export {
|
|
1144
2140
|
CF_LIVE_TRACE_GLOBAL_NAME,
|
|
1145
2141
|
CF_LIVE_TRACE_RUNTIME_SOURCE,
|
|
@@ -1151,13 +2147,26 @@ export {
|
|
|
1151
2147
|
buildInstallExpression,
|
|
1152
2148
|
buildStopExpression,
|
|
1153
2149
|
buildUrlSummaries,
|
|
2150
|
+
compactTraceEvent,
|
|
1154
2151
|
createSecretRedactor,
|
|
2152
|
+
createTraceSession,
|
|
2153
|
+
detectBodyFormat,
|
|
2154
|
+
inspectTraceBody,
|
|
2155
|
+
inspectTraceBodyResult,
|
|
2156
|
+
listTraceEvents,
|
|
2157
|
+
listTraceSessions,
|
|
1155
2158
|
normalizeEventUrl,
|
|
1156
2159
|
openInspectorTunnel,
|
|
1157
2160
|
parseDrainResult,
|
|
1158
2161
|
prepareCfSession,
|
|
2162
|
+
pruneTraceSessions,
|
|
2163
|
+
readTraceEvent,
|
|
2164
|
+
searchTraceRecords,
|
|
1159
2165
|
startNodeInspector,
|
|
2166
|
+
traceSessionsRoot,
|
|
1160
2167
|
truncatePreview,
|
|
1161
|
-
tryStartNodeInspector
|
|
2168
|
+
tryStartNodeInspector,
|
|
2169
|
+
visitTraceEvents,
|
|
2170
|
+
writeTraceEvent
|
|
1162
2171
|
};
|
|
1163
2172
|
//# sourceMappingURL=index.js.map
|