@apifuse/provider-sdk 2.2.0-beta.11 → 2.2.0-beta.13
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/AUTHORING.md +238 -0
- package/CHANGELOG.md +14 -0
- package/README.md +44 -2
- package/bin/apifuse-pack-smoke.ts +14 -0
- package/bin/apifuse-pack-types.ts +40 -1
- package/bin/apifuse-record.ts +622 -57
- package/bin/apifuse-submit-check.ts +43 -10
- package/dist/config/loader.d.ts +9 -1
- package/dist/config/loader.js +9 -0
- package/dist/define.d.ts +2 -1
- package/dist/define.js +61 -3
- package/dist/errors.d.ts +5 -0
- package/dist/errors.js +15 -0
- package/dist/fixture-sanitization.d.ts +26 -0
- package/dist/fixture-sanitization.js +216 -0
- package/dist/index.d.ts +4 -3
- package/dist/index.js +2 -1
- package/dist/provider.d.ts +2 -1
- package/dist/provider.js +1 -0
- package/dist/runtime/http.js +86 -32
- package/dist/runtime/instrumentation.js +295 -9
- package/dist/runtime/native-network.d.ts +53 -0
- package/dist/runtime/native-network.js +477 -0
- package/dist/runtime/proxy-nodemaven.d.ts +14 -0
- package/dist/runtime/proxy-nodemaven.js +20 -2
- package/dist/runtime/request-options.d.ts +68 -1
- package/dist/runtime/request-options.js +548 -0
- package/dist/runtime/stealth.d.ts +3 -1
- package/dist/runtime/stealth.js +352 -86
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.js +1 -1
- package/dist/server/self-test-input-tokens.d.ts +2 -1
- package/dist/server/self-test-input-tokens.js +18 -14
- package/dist/stream-evidence.d.ts +74 -0
- package/dist/stream-evidence.js +785 -0
- package/dist/testing/index.d.ts +1 -1
- package/dist/testing/index.js +1 -1
- package/dist/testing/run.d.ts +32 -2
- package/dist/testing/run.js +451 -19
- package/dist/types.d.ts +201 -7
- package/package.json +3 -1
- package/src/config/loader.ts +22 -1
- package/src/define.ts +81 -3
- package/src/errors.ts +15 -0
- package/src/fixture-sanitization.ts +247 -0
- package/src/index.ts +45 -1
- package/src/provider.ts +37 -0
- package/src/runtime/http.ts +144 -38
- package/src/runtime/instrumentation.ts +424 -8
- package/src/runtime/native-network.ts +600 -0
- package/src/runtime/proxy-nodemaven.ts +37 -2
- package/src/runtime/request-options.ts +680 -1
- package/src/runtime/stealth.ts +420 -88
- package/src/server/index.ts +4 -1
- package/src/server/self-test-input-tokens.ts +29 -14
- package/src/stream-evidence.ts +988 -0
- package/src/testing/index.ts +9 -1
- package/src/testing/run.ts +608 -12
- package/src/types.ts +235 -7
|
@@ -1,4 +1,22 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
HttpStreamResponse,
|
|
3
|
+
ProviderContext,
|
|
4
|
+
RequestOptions,
|
|
5
|
+
SseMessage,
|
|
6
|
+
StealthSession,
|
|
7
|
+
StealthRedirectRunResult,
|
|
8
|
+
} from "../types.js";
|
|
9
|
+
import { readableBytes, readableLines, readableTextChunks } from "../stream.js";
|
|
10
|
+
import {
|
|
11
|
+
parseHttpRequestInvocation,
|
|
12
|
+
isSensitiveKey,
|
|
13
|
+
redactSensitiveError,
|
|
14
|
+
redactSensitiveText,
|
|
15
|
+
redactUrlQueryParams,
|
|
16
|
+
requestOptionsFromHttpInvocation,
|
|
17
|
+
serializeRequestUrl,
|
|
18
|
+
type SerializedRequestUrl,
|
|
19
|
+
} from "./request-options.js";
|
|
2
20
|
import {
|
|
3
21
|
type CreateTraceContextOptions,
|
|
4
22
|
createTraceContext,
|
|
@@ -15,6 +33,16 @@ export type InstrumentedProviderContext<T extends ProviderContext> = Omit<T, "tr
|
|
|
15
33
|
type InstrumentedNamespace = "http" | "stealth" | "browser" | "session" | "state";
|
|
16
34
|
|
|
17
35
|
const BROWSER_PAGE_METHODS = new Set(["goto", "fill", "click", "type", "waitForSelector"]);
|
|
36
|
+
const DIAGNOSTIC_BASE_URL = "http://apifuse-instrumentation.invalid";
|
|
37
|
+
|
|
38
|
+
type RequestDiagnostics = {
|
|
39
|
+
diagnosticUrlDegraded?: boolean;
|
|
40
|
+
requestId?: string;
|
|
41
|
+
serializedUrl?: SerializedRequestUrl;
|
|
42
|
+
sensitiveParamNames: readonly string[];
|
|
43
|
+
sensitiveValues: readonly string[];
|
|
44
|
+
traceUrl?: string;
|
|
45
|
+
};
|
|
18
46
|
|
|
19
47
|
function isThenable(value: unknown): value is PromiseLike<unknown> {
|
|
20
48
|
return (
|
|
@@ -92,12 +120,231 @@ function getUrl(
|
|
|
92
120
|
return undefined;
|
|
93
121
|
}
|
|
94
122
|
|
|
123
|
+
function requestOptionsForInvocation(
|
|
124
|
+
namespace: InstrumentedNamespace,
|
|
125
|
+
methodName: string,
|
|
126
|
+
args: readonly unknown[],
|
|
127
|
+
): RequestOptions | undefined {
|
|
128
|
+
if (namespace === "http") {
|
|
129
|
+
const invocation = parseHttpRequestInvocation(methodName, [...args]);
|
|
130
|
+
return invocation ? requestOptionsFromHttpInvocation(invocation) : undefined;
|
|
131
|
+
}
|
|
132
|
+
if (namespace !== "stealth" || methodName !== "fetch" || typeof args[0] !== "string") {
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
const options = args[1];
|
|
136
|
+
return options !== null && typeof options === "object" && !Array.isArray(options)
|
|
137
|
+
? (options as RequestOptions)
|
|
138
|
+
: undefined;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function fallbackSensitiveValues(options?: RequestOptions): readonly string[] {
|
|
142
|
+
const sensitiveParams = options?.sensitiveParams;
|
|
143
|
+
if (!sensitiveParams || typeof sensitiveParams !== "object") return [];
|
|
144
|
+
return Object.values(sensitiveParams).map(String);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function stripDiagnosticBase(url: string): string {
|
|
148
|
+
return url.startsWith(DIAGNOSTIC_BASE_URL) ? url.slice(DIAGNOSTIC_BASE_URL.length) || "/" : url;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function serializeDiagnosticUrl(
|
|
152
|
+
url: string,
|
|
153
|
+
options?: RequestOptions,
|
|
154
|
+
): { degraded: boolean; serializedUrl: SerializedRequestUrl } {
|
|
155
|
+
try {
|
|
156
|
+
return {
|
|
157
|
+
degraded: false,
|
|
158
|
+
serializedUrl: serializeRequestUrl(url, options?.params, options?.sensitiveParams),
|
|
159
|
+
};
|
|
160
|
+
} catch {
|
|
161
|
+
try {
|
|
162
|
+
const absoluteUrl = new URL(url, DIAGNOSTIC_BASE_URL).toString();
|
|
163
|
+
const serialized = serializeRequestUrl(
|
|
164
|
+
absoluteUrl,
|
|
165
|
+
options?.params,
|
|
166
|
+
options?.sensitiveParams,
|
|
167
|
+
);
|
|
168
|
+
return {
|
|
169
|
+
degraded: false,
|
|
170
|
+
serializedUrl: {
|
|
171
|
+
requestUrl: stripDiagnosticBase(serialized.requestUrl),
|
|
172
|
+
redactedUrl: stripDiagnosticBase(serialized.redactedUrl),
|
|
173
|
+
sensitiveValues: serialized.sensitiveValues,
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
} catch {
|
|
177
|
+
const sensitiveParamNames = Object.keys(options?.sensitiveParams ?? {});
|
|
178
|
+
const structural = redactUrlQueryParams(url, sensitiveParamNames);
|
|
179
|
+
return {
|
|
180
|
+
degraded: true,
|
|
181
|
+
serializedUrl: {
|
|
182
|
+
requestUrl: url,
|
|
183
|
+
redactedUrl: structural.redactedUrl,
|
|
184
|
+
sensitiveValues: [
|
|
185
|
+
...new Set([...fallbackSensitiveValues(options), ...structural.sensitiveValues]),
|
|
186
|
+
],
|
|
187
|
+
},
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function snapshotRequestDiagnostics(
|
|
194
|
+
namespace: InstrumentedNamespace,
|
|
195
|
+
methodName: string,
|
|
196
|
+
args: readonly unknown[],
|
|
197
|
+
): RequestDiagnostics {
|
|
198
|
+
const options = requestOptionsForInvocation(namespace, methodName, args);
|
|
199
|
+
const url = typeof args[0] === "string" ? args[0] : undefined;
|
|
200
|
+
const hasSensitiveParams = Boolean(
|
|
201
|
+
options?.sensitiveParams && Object.keys(options.sensitiveParams).length > 0,
|
|
202
|
+
);
|
|
203
|
+
const sensitiveParamNames = Object.keys(options?.sensitiveParams ?? {});
|
|
204
|
+
const diagnosticUrl =
|
|
205
|
+
hasSensitiveParams && url ? serializeDiagnosticUrl(url, options) : undefined;
|
|
206
|
+
const traceUrl =
|
|
207
|
+
hasSensitiveParams && url
|
|
208
|
+
? serializeDiagnosticUrl(url, { sensitiveParams: options?.sensitiveParams }).serializedUrl
|
|
209
|
+
.redactedUrl
|
|
210
|
+
: undefined;
|
|
211
|
+
return {
|
|
212
|
+
...(diagnosticUrl?.degraded ? { diagnosticUrlDegraded: true } : {}),
|
|
213
|
+
...(hasSensitiveParams ? { requestId: crypto.randomUUID() } : {}),
|
|
214
|
+
serializedUrl: diagnosticUrl?.serializedUrl,
|
|
215
|
+
sensitiveParamNames,
|
|
216
|
+
sensitiveValues:
|
|
217
|
+
diagnosticUrl?.serializedUrl.sensitiveValues ?? fallbackSensitiveValues(options),
|
|
218
|
+
traceUrl,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function sanitizeRequestError(error: unknown, diagnostics: RequestDiagnostics): unknown {
|
|
223
|
+
return redactSensitiveError(
|
|
224
|
+
error,
|
|
225
|
+
diagnostics.sensitiveValues,
|
|
226
|
+
diagnostics.serializedUrl?.requestUrl,
|
|
227
|
+
diagnostics.serializedUrl?.redactedUrl,
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function isHttpStreamResponse(value: unknown): value is HttpStreamResponse {
|
|
232
|
+
return (
|
|
233
|
+
typeof value === "object" &&
|
|
234
|
+
value !== null &&
|
|
235
|
+
"body" in value &&
|
|
236
|
+
value.body instanceof ReadableStream
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function instrumentHttpStreamConsumption(
|
|
241
|
+
value: HttpStreamResponse,
|
|
242
|
+
recorder: NonNullable<ReturnType<typeof getTraceRecorder>>,
|
|
243
|
+
args: unknown[],
|
|
244
|
+
diagnostics: RequestDiagnostics,
|
|
245
|
+
): HttpStreamResponse {
|
|
246
|
+
const source = value.body;
|
|
247
|
+
let reader: ReadableStreamDefaultReader<Uint8Array> | undefined;
|
|
248
|
+
const body = new ReadableStream<Uint8Array>(
|
|
249
|
+
{
|
|
250
|
+
async pull(controller) {
|
|
251
|
+
try {
|
|
252
|
+
reader ??= source.getReader();
|
|
253
|
+
const chunk = await reader.read();
|
|
254
|
+
if (chunk.done) {
|
|
255
|
+
controller.close();
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
controller.enqueue(chunk.value);
|
|
259
|
+
} catch (error) {
|
|
260
|
+
const sanitizedError = sanitizeRequestError(error, diagnostics);
|
|
261
|
+
try {
|
|
262
|
+
await recorder.runSpan(
|
|
263
|
+
"http.stream.consume",
|
|
264
|
+
() => {
|
|
265
|
+
throw sanitizedError;
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
onError: (spanError) =>
|
|
269
|
+
buildSpanAttributes("http", "stream", args, undefined, spanError, diagnostics),
|
|
270
|
+
},
|
|
271
|
+
);
|
|
272
|
+
} catch (recordedError) {
|
|
273
|
+
controller.error(recordedError);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
},
|
|
277
|
+
async cancel(reason) {
|
|
278
|
+
try {
|
|
279
|
+
await (reader ? reader.cancel(reason) : source.cancel(reason));
|
|
280
|
+
} catch (error) {
|
|
281
|
+
throw sanitizeRequestError(error, diagnostics);
|
|
282
|
+
}
|
|
283
|
+
},
|
|
284
|
+
},
|
|
285
|
+
{ highWaterMark: 0 },
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
const instrumented = {
|
|
289
|
+
...value,
|
|
290
|
+
body,
|
|
291
|
+
bytes: () => readableBytes(body),
|
|
292
|
+
textChunks: () => readableTextChunks(body),
|
|
293
|
+
lines: () => readableLines(body),
|
|
294
|
+
} satisfies HttpStreamResponse;
|
|
295
|
+
try {
|
|
296
|
+
Object.assign(value, instrumented);
|
|
297
|
+
return value;
|
|
298
|
+
} catch {
|
|
299
|
+
return instrumented;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function isAsyncIterable<T = unknown>(value: unknown): value is AsyncIterable<T> {
|
|
304
|
+
return (
|
|
305
|
+
typeof value === "object" &&
|
|
306
|
+
value !== null &&
|
|
307
|
+
Symbol.asyncIterator in value &&
|
|
308
|
+
typeof value[Symbol.asyncIterator] === "function"
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function instrumentHttpSseConsumption(
|
|
313
|
+
value: AsyncIterable<SseMessage>,
|
|
314
|
+
recorder: NonNullable<ReturnType<typeof getTraceRecorder>>,
|
|
315
|
+
args: unknown[],
|
|
316
|
+
diagnostics: RequestDiagnostics,
|
|
317
|
+
): AsyncIterable<SseMessage> {
|
|
318
|
+
const source = value;
|
|
319
|
+
return {
|
|
320
|
+
async *[Symbol.asyncIterator]() {
|
|
321
|
+
try {
|
|
322
|
+
for await (const event of source) yield event;
|
|
323
|
+
} catch (error) {
|
|
324
|
+
const sanitizedError = sanitizeRequestError(error, diagnostics);
|
|
325
|
+
return await recorder.runSpan(
|
|
326
|
+
"http.sse.consume",
|
|
327
|
+
() => {
|
|
328
|
+
throw sanitizedError;
|
|
329
|
+
},
|
|
330
|
+
{
|
|
331
|
+
onError: (spanError) =>
|
|
332
|
+
buildSpanAttributes("http", "sse", args, undefined, spanError, diagnostics),
|
|
333
|
+
},
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
},
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
95
340
|
function getMethod(
|
|
96
341
|
namespace: InstrumentedNamespace,
|
|
97
342
|
methodName: string,
|
|
98
343
|
args: unknown[],
|
|
99
344
|
): string | undefined {
|
|
100
345
|
if (namespace === "http") {
|
|
346
|
+
// Preserve the original instrumentation contract: generic entry points are
|
|
347
|
+
// reported as REQUEST/STREAM/SSE, independent of transport options.
|
|
101
348
|
return methodName.toUpperCase();
|
|
102
349
|
}
|
|
103
350
|
|
|
@@ -119,9 +366,19 @@ function buildSpanAttributes(
|
|
|
119
366
|
args: unknown[],
|
|
120
367
|
result?: unknown,
|
|
121
368
|
error?: unknown,
|
|
369
|
+
diagnostics: RequestDiagnostics = { sensitiveParamNames: [], sensitiveValues: [] },
|
|
122
370
|
): Record<string, string | number | boolean> {
|
|
123
371
|
const attributes: Record<string, string | number | boolean> = {};
|
|
124
|
-
const
|
|
372
|
+
const rawUrl = getUrl(namespace, args, result);
|
|
373
|
+
const structuralUrl = diagnostics.traceUrl ?? rawUrl;
|
|
374
|
+
const url = structuralUrl
|
|
375
|
+
? redactSensitiveText(
|
|
376
|
+
structuralUrl,
|
|
377
|
+
diagnostics.sensitiveValues,
|
|
378
|
+
diagnostics.serializedUrl?.requestUrl,
|
|
379
|
+
diagnostics.serializedUrl?.redactedUrl,
|
|
380
|
+
)
|
|
381
|
+
: undefined;
|
|
125
382
|
const method = getMethod(namespace, methodName, args);
|
|
126
383
|
const status = error ? getErrorStatus(error) : getResponseStatus(namespace, result);
|
|
127
384
|
const duration = error ? undefined : getResponseDuration(result);
|
|
@@ -129,6 +386,8 @@ function buildSpanAttributes(
|
|
|
129
386
|
if (url) {
|
|
130
387
|
attributes.url = url;
|
|
131
388
|
}
|
|
389
|
+
if (diagnostics.requestId) attributes.request_id = diagnostics.requestId;
|
|
390
|
+
if (diagnostics.diagnosticUrlDegraded) attributes.redaction_degraded = true;
|
|
132
391
|
|
|
133
392
|
if (method) {
|
|
134
393
|
attributes.method = method;
|
|
@@ -153,6 +412,64 @@ function buildSpanAttributes(
|
|
|
153
412
|
return attributes;
|
|
154
413
|
}
|
|
155
414
|
|
|
415
|
+
function isStealthRedirectRunResult(value: unknown): value is StealthRedirectRunResult {
|
|
416
|
+
return (
|
|
417
|
+
typeof value === "object" &&
|
|
418
|
+
value !== null &&
|
|
419
|
+
"reason" in value &&
|
|
420
|
+
typeof value.reason === "string" &&
|
|
421
|
+
"hops" in value &&
|
|
422
|
+
Array.isArray(value.hops) &&
|
|
423
|
+
"final" in value &&
|
|
424
|
+
typeof value.final === "object" &&
|
|
425
|
+
value.final !== null
|
|
426
|
+
);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function buildStealthRedirectAttributes(
|
|
430
|
+
args: unknown[],
|
|
431
|
+
result: unknown,
|
|
432
|
+
error: unknown,
|
|
433
|
+
diagnostics: RequestDiagnostics,
|
|
434
|
+
): Record<string, string | number | boolean> {
|
|
435
|
+
const attributes = buildSpanAttributes("stealth", "fetch", args, undefined, error, diagnostics);
|
|
436
|
+
if (!isStealthRedirectRunResult(result)) return attributes;
|
|
437
|
+
|
|
438
|
+
attributes.redirect_reason = result.reason;
|
|
439
|
+
attributes.redirect_hop_count = result.hops.length;
|
|
440
|
+
attributes.status = result.final.status;
|
|
441
|
+
if (result.hops.length > 0) {
|
|
442
|
+
const sensitiveValues = new Set(diagnostics.sensitiveValues);
|
|
443
|
+
const path = result.hops
|
|
444
|
+
.map((hop) => {
|
|
445
|
+
const hopUrl = hop.nextUrl ?? hop.url;
|
|
446
|
+
const responseSensitiveParamNames = [...queryParamNames(hopUrl)].filter(isSensitiveKey);
|
|
447
|
+
const structural = redactUrlQueryParams(hopUrl, [
|
|
448
|
+
...new Set([...diagnostics.sensitiveParamNames, ...responseSensitiveParamNames]),
|
|
449
|
+
]);
|
|
450
|
+
for (const value of structural.sensitiveValues) sensitiveValues.add(value);
|
|
451
|
+
const safeUrl = redactSensitiveText(structural.redactedUrl, [...sensitiveValues]);
|
|
452
|
+
return `${hop.method} ${hop.status} ${safeUrl}`;
|
|
453
|
+
})
|
|
454
|
+
.join(" -> ");
|
|
455
|
+
attributes.redirect_path = redactSensitiveText(
|
|
456
|
+
path,
|
|
457
|
+
[...sensitiveValues],
|
|
458
|
+
diagnostics.serializedUrl?.requestUrl,
|
|
459
|
+
diagnostics.serializedUrl?.redactedUrl,
|
|
460
|
+
);
|
|
461
|
+
}
|
|
462
|
+
return attributes;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function queryParamNames(url: string): Set<string> {
|
|
466
|
+
const queryStart = url.indexOf("?");
|
|
467
|
+
if (queryStart === -1) return new Set();
|
|
468
|
+
const fragmentStart = url.indexOf("#", queryStart);
|
|
469
|
+
const query = url.slice(queryStart + 1, fragmentStart === -1 ? undefined : fragmentStart);
|
|
470
|
+
return new Set(new URLSearchParams(query).keys());
|
|
471
|
+
}
|
|
472
|
+
|
|
156
473
|
function getBrowserPageAttributes(
|
|
157
474
|
methodName: string,
|
|
158
475
|
args: unknown[],
|
|
@@ -238,10 +555,65 @@ function wrapPage<T extends object>(page: T, trace: TraceContext): T {
|
|
|
238
555
|
});
|
|
239
556
|
}
|
|
240
557
|
|
|
558
|
+
function wrapStealthRedirects(
|
|
559
|
+
redirects: StealthSession["redirects"],
|
|
560
|
+
trace: TraceContext,
|
|
561
|
+
): StealthSession["redirects"] {
|
|
562
|
+
const recorder = getTraceRecorder(trace);
|
|
563
|
+
if (!recorder) return redirects;
|
|
564
|
+
|
|
565
|
+
return {
|
|
566
|
+
run(...args: Parameters<StealthSession["redirects"]["run"]>) {
|
|
567
|
+
const diagnosticArgs = [args[0].url, args[0]];
|
|
568
|
+
const diagnostics = snapshotRequestDiagnostics("stealth", "fetch", diagnosticArgs);
|
|
569
|
+
let result: ReturnType<StealthSession["redirects"]["run"]>;
|
|
570
|
+
try {
|
|
571
|
+
result = redirects.run(...args);
|
|
572
|
+
} catch (error) {
|
|
573
|
+
const sanitizedError = sanitizeRequestError(error, diagnostics);
|
|
574
|
+
recorder
|
|
575
|
+
.runSpan(
|
|
576
|
+
"stealth.redirects.run",
|
|
577
|
+
() => {
|
|
578
|
+
throw sanitizedError;
|
|
579
|
+
},
|
|
580
|
+
{
|
|
581
|
+
onError: (spanError) =>
|
|
582
|
+
buildStealthRedirectAttributes(diagnosticArgs, undefined, spanError, diagnostics),
|
|
583
|
+
},
|
|
584
|
+
)
|
|
585
|
+
.catch(() => undefined);
|
|
586
|
+
throw sanitizedError;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
const sanitizedResult = Promise.resolve(result).catch((error: unknown) => {
|
|
590
|
+
throw sanitizeRequestError(error, diagnostics);
|
|
591
|
+
});
|
|
592
|
+
return recorder.runSpan("stealth.redirects.run", () => sanitizedResult, {
|
|
593
|
+
onSuccess: (spanResult) =>
|
|
594
|
+
buildStealthRedirectAttributes(diagnosticArgs, spanResult, undefined, diagnostics),
|
|
595
|
+
onError: (error) =>
|
|
596
|
+
buildStealthRedirectAttributes(diagnosticArgs, undefined, error, diagnostics),
|
|
597
|
+
});
|
|
598
|
+
},
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
function wrapStealthSession(session: StealthSession, trace: TraceContext): StealthSession {
|
|
603
|
+
const wrappedSession = wrapNamespace("stealth", session, trace);
|
|
604
|
+
const redirects = wrapStealthRedirects(session.redirects, trace);
|
|
605
|
+
return new Proxy(wrappedSession, {
|
|
606
|
+
get(target, property, receiver) {
|
|
607
|
+
return property === "redirects" ? redirects : Reflect.get(target, property, receiver);
|
|
608
|
+
},
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
|
|
241
612
|
function wrapNamespace<T extends object>(
|
|
242
613
|
namespace: InstrumentedNamespace,
|
|
243
614
|
target: T,
|
|
244
615
|
trace: TraceContext,
|
|
616
|
+
shouldInstrument?: (methodName: string, args: unknown[]) => boolean,
|
|
245
617
|
): T {
|
|
246
618
|
const recorder = getTraceRecorder(trace);
|
|
247
619
|
if (!recorder) {
|
|
@@ -338,6 +710,10 @@ function wrapNamespace<T extends object>(
|
|
|
338
710
|
}
|
|
339
711
|
|
|
340
712
|
const wrapped = (...args: unknown[]) => {
|
|
713
|
+
if (shouldInstrument && !shouldInstrument(methodName, args)) {
|
|
714
|
+
return Reflect.apply(value, namespaceTarget, args);
|
|
715
|
+
}
|
|
716
|
+
const requestDiagnostics = snapshotRequestDiagnostics(namespace, methodName, args);
|
|
341
717
|
// Invoke first and decide by the RETURN VALUE. `runSpan` always
|
|
342
718
|
// returns a Promise, so unconditionally span-wrapping every member
|
|
343
719
|
// silently rewrote synchronous contracts: `ctx.state.namespace()`
|
|
@@ -351,6 +727,7 @@ function wrapNamespace<T extends object>(
|
|
|
351
727
|
try {
|
|
352
728
|
result = Reflect.apply(value, namespaceTarget, args);
|
|
353
729
|
} catch (error) {
|
|
730
|
+
const sanitizedError = sanitizeRequestError(error, requestDiagnostics);
|
|
354
731
|
// A promise-returning implementation may still throw
|
|
355
732
|
// SYNCHRONOUSLY during pre-flight validation. Preserve the
|
|
356
733
|
// synchronous throw contract, but keep recording the failure
|
|
@@ -359,15 +736,22 @@ function wrapNamespace<T extends object>(
|
|
|
359
736
|
.runSpan(
|
|
360
737
|
`${namespace}.${methodName}`,
|
|
361
738
|
() => {
|
|
362
|
-
throw
|
|
739
|
+
throw sanitizedError;
|
|
363
740
|
},
|
|
364
741
|
{
|
|
365
742
|
onError: (spanError) =>
|
|
366
|
-
buildSpanAttributes(
|
|
743
|
+
buildSpanAttributes(
|
|
744
|
+
namespace,
|
|
745
|
+
methodName,
|
|
746
|
+
args,
|
|
747
|
+
undefined,
|
|
748
|
+
spanError,
|
|
749
|
+
requestDiagnostics,
|
|
750
|
+
),
|
|
367
751
|
},
|
|
368
752
|
)
|
|
369
753
|
.catch(() => undefined);
|
|
370
|
-
throw
|
|
754
|
+
throw sanitizedError;
|
|
371
755
|
}
|
|
372
756
|
if (!isThenable(result)) {
|
|
373
757
|
// The state namespace factory returns the object whose METHODS
|
|
@@ -382,13 +766,45 @@ function wrapNamespace<T extends object>(
|
|
|
382
766
|
) {
|
|
383
767
|
return wrapNamespace(namespace, result, trace);
|
|
384
768
|
}
|
|
769
|
+
if (
|
|
770
|
+
namespace === "stealth" &&
|
|
771
|
+
methodName === "createSession" &&
|
|
772
|
+
typeof result === "object" &&
|
|
773
|
+
result !== null
|
|
774
|
+
) {
|
|
775
|
+
return wrapStealthSession(result as StealthSession, trace);
|
|
776
|
+
}
|
|
385
777
|
return result;
|
|
386
778
|
}
|
|
387
|
-
|
|
779
|
+
const sanitizedResult = Promise.resolve(result).catch((error: unknown) => {
|
|
780
|
+
throw sanitizeRequestError(error, requestDiagnostics);
|
|
781
|
+
});
|
|
782
|
+
const tracedResult = recorder.runSpan(`${namespace}.${methodName}`, () => sanitizedResult, {
|
|
388
783
|
onSuccess: (spanResult) =>
|
|
389
|
-
buildSpanAttributes(
|
|
390
|
-
|
|
784
|
+
buildSpanAttributes(
|
|
785
|
+
namespace,
|
|
786
|
+
methodName,
|
|
787
|
+
args,
|
|
788
|
+
spanResult,
|
|
789
|
+
undefined,
|
|
790
|
+
requestDiagnostics,
|
|
791
|
+
),
|
|
792
|
+
onError: (error) =>
|
|
793
|
+
buildSpanAttributes(namespace, methodName, args, undefined, error, requestDiagnostics),
|
|
391
794
|
});
|
|
795
|
+
return namespace === "http" && methodName === "stream"
|
|
796
|
+
? tracedResult.then((spanResult) =>
|
|
797
|
+
isHttpStreamResponse(spanResult)
|
|
798
|
+
? instrumentHttpStreamConsumption(spanResult, recorder, args, requestDiagnostics)
|
|
799
|
+
: spanResult,
|
|
800
|
+
)
|
|
801
|
+
: namespace === "http" && methodName === "sse"
|
|
802
|
+
? tracedResult.then((spanResult) =>
|
|
803
|
+
isAsyncIterable<SseMessage>(spanResult)
|
|
804
|
+
? instrumentHttpSseConsumption(spanResult, recorder, args, requestDiagnostics)
|
|
805
|
+
: spanResult,
|
|
806
|
+
)
|
|
807
|
+
: tracedResult;
|
|
392
808
|
};
|
|
393
809
|
|
|
394
810
|
wrappedMethods.set(property, wrapped);
|