@apifuse/provider-sdk 2.2.0-beta.39 → 2.2.0-beta.40

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/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # @apifuse/provider-sdk Changelog
2
2
 
3
+ ## 2.2.0-beta.40
4
+
5
+ - Release candidate for main commit 7e2f05a954c23593a77e89e463a9bcbc2cdc7695.
6
+
3
7
  ## 2.2.0-beta.39
4
8
 
5
9
  - Release candidate for main commit ecbe349c44e15c873b7c58b42e14610db029eb5f.
@@ -64,6 +64,8 @@ export type SmartproxyAllocatorBodyClass = "network_error" | "http_error" | "emp
64
64
  export type ProxyUserAgentSource = "declared" | "defaulted";
65
65
  export type ProxyResolutionTelemetryEvent = {
66
66
  provider: ProxyVendorName;
67
+ /** Defaults to `"ok"` when omitted. */
68
+ outcome?: "ok" | "error";
67
69
  userAgentSource?: ProxyUserAgentSource;
68
70
  protocol?: ProxyProtocol;
69
71
  cacheStatus: ProxyCacheStatus;
@@ -158,6 +158,7 @@ function telemetryForPool(pool, cacheStatus, startedAt, extra = {}) {
158
158
  function telemetryForFailure(cacheStatus, startedAt, extra = {}) {
159
159
  return {
160
160
  provider: "smartproxy",
161
+ outcome: "error",
161
162
  cacheStatus,
162
163
  cacheHit: false,
163
164
  resolutionMs: Math.max(0, Date.now() - startedAt),
@@ -24,3 +24,5 @@ export declare function sanitizeUrlForLogs(value: string): string;
24
24
  export declare function requestPathForFixture(value: string): string;
25
25
  /** Scrubs secrets and terminal/log control characters before diagnostic text is emitted. */
26
26
  export declare function sanitizeDiagnosticText(value: string): string;
27
+ /** Encodes terminal/log control characters without applying value-level secret heuristics. */
28
+ export declare function encodeDiagnosticControls(value: string): string;
@@ -170,7 +170,8 @@ function isCredentialBearingUrl(value) {
170
170
  return false;
171
171
  }
172
172
  }
173
- function encodeDiagnosticControls(value) {
173
+ /** Encodes terminal/log control characters without applying value-level secret heuristics. */
174
+ export function encodeDiagnosticControls(value) {
174
175
  let result = "";
175
176
  for (const character of value) {
176
177
  const code = character.codePointAt(0) ?? 0;
package/dist/index.d.ts CHANGED
@@ -31,7 +31,7 @@ export { generateInsights } from "./runtime/insights.js";
31
31
  export { type InstrumentationOptions, type InstrumentedProviderContext, wrapWithInstrumentation, } from "./runtime/instrumentation.js";
32
32
  export type { PrevalidateResult } from "./runtime/prevalidate.js";
33
33
  export { getProviderBaseUrl } from "./runtime/provider.js";
34
- export type { ProxyTelemetryLogPayload } from "./runtime/proxy-telemetry.js";
34
+ export type { ProxyTelemetryLogPayload, ProxyTelemetryResolvedPayload, ProxyTelemetryUnresolvedPayload, } from "./runtime/proxy-telemetry.js";
35
35
  export { APIFUSE__CDP_POOL__URL, APIFUSE__RESOLVER__2CAPTCHA__API_KEY, APIFUSE__RESOLVER__CAPMONSTER__API_KEY, APIFUSE__RESOLVER__CAPSOLVER__API_KEY, APIFUSE__RESOLVER__TIMEOUT_MS, DEFAULT_RESOLVER_TIMEOUT_MS, } from "./runtime/resolver-config.js";
36
36
  export { createUnsupportedResolverClient } from "./runtime/resolver-shared.js";
37
37
  export type { ResolverRuntimeOptions } from "./runtime/resolver.js";
@@ -1,6 +1,7 @@
1
1
  import type { ProxyAttemptTelemetryEvent, ProxyCacheStatus, ProxyProtocol, ProxyResolutionTelemetryEvent, ProxyTelemetrySink, ProxyUserAgentSource, ProxyVendorFailoverTelemetryEvent, ProxyVendorName, SmartproxyAllocatorBodyClass } from "../config/loader.js";
2
2
  export declare const PROVIDER_TELEMETRY_HEADER = "X-ApiFuse-Provider-Telemetry";
3
- export type ProxyTelemetryLogPayload = {
3
+ export type ProxyTelemetryResolvedPayload = {
4
+ kind: "resolved";
4
5
  provider: ProxyVendorName;
5
6
  userAgentSource?: ProxyUserAgentSource;
6
7
  protocol?: ProxyProtocol;
@@ -39,6 +40,43 @@ export type ProxyTelemetryLogPayload = {
39
40
  a?: number;
40
41
  }[];
41
42
  };
43
+ export type ProxyTelemetryUnresolvedPayload = {
44
+ kind: "unresolved";
45
+ /** Distinct vendors that failed resolution or recorded a failover, in order seen. */
46
+ vendors: ProxyVendorName[];
47
+ cacheStatus?: ProxyCacheStatus;
48
+ cacheHit?: boolean;
49
+ resolutionMs?: number;
50
+ allocatorMs?: number;
51
+ allocatorStatus?: number;
52
+ allocatorBodyClass?: SmartproxyAllocatorBodyClass;
53
+ allocatorAttempts?: number;
54
+ lockWaitMs?: number;
55
+ redisReadMs?: number;
56
+ redisWriteMs?: number;
57
+ poolAgeMs?: number;
58
+ poolExpiresInMs?: number;
59
+ attempts?: number;
60
+ refreshes?: number;
61
+ failovers?: {
62
+ v: ProxyVendorName;
63
+ nx?: ProxyVendorName;
64
+ p: ProxyVendorFailoverTelemetryEvent["phase"];
65
+ r: ProxyVendorFailoverTelemetryEvent["reason"];
66
+ a?: number;
67
+ }[];
68
+ attemptSamples?: {
69
+ n: number;
70
+ a: number;
71
+ i?: number;
72
+ h?: string;
73
+ o: ProxyAttemptTelemetryEvent["outcome"];
74
+ c?: string;
75
+ s?: number;
76
+ d?: number;
77
+ }[];
78
+ };
79
+ export type ProxyTelemetryLogPayload = ProxyTelemetryResolvedPayload | ProxyTelemetryUnresolvedPayload;
42
80
  export declare class ProxyTelemetryCollector implements ProxyTelemetrySink {
43
81
  #private;
44
82
  recordProxyResolution(event: ProxyResolutionTelemetryEvent): void;
@@ -30,9 +30,12 @@ export class ProxyTelemetryCollector {
30
30
  #events = [];
31
31
  #attempts = [];
32
32
  #failovers = [];
33
+ #unresolvedVendors = [];
33
34
  recordProxyResolution(event) {
35
+ const outcome = event.outcome === "error" ? "error" : "ok";
34
36
  this.#events.push({
35
37
  provider: event.provider,
38
+ outcome,
36
39
  ...(event.userAgentSource ? { userAgentSource: event.userAgentSource } : {}),
37
40
  ...(event.protocol ? { protocol: event.protocol } : {}),
38
41
  cacheStatus: event.cacheStatus,
@@ -56,6 +59,9 @@ export class ProxyTelemetryCollector {
56
59
  attempts: Math.max(1, Math.floor(event.attempts || 1)),
57
60
  refreshes: event.refreshes === undefined ? undefined : Math.max(0, Math.floor(event.refreshes)),
58
61
  });
62
+ if (outcome === "error" && !this.#unresolvedVendors.includes(event.provider)) {
63
+ this.#unresolvedVendors.push(event.provider);
64
+ }
59
65
  }
60
66
  recordProxyVendorFailover(event) {
61
67
  if (this.#failovers.length >= MAX_PROXY_FAILOVER_SAMPLES)
@@ -67,6 +73,9 @@ export class ProxyTelemetryCollector {
67
73
  reason: event.reason,
68
74
  ...(event.attempt === undefined ? {} : { attempt: Math.max(0, Math.floor(event.attempt)) }),
69
75
  });
76
+ if (!this.#unresolvedVendors.includes(event.vendor)) {
77
+ this.#unresolvedVendors.push(event.vendor);
78
+ }
70
79
  }
71
80
  recordProxyAttempt(event) {
72
81
  if (this.#attempts.length >= MAX_PROXY_ATTEMPT_SAMPLES)
@@ -88,11 +97,90 @@ export class ProxyTelemetryCollector {
88
97
  }
89
98
  toLogPayload() {
90
99
  const [first, ...rest] = this.#events;
91
- if (!first)
92
- return undefined;
93
- // The serving vendor/protocol is the last recorded resolution (a failed
94
- // vendor records first, the vendor that served records last).
95
- const serving = this.#events[this.#events.length - 1] ?? first;
100
+ const okEvents = this.#events.filter((event) => event.outcome !== "error");
101
+ const attemptSamples = this.#attempts.map((attempt, index) => ({
102
+ n: index + 1,
103
+ a: attempt.attempt,
104
+ ...(attempt.poolIndex === undefined ? {} : { i: attempt.poolIndex }),
105
+ ...(attempt.proxyHash ? { h: attempt.proxyHash } : {}),
106
+ o: attempt.outcome,
107
+ ...(attempt.errorCode ? { c: attempt.errorCode } : {}),
108
+ ...(attempt.status === undefined ? {} : { s: attempt.status }),
109
+ ...(attempt.durationMs === undefined ? {} : { d: attempt.durationMs }),
110
+ }));
111
+ const failovers = this.#failovers.map((failover) => ({
112
+ v: failover.vendor,
113
+ ...(failover.nextVendor ? { nx: failover.nextVendor } : {}),
114
+ p: failover.phase,
115
+ r: failover.reason,
116
+ ...(failover.attempt === undefined ? {} : { a: failover.attempt }),
117
+ }));
118
+ if (okEvents.length === 0) {
119
+ if (!first && failovers.length === 0)
120
+ return undefined;
121
+ const failureEvents = this.#events.filter((event) => event.outcome === "error");
122
+ const [firstFailure, ...remainingFailures] = failureEvents;
123
+ const aggregate = firstFailure
124
+ ? remainingFailures.reduce((acc, event) => ({
125
+ provider: event.provider,
126
+ outcome: "error",
127
+ cacheStatus: worseStatus(acc.cacheStatus, event.cacheStatus),
128
+ cacheHit: acc.cacheHit && event.cacheHit,
129
+ resolutionMs: acc.resolutionMs + event.resolutionMs,
130
+ allocatorMs: sumOptional(acc.allocatorMs, event.allocatorMs),
131
+ allocatorStatus: event.allocatorStatus ?? acc.allocatorStatus,
132
+ allocatorBodyClass: event.allocatorBodyClass ?? acc.allocatorBodyClass,
133
+ allocatorAttempts: sumOptional(acc.allocatorAttempts, event.allocatorAttempts),
134
+ lockWaitMs: sumOptional(acc.lockWaitMs, event.lockWaitMs),
135
+ redisReadMs: sumOptional(acc.redisReadMs, event.redisReadMs),
136
+ redisWriteMs: sumOptional(acc.redisWriteMs, event.redisWriteMs),
137
+ poolAgeMs: maxOptional(acc.poolAgeMs, event.poolAgeMs),
138
+ poolExpiresInMs: maxOptional(acc.poolExpiresInMs, event.poolExpiresInMs),
139
+ attempts: acc.attempts + event.attempts,
140
+ refreshes: sumOptional(acc.refreshes, event.refreshes),
141
+ }), firstFailure)
142
+ : undefined;
143
+ return {
144
+ kind: "unresolved",
145
+ vendors: [...this.#unresolvedVendors],
146
+ ...(aggregate
147
+ ? {
148
+ cacheStatus: aggregate.cacheStatus,
149
+ cacheHit: aggregate.cacheHit,
150
+ resolutionMs: aggregate.resolutionMs,
151
+ ...(aggregate.allocatorMs !== undefined
152
+ ? { allocatorMs: aggregate.allocatorMs }
153
+ : {}),
154
+ ...(aggregate.allocatorStatus !== undefined
155
+ ? { allocatorStatus: aggregate.allocatorStatus }
156
+ : {}),
157
+ ...(aggregate.allocatorBodyClass !== undefined
158
+ ? { allocatorBodyClass: aggregate.allocatorBodyClass }
159
+ : {}),
160
+ ...(aggregate.allocatorAttempts !== undefined
161
+ ? { allocatorAttempts: aggregate.allocatorAttempts }
162
+ : {}),
163
+ ...(aggregate.lockWaitMs !== undefined ? { lockWaitMs: aggregate.lockWaitMs } : {}),
164
+ ...(aggregate.redisReadMs !== undefined
165
+ ? { redisReadMs: aggregate.redisReadMs }
166
+ : {}),
167
+ ...(aggregate.redisWriteMs !== undefined
168
+ ? { redisWriteMs: aggregate.redisWriteMs }
169
+ : {}),
170
+ ...(aggregate.poolAgeMs !== undefined ? { poolAgeMs: aggregate.poolAgeMs } : {}),
171
+ ...(aggregate.poolExpiresInMs !== undefined
172
+ ? { poolExpiresInMs: aggregate.poolExpiresInMs }
173
+ : {}),
174
+ attempts: aggregate.attempts,
175
+ ...(aggregate.refreshes !== undefined ? { refreshes: aggregate.refreshes } : {}),
176
+ }
177
+ : {}),
178
+ ...(failovers.length > 0 ? { failovers } : {}),
179
+ ...(attemptSamples.length > 0 ? { attemptSamples } : {}),
180
+ };
181
+ }
182
+ // The serving vendor/protocol is the last successful resolution.
183
+ const serving = okEvents[okEvents.length - 1] ?? first;
96
184
  const vendors = [];
97
185
  for (const event of this.#events) {
98
186
  if (!vendors.includes(event.provider))
@@ -117,6 +205,7 @@ export class ProxyTelemetryCollector {
117
205
  refreshes: sumOptional(acc.refreshes, event.refreshes),
118
206
  }), first);
119
207
  return {
208
+ kind: "resolved",
120
209
  provider: serving.provider,
121
210
  ...(aggregate.userAgentSource ? { userAgentSource: aggregate.userAgentSource } : {}),
122
211
  ...(serving.protocol ? { protocol: serving.protocol } : {}),
@@ -142,32 +231,9 @@ export class ProxyTelemetryCollector {
142
231
  : {}),
143
232
  attempts: aggregate.attempts,
144
233
  ...(aggregate.refreshes !== undefined ? { refreshes: aggregate.refreshes } : {}),
145
- ...(this.#attempts.length > 0
146
- ? {
147
- attemptSamples: this.#attempts.map((attempt, index) => ({
148
- n: index + 1,
149
- a: attempt.attempt,
150
- ...(attempt.poolIndex === undefined ? {} : { i: attempt.poolIndex }),
151
- ...(attempt.proxyHash ? { h: attempt.proxyHash } : {}),
152
- o: attempt.outcome,
153
- ...(attempt.errorCode ? { c: attempt.errorCode } : {}),
154
- ...(attempt.status === undefined ? {} : { s: attempt.status }),
155
- ...(attempt.durationMs === undefined ? {} : { d: attempt.durationMs }),
156
- })),
157
- }
158
- : {}),
234
+ ...(attemptSamples.length > 0 ? { attemptSamples } : {}),
159
235
  ...(vendors.length > 1 ? { vendors } : {}),
160
- ...(this.#failovers.length > 0
161
- ? {
162
- failovers: this.#failovers.map((failover) => ({
163
- v: failover.vendor,
164
- ...(failover.nextVendor ? { nx: failover.nextVendor } : {}),
165
- p: failover.phase,
166
- r: failover.reason,
167
- ...(failover.attempt === undefined ? {} : { a: failover.attempt }),
168
- })),
169
- }
170
- : {}),
236
+ ...(failovers.length > 0 ? { failovers } : {}),
171
237
  };
172
238
  }
173
239
  toHeaderValue() {
@@ -0,0 +1,12 @@
1
+ import type { TraceConfig } from "../types.js";
2
+ /** SDK-owned environment variable names for server trace output. */
3
+ export declare const APIFUSE__TRACE__ENABLED = "APIFUSE__TRACE__ENABLED";
4
+ export declare const APIFUSE__TRACE__EXPORTER = "APIFUSE__TRACE__EXPORTER";
5
+ type EnvLike = Record<string, string | undefined>;
6
+ /**
7
+ * Resolve server trace output configuration from SDK-owned environment
8
+ * variables. Invalid exporter values fail closed so a provider cannot crash at
9
+ * startup because of an observability setting.
10
+ */
11
+ export declare function resolveTraceConfigFromEnv(env?: EnvLike): TraceConfig | undefined;
12
+ export {};
@@ -0,0 +1,61 @@
1
+ /** SDK-owned environment variable names for server trace output. */
2
+ export const APIFUSE__TRACE__ENABLED = "APIFUSE__TRACE__ENABLED";
3
+ export const APIFUSE__TRACE__EXPORTER = "APIFUSE__TRACE__EXPORTER";
4
+ const TRACE_EXPORTER_LOOKUP = {
5
+ console: true,
6
+ json: true,
7
+ none: true,
8
+ };
9
+ const TRACE_EXPORTERS = new Set(Object.keys(TRACE_EXPORTER_LOOKUP));
10
+ const emittedWarnings = new Set();
11
+ function warnOnce(key, message) {
12
+ if (emittedWarnings.has(key))
13
+ return;
14
+ emittedWarnings.add(key);
15
+ console.warn(message);
16
+ }
17
+ function parseEnabled(raw) {
18
+ if (raw === undefined)
19
+ return undefined;
20
+ switch (raw.trim().toLowerCase()) {
21
+ case "1":
22
+ case "true":
23
+ case "yes":
24
+ case "on":
25
+ return true;
26
+ case "0":
27
+ case "false":
28
+ case "no":
29
+ case "off":
30
+ return false;
31
+ default:
32
+ warnOnce(APIFUSE__TRACE__ENABLED, "[apifuse] Invalid APIFUSE__TRACE__ENABLED; falling back to disabled tracing.");
33
+ return false;
34
+ }
35
+ }
36
+ /**
37
+ * Resolve server trace output configuration from SDK-owned environment
38
+ * variables. Invalid exporter values fail closed so a provider cannot crash at
39
+ * startup because of an observability setting.
40
+ */
41
+ export function resolveTraceConfigFromEnv(env = process.env) {
42
+ const enabledRaw = env[APIFUSE__TRACE__ENABLED];
43
+ const exporterRaw = env[APIFUSE__TRACE__EXPORTER];
44
+ if (enabledRaw === undefined && exporterRaw === undefined) {
45
+ return undefined;
46
+ }
47
+ const enabled = parseEnabled(enabledRaw);
48
+ const normalizedExporter = exporterRaw?.trim().toLowerCase();
49
+ const exporter = TRACE_EXPORTERS.has(normalizedExporter)
50
+ ? normalizedExporter
51
+ : exporterRaw === undefined
52
+ ? undefined
53
+ : "none";
54
+ if (exporterRaw !== undefined && exporter === "none" && normalizedExporter !== "none") {
55
+ warnOnce(APIFUSE__TRACE__EXPORTER, `[apifuse] Invalid APIFUSE__TRACE__EXPORTER value "${normalizedExporter ?? exporterRaw}" (OTLP is unsupported for server output); supported exporters are "console", "json", and "none"; falling back to exporter "none".`);
56
+ }
57
+ return {
58
+ ...(enabled !== undefined ? { enabled } : {}),
59
+ ...(exporter !== undefined ? { exporter } : {}),
60
+ };
61
+ }
@@ -1,5 +1,5 @@
1
1
  export type { ProxyCacheStatus, ProxyProtocol, ProxyUserAgentSource, ProxyVendorName, SmartproxyAllocatorBodyClass, } from "../config/loader.js";
2
- export type { ProxyTelemetryLogPayload } from "../runtime/proxy-telemetry.js";
2
+ export type { ProxyTelemetryLogPayload, ProxyTelemetryResolvedPayload, ProxyTelemetryUnresolvedPayload, } from "../runtime/proxy-telemetry.js";
3
3
  export { createServerApp, createServerAppAsync, ERROR_OBSERVABILITY_HEADER, type ErrorObservabilityDetails, type ProviderServerCloseOptions, type ProviderServerHandle, type ProviderServerLogEvent, type ProviderServerLogger, type ProviderServerOperationExecutor, type ProviderServerOperationExecutorInput, type ProviderServerOptions, type ProviderServerStatefulForwardEnvelope, type ServeOptions, serve, } from "./serve.js";
4
4
  export { computeSelfTestPlanDigest, createSelfTestApp, createSelfTestAuthFlowInvoke, createSelfTestInvoke, DEFAULT_SELF_TEST_REQUEST_BUDGET_MS, isSelfTestReadOnlyOperation, PROVIDER_RUNTIME_SELF_TEST_REQUEST_BUDGET_MS_ENV, resolveSelfTestPort, SELF_TEST_AUTH_FLOW_MULTI_TURN_SKIP_REASON, SELF_TEST_AUTH_FLOW_REJECTED_SKIP_REASON, SELF_TEST_HEALTHZ_PATH, SELF_TEST_PATH, SELF_TEST_SCHEMA_VERSION, type SelfTestAppOptions, type SelfTestAuthFlowInvoke, type SelfTestAuthFlowRoute, type SelfTestCaseResult, type SelfTestCaseStatus, type SelfTestOperationInvoke, type SelfTestRequest, SelfTestRequestSchema, type SelfTestResponse, } from "./self-test.js";
5
5
  export { type InputDateTokenCalendar, resolveHealthCheckInputDateTokens, } from "./self-test-input-tokens.js";
@@ -28,6 +28,7 @@ import { createProviderRuntimeStateFromEnv, createUnsupportedProviderRuntimeStat
28
28
  import { StealthCookieJar } from "../runtime/stealth-cookies.js";
29
29
  import { createSttClientFromEnv } from "../runtime/stt.js";
30
30
  import { createTraceContext } from "../runtime/trace.js";
31
+ import { resolveTraceConfigFromEnv } from "../runtime/trace-config.js";
31
32
  import { parseSchema } from "../schema.js";
32
33
  import { STATEFUL_NONCE_HEADER as STATEFUL_FORWARDING_NONCE_HEADER, STATEFUL_SIGNATURE_HEADER as STATEFUL_FORWARDING_SIGNATURE_HEADER, STATEFUL_TIMESTAMP_HEADER as STATEFUL_FORWARDING_TIMESTAMP_HEADER, verifyStatefulRequestSignature, } from "../stateful-signing.js";
33
34
  import { StatefulRoutingDeadlineError } from "../stateful/errors.js";
@@ -35,6 +36,7 @@ import { getStealthProfile } from "../stealth/profiles.js";
35
36
  import { APIFUSE_STREAM_DONE_EVENT, APIFUSE_STREAM_ERROR_EVENT, encodeSseEvent, error as streamError, } from "../stream.js";
36
37
  import { VALID_OPERATION_ERROR_STATUSES } from "../types.js";
37
38
  import { resolveSelfTestMasterSecrets } from "./self-test-token.js";
39
+ import { resolveServerTraceContextOptions } from "./trace-output.js";
38
40
  import { AuthFlowRequestSchema, OperationConnectionSchema, OperationRequestSchema, } from "./types.js";
39
41
  const DEFAULT_HOST = "0.0.0.0";
40
42
  const DEFAULT_PORT = 3000;
@@ -363,6 +365,7 @@ function resolveNativeProxyPolicy(provider) {
363
365
  return undefined;
364
366
  }
365
367
  function createProviderContext(provider, request, operationId, options, state = createUnsupportedProviderRuntimeState(), proxyTelemetry, signal) {
368
+ const traceConfig = resolveTraceConfigFromEnv();
366
369
  const baseUrl = getProviderBaseUrl(provider);
367
370
  const stealthBaseUrl = getProviderStealthBaseUrl(provider);
368
371
  const stealthProfile = getProviderStealthProfile(provider);
@@ -443,7 +446,13 @@ function createProviderContext(provider, request, operationId, options, state =
443
446
  },
444
447
  }
445
448
  : {}),
446
- trace: createTraceContext(),
449
+ trace: traceConfig
450
+ ? createTraceContext(resolveServerTraceContextOptions(traceConfig, {
451
+ request_id: request.requestId,
452
+ provider_id: provider.id,
453
+ operation_id: operationId,
454
+ }))
455
+ : createTraceContext(),
447
456
  auth: createAuthStub(),
448
457
  ocr: options.ocr ?? createOcrClientFromEnv(provider.ocr),
449
458
  stt: options.stt ?? createSttClientFromEnv(provider.stt),
@@ -0,0 +1,4 @@
1
+ import { type CreateTraceContextOptions } from "../runtime/trace.js";
2
+ import type { TraceConfig } from "../types.js";
3
+ /** Server-only trace output policy. Shared programmatic trace callers stay in-memory. */
4
+ export declare function resolveServerTraceContextOptions(config: TraceConfig, resourceAttributes: Record<string, string>): CreateTraceContextOptions;
@@ -0,0 +1,20 @@
1
+ import { resolveTraceContextOptions, } from "../runtime/trace.js";
2
+ import { sanitizeSpanForOutput } from "../trace-sanitization.js";
3
+ /** Server-only trace output policy. Shared programmatic trace callers stay in-memory. */
4
+ export function resolveServerTraceContextOptions(config, resourceAttributes) {
5
+ const resolved = resolveTraceContextOptions(config);
6
+ const outputEnabled = config.enabled !== false && config.exporter !== "none";
7
+ const consoleHook = outputEnabled && (config.exporter === "console" || config.exporter === "json")
8
+ ? (span) => console.log(JSON.stringify(sanitizeSpanForOutput(span, resourceAttributes)))
9
+ : undefined;
10
+ const onSpan = consoleHook && resolved.onSpan
11
+ ? (span) => {
12
+ consoleHook(span);
13
+ resolved.onSpan?.(span);
14
+ }
15
+ : (consoleHook ?? resolved.onSpan);
16
+ return {
17
+ maxSpans: resolved.maxSpans,
18
+ onSpan,
19
+ };
20
+ }
@@ -0,0 +1,5 @@
1
+ import type { TraceSpan } from "./types.js";
2
+ /** Span names are SDK-authored identifiers; retain them while preventing log injection. */
3
+ export declare function sanitizeSpanNameForOutput(value: string): string;
4
+ export declare function sanitizeTraceAttributes(attributes: Record<string, unknown>): Record<string, string | number | boolean>;
5
+ export declare function sanitizeSpanForOutput(span: TraceSpan, additionalAttributes?: Record<string, string>): TraceSpan;
@@ -0,0 +1,45 @@
1
+ import { encodeDiagnosticControls, isSensitiveFixtureKey, REDACTED_FIXTURE_VALUE, sanitizeDiagnosticText, } from "./fixture-sanitization.js";
2
+ const MAX_TRACE_TEXT_LENGTH = 300;
3
+ function sanitizeTraceText(value) {
4
+ const encoded = sanitizeDiagnosticText(value);
5
+ return encoded.length > MAX_TRACE_TEXT_LENGTH
6
+ ? `${encoded.slice(0, MAX_TRACE_TEXT_LENGTH)}… [truncated]`
7
+ : encoded;
8
+ }
9
+ /** Span names are SDK-authored identifiers; retain them while preventing log injection. */
10
+ export function sanitizeSpanNameForOutput(value) {
11
+ const encoded = encodeDiagnosticControls(value);
12
+ return encoded.length > MAX_TRACE_TEXT_LENGTH
13
+ ? `${encoded.slice(0, MAX_TRACE_TEXT_LENGTH)}… [truncated]`
14
+ : encoded;
15
+ }
16
+ export function sanitizeTraceAttributes(attributes) {
17
+ return Object.fromEntries(Object.entries(attributes).map(([key, value]) => [
18
+ sanitizeTraceText(key),
19
+ isSensitiveFixtureKey(key)
20
+ ? REDACTED_FIXTURE_VALUE
21
+ : typeof value === "string"
22
+ ? sanitizeTraceText(value)
23
+ : typeof value === "number" || typeof value === "boolean"
24
+ ? value
25
+ : sanitizeTraceText(String(value)),
26
+ ]));
27
+ }
28
+ export function sanitizeSpanForOutput(span, additionalAttributes) {
29
+ // Keep this schema explicit so future fields are not silently added to a
30
+ // process output path before their trust boundary has been reviewed.
31
+ return {
32
+ id: span.id,
33
+ name: sanitizeSpanNameForOutput(span.name),
34
+ startedAt: span.startedAt,
35
+ endedAt: span.endedAt,
36
+ duration_ms: span.duration_ms,
37
+ status: span.status,
38
+ attributes: sanitizeTraceAttributes({
39
+ ...span.attributes,
40
+ ...(additionalAttributes ?? {}),
41
+ }),
42
+ ...(span.error !== undefined ? { error: sanitizeTraceText(span.error) } : {}),
43
+ ...(span.parentId !== undefined ? { parentId: span.parentId } : {}),
44
+ };
45
+ }
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "2.2.0-beta.39",
2
+ "version": "2.2.0-beta.40",
3
3
  "name": "@apifuse/provider-sdk",
4
4
  "private": false,
5
5
  "type": "module",
@@ -107,6 +107,8 @@ export type ProxyUserAgentSource = "declared" | "defaulted";
107
107
 
108
108
  export type ProxyResolutionTelemetryEvent = {
109
109
  provider: ProxyVendorName;
110
+ /** Defaults to `"ok"` when omitted. */
111
+ outcome?: "ok" | "error";
110
112
  userAgentSource?: ProxyUserAgentSource;
111
113
  protocol?: ProxyProtocol;
112
114
  cacheStatus: ProxyCacheStatus;
@@ -370,6 +372,7 @@ function telemetryForFailure(
370
372
  ): ProxyResolutionTelemetryEvent {
371
373
  return {
372
374
  provider: "smartproxy",
375
+ outcome: "error",
373
376
  cacheStatus,
374
377
  cacheHit: false,
375
378
  resolutionMs: Math.max(0, Date.now() - startedAt),
@@ -916,7 +919,10 @@ export function resolvePolicyTransportAttemptCap(input: {
916
919
  ) {
917
920
  return budget;
918
921
  }
919
- const span = Math.min(maxPolicyProxyPoolSpan(input.policy as ProviderProxyPolicy), resolvePolicyProxyPoolSpan(input.policy as ProviderProxyPolicy));
922
+ const span = Math.min(
923
+ maxPolicyProxyPoolSpan(input.policy as ProviderProxyPolicy),
924
+ resolvePolicyProxyPoolSpan(input.policy as ProviderProxyPolicy),
925
+ );
920
926
  return Math.max(budget, span);
921
927
  }
922
928
 
@@ -200,7 +200,8 @@ function isCredentialBearingUrl(value: string): boolean {
200
200
  }
201
201
  }
202
202
 
203
- function encodeDiagnosticControls(value: string): string {
203
+ /** Encodes terminal/log control characters without applying value-level secret heuristics. */
204
+ export function encodeDiagnosticControls(value: string): string {
204
205
  let result = "";
205
206
  for (const character of value) {
206
207
  const code = character.codePointAt(0) ?? 0;
package/src/index.ts CHANGED
@@ -177,7 +177,11 @@ export {
177
177
  } from "./runtime/instrumentation.js";
178
178
  export type { PrevalidateResult } from "./runtime/prevalidate.js";
179
179
  export { getProviderBaseUrl } from "./runtime/provider.js";
180
- export type { ProxyTelemetryLogPayload } from "./runtime/proxy-telemetry.js";
180
+ export type {
181
+ ProxyTelemetryLogPayload,
182
+ ProxyTelemetryResolvedPayload,
183
+ ProxyTelemetryUnresolvedPayload,
184
+ } from "./runtime/proxy-telemetry.js";
181
185
  export {
182
186
  APIFUSE__CDP_POOL__URL,
183
187
  APIFUSE__RESOLVER__2CAPTCHA__API_KEY,
@@ -12,7 +12,8 @@ import type {
12
12
 
13
13
  export const PROVIDER_TELEMETRY_HEADER = "X-ApiFuse-Provider-Telemetry";
14
14
 
15
- export type ProxyTelemetryLogPayload = {
15
+ export type ProxyTelemetryResolvedPayload = {
16
+ kind: "resolved";
16
17
  provider: ProxyVendorName;
17
18
  userAgentSource?: ProxyUserAgentSource;
18
19
  protocol?: ProxyProtocol;
@@ -52,6 +53,47 @@ export type ProxyTelemetryLogPayload = {
52
53
  }[];
53
54
  };
54
55
 
56
+ export type ProxyTelemetryUnresolvedPayload = {
57
+ kind: "unresolved";
58
+ /** Distinct vendors that failed resolution or recorded a failover, in order seen. */
59
+ vendors: ProxyVendorName[];
60
+ cacheStatus?: ProxyCacheStatus;
61
+ cacheHit?: boolean;
62
+ resolutionMs?: number;
63
+ allocatorMs?: number;
64
+ allocatorStatus?: number;
65
+ allocatorBodyClass?: SmartproxyAllocatorBodyClass;
66
+ allocatorAttempts?: number;
67
+ lockWaitMs?: number;
68
+ redisReadMs?: number;
69
+ redisWriteMs?: number;
70
+ poolAgeMs?: number;
71
+ poolExpiresInMs?: number;
72
+ attempts?: number;
73
+ refreshes?: number;
74
+ failovers?: {
75
+ v: ProxyVendorName;
76
+ nx?: ProxyVendorName;
77
+ p: ProxyVendorFailoverTelemetryEvent["phase"];
78
+ r: ProxyVendorFailoverTelemetryEvent["reason"];
79
+ a?: number;
80
+ }[];
81
+ attemptSamples?: {
82
+ n: number;
83
+ a: number;
84
+ i?: number;
85
+ h?: string;
86
+ o: ProxyAttemptTelemetryEvent["outcome"];
87
+ c?: string;
88
+ s?: number;
89
+ d?: number;
90
+ }[];
91
+ };
92
+
93
+ export type ProxyTelemetryLogPayload =
94
+ | ProxyTelemetryResolvedPayload
95
+ | ProxyTelemetryUnresolvedPayload;
96
+
55
97
  type ProviderTelemetryHeader = {
56
98
  v: 1;
57
99
  proxy?: ProxyTelemetryLogPayload;
@@ -94,10 +136,13 @@ export class ProxyTelemetryCollector implements ProxyTelemetrySink {
94
136
  #events: ProxyResolutionTelemetryEvent[] = [];
95
137
  #attempts: ProxyAttemptTelemetryEvent[] = [];
96
138
  #failovers: ProxyVendorFailoverTelemetryEvent[] = [];
139
+ #unresolvedVendors: ProxyVendorName[] = [];
97
140
 
98
141
  recordProxyResolution(event: ProxyResolutionTelemetryEvent): void {
142
+ const outcome = event.outcome === "error" ? "error" : "ok";
99
143
  this.#events.push({
100
144
  provider: event.provider,
145
+ outcome,
101
146
  ...(event.userAgentSource ? { userAgentSource: event.userAgentSource } : {}),
102
147
  ...(event.protocol ? { protocol: event.protocol } : {}),
103
148
  cacheStatus: event.cacheStatus,
@@ -130,6 +175,9 @@ export class ProxyTelemetryCollector implements ProxyTelemetrySink {
130
175
  refreshes:
131
176
  event.refreshes === undefined ? undefined : Math.max(0, Math.floor(event.refreshes)),
132
177
  });
178
+ if (outcome === "error" && !this.#unresolvedVendors.includes(event.provider)) {
179
+ this.#unresolvedVendors.push(event.provider);
180
+ }
133
181
  }
134
182
 
135
183
  recordProxyVendorFailover(event: ProxyVendorFailoverTelemetryEvent): void {
@@ -141,6 +189,9 @@ export class ProxyTelemetryCollector implements ProxyTelemetrySink {
141
189
  reason: event.reason,
142
190
  ...(event.attempt === undefined ? {} : { attempt: Math.max(0, Math.floor(event.attempt)) }),
143
191
  });
192
+ if (!this.#unresolvedVendors.includes(event.vendor)) {
193
+ this.#unresolvedVendors.push(event.vendor);
194
+ }
144
195
  }
145
196
 
146
197
  recordProxyAttempt(event: ProxyAttemptTelemetryEvent): void {
@@ -163,11 +214,94 @@ export class ProxyTelemetryCollector implements ProxyTelemetrySink {
163
214
 
164
215
  toLogPayload(): ProxyTelemetryLogPayload | undefined {
165
216
  const [first, ...rest] = this.#events;
166
- if (!first) return undefined;
217
+ const okEvents = this.#events.filter((event) => event.outcome !== "error");
218
+ const attemptSamples = this.#attempts.map((attempt, index) => ({
219
+ n: index + 1,
220
+ a: attempt.attempt,
221
+ ...(attempt.poolIndex === undefined ? {} : { i: attempt.poolIndex }),
222
+ ...(attempt.proxyHash ? { h: attempt.proxyHash } : {}),
223
+ o: attempt.outcome,
224
+ ...(attempt.errorCode ? { c: attempt.errorCode } : {}),
225
+ ...(attempt.status === undefined ? {} : { s: attempt.status }),
226
+ ...(attempt.durationMs === undefined ? {} : { d: attempt.durationMs }),
227
+ }));
228
+ const failovers = this.#failovers.map((failover) => ({
229
+ v: failover.vendor,
230
+ ...(failover.nextVendor ? { nx: failover.nextVendor } : {}),
231
+ p: failover.phase,
232
+ r: failover.reason,
233
+ ...(failover.attempt === undefined ? {} : { a: failover.attempt }),
234
+ }));
167
235
 
168
- // The serving vendor/protocol is the last recorded resolution (a failed
169
- // vendor records first, the vendor that served records last).
170
- const serving = this.#events[this.#events.length - 1] ?? first;
236
+ if (okEvents.length === 0) {
237
+ if (!first && failovers.length === 0) return undefined;
238
+ const failureEvents = this.#events.filter((event) => event.outcome === "error");
239
+ const [firstFailure, ...remainingFailures] = failureEvents;
240
+ const aggregate = firstFailure
241
+ ? remainingFailures.reduce<ProxyResolutionTelemetryEvent>(
242
+ (acc, event) => ({
243
+ provider: event.provider,
244
+ outcome: "error",
245
+ cacheStatus: worseStatus(acc.cacheStatus, event.cacheStatus),
246
+ cacheHit: acc.cacheHit && event.cacheHit,
247
+ resolutionMs: acc.resolutionMs + event.resolutionMs,
248
+ allocatorMs: sumOptional(acc.allocatorMs, event.allocatorMs),
249
+ allocatorStatus: event.allocatorStatus ?? acc.allocatorStatus,
250
+ allocatorBodyClass: event.allocatorBodyClass ?? acc.allocatorBodyClass,
251
+ allocatorAttempts: sumOptional(acc.allocatorAttempts, event.allocatorAttempts),
252
+ lockWaitMs: sumOptional(acc.lockWaitMs, event.lockWaitMs),
253
+ redisReadMs: sumOptional(acc.redisReadMs, event.redisReadMs),
254
+ redisWriteMs: sumOptional(acc.redisWriteMs, event.redisWriteMs),
255
+ poolAgeMs: maxOptional(acc.poolAgeMs, event.poolAgeMs),
256
+ poolExpiresInMs: maxOptional(acc.poolExpiresInMs, event.poolExpiresInMs),
257
+ attempts: acc.attempts + event.attempts,
258
+ refreshes: sumOptional(acc.refreshes, event.refreshes),
259
+ }),
260
+ firstFailure,
261
+ )
262
+ : undefined;
263
+ return {
264
+ kind: "unresolved",
265
+ vendors: [...this.#unresolvedVendors],
266
+ ...(aggregate
267
+ ? {
268
+ cacheStatus: aggregate.cacheStatus,
269
+ cacheHit: aggregate.cacheHit,
270
+ resolutionMs: aggregate.resolutionMs,
271
+ ...(aggregate.allocatorMs !== undefined
272
+ ? { allocatorMs: aggregate.allocatorMs }
273
+ : {}),
274
+ ...(aggregate.allocatorStatus !== undefined
275
+ ? { allocatorStatus: aggregate.allocatorStatus }
276
+ : {}),
277
+ ...(aggregate.allocatorBodyClass !== undefined
278
+ ? { allocatorBodyClass: aggregate.allocatorBodyClass }
279
+ : {}),
280
+ ...(aggregate.allocatorAttempts !== undefined
281
+ ? { allocatorAttempts: aggregate.allocatorAttempts }
282
+ : {}),
283
+ ...(aggregate.lockWaitMs !== undefined ? { lockWaitMs: aggregate.lockWaitMs } : {}),
284
+ ...(aggregate.redisReadMs !== undefined
285
+ ? { redisReadMs: aggregate.redisReadMs }
286
+ : {}),
287
+ ...(aggregate.redisWriteMs !== undefined
288
+ ? { redisWriteMs: aggregate.redisWriteMs }
289
+ : {}),
290
+ ...(aggregate.poolAgeMs !== undefined ? { poolAgeMs: aggregate.poolAgeMs } : {}),
291
+ ...(aggregate.poolExpiresInMs !== undefined
292
+ ? { poolExpiresInMs: aggregate.poolExpiresInMs }
293
+ : {}),
294
+ attempts: aggregate.attempts,
295
+ ...(aggregate.refreshes !== undefined ? { refreshes: aggregate.refreshes } : {}),
296
+ }
297
+ : {}),
298
+ ...(failovers.length > 0 ? { failovers } : {}),
299
+ ...(attemptSamples.length > 0 ? { attemptSamples } : {}),
300
+ };
301
+ }
302
+
303
+ // The serving vendor/protocol is the last successful resolution.
304
+ const serving = okEvents[okEvents.length - 1] ?? first;
171
305
  const vendors: ProxyVendorName[] = [];
172
306
  for (const event of this.#events) {
173
307
  if (!vendors.includes(event.provider)) vendors.push(event.provider);
@@ -195,6 +329,7 @@ export class ProxyTelemetryCollector implements ProxyTelemetrySink {
195
329
  first,
196
330
  );
197
331
  return {
332
+ kind: "resolved",
198
333
  provider: serving.provider,
199
334
  ...(aggregate.userAgentSource ? { userAgentSource: aggregate.userAgentSource } : {}),
200
335
  ...(serving.protocol ? { protocol: serving.protocol } : {}),
@@ -220,32 +355,9 @@ export class ProxyTelemetryCollector implements ProxyTelemetrySink {
220
355
  : {}),
221
356
  attempts: aggregate.attempts,
222
357
  ...(aggregate.refreshes !== undefined ? { refreshes: aggregate.refreshes } : {}),
223
- ...(this.#attempts.length > 0
224
- ? {
225
- attemptSamples: this.#attempts.map((attempt, index) => ({
226
- n: index + 1,
227
- a: attempt.attempt,
228
- ...(attempt.poolIndex === undefined ? {} : { i: attempt.poolIndex }),
229
- ...(attempt.proxyHash ? { h: attempt.proxyHash } : {}),
230
- o: attempt.outcome,
231
- ...(attempt.errorCode ? { c: attempt.errorCode } : {}),
232
- ...(attempt.status === undefined ? {} : { s: attempt.status }),
233
- ...(attempt.durationMs === undefined ? {} : { d: attempt.durationMs }),
234
- })),
235
- }
236
- : {}),
358
+ ...(attemptSamples.length > 0 ? { attemptSamples } : {}),
237
359
  ...(vendors.length > 1 ? { vendors } : {}),
238
- ...(this.#failovers.length > 0
239
- ? {
240
- failovers: this.#failovers.map((failover) => ({
241
- v: failover.vendor,
242
- ...(failover.nextVendor ? { nx: failover.nextVendor } : {}),
243
- p: failover.phase,
244
- r: failover.reason,
245
- ...(failover.attempt === undefined ? {} : { a: failover.attempt }),
246
- })),
247
- }
248
- : {}),
360
+ ...(failovers.length > 0 ? { failovers } : {}),
249
361
  };
250
362
  }
251
363
 
@@ -0,0 +1,77 @@
1
+ import type { TraceConfig } from "../types.js";
2
+
3
+ /** SDK-owned environment variable names for server trace output. */
4
+ export const APIFUSE__TRACE__ENABLED = "APIFUSE__TRACE__ENABLED";
5
+ export const APIFUSE__TRACE__EXPORTER = "APIFUSE__TRACE__EXPORTER";
6
+
7
+ type EnvLike = Record<string, string | undefined>;
8
+
9
+ const TRACE_EXPORTER_LOOKUP: Record<Exclude<NonNullable<TraceConfig["exporter"]>, "otlp">, true> = {
10
+ console: true,
11
+ json: true,
12
+ none: true,
13
+ };
14
+ const TRACE_EXPORTERS = new Set(Object.keys(TRACE_EXPORTER_LOOKUP));
15
+
16
+ const emittedWarnings = new Set<string>();
17
+
18
+ function warnOnce(key: string, message: string): void {
19
+ if (emittedWarnings.has(key)) return;
20
+ emittedWarnings.add(key);
21
+ console.warn(message);
22
+ }
23
+
24
+ function parseEnabled(raw: string | undefined): boolean | undefined {
25
+ if (raw === undefined) return undefined;
26
+ switch (raw.trim().toLowerCase()) {
27
+ case "1":
28
+ case "true":
29
+ case "yes":
30
+ case "on":
31
+ return true;
32
+ case "0":
33
+ case "false":
34
+ case "no":
35
+ case "off":
36
+ return false;
37
+ default:
38
+ warnOnce(
39
+ APIFUSE__TRACE__ENABLED,
40
+ "[apifuse] Invalid APIFUSE__TRACE__ENABLED; falling back to disabled tracing.",
41
+ );
42
+ return false;
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Resolve server trace output configuration from SDK-owned environment
48
+ * variables. Invalid exporter values fail closed so a provider cannot crash at
49
+ * startup because of an observability setting.
50
+ */
51
+ export function resolveTraceConfigFromEnv(env: EnvLike = process.env): TraceConfig | undefined {
52
+ const enabledRaw = env[APIFUSE__TRACE__ENABLED];
53
+ const exporterRaw = env[APIFUSE__TRACE__EXPORTER];
54
+
55
+ if (enabledRaw === undefined && exporterRaw === undefined) {
56
+ return undefined;
57
+ }
58
+
59
+ const enabled = parseEnabled(enabledRaw);
60
+ const normalizedExporter = exporterRaw?.trim().toLowerCase();
61
+ const exporter = TRACE_EXPORTERS.has(normalizedExporter as NonNullable<TraceConfig["exporter"]>)
62
+ ? (normalizedExporter as NonNullable<TraceConfig["exporter"]>)
63
+ : exporterRaw === undefined
64
+ ? undefined
65
+ : "none";
66
+ if (exporterRaw !== undefined && exporter === "none" && normalizedExporter !== "none") {
67
+ warnOnce(
68
+ APIFUSE__TRACE__EXPORTER,
69
+ `[apifuse] Invalid APIFUSE__TRACE__EXPORTER value "${normalizedExporter ?? exporterRaw}" (OTLP is unsupported for server output); supported exporters are "console", "json", and "none"; falling back to exporter "none".`,
70
+ );
71
+ }
72
+
73
+ return {
74
+ ...(enabled !== undefined ? { enabled } : {}),
75
+ ...(exporter !== undefined ? { exporter } : {}),
76
+ };
77
+ }
@@ -5,7 +5,11 @@ export type {
5
5
  ProxyVendorName,
6
6
  SmartproxyAllocatorBodyClass,
7
7
  } from "../config/loader.js";
8
- export type { ProxyTelemetryLogPayload } from "../runtime/proxy-telemetry.js";
8
+ export type {
9
+ ProxyTelemetryLogPayload,
10
+ ProxyTelemetryResolvedPayload,
11
+ ProxyTelemetryUnresolvedPayload,
12
+ } from "../runtime/proxy-telemetry.js";
9
13
  export {
10
14
  createServerApp,
11
15
  createServerAppAsync,
@@ -74,6 +74,7 @@ import { StealthCookieJar } from "../runtime/stealth-cookies.js";
74
74
  import type * as StealthRuntimeModule from "../runtime/stealth.js";
75
75
  import { createSttClientFromEnv } from "../runtime/stt.js";
76
76
  import { createTraceContext } from "../runtime/trace.js";
77
+ import { resolveTraceConfigFromEnv } from "../runtime/trace-config.js";
77
78
  import { parseSchema } from "../schema.js";
78
79
  import {
79
80
  STATEFUL_NONCE_HEADER as STATEFUL_FORWARDING_NONCE_HEADER,
@@ -116,6 +117,7 @@ import type {
116
117
  import { VALID_OPERATION_ERROR_STATUSES } from "../types.js";
117
118
  import type { SelfTestCancellationLogEvent } from "./self-test.js";
118
119
  import { resolveSelfTestMasterSecrets } from "./self-test-token.js";
120
+ import { resolveServerTraceContextOptions } from "./trace-output.js";
119
121
  import {
120
122
  type AuthFlowRequest,
121
123
  AuthFlowRequestSchema,
@@ -608,6 +610,7 @@ function createProviderContext(
608
610
  proxyTelemetry?: ProxyTelemetryCollector,
609
611
  signal?: AbortSignal,
610
612
  ): ProviderContext {
613
+ const traceConfig = resolveTraceConfigFromEnv();
611
614
  const baseUrl = getProviderBaseUrl(provider);
612
615
  const stealthBaseUrl = getProviderStealthBaseUrl(provider);
613
616
  const stealthProfile = getProviderStealthProfile(provider);
@@ -711,7 +714,15 @@ function createProviderContext(
711
714
  },
712
715
  }
713
716
  : {}),
714
- trace: createTraceContext(),
717
+ trace: traceConfig
718
+ ? createTraceContext(
719
+ resolveServerTraceContextOptions(traceConfig, {
720
+ request_id: request.requestId,
721
+ provider_id: provider.id,
722
+ operation_id: operationId,
723
+ }),
724
+ )
725
+ : createTraceContext(),
715
726
  auth: createAuthStub(),
716
727
  ocr: options.ocr ?? createOcrClientFromEnv(provider.ocr),
717
728
  stt: options.stt ?? createSttClientFromEnv(provider.stt),
@@ -0,0 +1,32 @@
1
+ import {
2
+ resolveTraceContextOptions,
3
+ type CreateTraceContextOptions,
4
+ type Span,
5
+ } from "../runtime/trace.js";
6
+ import { sanitizeSpanForOutput } from "../trace-sanitization.js";
7
+ import type { TraceConfig } from "../types.js";
8
+
9
+ /** Server-only trace output policy. Shared programmatic trace callers stay in-memory. */
10
+ export function resolveServerTraceContextOptions(
11
+ config: TraceConfig,
12
+ resourceAttributes: Record<string, string>,
13
+ ): CreateTraceContextOptions {
14
+ const resolved = resolveTraceContextOptions(config);
15
+ const outputEnabled = config.enabled !== false && config.exporter !== "none";
16
+ const consoleHook =
17
+ outputEnabled && (config.exporter === "console" || config.exporter === "json")
18
+ ? (span: Span) => console.log(JSON.stringify(sanitizeSpanForOutput(span, resourceAttributes)))
19
+ : undefined;
20
+ const onSpan =
21
+ consoleHook && resolved.onSpan
22
+ ? (span: Span) => {
23
+ consoleHook(span);
24
+ resolved.onSpan?.(span);
25
+ }
26
+ : (consoleHook ?? resolved.onSpan);
27
+
28
+ return {
29
+ maxSpans: resolved.maxSpans,
30
+ onSpan,
31
+ };
32
+ }
@@ -0,0 +1,63 @@
1
+ import {
2
+ encodeDiagnosticControls,
3
+ isSensitiveFixtureKey,
4
+ REDACTED_FIXTURE_VALUE,
5
+ sanitizeDiagnosticText,
6
+ } from "./fixture-sanitization.js";
7
+ import type { TraceSpan } from "./types.js";
8
+
9
+ const MAX_TRACE_TEXT_LENGTH = 300;
10
+
11
+ function sanitizeTraceText(value: string): string {
12
+ const encoded = sanitizeDiagnosticText(value);
13
+ return encoded.length > MAX_TRACE_TEXT_LENGTH
14
+ ? `${encoded.slice(0, MAX_TRACE_TEXT_LENGTH)}… [truncated]`
15
+ : encoded;
16
+ }
17
+
18
+ /** Span names are SDK-authored identifiers; retain them while preventing log injection. */
19
+ export function sanitizeSpanNameForOutput(value: string): string {
20
+ const encoded = encodeDiagnosticControls(value);
21
+ return encoded.length > MAX_TRACE_TEXT_LENGTH
22
+ ? `${encoded.slice(0, MAX_TRACE_TEXT_LENGTH)}… [truncated]`
23
+ : encoded;
24
+ }
25
+
26
+ export function sanitizeTraceAttributes(
27
+ attributes: Record<string, unknown>,
28
+ ): Record<string, string | number | boolean> {
29
+ return Object.fromEntries(
30
+ Object.entries(attributes).map(([key, value]) => [
31
+ sanitizeTraceText(key),
32
+ isSensitiveFixtureKey(key)
33
+ ? REDACTED_FIXTURE_VALUE
34
+ : typeof value === "string"
35
+ ? sanitizeTraceText(value)
36
+ : typeof value === "number" || typeof value === "boolean"
37
+ ? value
38
+ : sanitizeTraceText(String(value)),
39
+ ]),
40
+ );
41
+ }
42
+
43
+ export function sanitizeSpanForOutput(
44
+ span: TraceSpan,
45
+ additionalAttributes?: Record<string, string>,
46
+ ): TraceSpan {
47
+ // Keep this schema explicit so future fields are not silently added to a
48
+ // process output path before their trust boundary has been reviewed.
49
+ return {
50
+ id: span.id,
51
+ name: sanitizeSpanNameForOutput(span.name),
52
+ startedAt: span.startedAt,
53
+ endedAt: span.endedAt,
54
+ duration_ms: span.duration_ms,
55
+ status: span.status,
56
+ attributes: sanitizeTraceAttributes({
57
+ ...span.attributes,
58
+ ...(additionalAttributes ?? {}),
59
+ }),
60
+ ...(span.error !== undefined ? { error: sanitizeTraceText(span.error) } : {}),
61
+ ...(span.parentId !== undefined ? { parentId: span.parentId } : {}),
62
+ };
63
+ }