@bitfab/sdk 0.38.10 → 0.40.0
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/dist/{chunk-UE4GGPM6.js → chunk-5NT4YDCQ.js} +237 -9
- package/dist/chunk-5NT4YDCQ.js.map +1 -0
- package/dist/{chunk-BNOVHUQB.js → chunk-A22EYRSY.js} +29 -1103
- package/dist/chunk-A22EYRSY.js.map +1 -0
- package/dist/chunk-E2V4HFSM.js +1863 -0
- package/dist/chunk-E2V4HFSM.js.map +1 -0
- package/dist/chunk-EXT5FK54.js +1150 -0
- package/dist/chunk-EXT5FK54.js.map +1 -0
- package/dist/http-2OFIGQOK.js +20 -0
- package/dist/http-OZODCFFA.js +19 -0
- package/dist/http-OZODCFFA.js.map +1 -0
- package/dist/index.cjs +302 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +234 -4
- package/dist/index.d.ts +234 -4
- package/dist/index.js +14 -8
- package/dist/node.cjs +302 -7
- package/dist/node.cjs.map +1 -1
- package/dist/node.d.cts +1 -1
- package/dist/node.d.ts +1 -1
- package/dist/node.js +14 -8
- package/dist/node.js.map +1 -1
- package/dist/{replay-SMUBBMNK.js → replay-352POXG3.js} +3 -2
- package/dist/replay-352POXG3.js.map +1 -0
- package/dist/replayCli.js +132 -146
- package/dist/replayCli.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-BNOVHUQB.js.map +0 -1
- package/dist/chunk-UE4GGPM6.js.map +0 -1
- /package/dist/{replay-SMUBBMNK.js.map → http-2OFIGQOK.js.map} +0 -0
|
@@ -0,0 +1,1863 @@
|
|
|
1
|
+
// src/readEnv.ts
|
|
2
|
+
function readEnv(name) {
|
|
3
|
+
if (typeof process !== "undefined" && process.env) {
|
|
4
|
+
return process.env[name];
|
|
5
|
+
}
|
|
6
|
+
return void 0;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// src/compress.ts
|
|
10
|
+
var DISABLE_COMPRESSION_ENV = "BITFAB_DISABLE_COMPRESSION";
|
|
11
|
+
var MIN_COMPRESSED_BYTES = 8192;
|
|
12
|
+
var gzipNode;
|
|
13
|
+
var _nodeGzipReady = (typeof process !== "undefined" && process.versions?.node ? (
|
|
14
|
+
// The join trick hides "node:zlib" from static analysis so bundlers that
|
|
15
|
+
// ban Node.js built-ins don't fail at build time. webpackIgnore tells
|
|
16
|
+
// webpack/turbopack to emit a native import() so Node.js can resolve the
|
|
17
|
+
// module at runtime. Same pattern as `asyncStorage.ts`.
|
|
18
|
+
import(
|
|
19
|
+
/* webpackIgnore: true */
|
|
20
|
+
["node", "zlib"].join(":")
|
|
21
|
+
).then(({ gzip }) => {
|
|
22
|
+
gzipNode = (data) => new Promise((resolve, reject) => {
|
|
23
|
+
gzip(data, (error, result) => {
|
|
24
|
+
if (error) {
|
|
25
|
+
reject(error);
|
|
26
|
+
} else {
|
|
27
|
+
resolve(result);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
}).catch(() => {
|
|
32
|
+
})
|
|
33
|
+
) : Promise.resolve()).then(() => {
|
|
34
|
+
});
|
|
35
|
+
function toArrayBuffer(view) {
|
|
36
|
+
return view.buffer.slice(
|
|
37
|
+
view.byteOffset,
|
|
38
|
+
view.byteOffset + view.byteLength
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
function compressedRequest(body, rawBytes, compressed) {
|
|
42
|
+
if (compressed.byteLength >= rawBytes) {
|
|
43
|
+
return { body, rawBytes, wireBytes: rawBytes };
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
body: compressed instanceof Uint8Array ? toArrayBuffer(compressed) : compressed,
|
|
47
|
+
contentEncoding: "gzip",
|
|
48
|
+
rawBytes,
|
|
49
|
+
wireBytes: compressed.byteLength
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
async function gzipViaStream(bytes) {
|
|
53
|
+
const stream = new Blob([bytes]).stream().pipeThrough(new CompressionStream("gzip"));
|
|
54
|
+
return await new Response(stream).arrayBuffer();
|
|
55
|
+
}
|
|
56
|
+
function encodeRequestBody(body) {
|
|
57
|
+
if (readEnv(DISABLE_COMPRESSION_ENV)) {
|
|
58
|
+
const rawBytes = new TextEncoder().encode(body).byteLength;
|
|
59
|
+
return { body, rawBytes, wireBytes: rawBytes };
|
|
60
|
+
}
|
|
61
|
+
const bytes = new TextEncoder().encode(body);
|
|
62
|
+
if (bytes.byteLength < MIN_COMPRESSED_BYTES) {
|
|
63
|
+
return {
|
|
64
|
+
body,
|
|
65
|
+
rawBytes: bytes.byteLength,
|
|
66
|
+
wireBytes: bytes.byteLength
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
if (gzipNode) {
|
|
70
|
+
return gzipNode(bytes).then(
|
|
71
|
+
(compressed) => compressedRequest(body, bytes.byteLength, compressed),
|
|
72
|
+
() => ({
|
|
73
|
+
body,
|
|
74
|
+
rawBytes: bytes.byteLength,
|
|
75
|
+
wireBytes: bytes.byteLength
|
|
76
|
+
})
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
if (typeof CompressionStream === "undefined") {
|
|
80
|
+
return {
|
|
81
|
+
body,
|
|
82
|
+
rawBytes: bytes.byteLength,
|
|
83
|
+
wireBytes: bytes.byteLength
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
return gzipViaStream(bytes).then(
|
|
87
|
+
(compressed) => compressedRequest(body, bytes.byteLength, compressed),
|
|
88
|
+
() => ({
|
|
89
|
+
body,
|
|
90
|
+
rawBytes: bytes.byteLength,
|
|
91
|
+
wireBytes: bytes.byteLength
|
|
92
|
+
})
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// src/version.generated.ts
|
|
97
|
+
var __version__ = "0.40.0";
|
|
98
|
+
var __packageName__ = "@bitfab/sdk";
|
|
99
|
+
|
|
100
|
+
// src/errors.ts
|
|
101
|
+
var BitfabError = class extends Error {
|
|
102
|
+
constructor(message, url, status, retryAfterMs) {
|
|
103
|
+
super(message);
|
|
104
|
+
this.url = url;
|
|
105
|
+
this.status = status;
|
|
106
|
+
this.retryAfterMs = retryAfterMs;
|
|
107
|
+
this.name = "BitfabError";
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
// src/asyncStorage.ts
|
|
112
|
+
var AsyncLocalStorageClass = null;
|
|
113
|
+
var initDone = false;
|
|
114
|
+
function registerAsyncLocalStorageClass(cls) {
|
|
115
|
+
if (!AsyncLocalStorageClass) {
|
|
116
|
+
AsyncLocalStorageClass = cls;
|
|
117
|
+
}
|
|
118
|
+
initDone = true;
|
|
119
|
+
}
|
|
120
|
+
var asyncStorageReady = (typeof process !== "undefined" && process.versions?.node ? (
|
|
121
|
+
// The join trick hides "node:async_hooks" from static analysis so
|
|
122
|
+
// bundlers that ban Node.js built-ins don't fail at build time.
|
|
123
|
+
// webpackIgnore tells webpack/turbopack to emit a native import()
|
|
124
|
+
// so Node.js can resolve the module at runtime.
|
|
125
|
+
import(
|
|
126
|
+
/* webpackIgnore: true */
|
|
127
|
+
["node", "async_hooks"].join(":")
|
|
128
|
+
).then(
|
|
129
|
+
(mod) => {
|
|
130
|
+
registerAsyncLocalStorageClass(mod.AsyncLocalStorage);
|
|
131
|
+
}
|
|
132
|
+
).catch(() => {
|
|
133
|
+
})
|
|
134
|
+
) : Promise.resolve()).then(() => {
|
|
135
|
+
initDone = true;
|
|
136
|
+
});
|
|
137
|
+
function createAsyncLocalStorage() {
|
|
138
|
+
return AsyncLocalStorageClass ? new AsyncLocalStorageClass() : null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// src/replayContext.ts
|
|
142
|
+
var replayContextStorage = null;
|
|
143
|
+
var REPLAY_CONTEXT_STORAGE_SYMBOL = /* @__PURE__ */ Symbol.for("bitfab.replayContextStorage");
|
|
144
|
+
var replayContextReady = asyncStorageReady.then(() => {
|
|
145
|
+
const shared = globalThis;
|
|
146
|
+
const existing = shared[REPLAY_CONTEXT_STORAGE_SYMBOL];
|
|
147
|
+
if (existing) {
|
|
148
|
+
replayContextStorage = existing;
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
const created = createAsyncLocalStorage();
|
|
152
|
+
if (created) {
|
|
153
|
+
shared[REPLAY_CONTEXT_STORAGE_SYMBOL] = created;
|
|
154
|
+
replayContextStorage = created;
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// src/payloadBudget.ts
|
|
159
|
+
var MAX_SPAN_CARRIER_BYTES = 28e5;
|
|
160
|
+
var MAX_COMPRESSIBLE_SPAN_CARRIER_BYTES = 78e5;
|
|
161
|
+
var textEncoder = typeof TextEncoder !== "undefined" ? new TextEncoder() : null;
|
|
162
|
+
function byteLength(value) {
|
|
163
|
+
return textEncoder ? textEncoder.encode(value).length : value.length;
|
|
164
|
+
}
|
|
165
|
+
function carrierByteLength(body) {
|
|
166
|
+
return carrierBytesOf(textEncoder ? textEncoder.encode(body) : null, body);
|
|
167
|
+
}
|
|
168
|
+
function carrierBytesOf(encoded, body) {
|
|
169
|
+
if (!encoded) {
|
|
170
|
+
return body.length + 2;
|
|
171
|
+
}
|
|
172
|
+
let extra = 2;
|
|
173
|
+
for (let i = 0; i < encoded.length; i++) {
|
|
174
|
+
const byte = encoded[i];
|
|
175
|
+
if (byte === 34 || byte === 92) {
|
|
176
|
+
extra += 1;
|
|
177
|
+
} else if (byte < 32) {
|
|
178
|
+
extra += byte === 8 || byte === 9 || byte === 10 || byte === 12 || byte === 13 ? 1 : 5;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return encoded.length + extra;
|
|
182
|
+
}
|
|
183
|
+
var MAX_BYTES_PER_UNIT = 3;
|
|
184
|
+
function fitsCarrierBudget(body, maxBytes = MAX_SPAN_CARRIER_BYTES) {
|
|
185
|
+
const units = body.length;
|
|
186
|
+
if (units * MAX_BYTES_PER_UNIT + 2 <= maxBytes) {
|
|
187
|
+
return true;
|
|
188
|
+
}
|
|
189
|
+
if (units + 2 > maxBytes) {
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
return carrierByteLength(body) <= maxBytes;
|
|
193
|
+
}
|
|
194
|
+
var STRUCTURAL_SPAN_KEYS = /* @__PURE__ */ new Set([
|
|
195
|
+
"name",
|
|
196
|
+
"type",
|
|
197
|
+
"function_name",
|
|
198
|
+
"error_source"
|
|
199
|
+
]);
|
|
200
|
+
function asRecord(value) {
|
|
201
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
202
|
+
}
|
|
203
|
+
function cloneTrimmable(payload) {
|
|
204
|
+
const copy = { ...payload };
|
|
205
|
+
const containers = [];
|
|
206
|
+
const spanData = asRecord(copy.span_data);
|
|
207
|
+
if (spanData) {
|
|
208
|
+
const clone = { ...spanData };
|
|
209
|
+
copy.span_data = clone;
|
|
210
|
+
containers.push(clone);
|
|
211
|
+
}
|
|
212
|
+
const rawSpan = asRecord(copy.rawSpan);
|
|
213
|
+
const rawSpanData = rawSpan && asRecord(rawSpan.span_data);
|
|
214
|
+
if (rawSpan && rawSpanData) {
|
|
215
|
+
const clone = { ...rawSpanData };
|
|
216
|
+
copy.rawSpan = { ...rawSpan, span_data: clone };
|
|
217
|
+
containers.push(clone);
|
|
218
|
+
}
|
|
219
|
+
if (containers.length === 0) {
|
|
220
|
+
containers.push(copy);
|
|
221
|
+
}
|
|
222
|
+
return { copy, containers };
|
|
223
|
+
}
|
|
224
|
+
function collectCandidates(containers) {
|
|
225
|
+
const candidates = [];
|
|
226
|
+
for (const container of containers) {
|
|
227
|
+
for (const [key, value] of Object.entries(container)) {
|
|
228
|
+
if (STRUCTURAL_SPAN_KEYS.has(key) || value == null) {
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
let size;
|
|
232
|
+
try {
|
|
233
|
+
size = byteLength(JSON.stringify(value) ?? "");
|
|
234
|
+
} catch {
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
candidates.push({ container, key, size });
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return candidates.sort((a, b) => b.size - a.size);
|
|
241
|
+
}
|
|
242
|
+
function trimPayloadToBudget(payload, encode, maxBytes = MAX_SPAN_CARRIER_BYTES) {
|
|
243
|
+
const { copy, containers } = cloneTrimmable(payload);
|
|
244
|
+
const candidates = collectCandidates(containers);
|
|
245
|
+
if (candidates.length === 0) {
|
|
246
|
+
return void 0;
|
|
247
|
+
}
|
|
248
|
+
const trimmed = [];
|
|
249
|
+
for (const candidate of candidates) {
|
|
250
|
+
candidate.container[candidate.key] = `<unserializable: too_large_${candidate.size}_bytes>`;
|
|
251
|
+
trimmed.push(candidate.key);
|
|
252
|
+
let body;
|
|
253
|
+
try {
|
|
254
|
+
body = encode(copy);
|
|
255
|
+
} catch {
|
|
256
|
+
return void 0;
|
|
257
|
+
}
|
|
258
|
+
if (fitsCarrierBudget(body, maxBytes)) {
|
|
259
|
+
return { value: copy, trimmed };
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return void 0;
|
|
263
|
+
}
|
|
264
|
+
function markPayloadTrimmed(value, trimmed, maxBytes = MAX_SPAN_CARRIER_BYTES) {
|
|
265
|
+
const existing = Array.isArray(value.errors) ? value.errors : [];
|
|
266
|
+
value.errors = [
|
|
267
|
+
...existing,
|
|
268
|
+
{
|
|
269
|
+
source: "sdk",
|
|
270
|
+
step: "payload_budget",
|
|
271
|
+
error: `trimmed oversized field(s) to fit the ${maxBytes}-byte span carrier budget: ${[
|
|
272
|
+
...new Set(trimmed)
|
|
273
|
+
].join(", ")}`
|
|
274
|
+
}
|
|
275
|
+
];
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// src/warnOnce.ts
|
|
279
|
+
var warned = /* @__PURE__ */ new Set();
|
|
280
|
+
function warnOnce(key, message) {
|
|
281
|
+
if (warned.has(key)) {
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
warned.add(key);
|
|
285
|
+
try {
|
|
286
|
+
console.warn(`[bitfab] ${message}`);
|
|
287
|
+
} catch {
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// src/serializePayload.ts
|
|
292
|
+
function serializePayloadBody(payload, maxCarrierBytes = MAX_SPAN_CARRIER_BYTES) {
|
|
293
|
+
const encoded = encodePayloadBody(payload);
|
|
294
|
+
if (fitsCarrierBudget(encoded.body, maxCarrierBytes)) {
|
|
295
|
+
return { body: encoded.body, dropped: encoded.dropped };
|
|
296
|
+
}
|
|
297
|
+
return applyPayloadBudget(encoded, maxCarrierBytes);
|
|
298
|
+
}
|
|
299
|
+
function applyPayloadBudget(encoded, maxCarrierBytes) {
|
|
300
|
+
const result = encoded.value ? trimPayloadToBudget(
|
|
301
|
+
encoded.value,
|
|
302
|
+
(value) => encodePayloadBody(value).body,
|
|
303
|
+
maxCarrierBytes
|
|
304
|
+
) : void 0;
|
|
305
|
+
if (!result) {
|
|
306
|
+
return { body: encoded.body, dropped: encoded.dropped };
|
|
307
|
+
}
|
|
308
|
+
warnOnce(
|
|
309
|
+
"payload:over-budget",
|
|
310
|
+
`a span payload exceeded the ${maxCarrierBytes}-byte carrier budget; its largest field(s) (${[
|
|
311
|
+
...new Set(result.trimmed)
|
|
312
|
+
].join(
|
|
313
|
+
", "
|
|
314
|
+
)}) were replaced with placeholders so the span still ships. The span is incomplete and may not be replayable.`
|
|
315
|
+
);
|
|
316
|
+
markPayloadTrimmed(result.value, result.trimmed, maxCarrierBytes);
|
|
317
|
+
return {
|
|
318
|
+
body: encodePayloadBody(result.value).body,
|
|
319
|
+
dropped: encoded.dropped
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
function encodePayloadBody(payload) {
|
|
323
|
+
try {
|
|
324
|
+
return { body: JSON.stringify(payload), dropped: [], value: payload };
|
|
325
|
+
} catch {
|
|
326
|
+
const dropped = [];
|
|
327
|
+
const sanitize = (value, seen) => {
|
|
328
|
+
const t = typeof value;
|
|
329
|
+
if (value === null || t === "string" || t === "number" || t === "boolean") {
|
|
330
|
+
return value;
|
|
331
|
+
}
|
|
332
|
+
if (t === "bigint") {
|
|
333
|
+
dropped.push("BigInt");
|
|
334
|
+
return "<unserializable: BigInt>";
|
|
335
|
+
}
|
|
336
|
+
if (t === "function") {
|
|
337
|
+
const name = value.name || "Function";
|
|
338
|
+
dropped.push(name);
|
|
339
|
+
return `<unserializable: ${name}>`;
|
|
340
|
+
}
|
|
341
|
+
if (t === "symbol") {
|
|
342
|
+
dropped.push("Symbol");
|
|
343
|
+
return "<unserializable: Symbol>";
|
|
344
|
+
}
|
|
345
|
+
if (t !== "object") {
|
|
346
|
+
return void 0;
|
|
347
|
+
}
|
|
348
|
+
const obj = value;
|
|
349
|
+
const className = obj.constructor?.name || "object";
|
|
350
|
+
if (seen.has(obj)) {
|
|
351
|
+
dropped.push(className);
|
|
352
|
+
return `<cycle: ${className}>`;
|
|
353
|
+
}
|
|
354
|
+
seen.add(obj);
|
|
355
|
+
let result;
|
|
356
|
+
if (Array.isArray(obj)) {
|
|
357
|
+
result = obj.map((item) => sanitize(item, seen));
|
|
358
|
+
} else if (typeof obj.toJSON === "function") {
|
|
359
|
+
try {
|
|
360
|
+
result = sanitize(obj.toJSON(), seen);
|
|
361
|
+
} catch {
|
|
362
|
+
dropped.push(className);
|
|
363
|
+
result = `<unserializable: ${className}>`;
|
|
364
|
+
}
|
|
365
|
+
} else {
|
|
366
|
+
try {
|
|
367
|
+
const out = {};
|
|
368
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
369
|
+
out[k] = sanitize(v, seen);
|
|
370
|
+
}
|
|
371
|
+
result = out;
|
|
372
|
+
} catch {
|
|
373
|
+
warnOnce(
|
|
374
|
+
"payload:field-getter-threw",
|
|
375
|
+
"a value with a throwing getter/proxy could not be serialized into a span payload; it was replaced with a placeholder. The span still ships with its other fields intact."
|
|
376
|
+
);
|
|
377
|
+
dropped.push(className);
|
|
378
|
+
result = `<unserializable: ${className}>`;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
seen.delete(obj);
|
|
382
|
+
return result;
|
|
383
|
+
};
|
|
384
|
+
let sanitized;
|
|
385
|
+
try {
|
|
386
|
+
sanitized = sanitize(payload, /* @__PURE__ */ new WeakSet());
|
|
387
|
+
} catch (error) {
|
|
388
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
389
|
+
const marker = { error: `payload_serialize_failed: ${message}` };
|
|
390
|
+
return { body: JSON.stringify(marker), dropped, value: marker };
|
|
391
|
+
}
|
|
392
|
+
const isRecord = typeof sanitized === "object" && sanitized !== null && !Array.isArray(sanitized);
|
|
393
|
+
if (dropped.length > 0 && isRecord) {
|
|
394
|
+
const obj = sanitized;
|
|
395
|
+
const existing = Array.isArray(obj.errors) ? obj.errors : [];
|
|
396
|
+
obj.errors = [
|
|
397
|
+
...existing,
|
|
398
|
+
{
|
|
399
|
+
source: "sdk",
|
|
400
|
+
step: "json_serialize",
|
|
401
|
+
error: `stubbed non-serializable value(s): ${[
|
|
402
|
+
...new Set(dropped)
|
|
403
|
+
].join(", ")}`
|
|
404
|
+
}
|
|
405
|
+
];
|
|
406
|
+
}
|
|
407
|
+
return {
|
|
408
|
+
body: JSON.stringify(sanitized),
|
|
409
|
+
dropped,
|
|
410
|
+
value: isRecord ? sanitized : void 0
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// src/otel.ts
|
|
416
|
+
import { SpanStatusCode } from "@opentelemetry/api";
|
|
417
|
+
import {
|
|
418
|
+
ExportResultCode
|
|
419
|
+
} from "@opentelemetry/core";
|
|
420
|
+
import { resourceFromAttributes } from "@opentelemetry/resources";
|
|
421
|
+
import {
|
|
422
|
+
AlwaysOnSampler,
|
|
423
|
+
BasicTracerProvider,
|
|
424
|
+
BatchSpanProcessor
|
|
425
|
+
} from "@opentelemetry/sdk-trace-base";
|
|
426
|
+
|
|
427
|
+
// src/transportTypes.ts
|
|
428
|
+
var DeliveryError = class extends Error {
|
|
429
|
+
constructor(message, options = {}) {
|
|
430
|
+
super(message);
|
|
431
|
+
this.name = "DeliveryError";
|
|
432
|
+
this.retryable = options.retryable ?? false;
|
|
433
|
+
this.oversized = options.oversized ?? false;
|
|
434
|
+
this.retryAfterMs = options.retryAfterMs;
|
|
435
|
+
}
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
// src/unrefTimer.ts
|
|
439
|
+
function unrefTimer(timer) {
|
|
440
|
+
const handle = timer;
|
|
441
|
+
if (typeof handle.unref === "function") {
|
|
442
|
+
handle.unref();
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// src/otel.ts
|
|
447
|
+
var OPERATION_ATTRIBUTE = "bitfab.operation";
|
|
448
|
+
var PAYLOAD_ATTRIBUTE = "bitfab.payload";
|
|
449
|
+
var MAX_EXPORT_REQUEST_BYTES = 3e6;
|
|
450
|
+
var MAX_DECOMPRESSED_REQUEST_BYTES = 8e6;
|
|
451
|
+
var MAX_REQUEST_BYTES_ENV = "BITFAB_OTEL_MAX_REQUEST_BYTES";
|
|
452
|
+
var EXPORT_CONCURRENCY_ENV = "BITFAB_OTEL_EXPORT_CONCURRENCY";
|
|
453
|
+
var MAX_QUEUE_SIZE = 8192;
|
|
454
|
+
var DIRECT_MAX_EXPORT_BATCH_SIZE = 512;
|
|
455
|
+
var DIRECT_MAX_REQUEST_BATCH_SIZE = 128;
|
|
456
|
+
var DEFAULT_EXPORT_CONCURRENCY = 32;
|
|
457
|
+
var MAX_EXPORT_CONCURRENCY = 64;
|
|
458
|
+
var SCHEDULE_DELAY_MILLIS = 5e3;
|
|
459
|
+
var EXPORT_TIMEOUT_MILLIS = 3e4;
|
|
460
|
+
var RETRY_BASE_DELAY_MILLIS = 100;
|
|
461
|
+
var RETRY_BACKOFF_CEILING_MILLIS = 5e3;
|
|
462
|
+
var MAX_SEND_ATTEMPTS = 3;
|
|
463
|
+
var DEFAULT_LIFECYCLE_TIMEOUT_MS = 3e4;
|
|
464
|
+
var liveTransports = /* @__PURE__ */ new Set();
|
|
465
|
+
var carrierRefs = /* @__PURE__ */ new WeakMap();
|
|
466
|
+
function readBoundedIntEnv(name, max, fallback, warnKey) {
|
|
467
|
+
const raw = readEnv(name);
|
|
468
|
+
if (raw === void 0) {
|
|
469
|
+
return fallback;
|
|
470
|
+
}
|
|
471
|
+
const value = Number(raw);
|
|
472
|
+
if (Number.isInteger(value) && value > 0 && value <= max) {
|
|
473
|
+
return value;
|
|
474
|
+
}
|
|
475
|
+
warnOnce(
|
|
476
|
+
warnKey,
|
|
477
|
+
`${name} must be a positive integer no greater than ${max}; using ${fallback}`
|
|
478
|
+
);
|
|
479
|
+
return fallback;
|
|
480
|
+
}
|
|
481
|
+
function logError(message, error) {
|
|
482
|
+
try {
|
|
483
|
+
if (error === void 0) {
|
|
484
|
+
console.error(`[bitfab] ${message}`);
|
|
485
|
+
} else {
|
|
486
|
+
console.error(`[bitfab] ${message}`, error);
|
|
487
|
+
}
|
|
488
|
+
} catch {
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
function otlpValue(value) {
|
|
492
|
+
if (typeof value === "boolean") {
|
|
493
|
+
return { boolValue: value };
|
|
494
|
+
}
|
|
495
|
+
if (typeof value === "number") {
|
|
496
|
+
return Number.isInteger(value) ? { intValue: String(value) } : { doubleValue: value };
|
|
497
|
+
}
|
|
498
|
+
if (typeof value === "string") {
|
|
499
|
+
return { stringValue: value };
|
|
500
|
+
}
|
|
501
|
+
if (Array.isArray(value)) {
|
|
502
|
+
return { arrayValue: { values: value.map(otlpValue) } };
|
|
503
|
+
}
|
|
504
|
+
return { stringValue: String(value) };
|
|
505
|
+
}
|
|
506
|
+
function otlpAttributes(attributes) {
|
|
507
|
+
if (!attributes) {
|
|
508
|
+
return [];
|
|
509
|
+
}
|
|
510
|
+
return Object.entries(attributes).filter(([, value]) => value !== void 0).map(([key, value]) => ({ key, value: otlpValue(value) }));
|
|
511
|
+
}
|
|
512
|
+
function hrTimeToNanoString(time) {
|
|
513
|
+
if (!time) {
|
|
514
|
+
return "0";
|
|
515
|
+
}
|
|
516
|
+
return `${time[0]}${String(time[1]).padStart(9, "0")}`;
|
|
517
|
+
}
|
|
518
|
+
function spanToOtlp(span) {
|
|
519
|
+
const spanContext = span.spanContext();
|
|
520
|
+
const result = {
|
|
521
|
+
traceId: spanContext.traceId,
|
|
522
|
+
spanId: spanContext.spanId,
|
|
523
|
+
name: span.name,
|
|
524
|
+
kind: span.kind + 1,
|
|
525
|
+
startTimeUnixNano: hrTimeToNanoString(span.startTime),
|
|
526
|
+
endTimeUnixNano: hrTimeToNanoString(span.endTime),
|
|
527
|
+
attributes: otlpAttributes(span.attributes),
|
|
528
|
+
droppedAttributesCount: span.droppedAttributesCount,
|
|
529
|
+
droppedEventsCount: span.droppedEventsCount,
|
|
530
|
+
droppedLinksCount: span.droppedLinksCount,
|
|
531
|
+
status: {
|
|
532
|
+
code: span.status.code,
|
|
533
|
+
...span.status.message ? { message: span.status.message } : {}
|
|
534
|
+
},
|
|
535
|
+
flags: spanContext.traceFlags
|
|
536
|
+
};
|
|
537
|
+
const parentSpanId = span.parentSpanContext?.spanId;
|
|
538
|
+
if (parentSpanId) {
|
|
539
|
+
result.parentSpanId = parentSpanId;
|
|
540
|
+
}
|
|
541
|
+
if (spanContext.traceState) {
|
|
542
|
+
result.traceState = spanContext.traceState.serialize();
|
|
543
|
+
}
|
|
544
|
+
return result;
|
|
545
|
+
}
|
|
546
|
+
var SPAN_SEPARATOR_BYTES = 1;
|
|
547
|
+
function encodeSpan(span) {
|
|
548
|
+
const json = JSON.stringify(spanToOtlp(span));
|
|
549
|
+
return {
|
|
550
|
+
json,
|
|
551
|
+
size: byteLength(json),
|
|
552
|
+
ref: carrierRefs.get(span)
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
function trimEncodedSpan(span) {
|
|
556
|
+
try {
|
|
557
|
+
const carrier = JSON.parse(span.json);
|
|
558
|
+
const attribute = carrier.attributes?.find(
|
|
559
|
+
(entry) => entry.key === PAYLOAD_ATTRIBUTE
|
|
560
|
+
);
|
|
561
|
+
const payloadBody = attribute?.value?.stringValue;
|
|
562
|
+
if (!attribute?.value || payloadBody === void 0) {
|
|
563
|
+
return void 0;
|
|
564
|
+
}
|
|
565
|
+
const payload = JSON.parse(payloadBody);
|
|
566
|
+
attribute.value.stringValue = serializePayloadBody(
|
|
567
|
+
payload,
|
|
568
|
+
MAX_SPAN_CARRIER_BYTES
|
|
569
|
+
).body;
|
|
570
|
+
const json = JSON.stringify(carrier);
|
|
571
|
+
return { json, size: byteLength(json) };
|
|
572
|
+
} catch {
|
|
573
|
+
return void 0;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
async function prepareRequest(body) {
|
|
577
|
+
const prepared = encodeRequestBody(body);
|
|
578
|
+
return prepared instanceof Promise ? await prepared : prepared;
|
|
579
|
+
}
|
|
580
|
+
function requestEnvelope(first) {
|
|
581
|
+
const scope = first.instrumentationScope;
|
|
582
|
+
const resource = JSON.stringify({
|
|
583
|
+
attributes: otlpAttributes(
|
|
584
|
+
first.resource.attributes
|
|
585
|
+
)
|
|
586
|
+
});
|
|
587
|
+
const scopeJson = JSON.stringify({
|
|
588
|
+
name: scope.name,
|
|
589
|
+
version: scope.version ?? ""
|
|
590
|
+
});
|
|
591
|
+
const head = `{"resourceSpans":[{"resource":${resource},"scopeSpans":[{"scope":${scopeJson},"spans":[`;
|
|
592
|
+
const tail = "]}]}]}";
|
|
593
|
+
return { head, tail, size: byteLength(head) + byteLength(tail) };
|
|
594
|
+
}
|
|
595
|
+
function encodeRequest(envelope, spans) {
|
|
596
|
+
return envelope.head + spans.map((span) => span.json).join(",") + envelope.tail;
|
|
597
|
+
}
|
|
598
|
+
function delay(ms) {
|
|
599
|
+
return new Promise((resolve) => {
|
|
600
|
+
const timer = setTimeout(resolve, ms);
|
|
601
|
+
unrefTimer(timer);
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
async function withDeadline(work, timeoutMs) {
|
|
605
|
+
let timer;
|
|
606
|
+
try {
|
|
607
|
+
return await Promise.race([
|
|
608
|
+
work,
|
|
609
|
+
new Promise((resolve) => {
|
|
610
|
+
timer = setTimeout(() => resolve(false), Math.max(0, timeoutMs));
|
|
611
|
+
unrefTimer(timer);
|
|
612
|
+
})
|
|
613
|
+
]);
|
|
614
|
+
} finally {
|
|
615
|
+
if (timer) {
|
|
616
|
+
clearTimeout(timer);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
async function mapWithConcurrency(items, limit, task) {
|
|
621
|
+
const results = new Array(items.length);
|
|
622
|
+
let next = 0;
|
|
623
|
+
const workers = Array.from(
|
|
624
|
+
{ length: Math.min(Math.max(limit, 1), items.length) },
|
|
625
|
+
async () => {
|
|
626
|
+
while (next < items.length) {
|
|
627
|
+
const index = next;
|
|
628
|
+
next += 1;
|
|
629
|
+
results[index] = await task(items[index]);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
);
|
|
633
|
+
await Promise.all(workers);
|
|
634
|
+
return results;
|
|
635
|
+
}
|
|
636
|
+
function isRetryable(error) {
|
|
637
|
+
return error instanceof DeliveryError && error.retryable;
|
|
638
|
+
}
|
|
639
|
+
function isOversized(error) {
|
|
640
|
+
return error instanceof DeliveryError && error.oversized;
|
|
641
|
+
}
|
|
642
|
+
function retryWaitMillis(error, attempt, remainingMillis) {
|
|
643
|
+
const requested = error instanceof DeliveryError ? error.retryAfterMs : void 0;
|
|
644
|
+
const affordable = remainingMillis / 2;
|
|
645
|
+
if (requested !== void 0) {
|
|
646
|
+
return requested < affordable ? requested : null;
|
|
647
|
+
}
|
|
648
|
+
const backoff = Math.min(
|
|
649
|
+
RETRY_BASE_DELAY_MILLIS * 2 ** attempt,
|
|
650
|
+
RETRY_BACKOFF_CEILING_MILLIS
|
|
651
|
+
);
|
|
652
|
+
const jittered = backoff / 2 + Math.random() * (backoff / 2);
|
|
653
|
+
return jittered < affordable ? jittered : null;
|
|
654
|
+
}
|
|
655
|
+
var BitfabSpanExporter = class {
|
|
656
|
+
constructor(directSender, maxRequestBytes, maxRequestBatchSize, exportConcurrency, onDelivered, exportTimeoutMillis = EXPORT_TIMEOUT_MILLIS) {
|
|
657
|
+
this.directSender = directSender;
|
|
658
|
+
this.maxRequestBytes = maxRequestBytes;
|
|
659
|
+
this.maxRequestBatchSize = maxRequestBatchSize;
|
|
660
|
+
this.exportConcurrency = exportConcurrency;
|
|
661
|
+
this.onDelivered = onDelivered;
|
|
662
|
+
this.exportTimeoutMillis = exportTimeoutMillis;
|
|
663
|
+
/** Epoch ms until which the server has asked this exporter to stay away. */
|
|
664
|
+
this.throttledUntil = 0;
|
|
665
|
+
}
|
|
666
|
+
export(spans, resultCallback) {
|
|
667
|
+
void this.exportAsync(spans).then(
|
|
668
|
+
(succeeded) => {
|
|
669
|
+
resultCallback({
|
|
670
|
+
code: succeeded ? ExportResultCode.SUCCESS : ExportResultCode.FAILED
|
|
671
|
+
});
|
|
672
|
+
},
|
|
673
|
+
(error) => {
|
|
674
|
+
resultCallback({ code: ExportResultCode.FAILED, error });
|
|
675
|
+
}
|
|
676
|
+
);
|
|
677
|
+
}
|
|
678
|
+
async exportAsync(spans) {
|
|
679
|
+
if (spans.length === 0) {
|
|
680
|
+
return true;
|
|
681
|
+
}
|
|
682
|
+
let encoded;
|
|
683
|
+
let envelope;
|
|
684
|
+
try {
|
|
685
|
+
encoded = spans.map(encodeSpan);
|
|
686
|
+
envelope = requestEnvelope(spans[0]);
|
|
687
|
+
} catch (error) {
|
|
688
|
+
logError("failed to encode an OpenTelemetry span batch", error);
|
|
689
|
+
return false;
|
|
690
|
+
}
|
|
691
|
+
const batches = this.buildRequestBatches(envelope, encoded);
|
|
692
|
+
const results = await mapWithConcurrency(
|
|
693
|
+
batches,
|
|
694
|
+
this.exportConcurrency,
|
|
695
|
+
(batch) => this.send(envelope, batch)
|
|
696
|
+
);
|
|
697
|
+
return results.every(Boolean);
|
|
698
|
+
}
|
|
699
|
+
buildRequestBatches(envelope, spans) {
|
|
700
|
+
const batches = [];
|
|
701
|
+
let current = [];
|
|
702
|
+
let size = envelope.size;
|
|
703
|
+
for (const span of spans) {
|
|
704
|
+
const addition = span.size + (current.length > 0 ? SPAN_SEPARATOR_BYTES : 0);
|
|
705
|
+
if (current.length > 0 && (current.length >= this.maxRequestBatchSize || size + addition > this.maxRequestBytes)) {
|
|
706
|
+
batches.push({ spans: current, size });
|
|
707
|
+
current = [];
|
|
708
|
+
size = envelope.size;
|
|
709
|
+
}
|
|
710
|
+
current.push(span);
|
|
711
|
+
size += span.size + (current.length > 1 ? SPAN_SEPARATOR_BYTES : 0);
|
|
712
|
+
}
|
|
713
|
+
if (current.length > 0) {
|
|
714
|
+
batches.push({ spans: current, size });
|
|
715
|
+
}
|
|
716
|
+
return batches;
|
|
717
|
+
}
|
|
718
|
+
async send(envelope, batch) {
|
|
719
|
+
try {
|
|
720
|
+
let requestSpans = batch.spans;
|
|
721
|
+
let requestRawBytes = batch.size;
|
|
722
|
+
let alreadyTrimmed = false;
|
|
723
|
+
while (true) {
|
|
724
|
+
if (requestRawBytes <= MAX_DECOMPRESSED_REQUEST_BYTES) {
|
|
725
|
+
const prepared = await prepareRequest(
|
|
726
|
+
encodeRequest(envelope, requestSpans)
|
|
727
|
+
);
|
|
728
|
+
if (prepared.wireBytes <= this.maxRequestBytes) {
|
|
729
|
+
await this.sendWithRetries(prepared);
|
|
730
|
+
this.reportDelivered(batch.spans);
|
|
731
|
+
return true;
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
if (batch.spans.length !== 1) {
|
|
735
|
+
logError(
|
|
736
|
+
"an OpenTelemetry span batch exceeded the configured request-size target and could not be exported"
|
|
737
|
+
);
|
|
738
|
+
return false;
|
|
739
|
+
}
|
|
740
|
+
if (alreadyTrimmed) {
|
|
741
|
+
logError(
|
|
742
|
+
"a single OpenTelemetry span exceeded the configured request-size target after trimming"
|
|
743
|
+
);
|
|
744
|
+
return false;
|
|
745
|
+
}
|
|
746
|
+
const trimmed = trimEncodedSpan(batch.spans[0]);
|
|
747
|
+
if (!trimmed) {
|
|
748
|
+
logError(
|
|
749
|
+
"a single OpenTelemetry span exceeded the configured request-size target and could not be trimmed"
|
|
750
|
+
);
|
|
751
|
+
return false;
|
|
752
|
+
}
|
|
753
|
+
requestSpans = [trimmed];
|
|
754
|
+
requestRawBytes = envelope.size + trimmed.size;
|
|
755
|
+
alreadyTrimmed = true;
|
|
756
|
+
}
|
|
757
|
+
} catch (error) {
|
|
758
|
+
if (isOversized(error)) {
|
|
759
|
+
logError(
|
|
760
|
+
batch.spans.length === 1 ? "a single OpenTelemetry span exceeded the ingestion request limit and could not be exported" : "an OpenTelemetry span batch exceeded the ingestion request limit and could not be exported"
|
|
761
|
+
);
|
|
762
|
+
return false;
|
|
763
|
+
}
|
|
764
|
+
logError("failed to export an OpenTelemetry span batch", error);
|
|
765
|
+
return false;
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
/**
|
|
769
|
+
* Retries transient failures. Span and trace-completion carriers are safe to
|
|
770
|
+
* retry: the server keys them idempotently on `sourceSpanId`/`sourceTraceId`,
|
|
771
|
+
* so a duplicate delivery cannot create a duplicate row.
|
|
772
|
+
*
|
|
773
|
+
* KNOWN LIMITATION: an `internal_trace` (a `call()` BAML trace) carries no
|
|
774
|
+
* such key, so retrying a batch that holds one can create a duplicate trace -
|
|
775
|
+
* including when a request times out client-side but the server goes on to
|
|
776
|
+
* persist it. Accepted deliberately for now, matching the other SDKs, rather
|
|
777
|
+
* than skipping retries for a whole batch or inventing an idempotency scheme
|
|
778
|
+
* the server does not yet understand. The fix is a client-supplied
|
|
779
|
+
* idempotency key that ingestion dedupes on.
|
|
780
|
+
*/
|
|
781
|
+
/**
|
|
782
|
+
* Remember a throttle the server asked for, so the requests fanned out
|
|
783
|
+
* alongside this one respect it too. Delaying only the request that was
|
|
784
|
+
* refused leaves the other seven in the window hitting a server that just
|
|
785
|
+
* asked for room.
|
|
786
|
+
*/
|
|
787
|
+
recordThrottle(error) {
|
|
788
|
+
const requested = error instanceof DeliveryError ? error.retryAfterMs : void 0;
|
|
789
|
+
if (requested !== void 0) {
|
|
790
|
+
this.throttledUntil = Math.max(
|
|
791
|
+
this.throttledUntil,
|
|
792
|
+
Date.now() + requested
|
|
793
|
+
);
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
/**
|
|
797
|
+
* Waits out an active throttle, or reports the batch undeliverable when the
|
|
798
|
+
* throttle outlasts what we are willing to hold it for. Either way nothing is
|
|
799
|
+
* sent while the server has asked us to stay away.
|
|
800
|
+
*/
|
|
801
|
+
async awaitThrottle(deadline) {
|
|
802
|
+
const remaining = this.throttledUntil - Date.now();
|
|
803
|
+
if (remaining <= 0) {
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
806
|
+
if (remaining >= (deadline - Date.now()) / 2) {
|
|
807
|
+
throw new DeliveryError(
|
|
808
|
+
`OTLP ingestion is throttled for another ${remaining}ms, longer than the export budget`
|
|
809
|
+
);
|
|
810
|
+
}
|
|
811
|
+
await delay(remaining);
|
|
812
|
+
}
|
|
813
|
+
async sendWithRetries(request) {
|
|
814
|
+
const deadline = Date.now() + this.exportTimeoutMillis;
|
|
815
|
+
for (let attempt = 0; attempt < MAX_SEND_ATTEMPTS; attempt += 1) {
|
|
816
|
+
try {
|
|
817
|
+
await this.awaitThrottle(deadline);
|
|
818
|
+
await this.directSender(request, Math.max(0, deadline - Date.now()));
|
|
819
|
+
return;
|
|
820
|
+
} catch (error) {
|
|
821
|
+
if (isOversized(error)) {
|
|
822
|
+
throw error;
|
|
823
|
+
}
|
|
824
|
+
this.recordThrottle(error);
|
|
825
|
+
if (attempt === MAX_SEND_ATTEMPTS - 1 || !isRetryable(error)) {
|
|
826
|
+
throw error;
|
|
827
|
+
}
|
|
828
|
+
const wait = retryWaitMillis(error, attempt, deadline - Date.now());
|
|
829
|
+
if (wait === null) {
|
|
830
|
+
throw error;
|
|
831
|
+
}
|
|
832
|
+
await delay(wait);
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
/**
|
|
837
|
+
* Announce the carriers a request delivered. Wrapped because a listener that
|
|
838
|
+
* throws must never turn a delivered batch into a failed export.
|
|
839
|
+
*/
|
|
840
|
+
reportDelivered(spans) {
|
|
841
|
+
if (this.onDelivered === void 0) {
|
|
842
|
+
return;
|
|
843
|
+
}
|
|
844
|
+
const refs = spans.map((span) => span.ref).filter((ref) => ref !== void 0);
|
|
845
|
+
if (refs.length === 0) {
|
|
846
|
+
return;
|
|
847
|
+
}
|
|
848
|
+
try {
|
|
849
|
+
this.onDelivered(refs);
|
|
850
|
+
} catch (error) {
|
|
851
|
+
logError("a delivery listener threw", error);
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
async shutdown() {
|
|
855
|
+
}
|
|
856
|
+
async forceFlush() {
|
|
857
|
+
}
|
|
858
|
+
};
|
|
859
|
+
var DeliveryTrackingExporter = class {
|
|
860
|
+
constructor(exporter) {
|
|
861
|
+
this.exporter = exporter;
|
|
862
|
+
// Deliberately unscoped, matching the Python SDK. An export can outlive
|
|
863
|
+
// OTel's export timeout and report failure after the flush that was waiting
|
|
864
|
+
// on it already returned, so that failure surfaces on the NEXT flush instead.
|
|
865
|
+
// That over-reports: a good flush can inherit an older failure. The
|
|
866
|
+
// alternative - discarding failures from completed flush windows - under-
|
|
867
|
+
// reports, and `BatchSpanProcessor` also runs scheduled exports that belong
|
|
868
|
+
// to no flush at all, so their failures would vanish entirely. For a
|
|
869
|
+
// telemetry SDK a false "flush failed" is investigable; a false "flush
|
|
870
|
+
// succeeded" silently loses traces. We take the noisy direction on purpose.
|
|
871
|
+
this.failedExports = 0;
|
|
872
|
+
}
|
|
873
|
+
export(spans, resultCallback) {
|
|
874
|
+
try {
|
|
875
|
+
this.exporter.export(spans, (result) => {
|
|
876
|
+
if (result.code !== ExportResultCode.SUCCESS) {
|
|
877
|
+
this.failedExports += 1;
|
|
878
|
+
}
|
|
879
|
+
resultCallback(result);
|
|
880
|
+
});
|
|
881
|
+
} catch (error) {
|
|
882
|
+
this.failedExports += 1;
|
|
883
|
+
resultCallback({ code: ExportResultCode.FAILED, error });
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
takeFailedExports() {
|
|
887
|
+
const failed = this.failedExports;
|
|
888
|
+
this.failedExports = 0;
|
|
889
|
+
return failed;
|
|
890
|
+
}
|
|
891
|
+
shutdown() {
|
|
892
|
+
return this.exporter.shutdown();
|
|
893
|
+
}
|
|
894
|
+
forceFlush() {
|
|
895
|
+
return this.exporter.forceFlush?.() ?? Promise.resolve();
|
|
896
|
+
}
|
|
897
|
+
};
|
|
898
|
+
var OtelBatchTransport = class {
|
|
899
|
+
constructor(options) {
|
|
900
|
+
this.closed = false;
|
|
901
|
+
const maxRequestBytes = options.maxRequestBytes ?? MAX_EXPORT_REQUEST_BYTES;
|
|
902
|
+
const maxRequestBatchSize = options.maxRequestBatchSize ?? DIRECT_MAX_REQUEST_BATCH_SIZE;
|
|
903
|
+
if (maxRequestBatchSize <= 0) {
|
|
904
|
+
throw new BitfabError("maxRequestBatchSize must be a positive integer");
|
|
905
|
+
}
|
|
906
|
+
this.deliveryTracker = new DeliveryTrackingExporter(
|
|
907
|
+
new BitfabSpanExporter(
|
|
908
|
+
options.directSender,
|
|
909
|
+
maxRequestBytes,
|
|
910
|
+
maxRequestBatchSize,
|
|
911
|
+
options.exportConcurrency ?? DEFAULT_EXPORT_CONCURRENCY,
|
|
912
|
+
options.onDelivered,
|
|
913
|
+
options.exportTimeoutMillis ?? EXPORT_TIMEOUT_MILLIS
|
|
914
|
+
)
|
|
915
|
+
);
|
|
916
|
+
this.processor = new BatchSpanProcessor(this.deliveryTracker, {
|
|
917
|
+
maxQueueSize: options.maxQueueSize ?? MAX_QUEUE_SIZE,
|
|
918
|
+
maxExportBatchSize: options.maxExportBatchSize ?? DIRECT_MAX_EXPORT_BATCH_SIZE,
|
|
919
|
+
scheduledDelayMillis: SCHEDULE_DELAY_MILLIS,
|
|
920
|
+
exportTimeoutMillis: options.exportTimeoutMillis ?? EXPORT_TIMEOUT_MILLIS
|
|
921
|
+
});
|
|
922
|
+
this.provider = new BasicTracerProvider({
|
|
923
|
+
sampler: new AlwaysOnSampler(),
|
|
924
|
+
resource: resourceFromAttributes({
|
|
925
|
+
"service.name": "bitfab-typescript-sdk",
|
|
926
|
+
"service.version": __version__
|
|
927
|
+
}),
|
|
928
|
+
spanLimits: {
|
|
929
|
+
attributeCountLimit: 2,
|
|
930
|
+
attributeValueLengthLimit: Number.POSITIVE_INFINITY
|
|
931
|
+
},
|
|
932
|
+
spanProcessors: [this.processor]
|
|
933
|
+
});
|
|
934
|
+
this.tracer = this.provider.getTracer("bitfab", __version__);
|
|
935
|
+
liveTransports.add(this);
|
|
936
|
+
}
|
|
937
|
+
submit(operation, payload, meta = {}) {
|
|
938
|
+
if (this.closed) {
|
|
939
|
+
warnOnce(
|
|
940
|
+
"otel-submit-after-shutdown",
|
|
941
|
+
"OpenTelemetry transport is shut down; dropping spans"
|
|
942
|
+
);
|
|
943
|
+
return;
|
|
944
|
+
}
|
|
945
|
+
try {
|
|
946
|
+
const { body, dropped } = serializePayloadBody(
|
|
947
|
+
payload,
|
|
948
|
+
MAX_COMPRESSIBLE_SPAN_CARRIER_BYTES
|
|
949
|
+
);
|
|
950
|
+
if (dropped.length > 0) {
|
|
951
|
+
warnOnce(
|
|
952
|
+
"otel-carrier-payload-stubbed",
|
|
953
|
+
`a span payload held non-serializable value(s) (${[
|
|
954
|
+
...new Set(dropped)
|
|
955
|
+
].join(", ")}); they were stubbed so the span still ships, but the trace may be incomplete or not replayable.`
|
|
956
|
+
);
|
|
957
|
+
}
|
|
958
|
+
const span = this.tracer.startSpan(meta.name ?? `bitfab.${operation}`, {
|
|
959
|
+
attributes: {
|
|
960
|
+
[OPERATION_ATTRIBUTE]: operation,
|
|
961
|
+
[PAYLOAD_ATTRIBUTE]: body
|
|
962
|
+
},
|
|
963
|
+
startTime: meta.startTime
|
|
964
|
+
});
|
|
965
|
+
if (meta.ref !== void 0) {
|
|
966
|
+
carrierRefs.set(span, meta.ref);
|
|
967
|
+
}
|
|
968
|
+
if (meta.errored === true) {
|
|
969
|
+
span.setStatus({ code: SpanStatusCode.ERROR });
|
|
970
|
+
}
|
|
971
|
+
endSpan(span, meta.endTime);
|
|
972
|
+
} catch (error) {
|
|
973
|
+
logError("failed to queue an OpenTelemetry span", error);
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
async flush(timeoutMs = DEFAULT_LIFECYCLE_TIMEOUT_MS) {
|
|
977
|
+
const pending = (this.pendingFlush ?? Promise.resolve(true)).then(
|
|
978
|
+
() => this.forceFlushOnce()
|
|
979
|
+
);
|
|
980
|
+
this.pendingFlush = pending.catch(() => false);
|
|
981
|
+
return withDeadline(pending, timeoutMs);
|
|
982
|
+
}
|
|
983
|
+
async forceFlushOnce() {
|
|
984
|
+
try {
|
|
985
|
+
await this.processor.forceFlush();
|
|
986
|
+
} catch (error) {
|
|
987
|
+
logError("failed to flush OpenTelemetry spans", error);
|
|
988
|
+
this.deliveryTracker.takeFailedExports();
|
|
989
|
+
return false;
|
|
990
|
+
}
|
|
991
|
+
return this.deliveryTracker.takeFailedExports() === 0;
|
|
992
|
+
}
|
|
993
|
+
async shutdown(timeoutMs = DEFAULT_LIFECYCLE_TIMEOUT_MS) {
|
|
994
|
+
const deadline = Date.now() + Math.max(timeoutMs, 0);
|
|
995
|
+
this.closed = true;
|
|
996
|
+
const flushed = await this.flush(Math.max(0, deadline - Date.now()));
|
|
997
|
+
liveTransports.delete(this);
|
|
998
|
+
const shutdownCompleted = await withDeadline(
|
|
999
|
+
this.provider.shutdown().then(() => true).catch((error) => {
|
|
1000
|
+
logError("failed to shut down the OpenTelemetry transport", error);
|
|
1001
|
+
return false;
|
|
1002
|
+
}),
|
|
1003
|
+
Math.max(0, deadline - Date.now())
|
|
1004
|
+
);
|
|
1005
|
+
return flushed && shutdownCompleted;
|
|
1006
|
+
}
|
|
1007
|
+
};
|
|
1008
|
+
function endSpan(span, endTime) {
|
|
1009
|
+
span.end(endTime);
|
|
1010
|
+
}
|
|
1011
|
+
function createOtelTransport(options) {
|
|
1012
|
+
return new OtelBatchTransport({
|
|
1013
|
+
...options,
|
|
1014
|
+
exportConcurrency: readBoundedIntEnv(
|
|
1015
|
+
EXPORT_CONCURRENCY_ENV,
|
|
1016
|
+
MAX_EXPORT_CONCURRENCY,
|
|
1017
|
+
DEFAULT_EXPORT_CONCURRENCY,
|
|
1018
|
+
"otel-export-concurrency-invalid"
|
|
1019
|
+
),
|
|
1020
|
+
maxRequestBytes: readBoundedIntEnv(
|
|
1021
|
+
MAX_REQUEST_BYTES_ENV,
|
|
1022
|
+
MAX_EXPORT_REQUEST_BYTES,
|
|
1023
|
+
MAX_EXPORT_REQUEST_BYTES,
|
|
1024
|
+
"otel-max-request-bytes-invalid"
|
|
1025
|
+
)
|
|
1026
|
+
});
|
|
1027
|
+
}
|
|
1028
|
+
async function forEachLiveTransport(timeoutMs, run) {
|
|
1029
|
+
const deadline = Date.now() + Math.max(timeoutMs, 0);
|
|
1030
|
+
let succeeded = true;
|
|
1031
|
+
for (const transport of [...liveTransports]) {
|
|
1032
|
+
succeeded = await run(transport, Math.max(0, deadline - Date.now())) && succeeded;
|
|
1033
|
+
}
|
|
1034
|
+
return succeeded;
|
|
1035
|
+
}
|
|
1036
|
+
function flushOtelTransports(timeoutMs = DEFAULT_LIFECYCLE_TIMEOUT_MS) {
|
|
1037
|
+
return forEachLiveTransport(
|
|
1038
|
+
timeoutMs,
|
|
1039
|
+
(transport, remaining) => transport.flush(remaining)
|
|
1040
|
+
);
|
|
1041
|
+
}
|
|
1042
|
+
function shutdownOtelTransports(timeoutMs = DEFAULT_LIFECYCLE_TIMEOUT_MS) {
|
|
1043
|
+
return forEachLiveTransport(
|
|
1044
|
+
timeoutMs,
|
|
1045
|
+
(transport, remaining) => transport.shutdown(remaining)
|
|
1046
|
+
);
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
// src/transport.ts
|
|
1050
|
+
function createTraceTransport(options) {
|
|
1051
|
+
return createOtelTransport(options);
|
|
1052
|
+
}
|
|
1053
|
+
function flushTraceTransports(timeoutMs) {
|
|
1054
|
+
return flushOtelTransports(timeoutMs);
|
|
1055
|
+
}
|
|
1056
|
+
function shutdownTraceTransports(timeoutMs) {
|
|
1057
|
+
return shutdownOtelTransports(timeoutMs);
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
// src/http.ts
|
|
1061
|
+
var REPLAY_DB_BRANCH_REQUEST_TIMEOUT_MS = 3e5;
|
|
1062
|
+
var OTLP_TRACES_ENDPOINT = "/api/sdk/otel/v1/traces";
|
|
1063
|
+
var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
|
|
1064
|
+
var EXIT_FLUSH_TIMEOUT_MS = 5e3;
|
|
1065
|
+
var DEFAULT_LIFECYCLE_TIMEOUT_MS2 = 3e4;
|
|
1066
|
+
var pendingTracePromises = /* @__PURE__ */ new Set();
|
|
1067
|
+
function awaitOnExit(promise) {
|
|
1068
|
+
pendingTracePromises.add(promise);
|
|
1069
|
+
void promise.finally(() => {
|
|
1070
|
+
pendingTracePromises.delete(promise);
|
|
1071
|
+
}).catch(() => {
|
|
1072
|
+
});
|
|
1073
|
+
return promise;
|
|
1074
|
+
}
|
|
1075
|
+
async function flushTraces(timeoutMs = 5e3) {
|
|
1076
|
+
const deadline = Date.now() + Math.max(timeoutMs, 0);
|
|
1077
|
+
const requestsFlushed = await awaitPendingRequests(timeoutMs);
|
|
1078
|
+
const transportsFlushed = await flushTraceTransports(
|
|
1079
|
+
Math.max(0, deadline - Date.now())
|
|
1080
|
+
);
|
|
1081
|
+
return requestsFlushed && transportsFlushed;
|
|
1082
|
+
}
|
|
1083
|
+
async function awaitPendingRequests(timeoutMs = DEFAULT_LIFECYCLE_TIMEOUT_MS2) {
|
|
1084
|
+
await replayContextReady.catch(() => {
|
|
1085
|
+
});
|
|
1086
|
+
return waitForPromises(Array.from(pendingTracePromises), timeoutMs);
|
|
1087
|
+
}
|
|
1088
|
+
async function waitForPromises(promises, timeoutMs) {
|
|
1089
|
+
if (promises.length === 0) {
|
|
1090
|
+
return true;
|
|
1091
|
+
}
|
|
1092
|
+
let timer;
|
|
1093
|
+
try {
|
|
1094
|
+
return await Promise.race([
|
|
1095
|
+
Promise.allSettled(promises).then(() => true),
|
|
1096
|
+
new Promise((resolve) => {
|
|
1097
|
+
timer = setTimeout(() => resolve(false), timeoutMs);
|
|
1098
|
+
unrefTimer(timer);
|
|
1099
|
+
})
|
|
1100
|
+
]);
|
|
1101
|
+
} finally {
|
|
1102
|
+
if (timer) {
|
|
1103
|
+
clearTimeout(timer);
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
if (typeof process !== "undefined" && process.versions != null && process.versions.node != null) {
|
|
1108
|
+
let isFlushing = false;
|
|
1109
|
+
process.on("beforeExit", () => {
|
|
1110
|
+
if (isFlushing) {
|
|
1111
|
+
return;
|
|
1112
|
+
}
|
|
1113
|
+
isFlushing = true;
|
|
1114
|
+
void Promise.allSettled([
|
|
1115
|
+
...Array.from(pendingTracePromises).map((p) => p.catch(() => {
|
|
1116
|
+
})),
|
|
1117
|
+
shutdownTraceTransports(EXIT_FLUSH_TIMEOUT_MS).catch(() => false)
|
|
1118
|
+
]).then(() => {
|
|
1119
|
+
isFlushing = false;
|
|
1120
|
+
});
|
|
1121
|
+
});
|
|
1122
|
+
}
|
|
1123
|
+
function readHeader(response, name) {
|
|
1124
|
+
try {
|
|
1125
|
+
return response.headers?.get(name) ?? null;
|
|
1126
|
+
} catch {
|
|
1127
|
+
return null;
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
function parseRetryAfterMs(header) {
|
|
1131
|
+
const value = header?.trim();
|
|
1132
|
+
if (!value) {
|
|
1133
|
+
return void 0;
|
|
1134
|
+
}
|
|
1135
|
+
const seconds = Number(value);
|
|
1136
|
+
if (Number.isFinite(seconds)) {
|
|
1137
|
+
return seconds >= 0 ? seconds * 1e3 : void 0;
|
|
1138
|
+
}
|
|
1139
|
+
const at = Date.parse(value);
|
|
1140
|
+
if (Number.isNaN(at)) {
|
|
1141
|
+
return void 0;
|
|
1142
|
+
}
|
|
1143
|
+
return Math.max(0, at - Date.now());
|
|
1144
|
+
}
|
|
1145
|
+
function carrierMeta(operation, payload, ref) {
|
|
1146
|
+
return {
|
|
1147
|
+
ref,
|
|
1148
|
+
name: carrierName(operation, payload),
|
|
1149
|
+
startTime: payloadTimestamp(payload, "started_at"),
|
|
1150
|
+
endTime: payloadTimestamp(payload, "ended_at"),
|
|
1151
|
+
errored: payloadHasError(payload)
|
|
1152
|
+
};
|
|
1153
|
+
}
|
|
1154
|
+
function carrierName(operation, payload) {
|
|
1155
|
+
if (operation === "external_span") {
|
|
1156
|
+
const spanData = asPayloadRecord(
|
|
1157
|
+
asPayloadRecord(payload.rawSpan)?.span_data
|
|
1158
|
+
);
|
|
1159
|
+
if (typeof spanData?.name === "string") {
|
|
1160
|
+
return spanData.name;
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
if (typeof payload.traceFunctionKey === "string") {
|
|
1164
|
+
return payload.traceFunctionKey;
|
|
1165
|
+
}
|
|
1166
|
+
return `bitfab.${operation}`;
|
|
1167
|
+
}
|
|
1168
|
+
function payloadTimestamp(payload, field) {
|
|
1169
|
+
const rawSpan = asPayloadRecord(payload.rawSpan);
|
|
1170
|
+
const rawTrace = asPayloadRecord(payload.externalTrace) ?? asPayloadRecord(payload.rawTrace);
|
|
1171
|
+
const raw = rawSpan?.[field] ?? rawTrace?.[field];
|
|
1172
|
+
if (typeof raw !== "string") {
|
|
1173
|
+
return void 0;
|
|
1174
|
+
}
|
|
1175
|
+
const parsed = Date.parse(raw);
|
|
1176
|
+
return Number.isNaN(parsed) ? void 0 : parsed;
|
|
1177
|
+
}
|
|
1178
|
+
function payloadHasError(payload) {
|
|
1179
|
+
const spanData = asPayloadRecord(asPayloadRecord(payload.rawSpan)?.span_data);
|
|
1180
|
+
if (spanData?.error != null) {
|
|
1181
|
+
return true;
|
|
1182
|
+
}
|
|
1183
|
+
const errors = payload.errors;
|
|
1184
|
+
return Array.isArray(errors) ? errors.length > 0 : Boolean(errors);
|
|
1185
|
+
}
|
|
1186
|
+
function asPayloadRecord(value) {
|
|
1187
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
1188
|
+
}
|
|
1189
|
+
function carrierRef(payload) {
|
|
1190
|
+
const traceId = sourceTraceIdOf(payload);
|
|
1191
|
+
if (traceId === void 0) {
|
|
1192
|
+
return void 0;
|
|
1193
|
+
}
|
|
1194
|
+
const rawSpan = payload.rawSpan;
|
|
1195
|
+
if (rawSpan === void 0) {
|
|
1196
|
+
return { traceId };
|
|
1197
|
+
}
|
|
1198
|
+
const spanId = rawSpan?.id;
|
|
1199
|
+
return {
|
|
1200
|
+
traceId,
|
|
1201
|
+
spanId: typeof spanId === "string" ? spanId : `submission-${++carrierSeq}`
|
|
1202
|
+
};
|
|
1203
|
+
}
|
|
1204
|
+
function sourceTraceIdOf(payload) {
|
|
1205
|
+
if (typeof payload.sourceTraceId === "string") {
|
|
1206
|
+
return payload.sourceTraceId;
|
|
1207
|
+
}
|
|
1208
|
+
const rawTrace = payload.externalTrace ?? payload.rawTrace;
|
|
1209
|
+
const id = rawTrace?.id;
|
|
1210
|
+
return typeof id === "string" ? id : void 0;
|
|
1211
|
+
}
|
|
1212
|
+
var carrierSeq = 0;
|
|
1213
|
+
var HttpClient = class {
|
|
1214
|
+
constructor(config) {
|
|
1215
|
+
// Only traces a caller asked about are tracked, so ordinary tracing stores
|
|
1216
|
+
// nothing here.
|
|
1217
|
+
this.traceDeliveries = /* @__PURE__ */ new Map();
|
|
1218
|
+
// Deferred span work owned by THIS client. The module-global set backs the
|
|
1219
|
+
// process-wide `flushTraces()` and the exit hook, but per-client lifecycle
|
|
1220
|
+
// must not wait on another client's slow finalize: a false `close()` failure
|
|
1221
|
+
// caused by unrelated work is worse than no signal at all.
|
|
1222
|
+
this.deferredWork = /* @__PURE__ */ new Set();
|
|
1223
|
+
this.closed = false;
|
|
1224
|
+
this.apiKey = config.apiKey;
|
|
1225
|
+
this.serviceUrl = config.serviceUrl;
|
|
1226
|
+
this.timeout = config.timeout ?? 12e4;
|
|
1227
|
+
}
|
|
1228
|
+
/**
|
|
1229
|
+
* Resolve the API key at the moment it is needed (request time), invoking
|
|
1230
|
+
* the function form if one was supplied. Never read at construction.
|
|
1231
|
+
*/
|
|
1232
|
+
resolveApiKey() {
|
|
1233
|
+
return typeof this.apiKey === "function" ? this.apiKey() : this.apiKey;
|
|
1234
|
+
}
|
|
1235
|
+
/**
|
|
1236
|
+
* This client's span transport, built on first use.
|
|
1237
|
+
*
|
|
1238
|
+
* Lazy on purpose: a client that never sends a span must never start a batch
|
|
1239
|
+
* worker. Every framework integration created from a `Bitfab` client shares
|
|
1240
|
+
* the owning client's `HttpClient`, so handlers reuse this one worker instead
|
|
1241
|
+
* of each spinning up their own.
|
|
1242
|
+
*/
|
|
1243
|
+
getTraceTransport() {
|
|
1244
|
+
if (this.closed) {
|
|
1245
|
+
warnOnce(
|
|
1246
|
+
"http-client-closed",
|
|
1247
|
+
"the Bitfab client is closed; dropping spans"
|
|
1248
|
+
);
|
|
1249
|
+
return void 0;
|
|
1250
|
+
}
|
|
1251
|
+
if (!this.traceTransport) {
|
|
1252
|
+
this.traceTransport = createTraceTransport({
|
|
1253
|
+
directSender: (request, timeoutMs) => this.deliverCarriers(request, timeoutMs),
|
|
1254
|
+
onDelivered: (refs) => this.recordDeliveredCarriers(refs)
|
|
1255
|
+
});
|
|
1256
|
+
}
|
|
1257
|
+
return this.traceTransport;
|
|
1258
|
+
}
|
|
1259
|
+
/**
|
|
1260
|
+
* Post one encoded batch and decide what the server's answer means, so the
|
|
1261
|
+
* transport never reads a response. Rejections and permanent statuses come
|
|
1262
|
+
* back as a non-retryable {@link DeliveryError}; anything the server might
|
|
1263
|
+
* still accept on a second try comes back retryable.
|
|
1264
|
+
*/
|
|
1265
|
+
async deliverCarriers(request, timeoutMs) {
|
|
1266
|
+
let response;
|
|
1267
|
+
try {
|
|
1268
|
+
response = await this.sendPrepared(
|
|
1269
|
+
OTLP_TRACES_ENDPOINT,
|
|
1270
|
+
request,
|
|
1271
|
+
{ timeout: timeoutMs }
|
|
1272
|
+
);
|
|
1273
|
+
} catch (error) {
|
|
1274
|
+
const status = error instanceof BitfabError ? error.status : void 0;
|
|
1275
|
+
if (status === void 0) {
|
|
1276
|
+
throw new DeliveryError(`OTLP ingestion failed: ${String(error)}`, {
|
|
1277
|
+
retryable: true
|
|
1278
|
+
});
|
|
1279
|
+
}
|
|
1280
|
+
throw new DeliveryError(`OTLP ingestion failed with HTTP ${status}`, {
|
|
1281
|
+
retryable: RETRYABLE_STATUSES.has(status),
|
|
1282
|
+
oversized: status === 413,
|
|
1283
|
+
...error instanceof BitfabError && error.retryAfterMs !== void 0 ? { retryAfterMs: error.retryAfterMs } : {}
|
|
1284
|
+
});
|
|
1285
|
+
}
|
|
1286
|
+
const serverTraceIds = asPayloadRecord(response?.traceIds);
|
|
1287
|
+
if (serverTraceIds !== void 0) {
|
|
1288
|
+
this.recordServerTraceIds(serverTraceIds);
|
|
1289
|
+
}
|
|
1290
|
+
const partialSuccess = asPayloadRecord(response?.partialSuccess);
|
|
1291
|
+
const rejected = partialSuccess?.rejectedSpans;
|
|
1292
|
+
if (rejected !== void 0 && rejected !== "0" && rejected !== 0) {
|
|
1293
|
+
throw new DeliveryError(
|
|
1294
|
+
`OTLP ingestion rejected ${rejected} span(s): ${partialSuccess?.errorMessage ?? "no reason provided"}`
|
|
1295
|
+
);
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
/**
|
|
1299
|
+
* Start tracking delivery for `traceIds`. Nothing is recorded for a trace
|
|
1300
|
+
* that was never tracked, so ordinary tracing costs no bookkeeping at all.
|
|
1301
|
+
*/
|
|
1302
|
+
trackTraceDeliveries(traceIds) {
|
|
1303
|
+
for (const traceId of traceIds) {
|
|
1304
|
+
if (!this.traceDeliveries.has(traceId)) {
|
|
1305
|
+
this.traceDeliveries.set(traceId, {
|
|
1306
|
+
submittedSpanIds: /* @__PURE__ */ new Set(),
|
|
1307
|
+
ackedSpanIds: /* @__PURE__ */ new Set(),
|
|
1308
|
+
closed: false,
|
|
1309
|
+
closingAcked: false
|
|
1310
|
+
});
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
/**
|
|
1315
|
+
* The server's assigned `traces.id` for a tracked trace if it has already
|
|
1316
|
+
* been read back off an ingest response, without stopping tracking. Lets a
|
|
1317
|
+
* replay surface the id mid-run for items whose spans already landed.
|
|
1318
|
+
*/
|
|
1319
|
+
peekServerTraceId(traceId) {
|
|
1320
|
+
return this.traceDeliveries.get(traceId)?.serverTraceId;
|
|
1321
|
+
}
|
|
1322
|
+
/** Whether any tracked trace has had its closing carrier submitted. */
|
|
1323
|
+
hasClosedDeliveries(traceIds) {
|
|
1324
|
+
return traceIds.some((traceId) => this.traceDeliveries.get(traceId)?.closed);
|
|
1325
|
+
}
|
|
1326
|
+
/**
|
|
1327
|
+
* Report what each tracked trace submitted and whether the server confirmed
|
|
1328
|
+
* it, and stop tracking them. Every id passed is freed, so a caller cannot
|
|
1329
|
+
* leak a record for a trace that never closed.
|
|
1330
|
+
*
|
|
1331
|
+
* `delivered` is only meaningful once a flush has settled: acks land before
|
|
1332
|
+
* an export resolves, so a flush that reported success has already collected
|
|
1333
|
+
* every ack it is going to collect.
|
|
1334
|
+
*/
|
|
1335
|
+
takeTraceDeliveries(traceIds) {
|
|
1336
|
+
const reports = {};
|
|
1337
|
+
for (const traceId of traceIds) {
|
|
1338
|
+
const delivery = this.traceDeliveries.get(traceId);
|
|
1339
|
+
if (delivery === void 0) {
|
|
1340
|
+
continue;
|
|
1341
|
+
}
|
|
1342
|
+
this.traceDeliveries.delete(traceId);
|
|
1343
|
+
reports[traceId] = {
|
|
1344
|
+
spanCount: delivery.submittedSpanIds.size,
|
|
1345
|
+
closed: delivery.closed,
|
|
1346
|
+
delivered: delivery.closingAcked && [...delivery.submittedSpanIds].every(
|
|
1347
|
+
(spanId) => delivery.ackedSpanIds.has(spanId)
|
|
1348
|
+
),
|
|
1349
|
+
serverTraceId: delivery.serverTraceId
|
|
1350
|
+
};
|
|
1351
|
+
}
|
|
1352
|
+
return reports;
|
|
1353
|
+
}
|
|
1354
|
+
/** Build a carrier's meta and record what it adds to its trace's expected set. */
|
|
1355
|
+
recordedMeta(operation, payload, ref) {
|
|
1356
|
+
this.recordSubmittedCarrier(ref);
|
|
1357
|
+
return carrierMeta(operation, payload, ref);
|
|
1358
|
+
}
|
|
1359
|
+
recordSubmittedCarrier(ref) {
|
|
1360
|
+
if (ref === void 0) {
|
|
1361
|
+
return;
|
|
1362
|
+
}
|
|
1363
|
+
const delivery = this.traceDeliveries.get(ref.traceId);
|
|
1364
|
+
if (delivery === void 0) {
|
|
1365
|
+
return;
|
|
1366
|
+
}
|
|
1367
|
+
if (ref.spanId === void 0) {
|
|
1368
|
+
delivery.closed = true;
|
|
1369
|
+
} else {
|
|
1370
|
+
delivery.submittedSpanIds.add(ref.spanId);
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
/**
|
|
1374
|
+
* Ingestion commits every carrier in a request before it answers, so a
|
|
1375
|
+
* delivered ref is proof its row exists: the same fact the replay status
|
|
1376
|
+
* endpoint would report, already in hand.
|
|
1377
|
+
*/
|
|
1378
|
+
/**
|
|
1379
|
+
* Record the server's assigned `traces.id` for each tracked source trace,
|
|
1380
|
+
* read back from the OTLP ingest response. Keyed by source trace id, the same
|
|
1381
|
+
* key the delivery ledger uses. Untracked ids are ignored.
|
|
1382
|
+
*/
|
|
1383
|
+
recordServerTraceIds(map) {
|
|
1384
|
+
for (const [sourceTraceId, serverTraceId] of Object.entries(map)) {
|
|
1385
|
+
if (typeof serverTraceId !== "string") {
|
|
1386
|
+
continue;
|
|
1387
|
+
}
|
|
1388
|
+
const delivery = this.traceDeliveries.get(sourceTraceId);
|
|
1389
|
+
if (delivery === void 0) {
|
|
1390
|
+
continue;
|
|
1391
|
+
}
|
|
1392
|
+
delivery.serverTraceId = serverTraceId;
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
recordDeliveredCarriers(refs) {
|
|
1396
|
+
for (const ref of refs) {
|
|
1397
|
+
const delivery = this.traceDeliveries.get(ref.traceId);
|
|
1398
|
+
if (delivery === void 0) {
|
|
1399
|
+
continue;
|
|
1400
|
+
}
|
|
1401
|
+
if (ref.spanId === void 0) {
|
|
1402
|
+
delivery.closingAcked = true;
|
|
1403
|
+
} else {
|
|
1404
|
+
delivery.ackedSpanIds.add(ref.spanId);
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
/**
|
|
1409
|
+
* Track deferred span work so this client's own lifecycle waits for it, and
|
|
1410
|
+
* so the process-wide flush and exit hook do too.
|
|
1411
|
+
*/
|
|
1412
|
+
trackDeferred(promise) {
|
|
1413
|
+
this.deferredWork.add(promise);
|
|
1414
|
+
void promise.finally(() => this.deferredWork.delete(promise)).catch(() => {
|
|
1415
|
+
});
|
|
1416
|
+
return awaitOnExit(promise);
|
|
1417
|
+
}
|
|
1418
|
+
/**
|
|
1419
|
+
* Settle only THIS client's deferred span work. Scoped deliberately: the
|
|
1420
|
+
* global set can contain another client's long-running finalize, and
|
|
1421
|
+
* attributing its timeout here would fail a client whose own work succeeded.
|
|
1422
|
+
*/
|
|
1423
|
+
async settleDeferredWork(timeoutMs = DEFAULT_LIFECYCLE_TIMEOUT_MS2) {
|
|
1424
|
+
await replayContextReady.catch(() => {
|
|
1425
|
+
});
|
|
1426
|
+
return waitForPromises(Array.from(this.deferredWork), timeoutMs);
|
|
1427
|
+
}
|
|
1428
|
+
/**
|
|
1429
|
+
* Wait for spans queued by this client to be delivered, within one deadline.
|
|
1430
|
+
* Returns false on delivery failure or timeout.
|
|
1431
|
+
*/
|
|
1432
|
+
async waitForPendingRequests(timeoutMs = DEFAULT_LIFECYCLE_TIMEOUT_MS2) {
|
|
1433
|
+
const deadline = Date.now() + Math.max(timeoutMs, 0);
|
|
1434
|
+
const settled = await this.settleDeferredWork(timeoutMs);
|
|
1435
|
+
const flushed = await this.traceTransport?.flush(Math.max(0, deadline - Date.now())) ?? true;
|
|
1436
|
+
return settled && flushed;
|
|
1437
|
+
}
|
|
1438
|
+
/**
|
|
1439
|
+
* Flush and permanently close this client's tracing transport. Idempotent:
|
|
1440
|
+
* a second call joins the first rather than tearing down a pipeline the
|
|
1441
|
+
* first call already owns.
|
|
1442
|
+
*/
|
|
1443
|
+
close(timeoutMs = DEFAULT_LIFECYCLE_TIMEOUT_MS2) {
|
|
1444
|
+
if (this.closing) {
|
|
1445
|
+
return this.closing;
|
|
1446
|
+
}
|
|
1447
|
+
const deadline = Date.now() + Math.max(timeoutMs, 0);
|
|
1448
|
+
this.closing = (async () => {
|
|
1449
|
+
const settled = await this.settleDeferredWork(
|
|
1450
|
+
Math.max(0, deadline - Date.now())
|
|
1451
|
+
);
|
|
1452
|
+
this.closed = true;
|
|
1453
|
+
const transport = this.traceTransport;
|
|
1454
|
+
this.traceTransport = void 0;
|
|
1455
|
+
const shutdownOk = await transport?.shutdown(Math.max(0, deadline - Date.now())) ?? true;
|
|
1456
|
+
return settled && shutdownOk;
|
|
1457
|
+
})();
|
|
1458
|
+
return this.closing;
|
|
1459
|
+
}
|
|
1460
|
+
/**
|
|
1461
|
+
* Make an HTTP request to the Bitfab API. Defaults to POST; pass
|
|
1462
|
+
* `options.method` to use a different verb (e.g. "PATCH").
|
|
1463
|
+
*
|
|
1464
|
+
* @param endpoint - The API endpoint (without base URL)
|
|
1465
|
+
* @param payload - The request body
|
|
1466
|
+
* @param options - Optional request options
|
|
1467
|
+
* @returns The parsed JSON response
|
|
1468
|
+
* @throws {BitfabError} If the request fails
|
|
1469
|
+
*/
|
|
1470
|
+
async request(endpoint, payload, options) {
|
|
1471
|
+
const { body, dropped } = serializePayloadBody(payload);
|
|
1472
|
+
if (dropped.length > 0) {
|
|
1473
|
+
try {
|
|
1474
|
+
console.warn(
|
|
1475
|
+
`Bitfab: request body to ${endpoint} held ${dropped.length} non-serializable value(s) (${[...new Set(dropped)].join(", ")}); they were stubbed so the span still sends, but the trace may be incomplete or not replayable. Capture a JSON-safe projection of this input to make it replayable.`
|
|
1476
|
+
);
|
|
1477
|
+
} catch {
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
return this.sendEncoded(endpoint, body, options);
|
|
1481
|
+
}
|
|
1482
|
+
/**
|
|
1483
|
+
* POST an already-encoded body. The span transport encodes its own batches,
|
|
1484
|
+
* so routing them back through {@link HttpClient.request} would encode the
|
|
1485
|
+
* same data twice.
|
|
1486
|
+
*/
|
|
1487
|
+
async sendEncoded(endpoint, body, options) {
|
|
1488
|
+
const prepared = encodeRequestBody(body);
|
|
1489
|
+
const encoded = prepared instanceof Promise ? await prepared : prepared;
|
|
1490
|
+
return this.sendPrepared(endpoint, encoded, options);
|
|
1491
|
+
}
|
|
1492
|
+
async sendPrepared(endpoint, encoded, options) {
|
|
1493
|
+
const url = `${this.serviceUrl}${endpoint}`;
|
|
1494
|
+
const timeout = options?.timeout ?? this.timeout;
|
|
1495
|
+
const method = options?.method ?? "POST";
|
|
1496
|
+
const controller = new AbortController();
|
|
1497
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
1498
|
+
const headers = {
|
|
1499
|
+
"Content-Type": "application/json",
|
|
1500
|
+
Authorization: `Bearer ${this.resolveApiKey() ?? ""}`
|
|
1501
|
+
};
|
|
1502
|
+
if (encoded.contentEncoding) {
|
|
1503
|
+
headers["Content-Encoding"] = encoded.contentEncoding;
|
|
1504
|
+
}
|
|
1505
|
+
try {
|
|
1506
|
+
const response = await fetch(url, {
|
|
1507
|
+
method,
|
|
1508
|
+
headers,
|
|
1509
|
+
body: encoded.body,
|
|
1510
|
+
signal: controller.signal
|
|
1511
|
+
});
|
|
1512
|
+
if (!response.ok) {
|
|
1513
|
+
const errorText = await response.text();
|
|
1514
|
+
throw new BitfabError(
|
|
1515
|
+
`HTTP ${response.status}: ${errorText.slice(0, 500)}`,
|
|
1516
|
+
void 0,
|
|
1517
|
+
response.status,
|
|
1518
|
+
parseRetryAfterMs(readHeader(response, "retry-after"))
|
|
1519
|
+
);
|
|
1520
|
+
}
|
|
1521
|
+
const result = await response.json();
|
|
1522
|
+
if (result.error) {
|
|
1523
|
+
if (result.url) {
|
|
1524
|
+
throw new BitfabError(
|
|
1525
|
+
`${result.error} Configure it at: ${this.serviceUrl}${result.url}`,
|
|
1526
|
+
result.url
|
|
1527
|
+
);
|
|
1528
|
+
}
|
|
1529
|
+
throw new BitfabError(result.error);
|
|
1530
|
+
}
|
|
1531
|
+
return result;
|
|
1532
|
+
} catch (error) {
|
|
1533
|
+
if (error instanceof BitfabError) {
|
|
1534
|
+
throw error;
|
|
1535
|
+
}
|
|
1536
|
+
if (error instanceof Error) {
|
|
1537
|
+
if (error.name === "AbortError") {
|
|
1538
|
+
throw new BitfabError(`Request timed out after ${timeout}ms`);
|
|
1539
|
+
}
|
|
1540
|
+
throw new BitfabError(error.message);
|
|
1541
|
+
}
|
|
1542
|
+
throw new BitfabError("Unknown error occurred");
|
|
1543
|
+
} finally {
|
|
1544
|
+
clearTimeout(timeoutId);
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
/**
|
|
1548
|
+
* Look up a function by name.
|
|
1549
|
+
* Blocks until complete - needed for function execution.
|
|
1550
|
+
*/
|
|
1551
|
+
async lookupFunction(name) {
|
|
1552
|
+
return this.request("/api/sdk/functions/lookup", { name });
|
|
1553
|
+
}
|
|
1554
|
+
async getAutoTracePolicy(traceFunctionKey, protocol) {
|
|
1555
|
+
return this.request("/api/sdk/auto-trace/policy", {
|
|
1556
|
+
traceFunctionKey,
|
|
1557
|
+
protocol
|
|
1558
|
+
});
|
|
1559
|
+
}
|
|
1560
|
+
async getTraceSpan(traceId, lookup) {
|
|
1561
|
+
const searchParams = new URLSearchParams();
|
|
1562
|
+
if (lookup.id !== void 0) {
|
|
1563
|
+
searchParams.set("id", lookup.id);
|
|
1564
|
+
} else {
|
|
1565
|
+
searchParams.set("name", lookup.name);
|
|
1566
|
+
searchParams.set("occurrence", String(lookup.occurrence ?? "last"));
|
|
1567
|
+
}
|
|
1568
|
+
const endpoint = `/api/sdk/traces/${encodeURIComponent(traceId)}/span?${searchParams.toString()}`;
|
|
1569
|
+
const response = await this.get(endpoint);
|
|
1570
|
+
return response.span;
|
|
1571
|
+
}
|
|
1572
|
+
/**
|
|
1573
|
+
* GET a JSON endpoint on the service with the client's API key. Throws a
|
|
1574
|
+
* `BitfabError` carrying the status text for any non-2xx response.
|
|
1575
|
+
*/
|
|
1576
|
+
async get(endpoint) {
|
|
1577
|
+
const url = `${this.serviceUrl}${endpoint}`;
|
|
1578
|
+
const controller = new AbortController();
|
|
1579
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
1580
|
+
try {
|
|
1581
|
+
const response = await fetch(url, {
|
|
1582
|
+
method: "GET",
|
|
1583
|
+
headers: { Authorization: `Bearer ${this.resolveApiKey() ?? ""}` },
|
|
1584
|
+
signal: controller.signal
|
|
1585
|
+
});
|
|
1586
|
+
if (!response.ok) {
|
|
1587
|
+
const errorText = await response.text();
|
|
1588
|
+
throw new BitfabError(
|
|
1589
|
+
`HTTP ${response.status}: ${errorText.slice(0, 500)}`,
|
|
1590
|
+
void 0,
|
|
1591
|
+
response.status,
|
|
1592
|
+
parseRetryAfterMs(readHeader(response, "retry-after"))
|
|
1593
|
+
);
|
|
1594
|
+
}
|
|
1595
|
+
return await response.json();
|
|
1596
|
+
} catch (error) {
|
|
1597
|
+
if (error instanceof BitfabError) {
|
|
1598
|
+
throw error;
|
|
1599
|
+
}
|
|
1600
|
+
if (error instanceof Error) {
|
|
1601
|
+
if (error.name === "AbortError") {
|
|
1602
|
+
throw new BitfabError(`Request timed out after ${this.timeout}ms`);
|
|
1603
|
+
}
|
|
1604
|
+
throw new BitfabError(error.message);
|
|
1605
|
+
}
|
|
1606
|
+
throw new BitfabError("Unknown error occurred");
|
|
1607
|
+
} finally {
|
|
1608
|
+
clearTimeout(timeoutId);
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
/**
|
|
1612
|
+
* Queue an internal trace (from local BAML execution via `call()`) onto this
|
|
1613
|
+
* client's batching transport. `functionId` moves into the payload because
|
|
1614
|
+
* the OTLP carrier has no path to carry it.
|
|
1615
|
+
*/
|
|
1616
|
+
sendInternalTrace(functionId, payload) {
|
|
1617
|
+
const body = {
|
|
1618
|
+
...payload,
|
|
1619
|
+
functionId,
|
|
1620
|
+
sdkPackage: __packageName__,
|
|
1621
|
+
sdkVersion: __version__
|
|
1622
|
+
};
|
|
1623
|
+
this.getTraceTransport()?.submit(
|
|
1624
|
+
"internal_trace",
|
|
1625
|
+
body,
|
|
1626
|
+
carrierMeta("internal_trace", body, void 0)
|
|
1627
|
+
);
|
|
1628
|
+
}
|
|
1629
|
+
/**
|
|
1630
|
+
* Queue an external span (from withSpan wrapper or OpenAI tracing) onto this
|
|
1631
|
+
* client's batching transport. Fire-and-forget: the transport owns delivery,
|
|
1632
|
+
* so callers await `flushTraces()` or `close()` rather than a per-span
|
|
1633
|
+
* promise.
|
|
1634
|
+
*/
|
|
1635
|
+
sendExternalSpan(payload) {
|
|
1636
|
+
this.getTraceTransport()?.submit(
|
|
1637
|
+
"external_span",
|
|
1638
|
+
{ ...payload, sdkVersion: __version__ },
|
|
1639
|
+
this.recordedMeta("external_span", payload, carrierRef(payload))
|
|
1640
|
+
);
|
|
1641
|
+
}
|
|
1642
|
+
/**
|
|
1643
|
+
* Queue an external trace completion (from OpenAI tracing) onto this
|
|
1644
|
+
* client's batching transport. Fire-and-forget for the same reason as
|
|
1645
|
+
* {@link HttpClient.sendExternalSpan}; replay confirms persistence with the
|
|
1646
|
+
* server-authoritative barrier in `replay.ts`, not by awaiting this call.
|
|
1647
|
+
*/
|
|
1648
|
+
sendExternalTrace(payload) {
|
|
1649
|
+
this.getTraceTransport()?.submit(
|
|
1650
|
+
"external_trace",
|
|
1651
|
+
{
|
|
1652
|
+
...payload,
|
|
1653
|
+
sdkPackage: __packageName__,
|
|
1654
|
+
sdkVersion: __version__
|
|
1655
|
+
},
|
|
1656
|
+
this.recordedMeta(
|
|
1657
|
+
"external_trace",
|
|
1658
|
+
payload,
|
|
1659
|
+
payload.completed === true ? carrierRef(payload) : void 0
|
|
1660
|
+
)
|
|
1661
|
+
);
|
|
1662
|
+
}
|
|
1663
|
+
/**
|
|
1664
|
+
* Partial update of an existing trace identified by its Bitfab trace ID.
|
|
1665
|
+
* Used by the detached `client.getTrace(id)` handle.
|
|
1666
|
+
*
|
|
1667
|
+
* Blocking, like the other trace-API calls: it resolves once the server has
|
|
1668
|
+
* applied the change and rejects if the server refused it. A patch targets a
|
|
1669
|
+
* trace that is already closed, so there is no batch for it to ride along
|
|
1670
|
+
* with and no later signal that would reveal a silent failure.
|
|
1671
|
+
*/
|
|
1672
|
+
async patchTrace(traceId, payload) {
|
|
1673
|
+
const endpoint = `/api/sdk/traces/${encodeURIComponent(traceId)}`;
|
|
1674
|
+
await this.request(endpoint, payload, { method: "PATCH" });
|
|
1675
|
+
}
|
|
1676
|
+
/**
|
|
1677
|
+
* Start a replay session by fetching historical traces.
|
|
1678
|
+
* Blocking call - creates a test run and returns lightweight item references.
|
|
1679
|
+
*/
|
|
1680
|
+
async startReplay(traceFunctionKey, limit, traceIds, name, codeChangeDescription, codeChangeFiles, includeDbBranchLease, experimentGroupId, datasetId, graderIds, dbBranchSettings) {
|
|
1681
|
+
const payload = { traceFunctionKey };
|
|
1682
|
+
if (limit !== void 0) {
|
|
1683
|
+
payload.limit = limit;
|
|
1684
|
+
}
|
|
1685
|
+
if (traceIds) {
|
|
1686
|
+
payload.traceIds = traceIds;
|
|
1687
|
+
}
|
|
1688
|
+
if (name !== void 0) {
|
|
1689
|
+
payload.name = name;
|
|
1690
|
+
}
|
|
1691
|
+
if (codeChangeDescription !== void 0) {
|
|
1692
|
+
payload.codeChangeDescription = codeChangeDescription;
|
|
1693
|
+
}
|
|
1694
|
+
if (codeChangeFiles !== void 0) {
|
|
1695
|
+
payload.codeChangeFiles = codeChangeFiles;
|
|
1696
|
+
}
|
|
1697
|
+
if (includeDbBranchLease) {
|
|
1698
|
+
payload.includeDbBranchLease = true;
|
|
1699
|
+
payload.lazyDbBranchLease = true;
|
|
1700
|
+
}
|
|
1701
|
+
if (experimentGroupId !== void 0) {
|
|
1702
|
+
payload.experimentGroupId = experimentGroupId;
|
|
1703
|
+
}
|
|
1704
|
+
if (datasetId !== void 0) {
|
|
1705
|
+
payload.datasetId = datasetId;
|
|
1706
|
+
}
|
|
1707
|
+
if (graderIds !== void 0) {
|
|
1708
|
+
payload.graderIds = graderIds;
|
|
1709
|
+
}
|
|
1710
|
+
if (dbBranchSettings !== void 0) {
|
|
1711
|
+
payload.dbBranchSettings = dbBranchSettings;
|
|
1712
|
+
}
|
|
1713
|
+
const timeout = includeDbBranchLease ? REPLAY_DB_BRANCH_REQUEST_TIMEOUT_MS : 3e4;
|
|
1714
|
+
return this.request("/api/sdk/replay/start", payload, {
|
|
1715
|
+
timeout
|
|
1716
|
+
});
|
|
1717
|
+
}
|
|
1718
|
+
/**
|
|
1719
|
+
* Fetch an external span by ID.
|
|
1720
|
+
* Blocking GET request.
|
|
1721
|
+
* The replay view limits rawData to input/output serialization fields.
|
|
1722
|
+
*/
|
|
1723
|
+
async getExternalSpan(spanId, options) {
|
|
1724
|
+
const query = options?.view === "replay" ? "?view=replay" : "";
|
|
1725
|
+
const url = `${this.serviceUrl}/api/sdk/externalSpans/${spanId}${query}`;
|
|
1726
|
+
const controller = new AbortController();
|
|
1727
|
+
const timeoutId = setTimeout(() => controller.abort(), 3e4);
|
|
1728
|
+
try {
|
|
1729
|
+
const response = await fetch(url, {
|
|
1730
|
+
method: "GET",
|
|
1731
|
+
headers: { Authorization: `Bearer ${this.resolveApiKey() ?? ""}` },
|
|
1732
|
+
signal: controller.signal
|
|
1733
|
+
});
|
|
1734
|
+
if (!response.ok) {
|
|
1735
|
+
const errorText = await response.text();
|
|
1736
|
+
throw new BitfabError(
|
|
1737
|
+
`HTTP ${response.status}: ${errorText.slice(0, 500)}`
|
|
1738
|
+
);
|
|
1739
|
+
}
|
|
1740
|
+
return await response.json();
|
|
1741
|
+
} catch (error) {
|
|
1742
|
+
if (error instanceof BitfabError) {
|
|
1743
|
+
throw error;
|
|
1744
|
+
}
|
|
1745
|
+
if (error instanceof Error) {
|
|
1746
|
+
if (error.name === "AbortError") {
|
|
1747
|
+
throw new BitfabError("Request timed out after 30000ms");
|
|
1748
|
+
}
|
|
1749
|
+
throw new BitfabError(error.message);
|
|
1750
|
+
}
|
|
1751
|
+
throw new BitfabError("Unknown error occurred");
|
|
1752
|
+
} finally {
|
|
1753
|
+
clearTimeout(timeoutId);
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
/**
|
|
1757
|
+
* Fetch the span tree for a root span.
|
|
1758
|
+
* Blocking GET request.
|
|
1759
|
+
*
|
|
1760
|
+
* Pass `includeOutputs: false` for a payload-free tree (structure +
|
|
1761
|
+
* `externalSpanId` only), so recorded outputs are fetched lazily per mocked
|
|
1762
|
+
* span instead of all up front. Omit it (default eager) for `mock: "all"`.
|
|
1763
|
+
* Pass `includeRootOutput: false` when the root was already fetched.
|
|
1764
|
+
*/
|
|
1765
|
+
async getSpanTree(externalSpanId, options) {
|
|
1766
|
+
const searchParams = new URLSearchParams();
|
|
1767
|
+
if (options?.includeOutputs === false) {
|
|
1768
|
+
searchParams.set("includeOutputs", "false");
|
|
1769
|
+
}
|
|
1770
|
+
if (options?.includeRootOutput === false) {
|
|
1771
|
+
searchParams.set("includeRootOutput", "false");
|
|
1772
|
+
}
|
|
1773
|
+
const encodedQuery = searchParams.toString();
|
|
1774
|
+
const query = encodedQuery ? `?${encodedQuery}` : "";
|
|
1775
|
+
const url = `${this.serviceUrl}/api/sdk/replay/spanTree/${externalSpanId}${query}`;
|
|
1776
|
+
const controller = new AbortController();
|
|
1777
|
+
const timeoutId = setTimeout(() => controller.abort(), 3e4);
|
|
1778
|
+
try {
|
|
1779
|
+
const response = await fetch(url, {
|
|
1780
|
+
method: "GET",
|
|
1781
|
+
headers: { Authorization: `Bearer ${this.resolveApiKey() ?? ""}` },
|
|
1782
|
+
signal: controller.signal
|
|
1783
|
+
});
|
|
1784
|
+
if (!response.ok) {
|
|
1785
|
+
const errorText = await response.text();
|
|
1786
|
+
throw new BitfabError(
|
|
1787
|
+
`HTTP ${response.status}: ${errorText.slice(0, 500)}`
|
|
1788
|
+
);
|
|
1789
|
+
}
|
|
1790
|
+
return await response.json();
|
|
1791
|
+
} catch (error) {
|
|
1792
|
+
if (error instanceof BitfabError) {
|
|
1793
|
+
throw error;
|
|
1794
|
+
}
|
|
1795
|
+
if (error instanceof Error) {
|
|
1796
|
+
if (error.name === "AbortError") {
|
|
1797
|
+
throw new BitfabError("Request timed out after 30000ms");
|
|
1798
|
+
}
|
|
1799
|
+
throw new BitfabError(error.message);
|
|
1800
|
+
}
|
|
1801
|
+
throw new BitfabError("Unknown error occurred");
|
|
1802
|
+
} finally {
|
|
1803
|
+
clearTimeout(timeoutId);
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
/**
|
|
1807
|
+
* Read which of a replay run's traces the server has fully persisted.
|
|
1808
|
+
*
|
|
1809
|
+
* With `expectedSpanCounts`, a trace appears in the response only once it
|
|
1810
|
+
* has a final status AND at least that many persisted spans, which is what
|
|
1811
|
+
* makes this a real barrier rather than a "the row exists" check.
|
|
1812
|
+
*/
|
|
1813
|
+
async getReplayStatus(testRunId, expectedSpanCounts) {
|
|
1814
|
+
return this.request(
|
|
1815
|
+
"/api/sdk/replay/status",
|
|
1816
|
+
{ testRunId, expectedSpanCounts },
|
|
1817
|
+
{ timeout: 3e4 }
|
|
1818
|
+
);
|
|
1819
|
+
}
|
|
1820
|
+
/**
|
|
1821
|
+
* Mark a replay test run as completed.
|
|
1822
|
+
* Blocking call.
|
|
1823
|
+
*/
|
|
1824
|
+
async completeReplay(testRunId) {
|
|
1825
|
+
return this.request(
|
|
1826
|
+
"/api/sdk/replay/complete",
|
|
1827
|
+
{ testRunId },
|
|
1828
|
+
{ timeout: 3e4 }
|
|
1829
|
+
);
|
|
1830
|
+
}
|
|
1831
|
+
/**
|
|
1832
|
+
* Ask the server to materialize a per-trace DB branch lease from a
|
|
1833
|
+
* captured `dbSnapshotRef`. Blocking - the resolver creates a Neon
|
|
1834
|
+
* snapshot + preview branch and polls operations to readiness, which
|
|
1835
|
+
* can take seconds.
|
|
1836
|
+
*/
|
|
1837
|
+
async resolveDbBranchLease(testRunId, traceId, dbBranchSettings) {
|
|
1838
|
+
return this.request(
|
|
1839
|
+
"/api/sdk/replay/resolveDbBranchLease",
|
|
1840
|
+
{ testRunId, traceId, dbBranchSettings },
|
|
1841
|
+
{ timeout: REPLAY_DB_BRANCH_REQUEST_TIMEOUT_MS }
|
|
1842
|
+
);
|
|
1843
|
+
}
|
|
1844
|
+
/** Release a previously-resolved DB branch by deleting its Neon branch. Idempotent server-side. */
|
|
1845
|
+
async releaseDbBranchLease(neonBranchId) {
|
|
1846
|
+
await this.request(
|
|
1847
|
+
"/api/sdk/replay/releaseDbBranchLease",
|
|
1848
|
+
{ neonBranchId },
|
|
1849
|
+
{ timeout: 3e4 }
|
|
1850
|
+
);
|
|
1851
|
+
}
|
|
1852
|
+
};
|
|
1853
|
+
|
|
1854
|
+
export {
|
|
1855
|
+
BitfabError,
|
|
1856
|
+
serializePayloadBody,
|
|
1857
|
+
awaitOnExit,
|
|
1858
|
+
flushTraces,
|
|
1859
|
+
awaitPendingRequests,
|
|
1860
|
+
parseRetryAfterMs,
|
|
1861
|
+
HttpClient
|
|
1862
|
+
};
|
|
1863
|
+
//# sourceMappingURL=chunk-E2V4HFSM.js.map
|