@apifuse/provider-sdk 2.2.0-beta.55 → 2.2.0-beta.56

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.
@@ -9,8 +9,19 @@ import type {
9
9
  ProxyVendorName,
10
10
  SmartproxyAllocatorBodyClass,
11
11
  } from "../config/loader.js";
12
+ import {
13
+ closedEnum,
14
+ RequestTelemetry,
15
+ type ClosedEnum,
16
+ type TelemetryContributor,
17
+ } from "./request-telemetry.js";
18
+ import { createTraceContext } from "./trace.js";
12
19
 
13
- export const PROVIDER_TELEMETRY_HEADER = "X-ApiFuse-Provider-Telemetry";
20
+ export { PROVIDER_TELEMETRY_HEADER } from "./request-telemetry.js";
21
+
22
+ declare const PROXY_HASH: unique symbol;
23
+ /** Bounded hexadecimal host hash used in proxy attempt samples. */
24
+ export type ProxyHash = ClosedEnum<string> & { readonly [PROXY_HASH]: true };
14
25
 
15
26
  export type ProxyTelemetryResolvedPayload = {
16
27
  kind: "resolved";
@@ -94,14 +105,51 @@ export type ProxyTelemetryLogPayload =
94
105
  | ProxyTelemetryResolvedPayload
95
106
  | ProxyTelemetryUnresolvedPayload;
96
107
 
97
- type ProviderTelemetryHeader = {
98
- v: 1;
99
- proxy?: ProxyTelemetryLogPayload;
108
+ /** Concrete gateway-safe projection of the unchanged proxy sibling. */
109
+ export type ProxyTelemetryHeaderPayload = {
110
+ kind: ClosedEnum<"resolved" | "unresolved">;
111
+ provider?: ClosedEnum<ProxyVendorName>;
112
+ userAgentSource?: ClosedEnum<ProxyUserAgentSource>;
113
+ protocol?: ClosedEnum<ProxyProtocol>;
114
+ cacheStatus?: ClosedEnum<ProxyCacheStatus>;
115
+ cacheHit?: boolean;
116
+ resolutionMs?: number;
117
+ allocatorMs?: number;
118
+ allocatorStatus?: number;
119
+ allocatorBodyClass?: ClosedEnum<SmartproxyAllocatorBodyClass>;
120
+ allocatorAttempts?: number;
121
+ lockWaitMs?: number;
122
+ redisReadMs?: number;
123
+ redisWriteMs?: number;
124
+ poolAgeMs?: number;
125
+ poolExpiresInMs?: number;
126
+ attempts?: number;
127
+ refreshes?: number;
128
+ attemptSamples?: {
129
+ n: number;
130
+ a: number;
131
+ i?: number;
132
+ h?: ProxyHash;
133
+ o: ClosedEnum<ProxyAttemptTelemetryEvent["outcome"]>;
134
+ c?: ClosedEnum<string>;
135
+ s?: number;
136
+ d?: number;
137
+ }[];
138
+ vendors?: ClosedEnum<ProxyVendorName>[];
139
+ failovers?: {
140
+ v: ClosedEnum<ProxyVendorName>;
141
+ nx?: ClosedEnum<ProxyVendorName>;
142
+ p: ClosedEnum<ProxyVendorFailoverTelemetryEvent["phase"]>;
143
+ r: ClosedEnum<ProxyVendorFailoverTelemetryEvent["reason"]>;
144
+ a?: number;
145
+ }[];
100
146
  };
101
147
 
102
- const MAX_HEADER_BYTES = 4_096;
103
148
  const MAX_PROXY_ATTEMPT_SAMPLES = 24;
104
149
  const MAX_PROXY_FAILOVER_SAMPLES = 12;
150
+ const MAX_RETAINED_PROXY_RESOLUTIONS = 64;
151
+ const PROXY_HEADER_TOKEN = /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,63}$/;
152
+ const PROXY_HEADER_HASH = /^[0-9a-f]{1,16}$/;
105
153
 
106
154
  const CACHE_STATUS_SEVERITY: Record<ProxyCacheStatus, number> = {
107
155
  disabled: 0,
@@ -128,19 +176,48 @@ function worseStatus(left: ProxyCacheStatus, right: ProxyCacheStatus): ProxyCach
128
176
  return CACHE_STATUS_SEVERITY[right] > CACHE_STATUS_SEVERITY[left] ? right : left;
129
177
  }
130
178
 
131
- function encodeBase64Url(value: string): string {
132
- return Buffer.from(value, "utf8").toString("base64url");
179
+ function aggregateResolution(
180
+ acc: ProxyResolutionTelemetryEvent,
181
+ event: ProxyResolutionTelemetryEvent,
182
+ ): ProxyResolutionTelemetryEvent {
183
+ return {
184
+ provider: event.provider,
185
+ userAgentSource: event.userAgentSource ?? acc.userAgentSource,
186
+ cacheStatus: worseStatus(acc.cacheStatus, event.cacheStatus),
187
+ cacheHit: acc.cacheHit && event.cacheHit,
188
+ resolutionMs: acc.resolutionMs + event.resolutionMs,
189
+ allocatorMs: sumOptional(acc.allocatorMs, event.allocatorMs),
190
+ allocatorStatus: event.allocatorStatus ?? acc.allocatorStatus,
191
+ allocatorBodyClass: event.allocatorBodyClass ?? acc.allocatorBodyClass,
192
+ allocatorAttempts: sumOptional(acc.allocatorAttempts, event.allocatorAttempts),
193
+ lockWaitMs: sumOptional(acc.lockWaitMs, event.lockWaitMs),
194
+ redisReadMs: sumOptional(acc.redisReadMs, event.redisReadMs),
195
+ redisWriteMs: sumOptional(acc.redisWriteMs, event.redisWriteMs),
196
+ poolAgeMs: maxOptional(acc.poolAgeMs, event.poolAgeMs),
197
+ poolExpiresInMs: maxOptional(acc.poolExpiresInMs, event.poolExpiresInMs),
198
+ attempts: acc.attempts + event.attempts,
199
+ refreshes: sumOptional(acc.refreshes, event.refreshes),
200
+ };
133
201
  }
134
202
 
135
- export class ProxyTelemetryCollector implements ProxyTelemetrySink {
203
+ export class ProxyTelemetryCollector
204
+ implements
205
+ ProxyTelemetrySink,
206
+ TelemetryContributor<ProxyTelemetryLogPayload, ProxyTelemetryHeaderPayload>
207
+ {
208
+ readonly key = "proxy" as const;
136
209
  #events: ProxyResolutionTelemetryEvent[] = [];
210
+ #resolutionCount = 0;
211
+ #aggregateResolution: ProxyResolutionTelemetryEvent | undefined;
212
+ #lastSuccessfulResolution: ProxyResolutionTelemetryEvent | undefined;
213
+ #vendors: ProxyVendorName[] = [];
137
214
  #attempts: ProxyAttemptTelemetryEvent[] = [];
138
215
  #failovers: ProxyVendorFailoverTelemetryEvent[] = [];
139
216
  #unresolvedVendors: ProxyVendorName[] = [];
140
217
 
141
218
  recordProxyResolution(event: ProxyResolutionTelemetryEvent): void {
142
219
  const outcome = event.outcome === "error" ? "error" : "ok";
143
- this.#events.push({
220
+ const normalized: ProxyResolutionTelemetryEvent = {
144
221
  provider: event.provider,
145
222
  outcome,
146
223
  ...(event.userAgentSource ? { userAgentSource: event.userAgentSource } : {}),
@@ -174,12 +251,29 @@ export class ProxyTelemetryCollector implements ProxyTelemetrySink {
174
251
  attempts: Math.max(1, Math.floor(event.attempts || 1)),
175
252
  refreshes:
176
253
  event.refreshes === undefined ? undefined : Math.max(0, Math.floor(event.refreshes)),
177
- });
254
+ };
255
+ this.#resolutionCount += 1;
256
+ if (this.#events.length < MAX_RETAINED_PROXY_RESOLUTIONS) this.#events.push(normalized);
257
+ this.#aggregateResolution = this.#aggregateResolution
258
+ ? aggregateResolution(this.#aggregateResolution, normalized)
259
+ : normalized;
260
+ if (outcome === "ok") this.#lastSuccessfulResolution = normalized;
261
+ if (!this.#vendors.includes(event.provider)) this.#vendors.push(event.provider);
178
262
  if (outcome === "error" && !this.#unresolvedVendors.includes(event.provider)) {
179
263
  this.#unresolvedVendors.push(event.provider);
180
264
  }
181
265
  }
182
266
 
267
+ /** Number of raw resolution events retained for bounded-history verification. */
268
+ get retainedResolutionEventCount(): number {
269
+ return this.#events.length;
270
+ }
271
+
272
+ /** Total resolutions included in the incremental aggregate. */
273
+ get resolutionEventCount(): number {
274
+ return this.#resolutionCount;
275
+ }
276
+
183
277
  recordProxyVendorFailover(event: ProxyVendorFailoverTelemetryEvent): void {
184
278
  if (this.#failovers.length >= MAX_PROXY_FAILOVER_SAMPLES) return;
185
279
  this.#failovers.push({
@@ -213,8 +307,7 @@ export class ProxyTelemetryCollector implements ProxyTelemetrySink {
213
307
  }
214
308
 
215
309
  toLogPayload(): ProxyTelemetryLogPayload | undefined {
216
- const [first, ...rest] = this.#events;
217
- const okEvents = this.#events.filter((event) => event.outcome !== "error");
310
+ const aggregate = this.#aggregateResolution;
218
311
  const attemptSamples = this.#attempts.map((attempt, index) => ({
219
312
  n: index + 1,
220
313
  a: attempt.attempt,
@@ -233,33 +326,8 @@ export class ProxyTelemetryCollector implements ProxyTelemetrySink {
233
326
  ...(failover.attempt === undefined ? {} : { a: failover.attempt }),
234
327
  }));
235
328
 
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;
329
+ if (!this.#lastSuccessfulResolution) {
330
+ if (!aggregate && failovers.length === 0) return undefined;
263
331
  return {
264
332
  kind: "unresolved",
265
333
  vendors: [...this.#unresolvedVendors],
@@ -301,33 +369,8 @@ export class ProxyTelemetryCollector implements ProxyTelemetrySink {
301
369
  }
302
370
 
303
371
  // The serving vendor/protocol is the last successful resolution.
304
- const serving = okEvents[okEvents.length - 1] ?? first;
305
- const vendors: ProxyVendorName[] = [];
306
- for (const event of this.#events) {
307
- if (!vendors.includes(event.provider)) vendors.push(event.provider);
308
- }
309
-
310
- const aggregate = rest.reduce<ProxyResolutionTelemetryEvent>(
311
- (acc, event) => ({
312
- provider: event.provider,
313
- userAgentSource: event.userAgentSource ?? acc.userAgentSource,
314
- cacheStatus: worseStatus(acc.cacheStatus, event.cacheStatus),
315
- cacheHit: acc.cacheHit && event.cacheHit,
316
- resolutionMs: acc.resolutionMs + event.resolutionMs,
317
- allocatorMs: sumOptional(acc.allocatorMs, event.allocatorMs),
318
- allocatorStatus: event.allocatorStatus ?? acc.allocatorStatus,
319
- allocatorBodyClass: event.allocatorBodyClass ?? acc.allocatorBodyClass,
320
- allocatorAttempts: sumOptional(acc.allocatorAttempts, event.allocatorAttempts),
321
- lockWaitMs: sumOptional(acc.lockWaitMs, event.lockWaitMs),
322
- redisReadMs: sumOptional(acc.redisReadMs, event.redisReadMs),
323
- redisWriteMs: sumOptional(acc.redisWriteMs, event.redisWriteMs),
324
- poolAgeMs: maxOptional(acc.poolAgeMs, event.poolAgeMs),
325
- poolExpiresInMs: maxOptional(acc.poolExpiresInMs, event.poolExpiresInMs),
326
- attempts: acc.attempts + event.attempts,
327
- refreshes: sumOptional(acc.refreshes, event.refreshes),
328
- }),
329
- first,
330
- );
372
+ if (!aggregate) return undefined;
373
+ const serving = this.#lastSuccessfulResolution;
331
374
  return {
332
375
  kind: "resolved",
333
376
  provider: serving.provider,
@@ -356,18 +399,61 @@ export class ProxyTelemetryCollector implements ProxyTelemetrySink {
356
399
  attempts: aggregate.attempts,
357
400
  ...(aggregate.refreshes !== undefined ? { refreshes: aggregate.refreshes } : {}),
358
401
  ...(attemptSamples.length > 0 ? { attemptSamples } : {}),
359
- ...(vendors.length > 1 ? { vendors } : {}),
402
+ ...(this.#vendors.length > 1 ? { vendors: [...this.#vendors] } : {}),
360
403
  ...(failovers.length > 0 ? { failovers } : {}),
361
404
  };
362
405
  }
363
406
 
364
- toHeaderValue(): string | undefined {
365
- const proxy = this.toLogPayload();
366
- if (!proxy) return undefined;
367
- const payload: ProviderTelemetryHeader = { v: 1, proxy };
407
+ toHeaderPayload(log: ProxyTelemetryLogPayload): ProxyTelemetryHeaderPayload {
408
+ const attemptSamples = log.attemptSamples?.map((sample) => ({
409
+ n: sample.n,
410
+ a: sample.a,
411
+ ...(sample.i === undefined ? {} : { i: sample.i }),
412
+ ...(sample.h && PROXY_HEADER_HASH.test(sample.h)
413
+ ? { h: closedEnum(sample.h) as ProxyHash }
414
+ : {}),
415
+ o: closedEnum(sample.o),
416
+ ...(sample.c && PROXY_HEADER_TOKEN.test(sample.c) ? { c: closedEnum(sample.c) } : {}),
417
+ ...(sample.s === undefined ? {} : { s: sample.s }),
418
+ ...(sample.d === undefined ? {} : { d: sample.d }),
419
+ }));
420
+ const failovers = log.failovers?.map((failover) => ({
421
+ ...failover,
422
+ v: closedEnum(failover.v),
423
+ ...(failover.nx ? { nx: closedEnum(failover.nx) } : {}),
424
+ p: closedEnum(failover.p),
425
+ r: closedEnum(failover.r),
426
+ }));
427
+ if (log.kind === "resolved") {
428
+ return {
429
+ ...log,
430
+ kind: closedEnum(log.kind),
431
+ provider: closedEnum(log.provider),
432
+ ...(log.userAgentSource ? { userAgentSource: closedEnum(log.userAgentSource) } : {}),
433
+ ...(log.protocol ? { protocol: closedEnum(log.protocol) } : {}),
434
+ cacheStatus: closedEnum(log.cacheStatus),
435
+ ...(log.allocatorBodyClass
436
+ ? { allocatorBodyClass: closedEnum(log.allocatorBodyClass) }
437
+ : {}),
438
+ ...(attemptSamples ? { attemptSamples } : {}),
439
+ ...(log.vendors ? { vendors: log.vendors.map((vendor) => closedEnum(vendor)) } : {}),
440
+ ...(failovers ? { failovers } : {}),
441
+ } as ProxyTelemetryHeaderPayload;
442
+ }
443
+ return {
444
+ ...log,
445
+ kind: closedEnum(log.kind),
446
+ vendors: log.vendors.map((vendor) => closedEnum(vendor)),
447
+ ...(log.cacheStatus ? { cacheStatus: closedEnum(log.cacheStatus) } : {}),
448
+ ...(log.allocatorBodyClass ? { allocatorBodyClass: closedEnum(log.allocatorBodyClass) } : {}),
449
+ ...(failovers ? { failovers } : {}),
450
+ ...(attemptSamples ? { attemptSamples } : {}),
451
+ } as ProxyTelemetryHeaderPayload;
452
+ }
368
453
 
369
- const encoded = encodeBase64Url(JSON.stringify(payload));
370
- if (encoded.length > MAX_HEADER_BYTES) return undefined;
371
- return encoded;
454
+ toHeaderValue(): string | undefined {
455
+ const telemetry = new RequestTelemetry(createTraceContext());
456
+ telemetry.register(this);
457
+ return telemetry.toHeaderValue();
372
458
  }
373
459
  }
@@ -0,0 +1,376 @@
1
+ import { PROVIDER_OBSERVABILITY_TAXONOMY_VERSION } from "../observability.js";
2
+ import type { ProxyTelemetrySink } from "../config/loader.js";
3
+ import type { ProxyTelemetryLogPayload } from "./proxy-telemetry.js";
4
+ import type { Span, TraceContext } from "./trace.js";
5
+
6
+ export const PROVIDER_TELEMETRY_HEADER = "X-ApiFuse-Provider-Telemetry";
7
+
8
+ export type TelemetryKey =
9
+ | "proxy"
10
+ | "resolver"
11
+ | "http"
12
+ | "stealth"
13
+ | "native"
14
+ | "browser"
15
+ | "ocr"
16
+ | "stt"
17
+ | "cache"
18
+ | "state"
19
+ | "events";
20
+
21
+ declare const CLOSED_ENUM: unique symbol;
22
+
23
+ /** A string from an SDK-declared, closed observability taxonomy. */
24
+ export type ClosedEnum<T extends string> = T & { readonly [CLOSED_ENUM]: true };
25
+
26
+ /** Brands a value from an SDK-declared closed string union without changing it. */
27
+ export function closedEnum<T extends string>(value: T): ClosedEnum<T> {
28
+ return value as ClosedEnum<T>;
29
+ }
30
+
31
+ /** Explicit exemption for the existing implementation-shaped cache key list. */
32
+ export type TenantOpaqueKeys = string[] & { readonly __tenantOpaqueCacheKeys: true };
33
+
34
+ /** Names the deliberate tenant cache-key exemption without changing its wire value. */
35
+ export function tenantOpaqueKeys(value: string[]): TenantOpaqueKeys {
36
+ return value as TenantOpaqueKeys;
37
+ }
38
+
39
+ /**
40
+ * Compile-time projection for the gateway ingestion contract. Plain strings,
41
+ * URLs, hostnames, free text, unknown values, and escape-hatch properties reduce to `never`.
42
+ */
43
+ export type GatewayIngestible<T> = 0 extends 1 & T
44
+ ? never
45
+ : T extends object
46
+ ? keyof T extends never
47
+ ? never
48
+ : {
49
+ [K in keyof T]: 0 extends 1 & T[K]
50
+ ? never
51
+ : unknown extends T[K]
52
+ ? never
53
+ : [NonNullable<T[K]>] extends [number | boolean | ClosedEnum<string>]
54
+ ? T[K]
55
+ : [NonNullable<T[K]>] extends [readonly (infer U)[]]
56
+ ? 0 extends 1 & U
57
+ ? never
58
+ : unknown extends U
59
+ ? never
60
+ : [U] extends [
61
+ number | boolean | ClosedEnum<string> | GatewayIngestible<U>,
62
+ ]
63
+ ? T[K]
64
+ : never
65
+ : [NonNullable<T[K]>] extends [object]
66
+ ? [NonNullable<T[K]>] extends [GatewayIngestible<NonNullable<T[K]>>]
67
+ ? T[K]
68
+ : never
69
+ : never;
70
+ }
71
+ : never;
72
+
73
+ /** Compile-time projection for tenant-visible, identity-neutral metadata. */
74
+ export type TenantNeutral<T> = 0 extends 1 & T
75
+ ? never
76
+ : T extends object
77
+ ? {
78
+ [K in keyof T]: K extends
79
+ | `vendor${string}`
80
+ | "provider"
81
+ | "engine"
82
+ | "model"
83
+ | `${string}Host`
84
+ ? never
85
+ : 0 extends 1 & T[K]
86
+ ? never
87
+ : unknown extends T[K]
88
+ ? never
89
+ : [NonNullable<T[K]>] extends [
90
+ string[] & { readonly __tenantOpaqueCacheKeys: true },
91
+ ]
92
+ ? K extends "keys"
93
+ ? T[K]
94
+ : never
95
+ : [NonNullable<T[K]>] extends [number | boolean | ClosedEnum<string>]
96
+ ? T[K]
97
+ : [NonNullable<T[K]>] extends [object]
98
+ ? [NonNullable<T[K]>] extends [TenantNeutral<NonNullable<T[K]>>]
99
+ ? T[K]
100
+ : never
101
+ : never;
102
+ }
103
+ : never;
104
+
105
+ export interface SpanIndex {
106
+ readonly spans: readonly Span[];
107
+ readonly byName: ReadonlyMap<string, readonly Span[]>;
108
+ count(name: string): number;
109
+ durationMs(name: string): number;
110
+ }
111
+
112
+ export interface TelemetryContributor<Log extends object, Header extends object> {
113
+ readonly key: TelemetryKey;
114
+ toLogPayload(spans: SpanIndex): Log | undefined;
115
+ toHeaderPayload(
116
+ log: Log,
117
+ ): 0 extends 1 & Header
118
+ ? never
119
+ : [Header] extends [GatewayIngestible<Header>]
120
+ ? Header | undefined
121
+ : never;
122
+ }
123
+
124
+ export type RequestTelemetryLogPayload = {
125
+ proxy?: ProxyTelemetryLogPayload;
126
+ } & Partial<Record<Exclude<TelemetryKey, "proxy">, object>>;
127
+
128
+ type RegisteredTelemetryContributor = {
129
+ readonly key: TelemetryKey;
130
+ toLogPayload(spans: SpanIndex): object | undefined;
131
+ toHeaderPayload(log: object): object | undefined;
132
+ };
133
+
134
+ type ProviderTelemetryEnvelope = {
135
+ v: 1;
136
+ taxonomy: typeof PROVIDER_OBSERVABILITY_TAXONOMY_VERSION;
137
+ truncated?: true;
138
+ } & Partial<Record<Exclude<TelemetryKey, "events">, object>>;
139
+
140
+ const MAX_HEADER_BYTES = 4_096;
141
+ const MAX_INGESTIBLE_ARRAY_LENGTH = 64;
142
+ const MAX_INGESTIBLE_OBJECT_KEYS = 32;
143
+ const MAX_INGESTIBLE_DEPTH = 4;
144
+ const INGESTIBLE_TOKEN = /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,63}$/;
145
+ const HEADER_PRIORITY = [
146
+ "proxy",
147
+ "resolver",
148
+ "native",
149
+ "http",
150
+ "stealth",
151
+ "browser",
152
+ "ocr",
153
+ "stt",
154
+ "cache",
155
+ "state",
156
+ ] as const satisfies readonly Exclude<TelemetryKey, "events">[];
157
+ const warnedInvalidKeys = new Set<TelemetryKey>();
158
+
159
+ function createSpanIndex(trace: TraceContext): SpanIndex {
160
+ const spans = trace.getSpans().slice();
161
+ const mutableByName = new Map<string, Span[]>();
162
+ for (const span of spans) {
163
+ const named = mutableByName.get(span.name);
164
+ if (named) named.push(span);
165
+ else mutableByName.set(span.name, [span]);
166
+ }
167
+ const byName = new Map<string, readonly Span[]>(mutableByName);
168
+ return {
169
+ spans,
170
+ byName,
171
+ count(name): number {
172
+ return byName.get(name)?.length ?? 0;
173
+ },
174
+ durationMs(name): number {
175
+ return (byName.get(name) ?? []).reduce((total, span) => total + span.duration_ms, 0);
176
+ },
177
+ };
178
+ }
179
+
180
+ function isPlainRecord(value: unknown): value is Record<string, unknown> {
181
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
182
+ const prototype = Object.getPrototypeOf(value);
183
+ return prototype === Object.prototype || prototype === null;
184
+ }
185
+
186
+ /**
187
+ * Bounded structural defence in depth against casts and accidental free text.
188
+ * Closed-enum brands are erased at runtime; the type-level guard is the contract,
189
+ * while this check only enforces safe token shapes and collection bounds.
190
+ */
191
+ export function isGatewayIngestible(value: unknown): boolean {
192
+ const ancestors = new Set<object>();
193
+ const visit = (candidate: unknown, depth: number): boolean => {
194
+ if (typeof candidate === "number") return Number.isFinite(candidate);
195
+ if (typeof candidate === "boolean") return true;
196
+ if (typeof candidate === "string") return INGESTIBLE_TOKEN.test(candidate);
197
+ if (candidate === null || typeof candidate !== "object") return false;
198
+ if (depth >= MAX_INGESTIBLE_DEPTH || ancestors.has(candidate)) return false;
199
+
200
+ if (Array.isArray(candidate)) {
201
+ if (candidate.length > MAX_INGESTIBLE_ARRAY_LENGTH) return false;
202
+ ancestors.add(candidate);
203
+ const valid = candidate.every(
204
+ (item, index) => Object.hasOwn(candidate, index) && visit(item, depth + 1),
205
+ );
206
+ ancestors.delete(candidate);
207
+ return valid;
208
+ }
209
+
210
+ if (!isPlainRecord(candidate)) return false;
211
+ const keys = Object.keys(candidate);
212
+ if (keys.length > MAX_INGESTIBLE_OBJECT_KEYS) return false;
213
+ ancestors.add(candidate);
214
+ const valid = keys.every((key) => visit(candidate[key], depth + 1));
215
+ ancestors.delete(candidate);
216
+ return valid;
217
+ };
218
+ try {
219
+ return visit(value, 0);
220
+ } catch {
221
+ return false;
222
+ }
223
+ }
224
+
225
+ function encodeEnvelope(envelope: ProviderTelemetryEnvelope): string {
226
+ return Buffer.from(JSON.stringify(envelope), "utf8").toString("base64url");
227
+ }
228
+
229
+ function warnInvalidSibling(key: TelemetryKey): void {
230
+ if (warnedInvalidKeys.has(key)) return;
231
+ warnedInvalidKeys.add(key);
232
+ try {
233
+ console.warn(
234
+ `[provider-sdk] Dropped invalid telemetry sibling "${key}"; contributors must return serializable log objects and bounded gateway-safe header values.`,
235
+ );
236
+ } catch {
237
+ // A host-provided console must not let telemetry fail the request path.
238
+ }
239
+ }
240
+
241
+ function isProxySink(value: unknown): value is ProxyTelemetrySink {
242
+ if (value === null || typeof value !== "object") return false;
243
+ try {
244
+ return typeof Reflect.get(value, "recordProxyResolution") === "function";
245
+ } catch {
246
+ return false;
247
+ }
248
+ }
249
+
250
+ /** One finalisation owner for every request-scoped telemetry contributor. */
251
+ export class RequestTelemetry {
252
+ readonly trace: TraceContext;
253
+ readonly contributors: Readonly<
254
+ Partial<
255
+ Record<
256
+ TelemetryKey,
257
+ {
258
+ readonly key: TelemetryKey;
259
+ toLogPayload(spans: SpanIndex): object | undefined;
260
+ }
261
+ >
262
+ >
263
+ >;
264
+ readonly #registered: Partial<Record<TelemetryKey, RegisteredTelemetryContributor>> = {};
265
+ readonly #exposed: Partial<
266
+ Record<
267
+ TelemetryKey,
268
+ {
269
+ readonly key: TelemetryKey;
270
+ toLogPayload(spans: SpanIndex): object | undefined;
271
+ }
272
+ >
273
+ > = {};
274
+ #proxy: ProxyTelemetrySink | undefined;
275
+
276
+ constructor(trace: TraceContext) {
277
+ this.trace = trace;
278
+ this.contributors = this.#exposed;
279
+ }
280
+
281
+ register<Log extends object, Header extends object>(
282
+ contributor: TelemetryContributor<Log, Header> &
283
+ (0 extends 1 & Log
284
+ ? never
285
+ : 0 extends 1 & Header
286
+ ? never
287
+ : [Header] extends [GatewayIngestible<Header>]
288
+ ? unknown
289
+ : never),
290
+ ): void {
291
+ if (this.#registered[contributor.key]) {
292
+ throw new TypeError(`Telemetry contributor "${contributor.key}" is already registered.`);
293
+ }
294
+ const registered: RegisteredTelemetryContributor = {
295
+ key: contributor.key,
296
+ toLogPayload: (spans) => contributor.toLogPayload(spans),
297
+ toHeaderPayload: (log) => contributor.toHeaderPayload(log as Log),
298
+ };
299
+ this.#registered[contributor.key] = registered;
300
+ this.#exposed[contributor.key] = contributor;
301
+ if (contributor.key === "proxy" && isProxySink(contributor)) this.#proxy = contributor;
302
+ }
303
+
304
+ get proxy(): ProxyTelemetrySink | undefined {
305
+ return this.#proxy;
306
+ }
307
+
308
+ toLogPayload(): RequestTelemetryLogPayload | undefined {
309
+ const spans = createSpanIndex(this.trace);
310
+ const payload: Partial<Record<TelemetryKey, object>> = {};
311
+ for (const key of Object.keys(this.#registered) as TelemetryKey[]) {
312
+ const contributor = this.#registered[key];
313
+ if (!contributor) continue;
314
+ try {
315
+ const sibling = contributor.toLogPayload(spans);
316
+ if (sibling === undefined) continue;
317
+ JSON.stringify(sibling);
318
+ payload[key] = sibling;
319
+ } catch {
320
+ warnInvalidSibling(key);
321
+ }
322
+ }
323
+ return Object.keys(payload).length > 0 ? (payload as RequestTelemetryLogPayload) : undefined;
324
+ }
325
+
326
+ toHeaderValue(): string | undefined {
327
+ const spans = createSpanIndex(this.trace);
328
+ const siblings: Partial<Record<Exclude<TelemetryKey, "events">, object>> = {};
329
+ for (const key of HEADER_PRIORITY) {
330
+ const contributor = this.#registered[key];
331
+ if (!contributor) continue;
332
+ try {
333
+ const log = contributor.toLogPayload(spans);
334
+ if (log === undefined) continue;
335
+ const projected = contributor.toHeaderPayload(log);
336
+ if (projected === undefined) continue;
337
+ if (!isGatewayIngestible(projected)) {
338
+ warnInvalidSibling(key);
339
+ continue;
340
+ }
341
+ const serialized = JSON.stringify(projected);
342
+ const decoded: unknown = JSON.parse(serialized);
343
+ if (!isGatewayIngestible(decoded) || !isPlainRecord(decoded)) {
344
+ warnInvalidSibling(key);
345
+ continue;
346
+ }
347
+ siblings[key] = decoded;
348
+ } catch {
349
+ warnInvalidSibling(key);
350
+ }
351
+ }
352
+
353
+ if (Object.keys(siblings).length === 0) return undefined;
354
+
355
+ // Gateway uses permissive json.Unmarshal (unknown keys are ignored) but
356
+ // requires v === 1. The observability taxonomy is additive and independent.
357
+ const envelope: ProviderTelemetryEnvelope = {
358
+ v: 1,
359
+ taxonomy: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
360
+ ...siblings,
361
+ };
362
+ let encoded = encodeEnvelope(envelope);
363
+ if (encoded.length <= MAX_HEADER_BYTES) return encoded;
364
+
365
+ for (let index = HEADER_PRIORITY.length - 1; index >= 0; index -= 1) {
366
+ const key = HEADER_PRIORITY[index];
367
+ if (!key || !(key in envelope)) continue;
368
+ delete envelope[key];
369
+ if (!HEADER_PRIORITY.some((candidate) => candidate in envelope)) return undefined;
370
+ envelope.truncated = true;
371
+ encoded = encodeEnvelope(envelope);
372
+ if (encoded.length <= MAX_HEADER_BYTES) return encoded;
373
+ }
374
+ return undefined;
375
+ }
376
+ }