@apifuse/provider-sdk 2.2.0-beta.61 → 2.2.0-beta.62

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.
@@ -0,0 +1,190 @@
1
+ import { Buffer } from "node:buffer";
2
+ import { closedEnum, } from "./request-telemetry.js";
3
+ export function httpTelemetryErrorCode(code) {
4
+ switch (code) {
5
+ case "transport_network_error":
6
+ case "transport_timeout":
7
+ case "transport_cancelled":
8
+ case "upstream_http_error":
9
+ return code;
10
+ default:
11
+ return "other";
12
+ }
13
+ }
14
+ const MAX_ATTEMPT_SAMPLES = 24;
15
+ const MAX_DIAGNOSTIC_TEXT = 300;
16
+ function integer(value) {
17
+ return Math.max(0, Math.floor(Number.isFinite(value) ? value : 0));
18
+ }
19
+ function saturatingAdd(a, b) {
20
+ if (!Number.isFinite(a) || !Number.isFinite(b))
21
+ return Number.MAX_SAFE_INTEGER;
22
+ return Math.min(Number.MAX_SAFE_INTEGER, integer(a) + integer(b));
23
+ }
24
+ function boundedText(value, redact) {
25
+ let result;
26
+ try {
27
+ result = redact?.(value) ?? value;
28
+ }
29
+ catch {
30
+ return "[REDACTION_FAILED]";
31
+ }
32
+ // Detach caller text through independent UTF-16 storage, preserving lone surrogates.
33
+ const length = Math.min(result.length, MAX_DIAGNOSTIC_TEXT);
34
+ const units = Buffer.alloc(length * 2);
35
+ for (let index = 0; index < length; index++)
36
+ units.writeUInt16LE(result.charCodeAt(index), index * 2);
37
+ return units.toString("utf16le");
38
+ }
39
+ export class HttpTelemetryCollector {
40
+ key = "http";
41
+ #redact;
42
+ #telemetryFailed = false;
43
+ #attempts = 0;
44
+ #retries = 0;
45
+ #timeouts = 0;
46
+ #proxyUsed = false;
47
+ #lastStatus;
48
+ #retryPreset;
49
+ #lastErrorCode;
50
+ #samples = [];
51
+ #dropped = 0;
52
+ #ms = 0;
53
+ #retry;
54
+ constructor(options = {}) {
55
+ this.#redact = options.redact;
56
+ }
57
+ markTelemetryFailed() {
58
+ this.#telemetryFailed = true;
59
+ }
60
+ startRequest(options) {
61
+ const preset = options.retryPreset;
62
+ let attempts = 0;
63
+ let retries = 0;
64
+ let lastErrorCode;
65
+ let lastStatus;
66
+ let retry;
67
+ let finished = false;
68
+ return {
69
+ recordAttempt: (event) => {
70
+ if (attempts > 0) {
71
+ retries = saturatingAdd(retries, 1);
72
+ this.#retries = saturatingAdd(this.#retries, 1);
73
+ }
74
+ attempts = saturatingAdd(attempts, 1);
75
+ this.#attempts = saturatingAdd(this.#attempts, 1);
76
+ if (event.e === "transport_timeout")
77
+ this.#timeouts = saturatingAdd(this.#timeouts, 1);
78
+ this.#proxyUsed ||= event.proxyUsed;
79
+ this.#retryPreset = preset;
80
+ if (event.status !== undefined)
81
+ this.#lastStatus = integer(event.status);
82
+ if (event.e)
83
+ this.#lastErrorCode = event.e;
84
+ if (this.#samples.length < MAX_ATTEMPT_SAMPLES) {
85
+ const text = (value) => boundedText(value, this.#redact);
86
+ this.#samples.push({
87
+ n: attempts,
88
+ ms: saturatingAdd(0, event.ms),
89
+ ...(event.status === undefined ? {} : { status: integer(event.status) }),
90
+ ...(event.e ? { e: event.e } : {}),
91
+ ...(event.diagnostics
92
+ ? {
93
+ diagnostics: {
94
+ name: text(event.diagnostics.name),
95
+ message: text(event.diagnostics.message),
96
+ ...(event.diagnostics.cause
97
+ ? {
98
+ cause: {
99
+ name: text(event.diagnostics.cause.name),
100
+ message: text(event.diagnostics.cause.message),
101
+ },
102
+ }
103
+ : {}),
104
+ },
105
+ }
106
+ : {}),
107
+ });
108
+ }
109
+ else
110
+ this.#dropped = saturatingAdd(this.#dropped, 1);
111
+ if (event.e) {
112
+ lastStatus = event.status;
113
+ // Historically status-retry outcomes set lastStatus only. Preserve those pins.
114
+ if (!event.statusRetry)
115
+ lastErrorCode = event.e;
116
+ }
117
+ else if (attempts > 1) {
118
+ retry = {
119
+ attempts,
120
+ retries,
121
+ ...(preset ? { preset: closedEnum(preset) } : {}),
122
+ transport: closedEnum("native"),
123
+ ...(lastErrorCode ? { lastErrorCode: closedEnum(lastErrorCode) } : {}),
124
+ ...(lastStatus ? { lastStatus: integer(lastStatus) } : {}),
125
+ };
126
+ this.#retry = retry;
127
+ }
128
+ },
129
+ finish: (ms) => {
130
+ if (!finished && attempts > 0)
131
+ this.#ms = saturatingAdd(this.#ms, ms);
132
+ finished = true;
133
+ },
134
+ toTenantRetryPayload: () => (!this.#telemetryFailed && retry ? { ...retry } : undefined),
135
+ };
136
+ }
137
+ toTenantRetryPayload() {
138
+ return !this.#telemetryFailed && this.#retry ? { ...this.#retry } : undefined;
139
+ }
140
+ toLogPayload() {
141
+ if (this.#attempts === 0 && !this.#telemetryFailed)
142
+ return undefined;
143
+ return {
144
+ ...(this.#telemetryFailed ? { telemetryFailed: true } : {}),
145
+ attempts: this.#attempts,
146
+ retries: this.#retries,
147
+ timeouts: this.#timeouts,
148
+ proxyUsed: this.#proxyUsed,
149
+ ...(this.#lastStatus === undefined ? {} : { lastStatus: this.#lastStatus }),
150
+ ...(this.#retryPreset ? { retryPreset: this.#retryPreset } : {}),
151
+ transport: "native",
152
+ ...(this.#lastErrorCode ? { lastErrorCode: this.#lastErrorCode } : {}),
153
+ attemptSamples: this.#samples.map((sample) => ({
154
+ ...sample,
155
+ ...(sample.diagnostics
156
+ ? {
157
+ diagnostics: {
158
+ ...sample.diagnostics,
159
+ ...(sample.diagnostics.cause ? { cause: { ...sample.diagnostics.cause } } : {}),
160
+ },
161
+ }
162
+ : {}),
163
+ })),
164
+ dropped: this.#dropped,
165
+ ms: this.#ms,
166
+ ...(this.#retry ? { retry: { ...this.#retry } } : {}),
167
+ };
168
+ }
169
+ toHeaderPayload(log) {
170
+ return {
171
+ attempts: log.attempts,
172
+ retries: log.retries,
173
+ timeouts: log.timeouts,
174
+ proxyUsed: log.proxyUsed,
175
+ ...(log.lastStatus === undefined ? {} : { lastStatus: log.lastStatus }),
176
+ ...(log.retryPreset ? { retryPreset: closedEnum(log.retryPreset) } : {}),
177
+ transport: closedEnum(log.transport),
178
+ ...(log.lastErrorCode ? { lastErrorCode: closedEnum(log.lastErrorCode) } : {}),
179
+ attemptSamples: log.attemptSamples.map(({ n, ms, status, e }) => ({
180
+ n,
181
+ ms,
182
+ ...(status === undefined ? {} : { status }),
183
+ ...(e ? { e: closedEnum(e) } : {}),
184
+ })),
185
+ dropped: log.dropped,
186
+ ms: log.ms,
187
+ ...(log.retry ? { retry: { ...log.retry } } : {}),
188
+ };
189
+ }
190
+ }
@@ -1,9 +1,40 @@
1
1
  import type { ProxyResolutionOptions } from "../config/loader.js";
2
2
  import type { HttpClient, HttpRetrySummary } from "../types.js";
3
+ import { type HttpTelemetrySink } from "./http-telemetry.js";
3
4
  export type HttpClientOptions = ProxyResolutionOptions & {
4
5
  warn?: (message: string) => void;
5
6
  userAgent?: string;
6
7
  onRetrySummary?: (summary: HttpRetrySummary) => void;
8
+ /**
9
+ * Synchronous observer for HTTP attempts, retries, timeouts, status, duration,
10
+ * and observed proxy use. Observer failures never change transport behavior.
11
+ * All hooks and their property reads are guarded, including `onRetrySummary`
12
+ * and the failure reporter. Invalid asynchronous results are ignored and never
13
+ * awaited. Observer failures set the log-only `telemetryFailed` marker; the
14
+ * marker and redacted error diagnostics are excluded from headers and tenant
15
+ * metadata. Tenant `meta.retry` retains its existing response shape.
16
+ *
17
+ * After `ProviderEngine.attach`, the server wraps supported host HTTP bindings
18
+ * with the request collector. The adapter preserves arguments, direct-call,
19
+ * explicit `.call`/`.apply`, and unbound receivers, plus the original returned
20
+ * promise, response, stream, and error. Streams and SSE are observed at headers
21
+ * without consuming their bodies. SDK clients report every observed attempt
22
+ * and keep any observer configured here; routing is scoped to each call.
23
+ *
24
+ * Binding an existing adapter reuses its identity and replaces its collector:
25
+ * the latest bind receives subsequent calls, the earlier collector receives no
26
+ * further calls, and in-flight calls keep the collector captured at invocation.
27
+ * An opaque host invocation is one observed attempt with zero observed retries;
28
+ * its undisclosed retries and proxy settings cannot be inferred, and tenant
29
+ * `meta.retry` is omitted. Status, duration, and error class remain available
30
+ * in logs and telemetry headers.
31
+ *
32
+ * Non-object bindings and objects without a callable `request`, `get`, `post`,
33
+ * `put`, `delete`, `stream`, or `sse` method are left untouched, with no `http`
34
+ * telemetry sibling and one process-wide warning for this reason:
35
+ * `[apifuse] http telemetry not attached; reason=unsupported_binding_shape`.
36
+ */
37
+ httpTelemetry?: HttpTelemetrySink;
7
38
  signal?: AbortSignal;
8
39
  };
9
40
  export declare function createHttpClient(baseUrl?: string, clientOptions?: HttpClientOptions): HttpClient;