@bitfab/sdk 0.33.7 → 0.34.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/README.md +19 -0
- package/dist/chunk-4J36FZ4K.js +2299 -0
- package/dist/chunk-4J36FZ4K.js.map +1 -0
- package/dist/{chunk-DZJ5K75J.js → chunk-ESKBRHVG.js} +87 -604
- package/dist/chunk-ESKBRHVG.js.map +1 -0
- package/dist/index.cjs +2297 -1365
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +374 -28
- package/dist/index.d.ts +374 -28
- package/dist/index.js +5 -5
- package/dist/node.cjs +2287 -1355
- package/dist/node.cjs.map +1 -1
- package/dist/node.js +5 -5
- package/dist/{replay-SRQI4QMY.js → replay-QFNYP7EY.js} +2 -2
- package/package.json +6 -1
- package/dist/chunk-DPV6PBWE.js +0 -916
- package/dist/chunk-DPV6PBWE.js.map +0 -1
- package/dist/chunk-DZJ5K75J.js.map +0 -1
- /package/dist/{replay-SRQI4QMY.js.map → replay-QFNYP7EY.js.map} +0 -0
|
@@ -0,0 +1,2299 @@
|
|
|
1
|
+
var __typeError = (msg) => {
|
|
2
|
+
throw TypeError(msg);
|
|
3
|
+
};
|
|
4
|
+
var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg);
|
|
5
|
+
var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj));
|
|
6
|
+
var __privateAdd = (obj, member, value) => member.has(obj) ? __typeError("Cannot add the same private member more than once") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
|
|
7
|
+
var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), setter ? setter.call(obj, value) : member.set(obj, value), value);
|
|
8
|
+
|
|
9
|
+
// src/codeChange.ts
|
|
10
|
+
var MAX_FILES = 60;
|
|
11
|
+
var MAX_FILE_BYTES = 5e5;
|
|
12
|
+
var MAX_TOTAL_BYTES = 2e6;
|
|
13
|
+
var TRUNK_CANDIDATES = [
|
|
14
|
+
"origin/HEAD",
|
|
15
|
+
"origin/main",
|
|
16
|
+
"origin/master",
|
|
17
|
+
"main",
|
|
18
|
+
"master"
|
|
19
|
+
];
|
|
20
|
+
var NUL = String.fromCharCode(0);
|
|
21
|
+
async function resolveAutoCodeChange(label) {
|
|
22
|
+
if (typeof process === "undefined") {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
if (process.env?.BITFAB_DISABLE_CODE_CHANGE_CAPTURE) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
const fromEnv = await readCodeChangeFile();
|
|
29
|
+
if (fromEnv) {
|
|
30
|
+
return fromEnv;
|
|
31
|
+
}
|
|
32
|
+
return captureCodeChangeFromGit(process.cwd?.() ?? ".", label);
|
|
33
|
+
}
|
|
34
|
+
async function readCodeChangeFile() {
|
|
35
|
+
const path = process.env?.BITFAB_CODE_CHANGE_PATH;
|
|
36
|
+
if (!path) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
const { readFile } = await import("fs/promises");
|
|
41
|
+
const parsed = JSON.parse(await readFile(path, "utf8"));
|
|
42
|
+
const files = Array.isArray(parsed?.files) && parsed.files.every(
|
|
43
|
+
(f) => typeof f === "object" && f !== null && !Array.isArray(f)
|
|
44
|
+
) ? parsed.files : void 0;
|
|
45
|
+
const description = typeof parsed?.description === "string" ? parsed.description : void 0;
|
|
46
|
+
if (!files && description === void 0) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
return { description, files };
|
|
50
|
+
} catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
async function captureCodeChangeFromGit(cwd, label) {
|
|
55
|
+
let execFile;
|
|
56
|
+
let readFile;
|
|
57
|
+
try {
|
|
58
|
+
;
|
|
59
|
+
({ execFile } = await import("child_process"));
|
|
60
|
+
({ readFile } = await import("fs/promises"));
|
|
61
|
+
} catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
const git = (dir, args) => new Promise((resolve) => {
|
|
65
|
+
execFile(
|
|
66
|
+
"git",
|
|
67
|
+
args,
|
|
68
|
+
// 30s timeout so a hung git (e.g. a network-touching ref op) can't
|
|
69
|
+
// block the whole replay indefinitely.
|
|
70
|
+
{ cwd: dir, maxBuffer: 64 * 1024 * 1024, timeout: 3e4 },
|
|
71
|
+
(err, stdout) => resolve(err ? null : stdout)
|
|
72
|
+
);
|
|
73
|
+
});
|
|
74
|
+
try {
|
|
75
|
+
const root = (await git(cwd, ["rev-parse", "--show-toplevel"]))?.trim();
|
|
76
|
+
if (!root) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
const resolved = await resolveBase(git, root);
|
|
80
|
+
if (!resolved) {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
const { base, fromTrunk } = resolved;
|
|
84
|
+
const blobBytes = async (ref, path) => {
|
|
85
|
+
const out = await git(root, ["cat-file", "-s", `${ref}:${path}`]);
|
|
86
|
+
const n = out ? Number.parseInt(out.trim(), 10) : Number.NaN;
|
|
87
|
+
return Number.isFinite(n) ? n : 0;
|
|
88
|
+
};
|
|
89
|
+
const workingBytes = async (path) => {
|
|
90
|
+
try {
|
|
91
|
+
const { stat } = await import("fs/promises");
|
|
92
|
+
const { join } = await import("path");
|
|
93
|
+
return (await stat(join(root, path))).size;
|
|
94
|
+
} catch {
|
|
95
|
+
return 0;
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
const tracked = await git(root, [
|
|
99
|
+
"diff",
|
|
100
|
+
"--name-status",
|
|
101
|
+
"--no-renames",
|
|
102
|
+
"-z",
|
|
103
|
+
base,
|
|
104
|
+
"--",
|
|
105
|
+
":!.bitfab"
|
|
106
|
+
]);
|
|
107
|
+
const untracked = await git(root, [
|
|
108
|
+
"ls-files",
|
|
109
|
+
"--others",
|
|
110
|
+
"--exclude-standard",
|
|
111
|
+
"-z",
|
|
112
|
+
"--",
|
|
113
|
+
":!.bitfab"
|
|
114
|
+
]);
|
|
115
|
+
const entries = [
|
|
116
|
+
...parseNameStatusZ(tracked ?? ""),
|
|
117
|
+
...(untracked ?? "").split(NUL).filter((p) => p.length > 0).map((path) => ({ status: "A", path }))
|
|
118
|
+
];
|
|
119
|
+
if (entries.length === 0) {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
const files = [];
|
|
123
|
+
let totalBytes = 0;
|
|
124
|
+
for (const { status, path } of entries) {
|
|
125
|
+
if (files.length >= MAX_FILES) {
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
const beforeBytes = status === "A" ? 0 : await blobBytes(base, path);
|
|
129
|
+
const afterBytes = status === "D" ? 0 : await workingBytes(path);
|
|
130
|
+
if (beforeBytes > MAX_FILE_BYTES || afterBytes > MAX_FILE_BYTES) {
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
const before = (status === "A" ? "" : await git(root, ["show", `${base}:${path}`]) ?? "").replace(/\r\n/g, "\n");
|
|
134
|
+
const after = (status === "D" ? "" : await readWorkingFile(readFile, root, path)).replace(/\r\n/g, "\n");
|
|
135
|
+
if (before === after) {
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
const size = Buffer.byteLength(before, "utf8") + Buffer.byteLength(after, "utf8");
|
|
139
|
+
if (totalBytes + size > MAX_TOTAL_BYTES || looksBinary(before) || looksBinary(after)) {
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
totalBytes += size;
|
|
143
|
+
files.push({ path, before, after });
|
|
144
|
+
}
|
|
145
|
+
if (files.length === 0) {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
const subject = (await git(root, ["log", "-1", "--format=%s", "HEAD"]))?.trim();
|
|
149
|
+
const fileWord = files.length === 1 ? "file" : "files";
|
|
150
|
+
const head = label?.trim() || subject || "Working-tree change";
|
|
151
|
+
const against = fromTrunk ? "vs trunk" : "uncommitted (vs HEAD)";
|
|
152
|
+
return {
|
|
153
|
+
description: `${head} (${files.length} ${fileWord} changed ${against})`,
|
|
154
|
+
files
|
|
155
|
+
};
|
|
156
|
+
} catch {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
async function resolveBase(git, root) {
|
|
161
|
+
const forced = process.env?.BITFAB_CODE_CHANGE_BASE;
|
|
162
|
+
if (forced && await refExists(git, root, forced)) {
|
|
163
|
+
const base = (await git(root, ["merge-base", "HEAD", forced]))?.trim() || (await git(root, ["rev-parse", "--verify", forced]))?.trim() || null;
|
|
164
|
+
return base ? { base, fromTrunk: true } : null;
|
|
165
|
+
}
|
|
166
|
+
for (const candidate of TRUNK_CANDIDATES) {
|
|
167
|
+
if (!await refExists(git, root, candidate)) {
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
const mb = (await git(root, ["merge-base", "HEAD", candidate]))?.trim();
|
|
171
|
+
if (mb) {
|
|
172
|
+
return { base: mb, fromTrunk: true };
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return await refExists(git, root, "HEAD") ? { base: "HEAD", fromTrunk: false } : null;
|
|
176
|
+
}
|
|
177
|
+
async function refExists(git, root, ref) {
|
|
178
|
+
return await git(root, ["rev-parse", "--verify", `${ref}^{object}`]) !== null;
|
|
179
|
+
}
|
|
180
|
+
async function readWorkingFile(readFile, root, path) {
|
|
181
|
+
try {
|
|
182
|
+
const { join } = await import("path");
|
|
183
|
+
return await readFile(join(root, path), "utf8");
|
|
184
|
+
} catch {
|
|
185
|
+
return "";
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
function parseNameStatusZ(raw) {
|
|
189
|
+
const parts = raw.split(NUL).filter((p) => p.length > 0);
|
|
190
|
+
const out = [];
|
|
191
|
+
for (let i = 0; i + 1 < parts.length; i += 2) {
|
|
192
|
+
out.push({ status: parts[i].charAt(0), path: parts[i + 1] });
|
|
193
|
+
}
|
|
194
|
+
return out;
|
|
195
|
+
}
|
|
196
|
+
function looksBinary(s) {
|
|
197
|
+
return s.slice(0, 8e3).includes(NUL);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// src/errors.ts
|
|
201
|
+
var BitfabError = class extends Error {
|
|
202
|
+
constructor(message, url, status) {
|
|
203
|
+
super(message);
|
|
204
|
+
this.url = url;
|
|
205
|
+
this.status = status;
|
|
206
|
+
this.name = "BitfabError";
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
// src/version.generated.ts
|
|
211
|
+
var __version__ = "0.34.0";
|
|
212
|
+
|
|
213
|
+
// src/constants.ts
|
|
214
|
+
var DEFAULT_SERVICE_URL = "https://bitfab.ai";
|
|
215
|
+
|
|
216
|
+
// src/asyncStorage.ts
|
|
217
|
+
var AsyncLocalStorageClass = null;
|
|
218
|
+
var initDone = false;
|
|
219
|
+
function registerAsyncLocalStorageClass(cls) {
|
|
220
|
+
if (!AsyncLocalStorageClass) {
|
|
221
|
+
AsyncLocalStorageClass = cls;
|
|
222
|
+
}
|
|
223
|
+
initDone = true;
|
|
224
|
+
}
|
|
225
|
+
function assertAsyncStorageRegistered() {
|
|
226
|
+
if (!AsyncLocalStorageClass) {
|
|
227
|
+
console.warn(
|
|
228
|
+
"Bitfab: AsyncLocalStorage not available - nested span context will not propagate."
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
var asyncStorageReady = (typeof process !== "undefined" && process.versions?.node ? (
|
|
233
|
+
// The join trick hides "node:async_hooks" from static analysis so
|
|
234
|
+
// bundlers that ban Node.js built-ins don't fail at build time.
|
|
235
|
+
// webpackIgnore tells webpack/turbopack to emit a native import()
|
|
236
|
+
// so Node.js can resolve the module at runtime.
|
|
237
|
+
import(
|
|
238
|
+
/* webpackIgnore: true */
|
|
239
|
+
["node", "async_hooks"].join(":")
|
|
240
|
+
).then(
|
|
241
|
+
(mod) => {
|
|
242
|
+
registerAsyncLocalStorageClass(mod.AsyncLocalStorage);
|
|
243
|
+
}
|
|
244
|
+
).catch(() => {
|
|
245
|
+
})
|
|
246
|
+
) : Promise.resolve()).then(() => {
|
|
247
|
+
initDone = true;
|
|
248
|
+
});
|
|
249
|
+
function isAsyncStorageInitDone() {
|
|
250
|
+
return initDone;
|
|
251
|
+
}
|
|
252
|
+
function createAsyncLocalStorage() {
|
|
253
|
+
return AsyncLocalStorageClass ? new AsyncLocalStorageClass() : null;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// src/replayContext.ts
|
|
257
|
+
var replayContextStorage = null;
|
|
258
|
+
var REPLAY_CONTEXT_STORAGE_SYMBOL = /* @__PURE__ */ Symbol.for("bitfab.replayContextStorage");
|
|
259
|
+
var replayContextReady = asyncStorageReady.then(() => {
|
|
260
|
+
const shared = globalThis;
|
|
261
|
+
const existing = shared[REPLAY_CONTEXT_STORAGE_SYMBOL];
|
|
262
|
+
if (existing) {
|
|
263
|
+
replayContextStorage = existing;
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
const created = createAsyncLocalStorage();
|
|
267
|
+
if (created) {
|
|
268
|
+
shared[REPLAY_CONTEXT_STORAGE_SYMBOL] = created;
|
|
269
|
+
replayContextStorage = created;
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
function getReplayContext() {
|
|
273
|
+
return replayContextStorage?.getStore() ?? null;
|
|
274
|
+
}
|
|
275
|
+
function runWithReplayContext(ctx, fn) {
|
|
276
|
+
if (replayContextStorage) {
|
|
277
|
+
return replayContextStorage.run(ctx, fn);
|
|
278
|
+
}
|
|
279
|
+
return fn();
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// src/warnOnce.ts
|
|
283
|
+
var warned = /* @__PURE__ */ new Set();
|
|
284
|
+
function warnOnce(key, message) {
|
|
285
|
+
if (warned.has(key)) {
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
warned.add(key);
|
|
289
|
+
try {
|
|
290
|
+
console.warn(`[bitfab] ${message}`);
|
|
291
|
+
} catch {
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// src/serializePayload.ts
|
|
296
|
+
function serializePayloadBody(payload) {
|
|
297
|
+
try {
|
|
298
|
+
return { body: JSON.stringify(payload), dropped: [] };
|
|
299
|
+
} catch {
|
|
300
|
+
const dropped = [];
|
|
301
|
+
const sanitize = (value, seen) => {
|
|
302
|
+
const t = typeof value;
|
|
303
|
+
if (value === null || t === "string" || t === "number" || t === "boolean") {
|
|
304
|
+
return value;
|
|
305
|
+
}
|
|
306
|
+
if (t === "bigint") {
|
|
307
|
+
dropped.push("BigInt");
|
|
308
|
+
return "<unserializable: BigInt>";
|
|
309
|
+
}
|
|
310
|
+
if (t === "function") {
|
|
311
|
+
const name = value.name || "Function";
|
|
312
|
+
dropped.push(name);
|
|
313
|
+
return `<unserializable: ${name}>`;
|
|
314
|
+
}
|
|
315
|
+
if (t === "symbol") {
|
|
316
|
+
dropped.push("Symbol");
|
|
317
|
+
return "<unserializable: Symbol>";
|
|
318
|
+
}
|
|
319
|
+
if (t !== "object") {
|
|
320
|
+
return void 0;
|
|
321
|
+
}
|
|
322
|
+
const obj = value;
|
|
323
|
+
const className = obj.constructor?.name || "object";
|
|
324
|
+
if (seen.has(obj)) {
|
|
325
|
+
dropped.push(className);
|
|
326
|
+
return `<cycle: ${className}>`;
|
|
327
|
+
}
|
|
328
|
+
seen.add(obj);
|
|
329
|
+
let result;
|
|
330
|
+
if (Array.isArray(obj)) {
|
|
331
|
+
result = obj.map((item) => sanitize(item, seen));
|
|
332
|
+
} else if (typeof obj.toJSON === "function") {
|
|
333
|
+
try {
|
|
334
|
+
result = sanitize(obj.toJSON(), seen);
|
|
335
|
+
} catch {
|
|
336
|
+
dropped.push(className);
|
|
337
|
+
result = `<unserializable: ${className}>`;
|
|
338
|
+
}
|
|
339
|
+
} else {
|
|
340
|
+
try {
|
|
341
|
+
const out = {};
|
|
342
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
343
|
+
out[k] = sanitize(v, seen);
|
|
344
|
+
}
|
|
345
|
+
result = out;
|
|
346
|
+
} catch {
|
|
347
|
+
warnOnce(
|
|
348
|
+
"payload:field-getter-threw",
|
|
349
|
+
"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."
|
|
350
|
+
);
|
|
351
|
+
dropped.push(className);
|
|
352
|
+
result = `<unserializable: ${className}>`;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
seen.delete(obj);
|
|
356
|
+
return result;
|
|
357
|
+
};
|
|
358
|
+
let sanitized;
|
|
359
|
+
try {
|
|
360
|
+
sanitized = sanitize(payload, /* @__PURE__ */ new WeakSet());
|
|
361
|
+
} catch (error) {
|
|
362
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
363
|
+
return {
|
|
364
|
+
body: JSON.stringify({ error: `payload_serialize_failed: ${message}` }),
|
|
365
|
+
dropped
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
if (dropped.length > 0 && typeof sanitized === "object" && sanitized !== null && !Array.isArray(sanitized)) {
|
|
369
|
+
const obj = sanitized;
|
|
370
|
+
const existing = Array.isArray(obj.errors) ? obj.errors : [];
|
|
371
|
+
obj.errors = [
|
|
372
|
+
...existing,
|
|
373
|
+
{
|
|
374
|
+
source: "sdk",
|
|
375
|
+
step: "json_serialize",
|
|
376
|
+
error: `stubbed non-serializable value(s): ${[
|
|
377
|
+
...new Set(dropped)
|
|
378
|
+
].join(", ")}`
|
|
379
|
+
}
|
|
380
|
+
];
|
|
381
|
+
}
|
|
382
|
+
return { body: JSON.stringify(sanitized), dropped };
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// src/otel.ts
|
|
387
|
+
import { SpanStatusCode } from "@opentelemetry/api";
|
|
388
|
+
import {
|
|
389
|
+
ExportResultCode
|
|
390
|
+
} from "@opentelemetry/core";
|
|
391
|
+
import { resourceFromAttributes } from "@opentelemetry/resources";
|
|
392
|
+
import {
|
|
393
|
+
AlwaysOnSampler,
|
|
394
|
+
BasicTracerProvider,
|
|
395
|
+
BatchSpanProcessor
|
|
396
|
+
} from "@opentelemetry/sdk-trace-base";
|
|
397
|
+
|
|
398
|
+
// src/readEnv.ts
|
|
399
|
+
function readEnv(name) {
|
|
400
|
+
if (typeof process !== "undefined" && process.env) {
|
|
401
|
+
return process.env[name];
|
|
402
|
+
}
|
|
403
|
+
return void 0;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// src/unrefTimer.ts
|
|
407
|
+
function unrefTimer(timer) {
|
|
408
|
+
const handle = timer;
|
|
409
|
+
if (typeof handle.unref === "function") {
|
|
410
|
+
handle.unref();
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// src/otel.ts
|
|
415
|
+
var OPERATION_ATTRIBUTE = "bitfab.operation";
|
|
416
|
+
var PAYLOAD_ATTRIBUTE = "bitfab.payload";
|
|
417
|
+
var OTLP_TRACES_ENDPOINT = "/api/sdk/otel/v1/traces";
|
|
418
|
+
var MAX_EXPORT_REQUEST_BYTES = 3e6;
|
|
419
|
+
var MAX_REQUEST_BYTES_ENV = "BITFAB_OTEL_MAX_REQUEST_BYTES";
|
|
420
|
+
var EXPORT_CONCURRENCY_ENV = "BITFAB_OTEL_EXPORT_CONCURRENCY";
|
|
421
|
+
var COLLECTOR_ENDPOINT_ENV = "BITFAB_OTEL_EXPORTER_ENDPOINT";
|
|
422
|
+
var MAX_QUEUE_SIZE = 8192;
|
|
423
|
+
var DIRECT_MAX_EXPORT_BATCH_SIZE = 512;
|
|
424
|
+
var COLLECTOR_MAX_EXPORT_BATCH_SIZE = 32;
|
|
425
|
+
var DIRECT_MAX_REQUEST_BATCH_SIZE = 8;
|
|
426
|
+
var DEFAULT_EXPORT_CONCURRENCY = 32;
|
|
427
|
+
var MAX_EXPORT_CONCURRENCY = 64;
|
|
428
|
+
var SCHEDULE_DELAY_MILLIS = 5e3;
|
|
429
|
+
var EXPORT_TIMEOUT_MILLIS = 3e4;
|
|
430
|
+
var RETRY_DELAY_MILLIS = 100;
|
|
431
|
+
var MAX_SEND_ATTEMPTS = 3;
|
|
432
|
+
var DEFAULT_LIFECYCLE_TIMEOUT_MS = 3e4;
|
|
433
|
+
var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([408, 425, 429]);
|
|
434
|
+
var liveTransports = /* @__PURE__ */ new Set();
|
|
435
|
+
var traceSubmissionSpanIds = /* @__PURE__ */ new Map();
|
|
436
|
+
var replayTraceSubmissions = /* @__PURE__ */ new Set();
|
|
437
|
+
var submissionCounter = 0;
|
|
438
|
+
function readBoundedIntEnv(name, max, fallback, warnKey) {
|
|
439
|
+
const raw = readEnv(name);
|
|
440
|
+
if (raw === void 0) {
|
|
441
|
+
return fallback;
|
|
442
|
+
}
|
|
443
|
+
const value = Number(raw);
|
|
444
|
+
if (Number.isInteger(value) && value > 0 && value <= max) {
|
|
445
|
+
return value;
|
|
446
|
+
}
|
|
447
|
+
warnOnce(
|
|
448
|
+
warnKey,
|
|
449
|
+
`${name} must be a positive integer no greater than ${max}; using ${fallback}`
|
|
450
|
+
);
|
|
451
|
+
return fallback;
|
|
452
|
+
}
|
|
453
|
+
function logError(message, error) {
|
|
454
|
+
try {
|
|
455
|
+
if (error === void 0) {
|
|
456
|
+
console.error(`[bitfab] ${message}`);
|
|
457
|
+
} else {
|
|
458
|
+
console.error(`[bitfab] ${message}`, error);
|
|
459
|
+
}
|
|
460
|
+
} catch {
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
function recordTraceSubmission(operation, payload) {
|
|
464
|
+
const sourceTraceId = resolveSourceTraceId(payload);
|
|
465
|
+
if (sourceTraceId === void 0) {
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
if (operation === "external_span") {
|
|
469
|
+
const rawSpan = asRecord(payload.rawSpan);
|
|
470
|
+
if (typeof rawSpan?.id !== "string") {
|
|
471
|
+
submissionCounter += 1;
|
|
472
|
+
}
|
|
473
|
+
const sourceSpanId = typeof rawSpan?.id === "string" ? rawSpan.id : `submission-${submissionCounter}`;
|
|
474
|
+
const existing = traceSubmissionSpanIds.get(sourceTraceId);
|
|
475
|
+
if (existing) {
|
|
476
|
+
existing.add(sourceSpanId);
|
|
477
|
+
} else {
|
|
478
|
+
traceSubmissionSpanIds.set(sourceTraceId, /* @__PURE__ */ new Set([sourceSpanId]));
|
|
479
|
+
}
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
if (payload.completed !== true) {
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
if (typeof payload.testRunId === "string") {
|
|
486
|
+
replayTraceSubmissions.add(sourceTraceId);
|
|
487
|
+
if (!traceSubmissionSpanIds.has(sourceTraceId)) {
|
|
488
|
+
traceSubmissionSpanIds.set(sourceTraceId, /* @__PURE__ */ new Set());
|
|
489
|
+
}
|
|
490
|
+
} else {
|
|
491
|
+
traceSubmissionSpanIds.delete(sourceTraceId);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
function takeReplaySpanCounts(traceIds) {
|
|
495
|
+
const counts = {};
|
|
496
|
+
for (const traceId of traceIds) {
|
|
497
|
+
if (!replayTraceSubmissions.has(traceId)) {
|
|
498
|
+
continue;
|
|
499
|
+
}
|
|
500
|
+
counts[traceId] = traceSubmissionSpanIds.get(traceId)?.size ?? 0;
|
|
501
|
+
traceSubmissionSpanIds.delete(traceId);
|
|
502
|
+
replayTraceSubmissions.delete(traceId);
|
|
503
|
+
}
|
|
504
|
+
return counts;
|
|
505
|
+
}
|
|
506
|
+
function asRecord(value) {
|
|
507
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
508
|
+
}
|
|
509
|
+
function resolveSourceTraceId(payload) {
|
|
510
|
+
if (typeof payload.sourceTraceId === "string") {
|
|
511
|
+
return payload.sourceTraceId;
|
|
512
|
+
}
|
|
513
|
+
const rawTrace = asRecord(payload.externalTrace) ?? asRecord(payload.rawTrace);
|
|
514
|
+
return typeof rawTrace?.id === "string" ? rawTrace.id : void 0;
|
|
515
|
+
}
|
|
516
|
+
function otlpValue(value) {
|
|
517
|
+
if (typeof value === "boolean") {
|
|
518
|
+
return { boolValue: value };
|
|
519
|
+
}
|
|
520
|
+
if (typeof value === "number") {
|
|
521
|
+
return Number.isInteger(value) ? { intValue: String(value) } : { doubleValue: value };
|
|
522
|
+
}
|
|
523
|
+
if (typeof value === "string") {
|
|
524
|
+
return { stringValue: value };
|
|
525
|
+
}
|
|
526
|
+
if (Array.isArray(value)) {
|
|
527
|
+
return { arrayValue: { values: value.map(otlpValue) } };
|
|
528
|
+
}
|
|
529
|
+
return { stringValue: String(value) };
|
|
530
|
+
}
|
|
531
|
+
function otlpAttributes(attributes) {
|
|
532
|
+
if (!attributes) {
|
|
533
|
+
return [];
|
|
534
|
+
}
|
|
535
|
+
return Object.entries(attributes).filter(([, value]) => value !== void 0).map(([key, value]) => ({ key, value: otlpValue(value) }));
|
|
536
|
+
}
|
|
537
|
+
function hrTimeToNanoString(time) {
|
|
538
|
+
if (!time) {
|
|
539
|
+
return "0";
|
|
540
|
+
}
|
|
541
|
+
return `${time[0]}${String(time[1]).padStart(9, "0")}`;
|
|
542
|
+
}
|
|
543
|
+
function spanToOtlp(span) {
|
|
544
|
+
const spanContext = span.spanContext();
|
|
545
|
+
const result = {
|
|
546
|
+
traceId: spanContext.traceId,
|
|
547
|
+
spanId: spanContext.spanId,
|
|
548
|
+
name: span.name,
|
|
549
|
+
kind: span.kind + 1,
|
|
550
|
+
startTimeUnixNano: hrTimeToNanoString(span.startTime),
|
|
551
|
+
endTimeUnixNano: hrTimeToNanoString(span.endTime),
|
|
552
|
+
attributes: otlpAttributes(span.attributes),
|
|
553
|
+
droppedAttributesCount: span.droppedAttributesCount,
|
|
554
|
+
droppedEventsCount: span.droppedEventsCount,
|
|
555
|
+
droppedLinksCount: span.droppedLinksCount,
|
|
556
|
+
status: {
|
|
557
|
+
code: span.status.code,
|
|
558
|
+
...span.status.message ? { message: span.status.message } : {}
|
|
559
|
+
},
|
|
560
|
+
flags: spanContext.traceFlags
|
|
561
|
+
};
|
|
562
|
+
const parentSpanId = span.parentSpanContext?.spanId;
|
|
563
|
+
if (parentSpanId) {
|
|
564
|
+
result.parentSpanId = parentSpanId;
|
|
565
|
+
}
|
|
566
|
+
if (spanContext.traceState) {
|
|
567
|
+
result.traceState = spanContext.traceState.serialize();
|
|
568
|
+
}
|
|
569
|
+
return result;
|
|
570
|
+
}
|
|
571
|
+
function buildOtlpRequest(first, spans) {
|
|
572
|
+
const scope = first.instrumentationScope;
|
|
573
|
+
return {
|
|
574
|
+
resourceSpans: [
|
|
575
|
+
{
|
|
576
|
+
resource: {
|
|
577
|
+
attributes: otlpAttributes(
|
|
578
|
+
first.resource.attributes
|
|
579
|
+
)
|
|
580
|
+
},
|
|
581
|
+
scopeSpans: [
|
|
582
|
+
{
|
|
583
|
+
scope: { name: scope.name, version: scope.version ?? "" },
|
|
584
|
+
spans
|
|
585
|
+
}
|
|
586
|
+
]
|
|
587
|
+
}
|
|
588
|
+
]
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
function encodedSize(value) {
|
|
592
|
+
const json = JSON.stringify(value);
|
|
593
|
+
if (typeof TextEncoder !== "undefined") {
|
|
594
|
+
return new TextEncoder().encode(json).length;
|
|
595
|
+
}
|
|
596
|
+
return json.length;
|
|
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
|
+
var OtlpPayloadTooLargeError = class extends Error {
|
|
637
|
+
};
|
|
638
|
+
var OtlpPartialSuccessError = class extends Error {
|
|
639
|
+
};
|
|
640
|
+
function responseStatus(error) {
|
|
641
|
+
return error instanceof BitfabError ? error.status : void 0;
|
|
642
|
+
}
|
|
643
|
+
function isRetryable(error) {
|
|
644
|
+
const status = responseStatus(error);
|
|
645
|
+
if (status === void 0) {
|
|
646
|
+
return true;
|
|
647
|
+
}
|
|
648
|
+
return RETRYABLE_STATUSES.has(status) || status >= 500;
|
|
649
|
+
}
|
|
650
|
+
var BitfabSpanExporter = class {
|
|
651
|
+
constructor(directSender, maxRequestBytes, maxRequestBatchSize, exportConcurrency) {
|
|
652
|
+
this.directSender = directSender;
|
|
653
|
+
this.maxRequestBytes = maxRequestBytes;
|
|
654
|
+
this.maxRequestBatchSize = maxRequestBatchSize;
|
|
655
|
+
this.exportConcurrency = exportConcurrency;
|
|
656
|
+
}
|
|
657
|
+
export(spans, resultCallback) {
|
|
658
|
+
void this.exportAsync(spans).then(
|
|
659
|
+
(succeeded) => {
|
|
660
|
+
resultCallback({
|
|
661
|
+
code: succeeded ? ExportResultCode.SUCCESS : ExportResultCode.FAILED
|
|
662
|
+
});
|
|
663
|
+
},
|
|
664
|
+
(error) => {
|
|
665
|
+
resultCallback({ code: ExportResultCode.FAILED, error });
|
|
666
|
+
}
|
|
667
|
+
);
|
|
668
|
+
}
|
|
669
|
+
async exportAsync(spans) {
|
|
670
|
+
if (spans.length === 0) {
|
|
671
|
+
return true;
|
|
672
|
+
}
|
|
673
|
+
let encoded;
|
|
674
|
+
try {
|
|
675
|
+
encoded = spans.map(spanToOtlp);
|
|
676
|
+
} catch (error) {
|
|
677
|
+
logError("failed to encode an OpenTelemetry span batch", error);
|
|
678
|
+
return false;
|
|
679
|
+
}
|
|
680
|
+
const first = spans[0];
|
|
681
|
+
const batches = this.buildRequestBatches(first, encoded);
|
|
682
|
+
const results = await mapWithConcurrency(
|
|
683
|
+
batches,
|
|
684
|
+
this.exportConcurrency,
|
|
685
|
+
(batch) => this.send(first, batch)
|
|
686
|
+
);
|
|
687
|
+
return results.every(Boolean);
|
|
688
|
+
}
|
|
689
|
+
buildRequestBatches(first, spans) {
|
|
690
|
+
const batches = [];
|
|
691
|
+
let current = [];
|
|
692
|
+
for (const span of spans) {
|
|
693
|
+
if (current.length >= this.maxRequestBatchSize) {
|
|
694
|
+
batches.push(current);
|
|
695
|
+
current = [];
|
|
696
|
+
}
|
|
697
|
+
const candidate = [...current, span];
|
|
698
|
+
if (current.length > 0 && encodedSize(buildOtlpRequest(first, candidate)) > this.maxRequestBytes) {
|
|
699
|
+
batches.push(current);
|
|
700
|
+
current = [span];
|
|
701
|
+
} else {
|
|
702
|
+
current = candidate;
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
if (current.length > 0) {
|
|
706
|
+
batches.push(current);
|
|
707
|
+
}
|
|
708
|
+
return batches;
|
|
709
|
+
}
|
|
710
|
+
async send(first, spans) {
|
|
711
|
+
const payload = buildOtlpRequest(first, spans);
|
|
712
|
+
if (encodedSize(payload) > this.maxRequestBytes) {
|
|
713
|
+
logError(
|
|
714
|
+
"a single OpenTelemetry span exceeded the configured request-size target and could not be exported"
|
|
715
|
+
);
|
|
716
|
+
return false;
|
|
717
|
+
}
|
|
718
|
+
try {
|
|
719
|
+
await this.sendWithRetries(payload);
|
|
720
|
+
return true;
|
|
721
|
+
} catch (error) {
|
|
722
|
+
if (error instanceof OtlpPayloadTooLargeError) {
|
|
723
|
+
logError(
|
|
724
|
+
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"
|
|
725
|
+
);
|
|
726
|
+
return false;
|
|
727
|
+
}
|
|
728
|
+
if (error instanceof OtlpPartialSuccessError) {
|
|
729
|
+
return false;
|
|
730
|
+
}
|
|
731
|
+
logError("failed to export an OpenTelemetry span batch", error);
|
|
732
|
+
return false;
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
/**
|
|
736
|
+
* Retries transient failures. Span and trace-completion carriers are safe to
|
|
737
|
+
* retry: the server keys them idempotently on `sourceSpanId`/`sourceTraceId`,
|
|
738
|
+
* so a duplicate delivery cannot create a duplicate row.
|
|
739
|
+
*
|
|
740
|
+
* KNOWN LIMITATION: an `internal_trace` (a `call()` BAML trace) carries no
|
|
741
|
+
* such key, so retrying a batch that holds one can create a duplicate trace -
|
|
742
|
+
* including when a request times out client-side but the server goes on to
|
|
743
|
+
* persist it. Accepted deliberately for now, matching the other SDKs, rather
|
|
744
|
+
* than skipping retries for a whole batch or inventing an idempotency scheme
|
|
745
|
+
* the server does not yet understand. The fix is a client-supplied
|
|
746
|
+
* idempotency key that ingestion dedupes on.
|
|
747
|
+
*/
|
|
748
|
+
async sendWithRetries(payload) {
|
|
749
|
+
for (let attempt = 0; attempt < MAX_SEND_ATTEMPTS; attempt += 1) {
|
|
750
|
+
try {
|
|
751
|
+
const response = await this.directSender(
|
|
752
|
+
OTLP_TRACES_ENDPOINT,
|
|
753
|
+
payload,
|
|
754
|
+
EXPORT_TIMEOUT_MILLIS
|
|
755
|
+
);
|
|
756
|
+
const partialSuccess = asRecord(response?.partialSuccess);
|
|
757
|
+
const rejected = partialSuccess?.rejectedSpans;
|
|
758
|
+
if (rejected !== void 0 && rejected !== "0" && rejected !== 0) {
|
|
759
|
+
logError(
|
|
760
|
+
`OTLP ingestion rejected ${rejected} span(s): ${partialSuccess?.errorMessage ?? "no reason provided"}`
|
|
761
|
+
);
|
|
762
|
+
throw new OtlpPartialSuccessError();
|
|
763
|
+
}
|
|
764
|
+
return;
|
|
765
|
+
} catch (error) {
|
|
766
|
+
if (error instanceof OtlpPartialSuccessError) {
|
|
767
|
+
throw error;
|
|
768
|
+
}
|
|
769
|
+
if (responseStatus(error) === 413) {
|
|
770
|
+
throw new OtlpPayloadTooLargeError();
|
|
771
|
+
}
|
|
772
|
+
if (attempt === MAX_SEND_ATTEMPTS - 1 || !isRetryable(error)) {
|
|
773
|
+
throw error;
|
|
774
|
+
}
|
|
775
|
+
await delay(RETRY_DELAY_MILLIS);
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
async shutdown() {
|
|
780
|
+
}
|
|
781
|
+
async forceFlush() {
|
|
782
|
+
}
|
|
783
|
+
};
|
|
784
|
+
var CollectorSpanExporter = class {
|
|
785
|
+
constructor(endpoint, apiKey, maxRequestBytes) {
|
|
786
|
+
this.endpoint = endpoint;
|
|
787
|
+
this.apiKey = apiKey;
|
|
788
|
+
this.maxRequestBytes = maxRequestBytes;
|
|
789
|
+
}
|
|
790
|
+
/**
|
|
791
|
+
* Loaded through a dynamic import rather than a top-level one so bundlers
|
|
792
|
+
* code-split it: Collector delivery is opt-in, and a consumer who never sets
|
|
793
|
+
* an endpoint should not pay for the exporter in their initial bundle. It is
|
|
794
|
+
* a hard dependency, so this cannot fail for want of the package.
|
|
795
|
+
*/
|
|
796
|
+
loadExporterModule() {
|
|
797
|
+
if (!this.pendingModule) {
|
|
798
|
+
this.pendingModule = import("@opentelemetry/exporter-trace-otlp-proto");
|
|
799
|
+
}
|
|
800
|
+
return this.pendingModule;
|
|
801
|
+
}
|
|
802
|
+
export(spans, resultCallback) {
|
|
803
|
+
void this.exportAsync(spans).then(
|
|
804
|
+
(succeeded) => {
|
|
805
|
+
resultCallback({
|
|
806
|
+
code: succeeded ? ExportResultCode.SUCCESS : ExportResultCode.FAILED
|
|
807
|
+
});
|
|
808
|
+
},
|
|
809
|
+
(error) => {
|
|
810
|
+
resultCallback({ code: ExportResultCode.FAILED, error });
|
|
811
|
+
}
|
|
812
|
+
);
|
|
813
|
+
}
|
|
814
|
+
async exportAsync(spans) {
|
|
815
|
+
if (spans.length === 0) {
|
|
816
|
+
return true;
|
|
817
|
+
}
|
|
818
|
+
let delegate;
|
|
819
|
+
try {
|
|
820
|
+
delegate = await this.resolveDelegate();
|
|
821
|
+
} catch (error) {
|
|
822
|
+
logError("failed to build the OTLP Collector exporter", error);
|
|
823
|
+
return false;
|
|
824
|
+
}
|
|
825
|
+
const results = await Promise.all(
|
|
826
|
+
this.partition(spans).map(
|
|
827
|
+
(batch) => new Promise((resolve) => {
|
|
828
|
+
try {
|
|
829
|
+
delegate.export(batch, (result) => {
|
|
830
|
+
resolve(result.code === ExportResultCode.SUCCESS);
|
|
831
|
+
});
|
|
832
|
+
} catch (error) {
|
|
833
|
+
logError("Collector export threw", error);
|
|
834
|
+
resolve(false);
|
|
835
|
+
}
|
|
836
|
+
})
|
|
837
|
+
)
|
|
838
|
+
);
|
|
839
|
+
return results.every(Boolean);
|
|
840
|
+
}
|
|
841
|
+
/**
|
|
842
|
+
* Partition by the encoded JSON size of each carrier rather than its encoded
|
|
843
|
+
* protobuf size. Protobuf is strictly smaller than the equivalent JSON for
|
|
844
|
+
* these payloads, so the JSON figure is a conservative bound that keeps every
|
|
845
|
+
* request under the target without pulling `@opentelemetry/otlp-transformer`
|
|
846
|
+
* into the dependency set purely to measure bytes.
|
|
847
|
+
*/
|
|
848
|
+
partition(spans) {
|
|
849
|
+
const batches = [];
|
|
850
|
+
let current = [];
|
|
851
|
+
let currentSize = 0;
|
|
852
|
+
for (const span of spans) {
|
|
853
|
+
const size = encodedSize(spanToOtlp(span));
|
|
854
|
+
if (current.length > 0 && currentSize + size > this.maxRequestBytes) {
|
|
855
|
+
batches.push(current);
|
|
856
|
+
current = [];
|
|
857
|
+
currentSize = 0;
|
|
858
|
+
}
|
|
859
|
+
current.push(span);
|
|
860
|
+
currentSize += size;
|
|
861
|
+
}
|
|
862
|
+
if (current.length > 0) {
|
|
863
|
+
batches.push(current);
|
|
864
|
+
}
|
|
865
|
+
return batches;
|
|
866
|
+
}
|
|
867
|
+
async resolveDelegate() {
|
|
868
|
+
const apiKey = this.apiKey() ?? "";
|
|
869
|
+
if (this.delegate && this.delegateApiKey === apiKey) {
|
|
870
|
+
return this.delegate;
|
|
871
|
+
}
|
|
872
|
+
const { OTLPTraceExporter } = await this.loadExporterModule();
|
|
873
|
+
const previous = this.delegate;
|
|
874
|
+
this.delegate = new OTLPTraceExporter({
|
|
875
|
+
url: this.endpoint,
|
|
876
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
877
|
+
timeoutMillis: EXPORT_TIMEOUT_MILLIS
|
|
878
|
+
});
|
|
879
|
+
this.delegateApiKey = apiKey;
|
|
880
|
+
if (previous) {
|
|
881
|
+
void previous.shutdown().catch(() => {
|
|
882
|
+
});
|
|
883
|
+
}
|
|
884
|
+
return this.delegate;
|
|
885
|
+
}
|
|
886
|
+
async shutdown() {
|
|
887
|
+
await this.delegate?.shutdown();
|
|
888
|
+
}
|
|
889
|
+
async forceFlush() {
|
|
890
|
+
await this.delegate?.forceFlush?.();
|
|
891
|
+
}
|
|
892
|
+
};
|
|
893
|
+
var DeliveryTrackingExporter = class {
|
|
894
|
+
constructor(exporter) {
|
|
895
|
+
this.exporter = exporter;
|
|
896
|
+
// Deliberately unscoped, matching the Python SDK. An export can outlive
|
|
897
|
+
// OTel's export timeout and report failure after the flush that was waiting
|
|
898
|
+
// on it already returned, so that failure surfaces on the NEXT flush instead.
|
|
899
|
+
// That over-reports: a good flush can inherit an older failure. The
|
|
900
|
+
// alternative - discarding failures from completed flush windows - under-
|
|
901
|
+
// reports, and `BatchSpanProcessor` also runs scheduled exports that belong
|
|
902
|
+
// to no flush at all, so their failures would vanish entirely. For a
|
|
903
|
+
// telemetry SDK a false "flush failed" is investigable; a false "flush
|
|
904
|
+
// succeeded" silently loses traces. We take the noisy direction on purpose.
|
|
905
|
+
this.failedExports = 0;
|
|
906
|
+
}
|
|
907
|
+
export(spans, resultCallback) {
|
|
908
|
+
try {
|
|
909
|
+
this.exporter.export(spans, (result) => {
|
|
910
|
+
if (result.code !== ExportResultCode.SUCCESS) {
|
|
911
|
+
this.failedExports += 1;
|
|
912
|
+
}
|
|
913
|
+
resultCallback(result);
|
|
914
|
+
});
|
|
915
|
+
} catch (error) {
|
|
916
|
+
this.failedExports += 1;
|
|
917
|
+
resultCallback({ code: ExportResultCode.FAILED, error });
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
takeFailedExports() {
|
|
921
|
+
const failed = this.failedExports;
|
|
922
|
+
this.failedExports = 0;
|
|
923
|
+
return failed;
|
|
924
|
+
}
|
|
925
|
+
shutdown() {
|
|
926
|
+
return this.exporter.shutdown();
|
|
927
|
+
}
|
|
928
|
+
forceFlush() {
|
|
929
|
+
return this.exporter.forceFlush?.() ?? Promise.resolve();
|
|
930
|
+
}
|
|
931
|
+
};
|
|
932
|
+
var OtelBatchTransport = class {
|
|
933
|
+
constructor(options) {
|
|
934
|
+
this.closed = false;
|
|
935
|
+
const collectorEndpoint = options.collectorEndpoint;
|
|
936
|
+
const maxRequestBytes = options.maxRequestBytes ?? MAX_EXPORT_REQUEST_BYTES;
|
|
937
|
+
const maxRequestBatchSize = options.maxRequestBatchSize ?? DIRECT_MAX_REQUEST_BATCH_SIZE;
|
|
938
|
+
if (maxRequestBatchSize <= 0) {
|
|
939
|
+
throw new BitfabError("maxRequestBatchSize must be a positive integer");
|
|
940
|
+
}
|
|
941
|
+
this.deliveryTracker = new DeliveryTrackingExporter(
|
|
942
|
+
collectorEndpoint === void 0 ? new BitfabSpanExporter(
|
|
943
|
+
options.directSender,
|
|
944
|
+
maxRequestBytes,
|
|
945
|
+
maxRequestBatchSize,
|
|
946
|
+
options.exportConcurrency ?? DEFAULT_EXPORT_CONCURRENCY
|
|
947
|
+
) : new CollectorSpanExporter(
|
|
948
|
+
normalizeCollectorEndpoint(collectorEndpoint),
|
|
949
|
+
options.apiKey,
|
|
950
|
+
maxRequestBytes
|
|
951
|
+
)
|
|
952
|
+
);
|
|
953
|
+
this.processor = new BatchSpanProcessor(this.deliveryTracker, {
|
|
954
|
+
maxQueueSize: options.maxQueueSize ?? MAX_QUEUE_SIZE,
|
|
955
|
+
maxExportBatchSize: options.maxExportBatchSize ?? (collectorEndpoint === void 0 ? DIRECT_MAX_EXPORT_BATCH_SIZE : COLLECTOR_MAX_EXPORT_BATCH_SIZE),
|
|
956
|
+
scheduledDelayMillis: SCHEDULE_DELAY_MILLIS,
|
|
957
|
+
exportTimeoutMillis: options.exportTimeoutMillis ?? EXPORT_TIMEOUT_MILLIS
|
|
958
|
+
});
|
|
959
|
+
this.provider = new BasicTracerProvider({
|
|
960
|
+
sampler: new AlwaysOnSampler(),
|
|
961
|
+
resource: resourceFromAttributes({
|
|
962
|
+
"service.name": "bitfab-typescript-sdk",
|
|
963
|
+
"service.version": __version__
|
|
964
|
+
}),
|
|
965
|
+
spanLimits: {
|
|
966
|
+
attributeCountLimit: 2,
|
|
967
|
+
attributeValueLengthLimit: Number.POSITIVE_INFINITY
|
|
968
|
+
},
|
|
969
|
+
spanProcessors: [this.processor]
|
|
970
|
+
});
|
|
971
|
+
this.tracer = this.provider.getTracer("bitfab", __version__);
|
|
972
|
+
liveTransports.add(this);
|
|
973
|
+
}
|
|
974
|
+
submit(operation, payload) {
|
|
975
|
+
recordTraceSubmission(operation, payload);
|
|
976
|
+
if (this.closed) {
|
|
977
|
+
warnOnce(
|
|
978
|
+
"otel-submit-after-shutdown",
|
|
979
|
+
"OpenTelemetry transport is shut down; dropping spans"
|
|
980
|
+
);
|
|
981
|
+
return;
|
|
982
|
+
}
|
|
983
|
+
try {
|
|
984
|
+
const { body, dropped } = serializePayloadBody(payload);
|
|
985
|
+
if (dropped.length > 0) {
|
|
986
|
+
warnOnce(
|
|
987
|
+
"otel-carrier-payload-stubbed",
|
|
988
|
+
`a span payload held non-serializable value(s) (${[
|
|
989
|
+
...new Set(dropped)
|
|
990
|
+
].join(", ")}); they were stubbed so the span still ships, but the trace may be incomplete or not replayable.`
|
|
991
|
+
);
|
|
992
|
+
}
|
|
993
|
+
const span = this.tracer.startSpan(spanName(operation, payload), {
|
|
994
|
+
attributes: {
|
|
995
|
+
[OPERATION_ATTRIBUTE]: operation,
|
|
996
|
+
[PAYLOAD_ATTRIBUTE]: body
|
|
997
|
+
},
|
|
998
|
+
startTime: payloadTimestamp(payload, "started_at")
|
|
999
|
+
});
|
|
1000
|
+
if (hasError(payload)) {
|
|
1001
|
+
span.setStatus({ code: SpanStatusCode.ERROR });
|
|
1002
|
+
}
|
|
1003
|
+
endSpan(span, payloadTimestamp(payload, "ended_at"));
|
|
1004
|
+
} catch (error) {
|
|
1005
|
+
logError("failed to queue an OpenTelemetry span", error);
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
async flush(timeoutMs = DEFAULT_LIFECYCLE_TIMEOUT_MS) {
|
|
1009
|
+
const pending = (this.pendingFlush ?? Promise.resolve(true)).then(
|
|
1010
|
+
() => this.forceFlushOnce()
|
|
1011
|
+
);
|
|
1012
|
+
this.pendingFlush = pending.catch(() => false);
|
|
1013
|
+
return withDeadline(pending, timeoutMs);
|
|
1014
|
+
}
|
|
1015
|
+
async forceFlushOnce() {
|
|
1016
|
+
try {
|
|
1017
|
+
await this.processor.forceFlush();
|
|
1018
|
+
} catch (error) {
|
|
1019
|
+
logError("failed to flush OpenTelemetry spans", error);
|
|
1020
|
+
this.deliveryTracker.takeFailedExports();
|
|
1021
|
+
return false;
|
|
1022
|
+
}
|
|
1023
|
+
return this.deliveryTracker.takeFailedExports() === 0;
|
|
1024
|
+
}
|
|
1025
|
+
async shutdown(timeoutMs = DEFAULT_LIFECYCLE_TIMEOUT_MS) {
|
|
1026
|
+
const deadline = Date.now() + Math.max(timeoutMs, 0);
|
|
1027
|
+
this.closed = true;
|
|
1028
|
+
const flushed = await this.flush(Math.max(0, deadline - Date.now()));
|
|
1029
|
+
liveTransports.delete(this);
|
|
1030
|
+
const shutdownCompleted = await withDeadline(
|
|
1031
|
+
this.provider.shutdown().then(() => true).catch((error) => {
|
|
1032
|
+
logError("failed to shut down the OpenTelemetry transport", error);
|
|
1033
|
+
return false;
|
|
1034
|
+
}),
|
|
1035
|
+
Math.max(0, deadline - Date.now())
|
|
1036
|
+
);
|
|
1037
|
+
return flushed && shutdownCompleted;
|
|
1038
|
+
}
|
|
1039
|
+
};
|
|
1040
|
+
function normalizeCollectorEndpoint(endpoint) {
|
|
1041
|
+
const trimmed = endpoint.replace(/\/+$/, "");
|
|
1042
|
+
return trimmed.endsWith("/v1/traces") ? trimmed : `${trimmed}/v1/traces`;
|
|
1043
|
+
}
|
|
1044
|
+
function endSpan(span, endTime) {
|
|
1045
|
+
span.end(endTime);
|
|
1046
|
+
}
|
|
1047
|
+
function spanName(operation, payload) {
|
|
1048
|
+
if (operation === "external_span") {
|
|
1049
|
+
const spanData = asRecord(asRecord(payload.rawSpan)?.span_data);
|
|
1050
|
+
if (typeof spanData?.name === "string") {
|
|
1051
|
+
return spanData.name;
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
if (typeof payload.traceFunctionKey === "string") {
|
|
1055
|
+
return payload.traceFunctionKey;
|
|
1056
|
+
}
|
|
1057
|
+
return `bitfab.${operation}`;
|
|
1058
|
+
}
|
|
1059
|
+
function payloadTimestamp(payload, field) {
|
|
1060
|
+
const rawSpan = asRecord(payload.rawSpan);
|
|
1061
|
+
const rawTrace = asRecord(payload.externalTrace) ?? asRecord(payload.rawTrace);
|
|
1062
|
+
const raw = rawSpan?.[field] ?? rawTrace?.[field];
|
|
1063
|
+
if (typeof raw !== "string") {
|
|
1064
|
+
return void 0;
|
|
1065
|
+
}
|
|
1066
|
+
const parsed = Date.parse(raw);
|
|
1067
|
+
return Number.isNaN(parsed) ? void 0 : parsed;
|
|
1068
|
+
}
|
|
1069
|
+
function hasError(payload) {
|
|
1070
|
+
const spanData = asRecord(asRecord(payload.rawSpan)?.span_data);
|
|
1071
|
+
if (spanData?.error != null) {
|
|
1072
|
+
return true;
|
|
1073
|
+
}
|
|
1074
|
+
const errors = payload.errors;
|
|
1075
|
+
return Array.isArray(errors) ? errors.length > 0 : Boolean(errors);
|
|
1076
|
+
}
|
|
1077
|
+
function createOtelTransport(options) {
|
|
1078
|
+
return new OtelBatchTransport({
|
|
1079
|
+
...options,
|
|
1080
|
+
collectorEndpoint: readEnv(COLLECTOR_ENDPOINT_ENV) || void 0,
|
|
1081
|
+
exportConcurrency: readBoundedIntEnv(
|
|
1082
|
+
EXPORT_CONCURRENCY_ENV,
|
|
1083
|
+
MAX_EXPORT_CONCURRENCY,
|
|
1084
|
+
DEFAULT_EXPORT_CONCURRENCY,
|
|
1085
|
+
"otel-export-concurrency-invalid"
|
|
1086
|
+
),
|
|
1087
|
+
maxRequestBytes: readBoundedIntEnv(
|
|
1088
|
+
MAX_REQUEST_BYTES_ENV,
|
|
1089
|
+
MAX_EXPORT_REQUEST_BYTES,
|
|
1090
|
+
MAX_EXPORT_REQUEST_BYTES,
|
|
1091
|
+
"otel-max-request-bytes-invalid"
|
|
1092
|
+
)
|
|
1093
|
+
});
|
|
1094
|
+
}
|
|
1095
|
+
async function forEachLiveTransport(timeoutMs, run) {
|
|
1096
|
+
const deadline = Date.now() + Math.max(timeoutMs, 0);
|
|
1097
|
+
let succeeded = true;
|
|
1098
|
+
for (const transport of [...liveTransports]) {
|
|
1099
|
+
succeeded = await run(transport, Math.max(0, deadline - Date.now())) && succeeded;
|
|
1100
|
+
}
|
|
1101
|
+
return succeeded;
|
|
1102
|
+
}
|
|
1103
|
+
function flushOtelTransports(timeoutMs = DEFAULT_LIFECYCLE_TIMEOUT_MS) {
|
|
1104
|
+
return forEachLiveTransport(
|
|
1105
|
+
timeoutMs,
|
|
1106
|
+
(transport, remaining) => transport.flush(remaining)
|
|
1107
|
+
);
|
|
1108
|
+
}
|
|
1109
|
+
function shutdownOtelTransports(timeoutMs = DEFAULT_LIFECYCLE_TIMEOUT_MS) {
|
|
1110
|
+
return forEachLiveTransport(
|
|
1111
|
+
timeoutMs,
|
|
1112
|
+
(transport, remaining) => transport.shutdown(remaining)
|
|
1113
|
+
);
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
// src/transport.ts
|
|
1117
|
+
function createTraceTransport(options) {
|
|
1118
|
+
return createOtelTransport(options);
|
|
1119
|
+
}
|
|
1120
|
+
function flushTraceTransports(timeoutMs) {
|
|
1121
|
+
return flushOtelTransports(timeoutMs);
|
|
1122
|
+
}
|
|
1123
|
+
function shutdownTraceTransports(timeoutMs) {
|
|
1124
|
+
return shutdownOtelTransports(timeoutMs);
|
|
1125
|
+
}
|
|
1126
|
+
function takeReplaySpanCounts2(traceIds) {
|
|
1127
|
+
return takeReplaySpanCounts(traceIds);
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
// src/http.ts
|
|
1131
|
+
var REPLAY_DB_BRANCH_REQUEST_TIMEOUT_MS = 3e5;
|
|
1132
|
+
var EXIT_FLUSH_TIMEOUT_MS = 5e3;
|
|
1133
|
+
var DEFAULT_LIFECYCLE_TIMEOUT_MS2 = 3e4;
|
|
1134
|
+
var pendingTracePromises = /* @__PURE__ */ new Set();
|
|
1135
|
+
function awaitOnExit(promise) {
|
|
1136
|
+
pendingTracePromises.add(promise);
|
|
1137
|
+
void promise.finally(() => {
|
|
1138
|
+
pendingTracePromises.delete(promise);
|
|
1139
|
+
}).catch(() => {
|
|
1140
|
+
});
|
|
1141
|
+
return promise;
|
|
1142
|
+
}
|
|
1143
|
+
async function flushTraces(timeoutMs = 5e3) {
|
|
1144
|
+
const deadline = Date.now() + Math.max(timeoutMs, 0);
|
|
1145
|
+
const requestsFlushed = await awaitPendingRequests(timeoutMs);
|
|
1146
|
+
const transportsFlushed = await flushTraceTransports(
|
|
1147
|
+
Math.max(0, deadline - Date.now())
|
|
1148
|
+
);
|
|
1149
|
+
return requestsFlushed && transportsFlushed;
|
|
1150
|
+
}
|
|
1151
|
+
async function awaitPendingRequests(timeoutMs = DEFAULT_LIFECYCLE_TIMEOUT_MS2) {
|
|
1152
|
+
await replayContextReady.catch(() => {
|
|
1153
|
+
});
|
|
1154
|
+
return waitForPromises(Array.from(pendingTracePromises), timeoutMs);
|
|
1155
|
+
}
|
|
1156
|
+
async function waitForPromises(promises, timeoutMs) {
|
|
1157
|
+
if (promises.length === 0) {
|
|
1158
|
+
return true;
|
|
1159
|
+
}
|
|
1160
|
+
let timer;
|
|
1161
|
+
try {
|
|
1162
|
+
return await Promise.race([
|
|
1163
|
+
Promise.allSettled(promises).then(() => true),
|
|
1164
|
+
new Promise((resolve) => {
|
|
1165
|
+
timer = setTimeout(() => resolve(false), timeoutMs);
|
|
1166
|
+
unrefTimer(timer);
|
|
1167
|
+
})
|
|
1168
|
+
]);
|
|
1169
|
+
} finally {
|
|
1170
|
+
if (timer) {
|
|
1171
|
+
clearTimeout(timer);
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
if (typeof process !== "undefined" && process.versions != null && process.versions.node != null) {
|
|
1176
|
+
let isFlushing = false;
|
|
1177
|
+
process.on("beforeExit", () => {
|
|
1178
|
+
if (isFlushing) {
|
|
1179
|
+
return;
|
|
1180
|
+
}
|
|
1181
|
+
isFlushing = true;
|
|
1182
|
+
void Promise.allSettled([
|
|
1183
|
+
...Array.from(pendingTracePromises).map((p) => p.catch(() => {
|
|
1184
|
+
})),
|
|
1185
|
+
shutdownTraceTransports(EXIT_FLUSH_TIMEOUT_MS).catch(() => false)
|
|
1186
|
+
]).then(() => {
|
|
1187
|
+
isFlushing = false;
|
|
1188
|
+
});
|
|
1189
|
+
});
|
|
1190
|
+
}
|
|
1191
|
+
var HttpClient = class {
|
|
1192
|
+
constructor(config) {
|
|
1193
|
+
// Deferred span work owned by THIS client. The module-global set backs the
|
|
1194
|
+
// process-wide `flushTraces()` and the exit hook, but per-client lifecycle
|
|
1195
|
+
// must not wait on another client's slow finalize: a false `close()` failure
|
|
1196
|
+
// caused by unrelated work is worse than no signal at all.
|
|
1197
|
+
this.deferredWork = /* @__PURE__ */ new Set();
|
|
1198
|
+
this.closed = false;
|
|
1199
|
+
this.apiKey = config.apiKey;
|
|
1200
|
+
this.serviceUrl = config.serviceUrl;
|
|
1201
|
+
this.timeout = config.timeout ?? 12e4;
|
|
1202
|
+
}
|
|
1203
|
+
/**
|
|
1204
|
+
* Resolve the API key at the moment it is needed (request time), invoking
|
|
1205
|
+
* the function form if one was supplied. Never read at construction.
|
|
1206
|
+
*/
|
|
1207
|
+
resolveApiKey() {
|
|
1208
|
+
return typeof this.apiKey === "function" ? this.apiKey() : this.apiKey;
|
|
1209
|
+
}
|
|
1210
|
+
/**
|
|
1211
|
+
* This client's span transport, built on first use.
|
|
1212
|
+
*
|
|
1213
|
+
* Lazy on purpose: a client that never sends a span must never start a batch
|
|
1214
|
+
* worker. Every framework integration created from a `Bitfab` client shares
|
|
1215
|
+
* the owning client's `HttpClient`, so handlers reuse this one worker instead
|
|
1216
|
+
* of each spinning up their own.
|
|
1217
|
+
*/
|
|
1218
|
+
getTraceTransport() {
|
|
1219
|
+
if (this.closed) {
|
|
1220
|
+
warnOnce(
|
|
1221
|
+
"http-client-closed",
|
|
1222
|
+
"the Bitfab client is closed; dropping spans"
|
|
1223
|
+
);
|
|
1224
|
+
return void 0;
|
|
1225
|
+
}
|
|
1226
|
+
if (!this.traceTransport) {
|
|
1227
|
+
this.traceTransport = createTraceTransport({
|
|
1228
|
+
apiKey: () => this.resolveApiKey(),
|
|
1229
|
+
directSender: (endpoint, payload, timeoutMs) => this.request(endpoint, payload, {
|
|
1230
|
+
timeout: timeoutMs
|
|
1231
|
+
})
|
|
1232
|
+
});
|
|
1233
|
+
}
|
|
1234
|
+
return this.traceTransport;
|
|
1235
|
+
}
|
|
1236
|
+
/**
|
|
1237
|
+
* Track deferred span work so this client's own lifecycle waits for it, and
|
|
1238
|
+
* so the process-wide flush and exit hook do too.
|
|
1239
|
+
*/
|
|
1240
|
+
trackDeferred(promise) {
|
|
1241
|
+
this.deferredWork.add(promise);
|
|
1242
|
+
void promise.finally(() => this.deferredWork.delete(promise)).catch(() => {
|
|
1243
|
+
});
|
|
1244
|
+
return awaitOnExit(promise);
|
|
1245
|
+
}
|
|
1246
|
+
/**
|
|
1247
|
+
* Settle only THIS client's deferred span work. Scoped deliberately: the
|
|
1248
|
+
* global set can contain another client's long-running finalize, and
|
|
1249
|
+
* attributing its timeout here would fail a client whose own work succeeded.
|
|
1250
|
+
*/
|
|
1251
|
+
async settleDeferredWork(timeoutMs = DEFAULT_LIFECYCLE_TIMEOUT_MS2) {
|
|
1252
|
+
await replayContextReady.catch(() => {
|
|
1253
|
+
});
|
|
1254
|
+
return waitForPromises(Array.from(this.deferredWork), timeoutMs);
|
|
1255
|
+
}
|
|
1256
|
+
/**
|
|
1257
|
+
* Wait for spans queued by this client to be delivered, within one deadline.
|
|
1258
|
+
* Returns false on delivery failure or timeout.
|
|
1259
|
+
*/
|
|
1260
|
+
async waitForPendingRequests(timeoutMs = DEFAULT_LIFECYCLE_TIMEOUT_MS2) {
|
|
1261
|
+
const deadline = Date.now() + Math.max(timeoutMs, 0);
|
|
1262
|
+
const settled = await this.settleDeferredWork(timeoutMs);
|
|
1263
|
+
const flushed = await this.traceTransport?.flush(Math.max(0, deadline - Date.now())) ?? true;
|
|
1264
|
+
return settled && flushed;
|
|
1265
|
+
}
|
|
1266
|
+
/**
|
|
1267
|
+
* Flush and permanently close this client's tracing transport. Idempotent:
|
|
1268
|
+
* a second call joins the first rather than tearing down a pipeline the
|
|
1269
|
+
* first call already owns.
|
|
1270
|
+
*/
|
|
1271
|
+
close(timeoutMs = DEFAULT_LIFECYCLE_TIMEOUT_MS2) {
|
|
1272
|
+
if (this.closing) {
|
|
1273
|
+
return this.closing;
|
|
1274
|
+
}
|
|
1275
|
+
const deadline = Date.now() + Math.max(timeoutMs, 0);
|
|
1276
|
+
this.closing = (async () => {
|
|
1277
|
+
const settled = await this.settleDeferredWork(
|
|
1278
|
+
Math.max(0, deadline - Date.now())
|
|
1279
|
+
);
|
|
1280
|
+
this.closed = true;
|
|
1281
|
+
const transport = this.traceTransport;
|
|
1282
|
+
this.traceTransport = void 0;
|
|
1283
|
+
const shutdownOk = await transport?.shutdown(Math.max(0, deadline - Date.now())) ?? true;
|
|
1284
|
+
return settled && shutdownOk;
|
|
1285
|
+
})();
|
|
1286
|
+
return this.closing;
|
|
1287
|
+
}
|
|
1288
|
+
/**
|
|
1289
|
+
* Make an HTTP request to the Bitfab API. Defaults to POST; pass
|
|
1290
|
+
* `options.method` to use a different verb (e.g. "PATCH").
|
|
1291
|
+
*
|
|
1292
|
+
* @param endpoint - The API endpoint (without base URL)
|
|
1293
|
+
* @param payload - The request body
|
|
1294
|
+
* @param options - Optional request options
|
|
1295
|
+
* @returns The parsed JSON response
|
|
1296
|
+
* @throws {BitfabError} If the request fails
|
|
1297
|
+
*/
|
|
1298
|
+
async request(endpoint, payload, options) {
|
|
1299
|
+
const url = `${this.serviceUrl}${endpoint}`;
|
|
1300
|
+
const timeout = options?.timeout ?? this.timeout;
|
|
1301
|
+
const method = options?.method ?? "POST";
|
|
1302
|
+
const controller = new AbortController();
|
|
1303
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
1304
|
+
const { body, dropped } = serializePayloadBody(payload);
|
|
1305
|
+
if (dropped.length > 0) {
|
|
1306
|
+
try {
|
|
1307
|
+
console.warn(
|
|
1308
|
+
`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.`
|
|
1309
|
+
);
|
|
1310
|
+
} catch {
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
try {
|
|
1314
|
+
const response = await fetch(url, {
|
|
1315
|
+
method,
|
|
1316
|
+
headers: {
|
|
1317
|
+
"Content-Type": "application/json",
|
|
1318
|
+
Authorization: `Bearer ${this.resolveApiKey() ?? ""}`
|
|
1319
|
+
},
|
|
1320
|
+
body,
|
|
1321
|
+
signal: controller.signal
|
|
1322
|
+
});
|
|
1323
|
+
if (!response.ok) {
|
|
1324
|
+
const errorText = await response.text();
|
|
1325
|
+
throw new BitfabError(
|
|
1326
|
+
`HTTP ${response.status}: ${errorText.slice(0, 500)}`,
|
|
1327
|
+
void 0,
|
|
1328
|
+
response.status
|
|
1329
|
+
);
|
|
1330
|
+
}
|
|
1331
|
+
const result = await response.json();
|
|
1332
|
+
if (result.error) {
|
|
1333
|
+
if (result.url) {
|
|
1334
|
+
throw new BitfabError(
|
|
1335
|
+
`${result.error} Configure it at: ${this.serviceUrl}${result.url}`,
|
|
1336
|
+
result.url
|
|
1337
|
+
);
|
|
1338
|
+
}
|
|
1339
|
+
throw new BitfabError(result.error);
|
|
1340
|
+
}
|
|
1341
|
+
return result;
|
|
1342
|
+
} catch (error) {
|
|
1343
|
+
if (error instanceof BitfabError) {
|
|
1344
|
+
throw error;
|
|
1345
|
+
}
|
|
1346
|
+
if (error instanceof Error) {
|
|
1347
|
+
if (error.name === "AbortError") {
|
|
1348
|
+
throw new BitfabError(`Request timed out after ${timeout}ms`);
|
|
1349
|
+
}
|
|
1350
|
+
throw new BitfabError(error.message);
|
|
1351
|
+
}
|
|
1352
|
+
throw new BitfabError("Unknown error occurred");
|
|
1353
|
+
} finally {
|
|
1354
|
+
clearTimeout(timeoutId);
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
/**
|
|
1358
|
+
* Look up a function by name.
|
|
1359
|
+
* Blocks until complete - needed for function execution.
|
|
1360
|
+
*/
|
|
1361
|
+
async lookupFunction(name) {
|
|
1362
|
+
return this.request("/api/sdk/functions/lookup", { name });
|
|
1363
|
+
}
|
|
1364
|
+
async getTraceSpan(traceId, lookup) {
|
|
1365
|
+
const searchParams = new URLSearchParams();
|
|
1366
|
+
if (lookup.id !== void 0) {
|
|
1367
|
+
searchParams.set("id", lookup.id);
|
|
1368
|
+
} else {
|
|
1369
|
+
searchParams.set("name", lookup.name);
|
|
1370
|
+
searchParams.set("occurrence", String(lookup.occurrence ?? "last"));
|
|
1371
|
+
}
|
|
1372
|
+
const endpoint = `/api/sdk/traces/${encodeURIComponent(traceId)}/span?${searchParams.toString()}`;
|
|
1373
|
+
const response = await this.get(endpoint);
|
|
1374
|
+
return response.span;
|
|
1375
|
+
}
|
|
1376
|
+
async get(endpoint) {
|
|
1377
|
+
const url = `${this.serviceUrl}${endpoint}`;
|
|
1378
|
+
const controller = new AbortController();
|
|
1379
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
1380
|
+
try {
|
|
1381
|
+
const response = await fetch(url, {
|
|
1382
|
+
method: "GET",
|
|
1383
|
+
headers: { Authorization: `Bearer ${this.resolveApiKey() ?? ""}` },
|
|
1384
|
+
signal: controller.signal
|
|
1385
|
+
});
|
|
1386
|
+
if (!response.ok) {
|
|
1387
|
+
const errorText = await response.text();
|
|
1388
|
+
throw new BitfabError(
|
|
1389
|
+
`HTTP ${response.status}: ${errorText.slice(0, 500)}`
|
|
1390
|
+
);
|
|
1391
|
+
}
|
|
1392
|
+
return await response.json();
|
|
1393
|
+
} catch (error) {
|
|
1394
|
+
if (error instanceof BitfabError) {
|
|
1395
|
+
throw error;
|
|
1396
|
+
}
|
|
1397
|
+
if (error instanceof Error) {
|
|
1398
|
+
if (error.name === "AbortError") {
|
|
1399
|
+
throw new BitfabError(`Request timed out after ${this.timeout}ms`);
|
|
1400
|
+
}
|
|
1401
|
+
throw new BitfabError(error.message);
|
|
1402
|
+
}
|
|
1403
|
+
throw new BitfabError("Unknown error occurred");
|
|
1404
|
+
} finally {
|
|
1405
|
+
clearTimeout(timeoutId);
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
/**
|
|
1409
|
+
* Queue an internal trace (from local BAML execution via `call()`) onto this
|
|
1410
|
+
* client's batching transport. `functionId` moves into the payload because
|
|
1411
|
+
* the OTLP carrier has no path to carry it.
|
|
1412
|
+
*/
|
|
1413
|
+
sendInternalTrace(functionId, payload) {
|
|
1414
|
+
this.getTraceTransport()?.submit("internal_trace", {
|
|
1415
|
+
...payload,
|
|
1416
|
+
functionId,
|
|
1417
|
+
sdkVersion: __version__
|
|
1418
|
+
});
|
|
1419
|
+
}
|
|
1420
|
+
/**
|
|
1421
|
+
* Queue an external span (from withSpan wrapper or OpenAI tracing) onto this
|
|
1422
|
+
* client's batching transport. Fire-and-forget: the transport owns delivery,
|
|
1423
|
+
* so callers await `flushTraces()` or `close()` rather than a per-span
|
|
1424
|
+
* promise.
|
|
1425
|
+
*/
|
|
1426
|
+
sendExternalSpan(payload) {
|
|
1427
|
+
this.getTraceTransport()?.submit("external_span", {
|
|
1428
|
+
...payload,
|
|
1429
|
+
sdkVersion: __version__
|
|
1430
|
+
});
|
|
1431
|
+
}
|
|
1432
|
+
/**
|
|
1433
|
+
* Queue an external trace completion (from OpenAI tracing) onto this
|
|
1434
|
+
* client's batching transport. Fire-and-forget for the same reason as
|
|
1435
|
+
* {@link HttpClient.sendExternalSpan}; replay confirms persistence with the
|
|
1436
|
+
* server-authoritative barrier in `replay.ts`, not by awaiting this call.
|
|
1437
|
+
*/
|
|
1438
|
+
sendExternalTrace(payload) {
|
|
1439
|
+
this.getTraceTransport()?.submit("external_trace", {
|
|
1440
|
+
...payload,
|
|
1441
|
+
sdkVersion: __version__
|
|
1442
|
+
});
|
|
1443
|
+
}
|
|
1444
|
+
/**
|
|
1445
|
+
* Partial update of an existing trace identified by its Bitfab trace ID.
|
|
1446
|
+
* Used by the detached `client.getTrace(id)` handle.
|
|
1447
|
+
*
|
|
1448
|
+
* Blocking, like the other trace-API calls: it resolves once the server has
|
|
1449
|
+
* applied the change and rejects if the server refused it. A patch targets a
|
|
1450
|
+
* trace that is already closed, so there is no batch for it to ride along
|
|
1451
|
+
* with and no later signal that would reveal a silent failure.
|
|
1452
|
+
*/
|
|
1453
|
+
async patchTrace(traceId, payload) {
|
|
1454
|
+
const endpoint = `/api/sdk/traces/${encodeURIComponent(traceId)}`;
|
|
1455
|
+
await this.request(endpoint, payload, { method: "PATCH" });
|
|
1456
|
+
}
|
|
1457
|
+
/**
|
|
1458
|
+
* Start a replay session by fetching historical traces.
|
|
1459
|
+
* Blocking call - creates a test run and returns lightweight item references.
|
|
1460
|
+
*/
|
|
1461
|
+
async startReplay(traceFunctionKey, limit, traceIds, name, codeChangeDescription, codeChangeFiles, includeDbBranchLease, experimentGroupId, datasetId, graderIds, dbBranchSettings) {
|
|
1462
|
+
const payload = { traceFunctionKey };
|
|
1463
|
+
if (limit !== void 0) {
|
|
1464
|
+
payload.limit = limit;
|
|
1465
|
+
}
|
|
1466
|
+
if (traceIds) {
|
|
1467
|
+
payload.traceIds = traceIds;
|
|
1468
|
+
}
|
|
1469
|
+
if (name !== void 0) {
|
|
1470
|
+
payload.name = name;
|
|
1471
|
+
}
|
|
1472
|
+
if (codeChangeDescription !== void 0) {
|
|
1473
|
+
payload.codeChangeDescription = codeChangeDescription;
|
|
1474
|
+
}
|
|
1475
|
+
if (codeChangeFiles !== void 0) {
|
|
1476
|
+
payload.codeChangeFiles = codeChangeFiles;
|
|
1477
|
+
}
|
|
1478
|
+
if (includeDbBranchLease) {
|
|
1479
|
+
payload.includeDbBranchLease = true;
|
|
1480
|
+
payload.lazyDbBranchLease = true;
|
|
1481
|
+
}
|
|
1482
|
+
if (experimentGroupId !== void 0) {
|
|
1483
|
+
payload.experimentGroupId = experimentGroupId;
|
|
1484
|
+
}
|
|
1485
|
+
if (datasetId !== void 0) {
|
|
1486
|
+
payload.datasetId = datasetId;
|
|
1487
|
+
}
|
|
1488
|
+
if (graderIds !== void 0) {
|
|
1489
|
+
payload.graderIds = graderIds;
|
|
1490
|
+
}
|
|
1491
|
+
if (dbBranchSettings !== void 0) {
|
|
1492
|
+
payload.dbBranchSettings = dbBranchSettings;
|
|
1493
|
+
}
|
|
1494
|
+
const timeout = includeDbBranchLease ? REPLAY_DB_BRANCH_REQUEST_TIMEOUT_MS : 3e4;
|
|
1495
|
+
return this.request("/api/sdk/replay/start", payload, {
|
|
1496
|
+
timeout
|
|
1497
|
+
});
|
|
1498
|
+
}
|
|
1499
|
+
/**
|
|
1500
|
+
* Fetch an external span by ID.
|
|
1501
|
+
* Blocking GET request.
|
|
1502
|
+
*/
|
|
1503
|
+
async getExternalSpan(spanId) {
|
|
1504
|
+
const url = `${this.serviceUrl}/api/sdk/externalSpans/${spanId}`;
|
|
1505
|
+
const controller = new AbortController();
|
|
1506
|
+
const timeoutId = setTimeout(() => controller.abort(), 3e4);
|
|
1507
|
+
try {
|
|
1508
|
+
const response = await fetch(url, {
|
|
1509
|
+
method: "GET",
|
|
1510
|
+
headers: { Authorization: `Bearer ${this.resolveApiKey() ?? ""}` },
|
|
1511
|
+
signal: controller.signal
|
|
1512
|
+
});
|
|
1513
|
+
if (!response.ok) {
|
|
1514
|
+
const errorText = await response.text();
|
|
1515
|
+
throw new BitfabError(
|
|
1516
|
+
`HTTP ${response.status}: ${errorText.slice(0, 500)}`
|
|
1517
|
+
);
|
|
1518
|
+
}
|
|
1519
|
+
return await response.json();
|
|
1520
|
+
} catch (error) {
|
|
1521
|
+
if (error instanceof BitfabError) {
|
|
1522
|
+
throw error;
|
|
1523
|
+
}
|
|
1524
|
+
if (error instanceof Error) {
|
|
1525
|
+
if (error.name === "AbortError") {
|
|
1526
|
+
throw new BitfabError("Request timed out after 30000ms");
|
|
1527
|
+
}
|
|
1528
|
+
throw new BitfabError(error.message);
|
|
1529
|
+
}
|
|
1530
|
+
throw new BitfabError("Unknown error occurred");
|
|
1531
|
+
} finally {
|
|
1532
|
+
clearTimeout(timeoutId);
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
/**
|
|
1536
|
+
* Fetch the span tree for a root span.
|
|
1537
|
+
* Blocking GET request.
|
|
1538
|
+
*
|
|
1539
|
+
* Pass `includeOutputs: false` for a payload-free tree (structure +
|
|
1540
|
+
* `externalSpanId` only), so recorded outputs are fetched lazily per mocked
|
|
1541
|
+
* span instead of all up front. Omit it (default eager) for `mock: "all"`.
|
|
1542
|
+
*/
|
|
1543
|
+
async getSpanTree(externalSpanId, options) {
|
|
1544
|
+
const query = options?.includeOutputs === false ? "?includeOutputs=false" : "";
|
|
1545
|
+
const url = `${this.serviceUrl}/api/sdk/replay/spanTree/${externalSpanId}${query}`;
|
|
1546
|
+
const controller = new AbortController();
|
|
1547
|
+
const timeoutId = setTimeout(() => controller.abort(), 3e4);
|
|
1548
|
+
try {
|
|
1549
|
+
const response = await fetch(url, {
|
|
1550
|
+
method: "GET",
|
|
1551
|
+
headers: { Authorization: `Bearer ${this.resolveApiKey() ?? ""}` },
|
|
1552
|
+
signal: controller.signal
|
|
1553
|
+
});
|
|
1554
|
+
if (!response.ok) {
|
|
1555
|
+
const errorText = await response.text();
|
|
1556
|
+
throw new BitfabError(
|
|
1557
|
+
`HTTP ${response.status}: ${errorText.slice(0, 500)}`
|
|
1558
|
+
);
|
|
1559
|
+
}
|
|
1560
|
+
return await response.json();
|
|
1561
|
+
} catch (error) {
|
|
1562
|
+
if (error instanceof BitfabError) {
|
|
1563
|
+
throw error;
|
|
1564
|
+
}
|
|
1565
|
+
if (error instanceof Error) {
|
|
1566
|
+
if (error.name === "AbortError") {
|
|
1567
|
+
throw new BitfabError("Request timed out after 30000ms");
|
|
1568
|
+
}
|
|
1569
|
+
throw new BitfabError(error.message);
|
|
1570
|
+
}
|
|
1571
|
+
throw new BitfabError("Unknown error occurred");
|
|
1572
|
+
} finally {
|
|
1573
|
+
clearTimeout(timeoutId);
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
/**
|
|
1577
|
+
* Read which of a replay run's traces the server has fully persisted.
|
|
1578
|
+
*
|
|
1579
|
+
* With `expectedSpanCounts`, a trace appears in the response only once it
|
|
1580
|
+
* has a final status AND at least that many persisted spans, which is what
|
|
1581
|
+
* makes this a real barrier rather than a "the row exists" check.
|
|
1582
|
+
*/
|
|
1583
|
+
async getReplayStatus(testRunId, expectedSpanCounts) {
|
|
1584
|
+
return this.request(
|
|
1585
|
+
"/api/sdk/replay/status",
|
|
1586
|
+
{ testRunId, expectedSpanCounts },
|
|
1587
|
+
{ timeout: 3e4 }
|
|
1588
|
+
);
|
|
1589
|
+
}
|
|
1590
|
+
/**
|
|
1591
|
+
* Mark a replay test run as completed.
|
|
1592
|
+
* Blocking call.
|
|
1593
|
+
*/
|
|
1594
|
+
async completeReplay(testRunId) {
|
|
1595
|
+
return this.request(
|
|
1596
|
+
"/api/sdk/replay/complete",
|
|
1597
|
+
{ testRunId },
|
|
1598
|
+
{ timeout: 3e4 }
|
|
1599
|
+
);
|
|
1600
|
+
}
|
|
1601
|
+
/**
|
|
1602
|
+
* Ask the server to materialize a per-trace DB branch lease from a
|
|
1603
|
+
* captured `dbSnapshotRef`. Blocking - the resolver creates a Neon
|
|
1604
|
+
* snapshot + preview branch and polls operations to readiness, which
|
|
1605
|
+
* can take seconds.
|
|
1606
|
+
*/
|
|
1607
|
+
async resolveDbBranchLease(testRunId, traceId, dbBranchSettings) {
|
|
1608
|
+
return this.request(
|
|
1609
|
+
"/api/sdk/replay/resolveDbBranchLease",
|
|
1610
|
+
{ testRunId, traceId, dbBranchSettings },
|
|
1611
|
+
{ timeout: REPLAY_DB_BRANCH_REQUEST_TIMEOUT_MS }
|
|
1612
|
+
);
|
|
1613
|
+
}
|
|
1614
|
+
/** Release a previously-resolved DB branch by deleting its Neon branch. Idempotent server-side. */
|
|
1615
|
+
async releaseDbBranchLease(neonBranchId) {
|
|
1616
|
+
await this.request(
|
|
1617
|
+
"/api/sdk/replay/releaseDbBranchLease",
|
|
1618
|
+
{ neonBranchId },
|
|
1619
|
+
{ timeout: 3e4 }
|
|
1620
|
+
);
|
|
1621
|
+
}
|
|
1622
|
+
};
|
|
1623
|
+
|
|
1624
|
+
// src/mockOverride.ts
|
|
1625
|
+
function resolveMockValue(value, ctx) {
|
|
1626
|
+
return typeof value === "function" ? value(ctx) : value;
|
|
1627
|
+
}
|
|
1628
|
+
function normalizeMockOverrides(mockOverride) {
|
|
1629
|
+
if (mockOverride === void 0) {
|
|
1630
|
+
return [];
|
|
1631
|
+
}
|
|
1632
|
+
return Array.isArray(mockOverride) ? mockOverride : [mockOverride];
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
// src/randomUuid.ts
|
|
1636
|
+
function randomUuid() {
|
|
1637
|
+
const globalCrypto = globalThis.crypto;
|
|
1638
|
+
if (typeof globalCrypto?.randomUUID === "function") {
|
|
1639
|
+
try {
|
|
1640
|
+
return globalCrypto.randomUUID();
|
|
1641
|
+
} catch {
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
warnOnce(
|
|
1645
|
+
"crypto-unavailable",
|
|
1646
|
+
"global crypto.randomUUID is unavailable; using a non-cryptographic fallback for trace/span ids. Tracing works normally (ids are correlation-only, not security-sensitive)."
|
|
1647
|
+
);
|
|
1648
|
+
return fallbackUuidV4();
|
|
1649
|
+
}
|
|
1650
|
+
function fallbackUuidV4() {
|
|
1651
|
+
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (char) => {
|
|
1652
|
+
const rand = Math.random() * 16 | 0;
|
|
1653
|
+
const value = char === "x" ? rand : rand & 3 | 8;
|
|
1654
|
+
return value.toString(16);
|
|
1655
|
+
});
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
// src/serialize.ts
|
|
1659
|
+
import superjson from "superjson";
|
|
1660
|
+
var MAX_SERIALIZED_BYTES = 512e3;
|
|
1661
|
+
var MAX_FRAMEWORK_SERIALIZED_BYTES = 2e6;
|
|
1662
|
+
function describeValue(value) {
|
|
1663
|
+
try {
|
|
1664
|
+
const ctorName = value?.constructor?.name;
|
|
1665
|
+
if (ctorName && ctorName !== "Object") {
|
|
1666
|
+
return ctorName;
|
|
1667
|
+
}
|
|
1668
|
+
} catch {
|
|
1669
|
+
}
|
|
1670
|
+
return typeof value;
|
|
1671
|
+
}
|
|
1672
|
+
function unserializableStub(value, reason) {
|
|
1673
|
+
warnOnce(
|
|
1674
|
+
`serialize:${reason.replace(/\d+/g, "N")}`,
|
|
1675
|
+
`a value could not be fully serialized for a span (${reason}); it was replaced with a placeholder. The span still ships, but its captured input/output is incomplete.`
|
|
1676
|
+
);
|
|
1677
|
+
let summary;
|
|
1678
|
+
try {
|
|
1679
|
+
summary = `<unserializable: ${describeValue(value)} (${reason})>`;
|
|
1680
|
+
} catch {
|
|
1681
|
+
summary = `<unserializable (${reason})>`;
|
|
1682
|
+
}
|
|
1683
|
+
return { json: summary };
|
|
1684
|
+
}
|
|
1685
|
+
function serializeValue(value) {
|
|
1686
|
+
try {
|
|
1687
|
+
const { json, meta } = superjson.serialize(value);
|
|
1688
|
+
let size;
|
|
1689
|
+
try {
|
|
1690
|
+
size = JSON.stringify(json).length;
|
|
1691
|
+
} catch {
|
|
1692
|
+
return unserializableStub(value, "stringify_failed_after_superjson");
|
|
1693
|
+
}
|
|
1694
|
+
if (size > MAX_SERIALIZED_BYTES) {
|
|
1695
|
+
return unserializableStub(value, `too_large_${size}_bytes`);
|
|
1696
|
+
}
|
|
1697
|
+
return meta ? { json, meta } : { json };
|
|
1698
|
+
} catch {
|
|
1699
|
+
try {
|
|
1700
|
+
return { json: JSON.parse(JSON.stringify(value)) };
|
|
1701
|
+
} catch {
|
|
1702
|
+
return unserializableStub(value, "json_stringify_failed");
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
function deserializeValue(serialized) {
|
|
1707
|
+
if (serialized.meta === void 0) {
|
|
1708
|
+
return serialized.json;
|
|
1709
|
+
}
|
|
1710
|
+
return superjson.deserialize({
|
|
1711
|
+
json: serialized.json,
|
|
1712
|
+
meta: serialized.meta
|
|
1713
|
+
});
|
|
1714
|
+
}
|
|
1715
|
+
var MAX_SAFE_DEPTH = 6;
|
|
1716
|
+
function toJsonSafe(value) {
|
|
1717
|
+
return toJsonSafeReport(value).safe;
|
|
1718
|
+
}
|
|
1719
|
+
function toJsonSafeReport(value) {
|
|
1720
|
+
const dropped = [];
|
|
1721
|
+
const safe = toJsonSafeInner(value, 0, /* @__PURE__ */ new WeakSet(), dropped);
|
|
1722
|
+
try {
|
|
1723
|
+
const size = JSON.stringify(safe)?.length ?? 0;
|
|
1724
|
+
if (size > MAX_FRAMEWORK_SERIALIZED_BYTES) {
|
|
1725
|
+
warnOnce(
|
|
1726
|
+
"toJsonSafe:too_large",
|
|
1727
|
+
`a framework payload exceeded ${MAX_FRAMEWORK_SERIALIZED_BYTES} bytes and was replaced with a placeholder so the span still ships. The captured state for this span is incomplete.`
|
|
1728
|
+
);
|
|
1729
|
+
return {
|
|
1730
|
+
safe: `<unserializable: too_large_${size}_bytes>`,
|
|
1731
|
+
dropped: [...dropped, `too_large_${size}_bytes`]
|
|
1732
|
+
};
|
|
1733
|
+
}
|
|
1734
|
+
} catch {
|
|
1735
|
+
}
|
|
1736
|
+
return { safe, dropped };
|
|
1737
|
+
}
|
|
1738
|
+
function toJsonSafeInner(value, depth, seen, dropped) {
|
|
1739
|
+
if (value === null || value === void 0) {
|
|
1740
|
+
return value;
|
|
1741
|
+
}
|
|
1742
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
1743
|
+
return value;
|
|
1744
|
+
}
|
|
1745
|
+
const className = value?.constructor?.name ?? typeof value;
|
|
1746
|
+
if (depth > MAX_SAFE_DEPTH) {
|
|
1747
|
+
dropped.push(className);
|
|
1748
|
+
return `<${className}>`;
|
|
1749
|
+
}
|
|
1750
|
+
if (typeof value !== "object") {
|
|
1751
|
+
if (typeof value === "function" || typeof value === "symbol") {
|
|
1752
|
+
dropped.push(className);
|
|
1753
|
+
}
|
|
1754
|
+
try {
|
|
1755
|
+
return String(value);
|
|
1756
|
+
} catch {
|
|
1757
|
+
dropped.push(className);
|
|
1758
|
+
return `<${className}>`;
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1761
|
+
if (seen.has(value)) {
|
|
1762
|
+
dropped.push(className);
|
|
1763
|
+
return `<cycle ${className}>`;
|
|
1764
|
+
}
|
|
1765
|
+
seen.add(value);
|
|
1766
|
+
let result;
|
|
1767
|
+
if (Array.isArray(value)) {
|
|
1768
|
+
result = value.map(
|
|
1769
|
+
(item) => toJsonSafeInner(item, depth + 1, seen, dropped)
|
|
1770
|
+
);
|
|
1771
|
+
} else if (typeof value.toJSON === "function") {
|
|
1772
|
+
try {
|
|
1773
|
+
result = toJsonSafeInner(
|
|
1774
|
+
value.toJSON(),
|
|
1775
|
+
depth + 1,
|
|
1776
|
+
seen,
|
|
1777
|
+
dropped
|
|
1778
|
+
);
|
|
1779
|
+
} catch {
|
|
1780
|
+
dropped.push(className);
|
|
1781
|
+
result = `<${className}>`;
|
|
1782
|
+
}
|
|
1783
|
+
} else {
|
|
1784
|
+
try {
|
|
1785
|
+
const obj = {};
|
|
1786
|
+
for (const [k, v] of Object.entries(value)) {
|
|
1787
|
+
if (!k.startsWith("_")) {
|
|
1788
|
+
obj[k] = toJsonSafeInner(v, depth + 1, seen, dropped);
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1791
|
+
result = obj;
|
|
1792
|
+
} catch {
|
|
1793
|
+
dropped.push(className);
|
|
1794
|
+
result = `<${className}>`;
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
seen.delete(value);
|
|
1798
|
+
return result;
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1801
|
+
// src/replay.ts
|
|
1802
|
+
var REPLAY_PERSISTENCE_TIMEOUT_MS = 3e4;
|
|
1803
|
+
function dbBranchEnabled(dbBranch) {
|
|
1804
|
+
return dbBranch !== void 0 && dbBranch !== false;
|
|
1805
|
+
}
|
|
1806
|
+
function resolveDbBranchSettings(dbBranch) {
|
|
1807
|
+
if (!dbBranch || dbBranch === true) {
|
|
1808
|
+
return void 0;
|
|
1809
|
+
}
|
|
1810
|
+
const { minCu, maxCu, warmupSql } = dbBranch;
|
|
1811
|
+
const settings = {
|
|
1812
|
+
...minCu === void 0 ? {} : { minCu },
|
|
1813
|
+
...maxCu === void 0 ? {} : { maxCu },
|
|
1814
|
+
...warmupSql === void 0 ? {} : { warmupSql }
|
|
1815
|
+
};
|
|
1816
|
+
return Object.keys(settings).length === 0 ? void 0 : settings;
|
|
1817
|
+
}
|
|
1818
|
+
var BITFAB_PROGRESS_PREFIX = "@@bitfab:progress ";
|
|
1819
|
+
function reportReplayProgress(progress) {
|
|
1820
|
+
const stderr = typeof process !== "undefined" ? process.stderr : void 0;
|
|
1821
|
+
if (!stderr) {
|
|
1822
|
+
return;
|
|
1823
|
+
}
|
|
1824
|
+
try {
|
|
1825
|
+
stderr.write(`${BITFAB_PROGRESS_PREFIX}${JSON.stringify(progress)}
|
|
1826
|
+
`);
|
|
1827
|
+
} catch {
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
function deserializeInputs(spanData) {
|
|
1831
|
+
const inputMeta = spanData.input_meta;
|
|
1832
|
+
const rawInput = spanData.input;
|
|
1833
|
+
if (inputMeta !== void 0 && inputMeta !== null) {
|
|
1834
|
+
const deserialized = deserializeValue({ json: rawInput, meta: inputMeta });
|
|
1835
|
+
if (Array.isArray(deserialized)) {
|
|
1836
|
+
return deserialized;
|
|
1837
|
+
}
|
|
1838
|
+
return deserialized !== void 0 && deserialized !== null ? [deserialized] : [];
|
|
1839
|
+
}
|
|
1840
|
+
if (Array.isArray(rawInput)) {
|
|
1841
|
+
return rawInput;
|
|
1842
|
+
}
|
|
1843
|
+
return rawInput !== void 0 && rawInput !== null ? [rawInput] : [];
|
|
1844
|
+
}
|
|
1845
|
+
function deserializeOutput(spanData) {
|
|
1846
|
+
const outputMeta = spanData.output_meta;
|
|
1847
|
+
const rawOutput = spanData.output;
|
|
1848
|
+
if (outputMeta !== void 0 && outputMeta !== null) {
|
|
1849
|
+
return deserializeValue({ json: rawOutput, meta: outputMeta });
|
|
1850
|
+
}
|
|
1851
|
+
return rawOutput;
|
|
1852
|
+
}
|
|
1853
|
+
function buildMockTree(rootNode) {
|
|
1854
|
+
const spans = /* @__PURE__ */ new Map();
|
|
1855
|
+
const counters = /* @__PURE__ */ new Map();
|
|
1856
|
+
function walk(node) {
|
|
1857
|
+
const key = node.traceFunctionKey;
|
|
1858
|
+
if (key) {
|
|
1859
|
+
const name = node.spanName || key;
|
|
1860
|
+
const counterKey = `${key}:${name}`;
|
|
1861
|
+
const index = counters.get(counterKey) ?? 0;
|
|
1862
|
+
counters.set(counterKey, index + 1);
|
|
1863
|
+
spans.set(`${counterKey}:${index}`, {
|
|
1864
|
+
sourceSpanId: node.sourceSpanId,
|
|
1865
|
+
externalSpanId: node.externalSpanId,
|
|
1866
|
+
output: node.output,
|
|
1867
|
+
outputMeta: node.outputMeta
|
|
1868
|
+
});
|
|
1869
|
+
}
|
|
1870
|
+
for (const child of node.children) {
|
|
1871
|
+
walk(child);
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1874
|
+
for (const child of rootNode.children) {
|
|
1875
|
+
walk(child);
|
|
1876
|
+
}
|
|
1877
|
+
return { spans };
|
|
1878
|
+
}
|
|
1879
|
+
async function processItem(httpClient, serverItem, fn, testRunId, mockStrategy, resolvedOverrides, replayedTraceId, includeDbBranchLease, dbBranchSettings, adaptInputs) {
|
|
1880
|
+
let lease = includeDbBranchLease ? serverItem.dbBranchLease : void 0;
|
|
1881
|
+
let leaseError = includeDbBranchLease ? serverItem.dbBranchLeaseError : void 0;
|
|
1882
|
+
let dbSnapshotRef = serverItem.dbSnapshotRef;
|
|
1883
|
+
let inputs = [];
|
|
1884
|
+
let originalOutput;
|
|
1885
|
+
let result;
|
|
1886
|
+
let error = null;
|
|
1887
|
+
const originalTraceId = serverItem.originalTraceId ?? serverItem.sourceTraceId;
|
|
1888
|
+
const originalSpanId = serverItem.originalSpanId ?? serverItem.sourceSpanId;
|
|
1889
|
+
try {
|
|
1890
|
+
if (includeDbBranchLease && !lease && !leaseError) {
|
|
1891
|
+
const resolved = await httpClient.resolveDbBranchLease(
|
|
1892
|
+
testRunId,
|
|
1893
|
+
originalTraceId,
|
|
1894
|
+
dbBranchSettings
|
|
1895
|
+
);
|
|
1896
|
+
lease = resolved.lease ?? void 0;
|
|
1897
|
+
leaseError = resolved.leaseError ?? void 0;
|
|
1898
|
+
dbSnapshotRef = resolved.dbSnapshotRef ?? dbSnapshotRef;
|
|
1899
|
+
}
|
|
1900
|
+
if (leaseError) {
|
|
1901
|
+
throw new BitfabError(
|
|
1902
|
+
`Replay requested a database branch for trace ${originalTraceId} but it could not be resolved (${leaseError.code}): ${leaseError.message}. The function was not run, because replaying it against the live database would produce a result that looks valid but did not use the historical data you asked for.`
|
|
1903
|
+
);
|
|
1904
|
+
}
|
|
1905
|
+
const span = await httpClient.getExternalSpan(originalSpanId);
|
|
1906
|
+
const spanData = span.rawData?.span_data ?? {};
|
|
1907
|
+
inputs = deserializeInputs(spanData);
|
|
1908
|
+
originalOutput = deserializeOutput(spanData);
|
|
1909
|
+
if (adaptInputs) {
|
|
1910
|
+
inputs = adaptInputs(inputs, {
|
|
1911
|
+
originalTraceId,
|
|
1912
|
+
originalSpanId,
|
|
1913
|
+
// Deprecated aliases for originalTraceId/originalSpanId.
|
|
1914
|
+
sourceTraceId: originalTraceId,
|
|
1915
|
+
sourceSpanId: originalSpanId
|
|
1916
|
+
});
|
|
1917
|
+
}
|
|
1918
|
+
const hasOverrides = resolvedOverrides.length > 0;
|
|
1919
|
+
const needTree = mockStrategy === "all" || mockStrategy === "marked" || hasOverrides;
|
|
1920
|
+
const includeOutputs = mockStrategy === "all";
|
|
1921
|
+
let mockTree;
|
|
1922
|
+
if (needTree) {
|
|
1923
|
+
try {
|
|
1924
|
+
const treeResponse = await httpClient.getSpanTree(originalSpanId, {
|
|
1925
|
+
includeOutputs
|
|
1926
|
+
});
|
|
1927
|
+
if (treeResponse.root) {
|
|
1928
|
+
mockTree = buildMockTree(treeResponse.root);
|
|
1929
|
+
} else if (mockStrategy === "all" || hasOverrides) {
|
|
1930
|
+
throw new BitfabError(
|
|
1931
|
+
`Replay mock strategy "${mockStrategy}"${hasOverrides ? " with overrides" : ""} requires a span tree root for original span ${originalSpanId}.`
|
|
1932
|
+
);
|
|
1933
|
+
} else {
|
|
1934
|
+
mockTree = void 0;
|
|
1935
|
+
}
|
|
1936
|
+
} catch (e) {
|
|
1937
|
+
if (mockStrategy === "all" || hasOverrides) {
|
|
1938
|
+
throw e;
|
|
1939
|
+
}
|
|
1940
|
+
mockTree = void 0;
|
|
1941
|
+
}
|
|
1942
|
+
}
|
|
1943
|
+
const outputCache = /* @__PURE__ */ new Map();
|
|
1944
|
+
const fetchSpanOutput = mockTree && !includeOutputs ? (externalSpanId) => {
|
|
1945
|
+
let pending = outputCache.get(externalSpanId);
|
|
1946
|
+
if (!pending) {
|
|
1947
|
+
pending = httpClient.getExternalSpan(externalSpanId).then(
|
|
1948
|
+
(s) => deserializeOutput(
|
|
1949
|
+
s.rawData?.span_data ?? {}
|
|
1950
|
+
)
|
|
1951
|
+
);
|
|
1952
|
+
outputCache.set(externalSpanId, pending);
|
|
1953
|
+
}
|
|
1954
|
+
return pending;
|
|
1955
|
+
} : void 0;
|
|
1956
|
+
const maybePromise = runWithReplayContext(
|
|
1957
|
+
{
|
|
1958
|
+
testRunId,
|
|
1959
|
+
traceId: replayedTraceId,
|
|
1960
|
+
inputSourceSpanId: span.id,
|
|
1961
|
+
inputSourceTraceId: span.externalTraceId,
|
|
1962
|
+
sourceBitfabTraceId: originalTraceId,
|
|
1963
|
+
mockTree,
|
|
1964
|
+
callCounters: mockTree ? /* @__PURE__ */ new Map() : void 0,
|
|
1965
|
+
mockStrategy,
|
|
1966
|
+
mockOverrides: hasOverrides ? resolvedOverrides : void 0,
|
|
1967
|
+
fetchSpanOutput,
|
|
1968
|
+
dbBranchLease: lease
|
|
1969
|
+
},
|
|
1970
|
+
() => fn(...inputs)
|
|
1971
|
+
);
|
|
1972
|
+
result = maybePromise instanceof Promise ? await maybePromise : maybePromise;
|
|
1973
|
+
} catch (e) {
|
|
1974
|
+
error = e instanceof Error ? e.message : String(e);
|
|
1975
|
+
} finally {
|
|
1976
|
+
if (lease) {
|
|
1977
|
+
try {
|
|
1978
|
+
await httpClient.releaseDbBranchLease(lease.neonBranchId);
|
|
1979
|
+
} catch (e) {
|
|
1980
|
+
try {
|
|
1981
|
+
console.warn(
|
|
1982
|
+
`Bitfab: failed to release DB branch ${lease.neonBranchId} (TTL janitor will catch it): ${e instanceof Error ? e.message : String(e)}`
|
|
1983
|
+
);
|
|
1984
|
+
} catch {
|
|
1985
|
+
}
|
|
1986
|
+
}
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
return {
|
|
1990
|
+
// Written in by replay() from the complete-replay response once the server
|
|
1991
|
+
// has minted this replay trace's row. Null until then: the client-side
|
|
1992
|
+
// correlation id (replayedTraceId) is never surfaced as the item's traceId.
|
|
1993
|
+
traceId: null,
|
|
1994
|
+
originalTraceId,
|
|
1995
|
+
originalSpanId,
|
|
1996
|
+
// Deprecated aliases for originalTraceId/originalSpanId.
|
|
1997
|
+
sourceTraceId: originalTraceId,
|
|
1998
|
+
sourceSpanId: originalSpanId,
|
|
1999
|
+
input: inputs,
|
|
2000
|
+
result,
|
|
2001
|
+
originalOutput,
|
|
2002
|
+
error,
|
|
2003
|
+
durationMs: serverItem.durationMs ?? null,
|
|
2004
|
+
// Filled in by replay() from the complete-replay response once the
|
|
2005
|
+
// replay traces are persisted and their spans aggregated server-side.
|
|
2006
|
+
// Null here (and on older servers) means "replay tokens not known".
|
|
2007
|
+
tokens: null,
|
|
2008
|
+
model: serverItem.model ?? null,
|
|
2009
|
+
dbSnapshotRef: dbSnapshotRef ?? null
|
|
2010
|
+
};
|
|
2011
|
+
}
|
|
2012
|
+
async function waitForReplayPersistence(httpClient, testRunId, replayedTraceIds) {
|
|
2013
|
+
const deferredSettled = await httpClient.settleDeferredWork(
|
|
2014
|
+
REPLAY_PERSISTENCE_TIMEOUT_MS
|
|
2015
|
+
);
|
|
2016
|
+
if (!deferredSettled) {
|
|
2017
|
+
throw new BitfabError(
|
|
2018
|
+
`Replay could not settle deferred span work before the deadline, so the expected span counts are incomplete (testRunId ${testRunId}).`
|
|
2019
|
+
);
|
|
2020
|
+
}
|
|
2021
|
+
const expectedSpanCounts = takeReplaySpanCounts2(replayedTraceIds);
|
|
2022
|
+
if (Object.keys(expectedSpanCounts).length === 0) {
|
|
2023
|
+
return;
|
|
2024
|
+
}
|
|
2025
|
+
const flushed = await flushTraces(REPLAY_PERSISTENCE_TIMEOUT_MS);
|
|
2026
|
+
const deadline = Date.now() + REPLAY_PERSISTENCE_TIMEOUT_MS;
|
|
2027
|
+
let missing = Object.keys(expectedSpanCounts).length;
|
|
2028
|
+
while (true) {
|
|
2029
|
+
const status = await httpClient.getReplayStatus(
|
|
2030
|
+
testRunId,
|
|
2031
|
+
expectedSpanCounts
|
|
2032
|
+
);
|
|
2033
|
+
const ready = status.traceIds ?? {};
|
|
2034
|
+
missing = Object.keys(expectedSpanCounts).filter(
|
|
2035
|
+
(traceId) => ready[traceId] === void 0
|
|
2036
|
+
).length;
|
|
2037
|
+
if (missing === 0) {
|
|
2038
|
+
return;
|
|
2039
|
+
}
|
|
2040
|
+
if (Date.now() >= deadline) {
|
|
2041
|
+
break;
|
|
2042
|
+
}
|
|
2043
|
+
await sleep(Math.min(100, Math.max(0, deadline - Date.now())));
|
|
2044
|
+
}
|
|
2045
|
+
const cause = flushed ? "" : " Delivery was also not confirmed before the flush deadline, so the spans likely never reached the server.";
|
|
2046
|
+
throw new BitfabError(
|
|
2047
|
+
`Replay traces were not fully persisted before the delivery deadline (testRunId ${testRunId}, missing ${missing} of ${Object.keys(expectedSpanCounts).length} trace(s)).${cause}`
|
|
2048
|
+
);
|
|
2049
|
+
}
|
|
2050
|
+
function sleep(ms) {
|
|
2051
|
+
return new Promise((resolve) => {
|
|
2052
|
+
const timer = setTimeout(resolve, ms);
|
|
2053
|
+
unrefTimer(timer);
|
|
2054
|
+
});
|
|
2055
|
+
}
|
|
2056
|
+
async function mapWithConcurrency2(tasks, maxConcurrency, onSettled) {
|
|
2057
|
+
const results = new Array(tasks.length);
|
|
2058
|
+
let nextIndex = 0;
|
|
2059
|
+
async function worker() {
|
|
2060
|
+
while (nextIndex < tasks.length) {
|
|
2061
|
+
const index = nextIndex++;
|
|
2062
|
+
const result = await tasks[index]();
|
|
2063
|
+
results[index] = result;
|
|
2064
|
+
onSettled?.(result, index);
|
|
2065
|
+
}
|
|
2066
|
+
}
|
|
2067
|
+
const workers = Array.from(
|
|
2068
|
+
{ length: Math.min(maxConcurrency, tasks.length) },
|
|
2069
|
+
() => worker()
|
|
2070
|
+
);
|
|
2071
|
+
await Promise.all(workers);
|
|
2072
|
+
return results;
|
|
2073
|
+
}
|
|
2074
|
+
async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, registeredOverrides = []) {
|
|
2075
|
+
if (options?.traceIds !== void 0) {
|
|
2076
|
+
if (options.traceIds.length === 0) {
|
|
2077
|
+
throw new BitfabError("traceIds must contain at least one trace ID.");
|
|
2078
|
+
}
|
|
2079
|
+
if (options.traceIds.length > 100) {
|
|
2080
|
+
throw new BitfabError(
|
|
2081
|
+
`traceIds supports at most 100 trace IDs per replay (got ${options.traceIds.length}).`
|
|
2082
|
+
);
|
|
2083
|
+
}
|
|
2084
|
+
}
|
|
2085
|
+
if (options?.limit !== void 0 && options?.traceIds !== void 0) {
|
|
2086
|
+
try {
|
|
2087
|
+
console.warn(
|
|
2088
|
+
"Bitfab: limit is ignored when traceIds is passed: the explicit trace ID list already determines how many traces replay."
|
|
2089
|
+
);
|
|
2090
|
+
} catch {
|
|
2091
|
+
}
|
|
2092
|
+
}
|
|
2093
|
+
await replayContextReady;
|
|
2094
|
+
let codeChangeDescription = options?.codeChangeDescription;
|
|
2095
|
+
let codeChangeFiles = options?.codeChangeFiles;
|
|
2096
|
+
if (codeChangeFiles === void 0) {
|
|
2097
|
+
const captured = await resolveAutoCodeChange(options?.name);
|
|
2098
|
+
if (captured) {
|
|
2099
|
+
codeChangeFiles = captured.files;
|
|
2100
|
+
if (codeChangeDescription === void 0) {
|
|
2101
|
+
codeChangeDescription = captured.description;
|
|
2102
|
+
}
|
|
2103
|
+
}
|
|
2104
|
+
}
|
|
2105
|
+
const {
|
|
2106
|
+
testRunId,
|
|
2107
|
+
testRunUrl,
|
|
2108
|
+
items: serverItems
|
|
2109
|
+
} = await httpClient.startReplay(
|
|
2110
|
+
traceFunctionKey,
|
|
2111
|
+
// limit is meaningless with explicit traceIds (the ID list determines
|
|
2112
|
+
// the count), so it's omitted from the request entirely.
|
|
2113
|
+
options?.traceIds ? void 0 : options?.limit ?? 5,
|
|
2114
|
+
options?.traceIds,
|
|
2115
|
+
options?.name,
|
|
2116
|
+
codeChangeDescription,
|
|
2117
|
+
codeChangeFiles,
|
|
2118
|
+
dbBranchEnabled(options?.dbBranch),
|
|
2119
|
+
// includeDbBranchLease
|
|
2120
|
+
options?.experimentGroupId,
|
|
2121
|
+
options?.datasetId,
|
|
2122
|
+
options?.graderIds,
|
|
2123
|
+
resolveDbBranchSettings(options?.dbBranch)
|
|
2124
|
+
);
|
|
2125
|
+
const mockStrategy = options?.mock ?? "marked";
|
|
2126
|
+
const maxConcurrency = options?.maxConcurrency ?? 10;
|
|
2127
|
+
const resolvedOverrides = [
|
|
2128
|
+
...normalizeMockOverrides(options?.mockOverride),
|
|
2129
|
+
...registeredOverrides
|
|
2130
|
+
];
|
|
2131
|
+
const replayedTraceIds = serverItems.map(() => randomUuid());
|
|
2132
|
+
const tasks = serverItems.map(
|
|
2133
|
+
(serverItem, index) => () => processItem(
|
|
2134
|
+
httpClient,
|
|
2135
|
+
serverItem,
|
|
2136
|
+
fn,
|
|
2137
|
+
testRunId,
|
|
2138
|
+
mockStrategy,
|
|
2139
|
+
resolvedOverrides,
|
|
2140
|
+
replayedTraceIds[index],
|
|
2141
|
+
dbBranchEnabled(options?.dbBranch),
|
|
2142
|
+
resolveDbBranchSettings(options?.dbBranch),
|
|
2143
|
+
options?.adaptInputs
|
|
2144
|
+
)
|
|
2145
|
+
);
|
|
2146
|
+
const total = tasks.length;
|
|
2147
|
+
let completed = 0;
|
|
2148
|
+
let succeeded = 0;
|
|
2149
|
+
let errored = 0;
|
|
2150
|
+
const resultItems = await mapWithConcurrency2(
|
|
2151
|
+
tasks,
|
|
2152
|
+
maxConcurrency,
|
|
2153
|
+
options?.onProgress ? (item) => {
|
|
2154
|
+
completed += 1;
|
|
2155
|
+
if (item.error === null) {
|
|
2156
|
+
succeeded += 1;
|
|
2157
|
+
} else {
|
|
2158
|
+
errored += 1;
|
|
2159
|
+
}
|
|
2160
|
+
try {
|
|
2161
|
+
options?.onProgress?.({
|
|
2162
|
+
testRunId,
|
|
2163
|
+
completed,
|
|
2164
|
+
total,
|
|
2165
|
+
succeeded,
|
|
2166
|
+
errored,
|
|
2167
|
+
item: {
|
|
2168
|
+
// The server replay trace id isn't known until completeReplay
|
|
2169
|
+
// runs (below), so it can't be reported mid-run and we never
|
|
2170
|
+
// emit the client-side placeholder. originalTraceId (the
|
|
2171
|
+
// historical trace) is known now and is what a UI keys on to
|
|
2172
|
+
// identify what just settled.
|
|
2173
|
+
traceId: null,
|
|
2174
|
+
originalTraceId: item.originalTraceId ?? null,
|
|
2175
|
+
originalSpanId: item.originalSpanId ?? null,
|
|
2176
|
+
// Deprecated aliases for originalTraceId/originalSpanId.
|
|
2177
|
+
sourceTraceId: item.originalTraceId ?? null,
|
|
2178
|
+
sourceSpanId: item.originalSpanId ?? null,
|
|
2179
|
+
input: item.input,
|
|
2180
|
+
result: item.result,
|
|
2181
|
+
originalOutput: item.originalOutput,
|
|
2182
|
+
error: item.error,
|
|
2183
|
+
durationMs: item.durationMs,
|
|
2184
|
+
tokens: item.tokens,
|
|
2185
|
+
model: item.model,
|
|
2186
|
+
dbSnapshotRef: item.dbSnapshotRef
|
|
2187
|
+
}
|
|
2188
|
+
});
|
|
2189
|
+
} catch {
|
|
2190
|
+
}
|
|
2191
|
+
} : void 0
|
|
2192
|
+
);
|
|
2193
|
+
await waitForReplayPersistence(httpClient, testRunId, replayedTraceIds);
|
|
2194
|
+
const completeResult = await httpClient.completeReplay(testRunId);
|
|
2195
|
+
const serverTraceIds = completeResult.traceIds;
|
|
2196
|
+
const replayTokens = completeResult.tokens;
|
|
2197
|
+
if (serverTraceIds !== void 0) {
|
|
2198
|
+
const missing = [];
|
|
2199
|
+
let completedCount = 0;
|
|
2200
|
+
for (let index = 0; index < resultItems.length; index += 1) {
|
|
2201
|
+
const item = resultItems[index];
|
|
2202
|
+
const localId = replayedTraceIds[index];
|
|
2203
|
+
const mapped = localId ? serverTraceIds[localId] : void 0;
|
|
2204
|
+
item.traceId = mapped ?? null;
|
|
2205
|
+
if (item.error === null) {
|
|
2206
|
+
completedCount += 1;
|
|
2207
|
+
if (mapped === void 0) {
|
|
2208
|
+
missing.push(localId ?? item.originalTraceId);
|
|
2209
|
+
}
|
|
2210
|
+
}
|
|
2211
|
+
if (mapped !== void 0) {
|
|
2212
|
+
item.tokens = replayTokens?.[mapped] ?? null;
|
|
2213
|
+
}
|
|
2214
|
+
}
|
|
2215
|
+
if (completedCount > 0 && missing.length === completedCount) {
|
|
2216
|
+
const serverCount = completeResult.traceCount !== void 0 ? ` The server persisted ${completeResult.traceCount} trace(s) for this run.` : "";
|
|
2217
|
+
throw new BitfabError(
|
|
2218
|
+
`Replay completed but the server has no persisted trace for any of the ${completedCount} completed item(s) (testRunId ${testRunId}).${serverCount} Trace uploads were awaited, so either the uploads failed (check for "Bitfab: Failed to create" errors above) or the replayed function is not wrapped with withSpan.`
|
|
2219
|
+
);
|
|
2220
|
+
}
|
|
2221
|
+
if (missing.length > 0) {
|
|
2222
|
+
try {
|
|
2223
|
+
console.error(
|
|
2224
|
+
`Bitfab: server has no persisted trace for ${missing.length} of ${completedCount} completed replay item(s) (testRunId ${testRunId}). Their replay token usage is unavailable and they cannot be labeled.`
|
|
2225
|
+
);
|
|
2226
|
+
} catch {
|
|
2227
|
+
}
|
|
2228
|
+
}
|
|
2229
|
+
}
|
|
2230
|
+
const result = {
|
|
2231
|
+
items: resultItems,
|
|
2232
|
+
testRunId,
|
|
2233
|
+
testRunUrl: `${serviceUrl}${testRunUrl}`
|
|
2234
|
+
};
|
|
2235
|
+
await writeReplayResultFile(result);
|
|
2236
|
+
try {
|
|
2237
|
+
options?.onProgress?.({
|
|
2238
|
+
type: "complete",
|
|
2239
|
+
testRunId,
|
|
2240
|
+
completed: total,
|
|
2241
|
+
total,
|
|
2242
|
+
succeeded,
|
|
2243
|
+
errored,
|
|
2244
|
+
result
|
|
2245
|
+
});
|
|
2246
|
+
} catch {
|
|
2247
|
+
}
|
|
2248
|
+
return result;
|
|
2249
|
+
}
|
|
2250
|
+
async function writeReplayResultFile(result) {
|
|
2251
|
+
const resultPath = typeof process !== "undefined" ? process.env?.BITFAB_REPLAY_RESULT_PATH : void 0;
|
|
2252
|
+
if (!resultPath) {
|
|
2253
|
+
return;
|
|
2254
|
+
}
|
|
2255
|
+
try {
|
|
2256
|
+
const [{ dirname }, { mkdir, writeFile }] = await Promise.all([
|
|
2257
|
+
import("path"),
|
|
2258
|
+
import("fs/promises")
|
|
2259
|
+
]);
|
|
2260
|
+
await mkdir(dirname(resultPath), { recursive: true });
|
|
2261
|
+
await writeFile(resultPath, `${JSON.stringify(result, null, 2)}
|
|
2262
|
+
`);
|
|
2263
|
+
} catch (err) {
|
|
2264
|
+
try {
|
|
2265
|
+
console.warn(
|
|
2266
|
+
`Bitfab: failed to write replay result to BITFAB_REPLAY_RESULT_PATH (${resultPath}): ${err instanceof Error ? err.message : String(err)}`
|
|
2267
|
+
);
|
|
2268
|
+
} catch {
|
|
2269
|
+
}
|
|
2270
|
+
}
|
|
2271
|
+
}
|
|
2272
|
+
|
|
2273
|
+
export {
|
|
2274
|
+
__privateGet,
|
|
2275
|
+
__privateAdd,
|
|
2276
|
+
__privateSet,
|
|
2277
|
+
__version__,
|
|
2278
|
+
DEFAULT_SERVICE_URL,
|
|
2279
|
+
BitfabError,
|
|
2280
|
+
registerAsyncLocalStorageClass,
|
|
2281
|
+
assertAsyncStorageRegistered,
|
|
2282
|
+
asyncStorageReady,
|
|
2283
|
+
isAsyncStorageInitDone,
|
|
2284
|
+
createAsyncLocalStorage,
|
|
2285
|
+
getReplayContext,
|
|
2286
|
+
warnOnce,
|
|
2287
|
+
flushTraces,
|
|
2288
|
+
HttpClient,
|
|
2289
|
+
serializeValue,
|
|
2290
|
+
deserializeValue,
|
|
2291
|
+
toJsonSafe,
|
|
2292
|
+
toJsonSafeReport,
|
|
2293
|
+
randomUuid,
|
|
2294
|
+
resolveMockValue,
|
|
2295
|
+
BITFAB_PROGRESS_PREFIX,
|
|
2296
|
+
reportReplayProgress,
|
|
2297
|
+
replay
|
|
2298
|
+
};
|
|
2299
|
+
//# sourceMappingURL=chunk-4J36FZ4K.js.map
|