bitfab 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/dist/index.cjs CHANGED
@@ -46,219 +46,41 @@ var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read fr
46
46
  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);
47
47
  var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), setter ? setter.call(obj, value) : member.set(obj, value), value);
48
48
 
49
+ // src/version.generated.ts
50
+ var __version__;
51
+ var init_version_generated = __esm({
52
+ "src/version.generated.ts"() {
53
+ "use strict";
54
+ __version__ = "0.34.0";
55
+ }
56
+ });
57
+
58
+ // src/constants.ts
59
+ var DEFAULT_SERVICE_URL;
60
+ var init_constants = __esm({
61
+ "src/constants.ts"() {
62
+ "use strict";
63
+ init_version_generated();
64
+ DEFAULT_SERVICE_URL = "https://bitfab.ai";
65
+ }
66
+ });
67
+
49
68
  // src/errors.ts
50
69
  var BitfabError;
51
70
  var init_errors = __esm({
52
71
  "src/errors.ts"() {
53
72
  "use strict";
54
73
  BitfabError = class extends Error {
55
- constructor(message, url) {
74
+ constructor(message, url, status) {
56
75
  super(message);
57
76
  this.url = url;
77
+ this.status = status;
58
78
  this.name = "BitfabError";
59
79
  }
60
80
  };
61
81
  }
62
82
  });
63
83
 
64
- // src/warnOnce.ts
65
- function warnOnce(key, message) {
66
- if (warned.has(key)) {
67
- return;
68
- }
69
- warned.add(key);
70
- try {
71
- console.warn(`[bitfab] ${message}`);
72
- } catch {
73
- }
74
- }
75
- var warned;
76
- var init_warnOnce = __esm({
77
- "src/warnOnce.ts"() {
78
- "use strict";
79
- warned = /* @__PURE__ */ new Set();
80
- }
81
- });
82
-
83
- // src/serialize.ts
84
- function describeValue(value) {
85
- try {
86
- const ctorName = value?.constructor?.name;
87
- if (ctorName && ctorName !== "Object") {
88
- return ctorName;
89
- }
90
- } catch {
91
- }
92
- return typeof value;
93
- }
94
- function unserializableStub(value, reason) {
95
- warnOnce(
96
- `serialize:${reason.replace(/\d+/g, "N")}`,
97
- `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.`
98
- );
99
- let summary;
100
- try {
101
- summary = `<unserializable: ${describeValue(value)} (${reason})>`;
102
- } catch {
103
- summary = `<unserializable (${reason})>`;
104
- }
105
- return { json: summary };
106
- }
107
- function serializeValue(value) {
108
- try {
109
- const { json, meta } = import_superjson.default.serialize(value);
110
- let size;
111
- try {
112
- size = JSON.stringify(json).length;
113
- } catch {
114
- return unserializableStub(value, "stringify_failed_after_superjson");
115
- }
116
- if (size > MAX_SERIALIZED_BYTES) {
117
- return unserializableStub(value, `too_large_${size}_bytes`);
118
- }
119
- return meta ? { json, meta } : { json };
120
- } catch {
121
- try {
122
- return { json: JSON.parse(JSON.stringify(value)) };
123
- } catch {
124
- return unserializableStub(value, "json_stringify_failed");
125
- }
126
- }
127
- }
128
- function deserializeValue(serialized) {
129
- if (serialized.meta === void 0) {
130
- return serialized.json;
131
- }
132
- return import_superjson.default.deserialize({
133
- json: serialized.json,
134
- meta: serialized.meta
135
- });
136
- }
137
- function toJsonSafe(value) {
138
- return toJsonSafeReport(value).safe;
139
- }
140
- function toJsonSafeReport(value) {
141
- const dropped = [];
142
- const safe = toJsonSafeInner(value, 0, /* @__PURE__ */ new WeakSet(), dropped);
143
- try {
144
- const size = JSON.stringify(safe)?.length ?? 0;
145
- if (size > MAX_FRAMEWORK_SERIALIZED_BYTES) {
146
- warnOnce(
147
- "toJsonSafe:too_large",
148
- `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.`
149
- );
150
- return {
151
- safe: `<unserializable: too_large_${size}_bytes>`,
152
- dropped: [...dropped, `too_large_${size}_bytes`]
153
- };
154
- }
155
- } catch {
156
- }
157
- return { safe, dropped };
158
- }
159
- function toJsonSafeInner(value, depth, seen, dropped) {
160
- if (value === null || value === void 0) {
161
- return value;
162
- }
163
- if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
164
- return value;
165
- }
166
- const className = value?.constructor?.name ?? typeof value;
167
- if (depth > MAX_SAFE_DEPTH) {
168
- dropped.push(className);
169
- return `<${className}>`;
170
- }
171
- if (typeof value !== "object") {
172
- if (typeof value === "function" || typeof value === "symbol") {
173
- dropped.push(className);
174
- }
175
- try {
176
- return String(value);
177
- } catch {
178
- dropped.push(className);
179
- return `<${className}>`;
180
- }
181
- }
182
- if (seen.has(value)) {
183
- dropped.push(className);
184
- return `<cycle ${className}>`;
185
- }
186
- seen.add(value);
187
- let result;
188
- if (Array.isArray(value)) {
189
- result = value.map(
190
- (item) => toJsonSafeInner(item, depth + 1, seen, dropped)
191
- );
192
- } else if (typeof value.toJSON === "function") {
193
- try {
194
- result = toJsonSafeInner(
195
- value.toJSON(),
196
- depth + 1,
197
- seen,
198
- dropped
199
- );
200
- } catch {
201
- dropped.push(className);
202
- result = `<${className}>`;
203
- }
204
- } else {
205
- try {
206
- const obj = {};
207
- for (const [k, v] of Object.entries(value)) {
208
- if (!k.startsWith("_")) {
209
- obj[k] = toJsonSafeInner(v, depth + 1, seen, dropped);
210
- }
211
- }
212
- result = obj;
213
- } catch {
214
- dropped.push(className);
215
- result = `<${className}>`;
216
- }
217
- }
218
- seen.delete(value);
219
- return result;
220
- }
221
- var import_superjson, MAX_SERIALIZED_BYTES, MAX_FRAMEWORK_SERIALIZED_BYTES, MAX_SAFE_DEPTH;
222
- var init_serialize = __esm({
223
- "src/serialize.ts"() {
224
- "use strict";
225
- import_superjson = __toESM(require("superjson"), 1);
226
- init_warnOnce();
227
- MAX_SERIALIZED_BYTES = 512e3;
228
- MAX_FRAMEWORK_SERIALIZED_BYTES = 2e6;
229
- MAX_SAFE_DEPTH = 6;
230
- }
231
- });
232
-
233
- // src/randomUuid.ts
234
- function randomUuid() {
235
- const globalCrypto = globalThis.crypto;
236
- if (typeof globalCrypto?.randomUUID === "function") {
237
- try {
238
- return globalCrypto.randomUUID();
239
- } catch {
240
- }
241
- }
242
- warnOnce(
243
- "crypto-unavailable",
244
- "global crypto.randomUUID is unavailable; using a non-cryptographic fallback for trace/span ids. Tracing works normally (ids are correlation-only, not security-sensitive)."
245
- );
246
- return fallbackUuidV4();
247
- }
248
- function fallbackUuidV4() {
249
- return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (char) => {
250
- const rand = Math.random() * 16 | 0;
251
- const value = char === "x" ? rand : rand & 3 | 8;
252
- return value.toString(16);
253
- });
254
- }
255
- var init_randomUuid = __esm({
256
- "src/randomUuid.ts"() {
257
- "use strict";
258
- init_warnOnce();
259
- }
260
- });
261
-
262
84
  // src/asyncStorage.ts
263
85
  function registerAsyncLocalStorageClass(cls) {
264
86
  if (!AsyncLocalStorageClass) {
@@ -298,22 +120,6 @@ var init_asyncStorage = __esm({
298
120
  }
299
121
  });
300
122
 
301
- // src/mockOverride.ts
302
- function resolveMockValue(value, ctx) {
303
- return typeof value === "function" ? value(ctx) : value;
304
- }
305
- function normalizeMockOverrides(mockOverride) {
306
- if (mockOverride === void 0) {
307
- return [];
308
- }
309
- return Array.isArray(mockOverride) ? mockOverride : [mockOverride];
310
- }
311
- var init_mockOverride = __esm({
312
- "src/mockOverride.ts"() {
313
- "use strict";
314
- }
315
- });
316
-
317
123
  // src/replayContext.ts
318
124
  function getReplayContext() {
319
125
  return replayContextStorage?.getStore() ?? null;
@@ -347,1184 +153,2306 @@ var init_replayContext = __esm({
347
153
  }
348
154
  });
349
155
 
350
- // src/codeChange.ts
351
- async function resolveAutoCodeChange(label) {
352
- if (typeof process === "undefined") {
353
- return null;
354
- }
355
- if (process.env?.BITFAB_DISABLE_CODE_CHANGE_CAPTURE) {
356
- return null;
156
+ // src/warnOnce.ts
157
+ function warnOnce(key, message) {
158
+ if (warned.has(key)) {
159
+ return;
357
160
  }
358
- const fromEnv = await readCodeChangeFile();
359
- if (fromEnv) {
360
- return fromEnv;
161
+ warned.add(key);
162
+ try {
163
+ console.warn(`[bitfab] ${message}`);
164
+ } catch {
361
165
  }
362
- return captureCodeChangeFromGit(process.cwd?.() ?? ".", label);
363
166
  }
364
- async function readCodeChangeFile() {
365
- const path = process.env?.BITFAB_CODE_CHANGE_PATH;
366
- if (!path) {
367
- return null;
167
+ var warned;
168
+ var init_warnOnce = __esm({
169
+ "src/warnOnce.ts"() {
170
+ "use strict";
171
+ warned = /* @__PURE__ */ new Set();
368
172
  }
173
+ });
174
+
175
+ // src/serializePayload.ts
176
+ function serializePayloadBody(payload) {
369
177
  try {
370
- const { readFile } = await import("fs/promises");
371
- const parsed = JSON.parse(await readFile(path, "utf8"));
372
- const files = Array.isArray(parsed?.files) && parsed.files.every(
373
- (f) => typeof f === "object" && f !== null && !Array.isArray(f)
374
- ) ? parsed.files : void 0;
375
- const description = typeof parsed?.description === "string" ? parsed.description : void 0;
376
- if (!files && description === void 0) {
377
- return null;
378
- }
379
- return { description, files };
380
- } catch {
381
- return null;
382
- }
383
- }
384
- async function captureCodeChangeFromGit(cwd, label) {
385
- let execFile;
386
- let readFile;
387
- try {
388
- ;
389
- ({ execFile } = await import("child_process"));
390
- ({ readFile } = await import("fs/promises"));
178
+ return { body: JSON.stringify(payload), dropped: [] };
391
179
  } catch {
392
- return null;
393
- }
394
- const git = (dir, args) => new Promise((resolve) => {
395
- execFile(
396
- "git",
397
- args,
398
- // 30s timeout so a hung git (e.g. a network-touching ref op) can't
399
- // block the whole replay indefinitely.
400
- { cwd: dir, maxBuffer: 64 * 1024 * 1024, timeout: 3e4 },
401
- (err, stdout) => resolve(err ? null : stdout)
402
- );
403
- });
404
- try {
405
- const root = (await git(cwd, ["rev-parse", "--show-toplevel"]))?.trim();
406
- if (!root) {
407
- return null;
408
- }
409
- const resolved = await resolveBase(git, root);
410
- if (!resolved) {
411
- return null;
412
- }
413
- const { base, fromTrunk } = resolved;
414
- const blobBytes = async (ref, path) => {
415
- const out = await git(root, ["cat-file", "-s", `${ref}:${path}`]);
416
- const n = out ? Number.parseInt(out.trim(), 10) : Number.NaN;
417
- return Number.isFinite(n) ? n : 0;
418
- };
419
- const workingBytes = async (path) => {
420
- try {
421
- const { stat } = await import("fs/promises");
422
- const { join } = await import("path");
423
- return (await stat(join(root, path))).size;
424
- } catch {
425
- return 0;
180
+ const dropped = [];
181
+ const sanitize = (value, seen) => {
182
+ const t = typeof value;
183
+ if (value === null || t === "string" || t === "number" || t === "boolean") {
184
+ return value;
426
185
  }
427
- };
428
- const tracked = await git(root, [
429
- "diff",
430
- "--name-status",
431
- "--no-renames",
432
- "-z",
433
- base,
434
- "--",
435
- ":!.bitfab"
436
- ]);
437
- const untracked = await git(root, [
438
- "ls-files",
439
- "--others",
440
- "--exclude-standard",
441
- "-z",
442
- "--",
443
- ":!.bitfab"
444
- ]);
445
- const entries = [
446
- ...parseNameStatusZ(tracked ?? ""),
447
- ...(untracked ?? "").split(NUL).filter((p) => p.length > 0).map((path) => ({ status: "A", path }))
448
- ];
449
- if (entries.length === 0) {
450
- return null;
451
- }
452
- const files = [];
453
- let totalBytes = 0;
454
- for (const { status, path } of entries) {
455
- if (files.length >= MAX_FILES) {
456
- break;
186
+ if (t === "bigint") {
187
+ dropped.push("BigInt");
188
+ return "<unserializable: BigInt>";
457
189
  }
458
- const beforeBytes = status === "A" ? 0 : await blobBytes(base, path);
459
- const afterBytes = status === "D" ? 0 : await workingBytes(path);
460
- if (beforeBytes > MAX_FILE_BYTES || afterBytes > MAX_FILE_BYTES) {
461
- continue;
190
+ if (t === "function") {
191
+ const name = value.name || "Function";
192
+ dropped.push(name);
193
+ return `<unserializable: ${name}>`;
462
194
  }
463
- const before = (status === "A" ? "" : await git(root, ["show", `${base}:${path}`]) ?? "").replace(/\r\n/g, "\n");
464
- const after = (status === "D" ? "" : await readWorkingFile(readFile, root, path)).replace(/\r\n/g, "\n");
465
- if (before === after) {
466
- continue;
195
+ if (t === "symbol") {
196
+ dropped.push("Symbol");
197
+ return "<unserializable: Symbol>";
467
198
  }
468
- const size = Buffer.byteLength(before, "utf8") + Buffer.byteLength(after, "utf8");
469
- if (totalBytes + size > MAX_TOTAL_BYTES || looksBinary(before) || looksBinary(after)) {
470
- continue;
199
+ if (t !== "object") {
200
+ return void 0;
471
201
  }
472
- totalBytes += size;
473
- files.push({ path, before, after });
202
+ const obj = value;
203
+ const className = obj.constructor?.name || "object";
204
+ if (seen.has(obj)) {
205
+ dropped.push(className);
206
+ return `<cycle: ${className}>`;
207
+ }
208
+ seen.add(obj);
209
+ let result;
210
+ if (Array.isArray(obj)) {
211
+ result = obj.map((item) => sanitize(item, seen));
212
+ } else if (typeof obj.toJSON === "function") {
213
+ try {
214
+ result = sanitize(obj.toJSON(), seen);
215
+ } catch {
216
+ dropped.push(className);
217
+ result = `<unserializable: ${className}>`;
218
+ }
219
+ } else {
220
+ try {
221
+ const out = {};
222
+ for (const [k, v] of Object.entries(obj)) {
223
+ out[k] = sanitize(v, seen);
224
+ }
225
+ result = out;
226
+ } catch {
227
+ warnOnce(
228
+ "payload:field-getter-threw",
229
+ "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."
230
+ );
231
+ dropped.push(className);
232
+ result = `<unserializable: ${className}>`;
233
+ }
234
+ }
235
+ seen.delete(obj);
236
+ return result;
237
+ };
238
+ let sanitized;
239
+ try {
240
+ sanitized = sanitize(payload, /* @__PURE__ */ new WeakSet());
241
+ } catch (error) {
242
+ const message = error instanceof Error ? error.message : String(error);
243
+ return {
244
+ body: JSON.stringify({ error: `payload_serialize_failed: ${message}` }),
245
+ dropped
246
+ };
474
247
  }
475
- if (files.length === 0) {
476
- return null;
248
+ if (dropped.length > 0 && typeof sanitized === "object" && sanitized !== null && !Array.isArray(sanitized)) {
249
+ const obj = sanitized;
250
+ const existing = Array.isArray(obj.errors) ? obj.errors : [];
251
+ obj.errors = [
252
+ ...existing,
253
+ {
254
+ source: "sdk",
255
+ step: "json_serialize",
256
+ error: `stubbed non-serializable value(s): ${[
257
+ ...new Set(dropped)
258
+ ].join(", ")}`
259
+ }
260
+ ];
477
261
  }
478
- const subject = (await git(root, ["log", "-1", "--format=%s", "HEAD"]))?.trim();
479
- const fileWord = files.length === 1 ? "file" : "files";
480
- const head = label?.trim() || subject || "Working-tree change";
481
- const against = fromTrunk ? "vs trunk" : "uncommitted (vs HEAD)";
482
- return {
483
- description: `${head} (${files.length} ${fileWord} changed ${against})`,
484
- files
485
- };
486
- } catch {
487
- return null;
262
+ return { body: JSON.stringify(sanitized), dropped };
488
263
  }
489
264
  }
490
- async function resolveBase(git, root) {
491
- const forced = process.env?.BITFAB_CODE_CHANGE_BASE;
492
- if (forced && await refExists(git, root, forced)) {
493
- const base = (await git(root, ["merge-base", "HEAD", forced]))?.trim() || (await git(root, ["rev-parse", "--verify", forced]))?.trim() || null;
494
- return base ? { base, fromTrunk: true } : null;
265
+ var init_serializePayload = __esm({
266
+ "src/serializePayload.ts"() {
267
+ "use strict";
268
+ init_warnOnce();
495
269
  }
496
- for (const candidate of TRUNK_CANDIDATES) {
497
- if (!await refExists(git, root, candidate)) {
498
- continue;
499
- }
500
- const mb = (await git(root, ["merge-base", "HEAD", candidate]))?.trim();
501
- if (mb) {
502
- return { base: mb, fromTrunk: true };
503
- }
270
+ });
271
+
272
+ // src/readEnv.ts
273
+ function readEnv(name) {
274
+ if (typeof process !== "undefined" && process.env) {
275
+ return process.env[name];
504
276
  }
505
- return await refExists(git, root, "HEAD") ? { base: "HEAD", fromTrunk: false } : null;
506
- }
507
- async function refExists(git, root, ref) {
508
- return await git(root, ["rev-parse", "--verify", `${ref}^{object}`]) !== null;
277
+ return void 0;
509
278
  }
510
- async function readWorkingFile(readFile, root, path) {
511
- try {
512
- const { join } = await import("path");
513
- return await readFile(join(root, path), "utf8");
514
- } catch {
515
- return "";
279
+ var init_readEnv = __esm({
280
+ "src/readEnv.ts"() {
281
+ "use strict";
516
282
  }
517
- }
518
- function parseNameStatusZ(raw) {
519
- const parts = raw.split(NUL).filter((p) => p.length > 0);
520
- const out = [];
521
- for (let i = 0; i + 1 < parts.length; i += 2) {
522
- out.push({ status: parts[i].charAt(0), path: parts[i + 1] });
283
+ });
284
+
285
+ // src/unrefTimer.ts
286
+ function unrefTimer(timer) {
287
+ const handle = timer;
288
+ if (typeof handle.unref === "function") {
289
+ handle.unref();
523
290
  }
524
- return out;
525
291
  }
526
- function looksBinary(s) {
527
- return s.slice(0, 8e3).includes(NUL);
528
- }
529
- var MAX_FILES, MAX_FILE_BYTES, MAX_TOTAL_BYTES, TRUNK_CANDIDATES, NUL;
530
- var init_codeChange = __esm({
531
- "src/codeChange.ts"() {
292
+ var init_unrefTimer = __esm({
293
+ "src/unrefTimer.ts"() {
532
294
  "use strict";
533
- MAX_FILES = 60;
534
- MAX_FILE_BYTES = 5e5;
535
- MAX_TOTAL_BYTES = 2e6;
536
- TRUNK_CANDIDATES = [
537
- "origin/HEAD",
538
- "origin/main",
539
- "origin/master",
540
- "main",
541
- "master"
542
- ];
543
- NUL = String.fromCharCode(0);
544
295
  }
545
296
  });
546
297
 
547
- // src/replay.ts
548
- var replay_exports = {};
549
- __export(replay_exports, {
550
- BITFAB_PROGRESS_PREFIX: () => BITFAB_PROGRESS_PREFIX,
551
- replay: () => replay,
552
- reportReplayProgress: () => reportReplayProgress
553
- });
554
- function dbBranchEnabled(dbBranch) {
555
- return dbBranch !== void 0 && dbBranch !== false;
298
+ // src/otel.ts
299
+ function readBoundedIntEnv(name, max, fallback, warnKey) {
300
+ const raw = readEnv(name);
301
+ if (raw === void 0) {
302
+ return fallback;
303
+ }
304
+ const value = Number(raw);
305
+ if (Number.isInteger(value) && value > 0 && value <= max) {
306
+ return value;
307
+ }
308
+ warnOnce(
309
+ warnKey,
310
+ `${name} must be a positive integer no greater than ${max}; using ${fallback}`
311
+ );
312
+ return fallback;
556
313
  }
557
- function resolveDbBranchSettings(dbBranch) {
558
- if (!dbBranch || dbBranch === true) {
559
- return void 0;
314
+ function logError(message, error) {
315
+ try {
316
+ if (error === void 0) {
317
+ console.error(`[bitfab] ${message}`);
318
+ } else {
319
+ console.error(`[bitfab] ${message}`, error);
320
+ }
321
+ } catch {
560
322
  }
561
- const { minCu, maxCu, warmupSql } = dbBranch;
562
- const settings = {
563
- ...minCu === void 0 ? {} : { minCu },
564
- ...maxCu === void 0 ? {} : { maxCu },
565
- ...warmupSql === void 0 ? {} : { warmupSql }
566
- };
567
- return Object.keys(settings).length === 0 ? void 0 : settings;
568
323
  }
569
- function reportReplayProgress(progress) {
570
- const stderr = typeof process !== "undefined" ? process.stderr : void 0;
571
- if (!stderr) {
324
+ function recordTraceSubmission(operation, payload) {
325
+ const sourceTraceId = resolveSourceTraceId(payload);
326
+ if (sourceTraceId === void 0) {
572
327
  return;
573
328
  }
574
- try {
575
- stderr.write(`${BITFAB_PROGRESS_PREFIX}${JSON.stringify(progress)}
576
- `);
577
- } catch {
329
+ if (operation === "external_span") {
330
+ const rawSpan = asRecord(payload.rawSpan);
331
+ if (typeof rawSpan?.id !== "string") {
332
+ submissionCounter += 1;
333
+ }
334
+ const sourceSpanId = typeof rawSpan?.id === "string" ? rawSpan.id : `submission-${submissionCounter}`;
335
+ const existing = traceSubmissionSpanIds.get(sourceTraceId);
336
+ if (existing) {
337
+ existing.add(sourceSpanId);
338
+ } else {
339
+ traceSubmissionSpanIds.set(sourceTraceId, /* @__PURE__ */ new Set([sourceSpanId]));
340
+ }
341
+ return;
342
+ }
343
+ if (payload.completed !== true) {
344
+ return;
345
+ }
346
+ if (typeof payload.testRunId === "string") {
347
+ replayTraceSubmissions.add(sourceTraceId);
348
+ if (!traceSubmissionSpanIds.has(sourceTraceId)) {
349
+ traceSubmissionSpanIds.set(sourceTraceId, /* @__PURE__ */ new Set());
350
+ }
351
+ } else {
352
+ traceSubmissionSpanIds.delete(sourceTraceId);
578
353
  }
579
354
  }
580
- function deserializeInputs(spanData) {
581
- const inputMeta = spanData.input_meta;
582
- const rawInput = spanData.input;
583
- if (inputMeta !== void 0 && inputMeta !== null) {
584
- const deserialized = deserializeValue({ json: rawInput, meta: inputMeta });
585
- if (Array.isArray(deserialized)) {
586
- return deserialized;
355
+ function takeReplaySpanCounts(traceIds) {
356
+ const counts = {};
357
+ for (const traceId of traceIds) {
358
+ if (!replayTraceSubmissions.has(traceId)) {
359
+ continue;
587
360
  }
588
- return deserialized !== void 0 && deserialized !== null ? [deserialized] : [];
361
+ counts[traceId] = traceSubmissionSpanIds.get(traceId)?.size ?? 0;
362
+ traceSubmissionSpanIds.delete(traceId);
363
+ replayTraceSubmissions.delete(traceId);
589
364
  }
590
- if (Array.isArray(rawInput)) {
591
- return rawInput;
365
+ return counts;
366
+ }
367
+ function asRecord(value) {
368
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
369
+ }
370
+ function resolveSourceTraceId(payload) {
371
+ if (typeof payload.sourceTraceId === "string") {
372
+ return payload.sourceTraceId;
592
373
  }
593
- return rawInput !== void 0 && rawInput !== null ? [rawInput] : [];
374
+ const rawTrace = asRecord(payload.externalTrace) ?? asRecord(payload.rawTrace);
375
+ return typeof rawTrace?.id === "string" ? rawTrace.id : void 0;
594
376
  }
595
- function deserializeOutput(spanData) {
596
- const outputMeta = spanData.output_meta;
597
- const rawOutput = spanData.output;
598
- if (outputMeta !== void 0 && outputMeta !== null) {
599
- return deserializeValue({ json: rawOutput, meta: outputMeta });
377
+ function otlpValue(value) {
378
+ if (typeof value === "boolean") {
379
+ return { boolValue: value };
600
380
  }
601
- return rawOutput;
381
+ if (typeof value === "number") {
382
+ return Number.isInteger(value) ? { intValue: String(value) } : { doubleValue: value };
383
+ }
384
+ if (typeof value === "string") {
385
+ return { stringValue: value };
386
+ }
387
+ if (Array.isArray(value)) {
388
+ return { arrayValue: { values: value.map(otlpValue) } };
389
+ }
390
+ return { stringValue: String(value) };
602
391
  }
603
- function buildMockTree(rootNode) {
604
- const spans = /* @__PURE__ */ new Map();
605
- const counters = /* @__PURE__ */ new Map();
606
- function walk(node) {
607
- const key = node.traceFunctionKey;
608
- if (key) {
609
- const name = node.spanName || key;
610
- const counterKey = `${key}:${name}`;
611
- const index = counters.get(counterKey) ?? 0;
612
- counters.set(counterKey, index + 1);
613
- spans.set(`${counterKey}:${index}`, {
614
- sourceSpanId: node.sourceSpanId,
615
- externalSpanId: node.externalSpanId,
616
- output: node.output,
617
- outputMeta: node.outputMeta
618
- });
619
- }
620
- for (const child of node.children) {
621
- walk(child);
622
- }
392
+ function otlpAttributes(attributes) {
393
+ if (!attributes) {
394
+ return [];
623
395
  }
624
- for (const child of rootNode.children) {
625
- walk(child);
396
+ return Object.entries(attributes).filter(([, value]) => value !== void 0).map(([key, value]) => ({ key, value: otlpValue(value) }));
397
+ }
398
+ function hrTimeToNanoString(time) {
399
+ if (!time) {
400
+ return "0";
626
401
  }
627
- return { spans };
402
+ return `${time[0]}${String(time[1]).padStart(9, "0")}`;
628
403
  }
629
- async function processItem(httpClient, serverItem, fn, testRunId, mockStrategy, resolvedOverrides, replayedTraceId, includeDbBranchLease, dbBranchSettings, adaptInputs) {
630
- let lease = includeDbBranchLease ? serverItem.dbBranchLease : void 0;
631
- let leaseError = includeDbBranchLease ? serverItem.dbBranchLeaseError : void 0;
632
- let dbSnapshotRef = serverItem.dbSnapshotRef;
633
- let inputs = [];
634
- let originalOutput;
635
- let result;
636
- let error = null;
637
- const pendingPersistence = [];
638
- const originalTraceId = serverItem.originalTraceId ?? serverItem.sourceTraceId;
639
- const originalSpanId = serverItem.originalSpanId ?? serverItem.sourceSpanId;
640
- try {
641
- if (includeDbBranchLease && !lease && !leaseError) {
642
- const resolved = await httpClient.resolveDbBranchLease(
643
- testRunId,
644
- originalTraceId,
645
- dbBranchSettings
646
- );
647
- lease = resolved.lease ?? void 0;
648
- leaseError = resolved.leaseError ?? void 0;
649
- dbSnapshotRef = resolved.dbSnapshotRef ?? dbSnapshotRef;
650
- }
651
- if (leaseError) {
652
- throw new BitfabError(
653
- `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.`
654
- );
655
- }
656
- const span = await httpClient.getExternalSpan(originalSpanId);
657
- const spanData = span.rawData?.span_data ?? {};
658
- inputs = deserializeInputs(spanData);
659
- originalOutput = deserializeOutput(spanData);
660
- if (adaptInputs) {
661
- inputs = adaptInputs(inputs, {
662
- originalTraceId,
663
- originalSpanId,
664
- // Deprecated aliases for originalTraceId/originalSpanId.
665
- sourceTraceId: originalTraceId,
666
- sourceSpanId: originalSpanId
667
- });
668
- }
669
- const hasOverrides = resolvedOverrides.length > 0;
670
- const needTree = mockStrategy === "all" || mockStrategy === "marked" || hasOverrides;
671
- const includeOutputs = mockStrategy === "all";
672
- let mockTree;
673
- if (needTree) {
674
- try {
675
- const treeResponse = await httpClient.getSpanTree(originalSpanId, {
676
- includeOutputs
677
- });
678
- if (treeResponse.root) {
679
- mockTree = buildMockTree(treeResponse.root);
680
- } else if (mockStrategy === "all" || hasOverrides) {
681
- throw new BitfabError(
682
- `Replay mock strategy "${mockStrategy}"${hasOverrides ? " with overrides" : ""} requires a span tree root for original span ${originalSpanId}.`
683
- );
684
- } else {
685
- mockTree = void 0;
686
- }
687
- } catch (e) {
688
- if (mockStrategy === "all" || hasOverrides) {
689
- throw e;
690
- }
691
- mockTree = void 0;
692
- }
693
- }
694
- const outputCache = /* @__PURE__ */ new Map();
695
- const fetchSpanOutput = mockTree && !includeOutputs ? (externalSpanId) => {
696
- let pending = outputCache.get(externalSpanId);
697
- if (!pending) {
698
- pending = httpClient.getExternalSpan(externalSpanId).then(
699
- (s) => deserializeOutput(
700
- s.rawData?.span_data ?? {}
701
- )
702
- );
703
- outputCache.set(externalSpanId, pending);
704
- }
705
- return pending;
706
- } : void 0;
707
- const maybePromise = runWithReplayContext(
708
- {
709
- testRunId,
710
- traceId: replayedTraceId,
711
- inputSourceSpanId: span.id,
712
- inputSourceTraceId: span.externalTraceId,
713
- sourceBitfabTraceId: originalTraceId,
714
- mockTree,
715
- callCounters: mockTree ? /* @__PURE__ */ new Map() : void 0,
716
- mockStrategy,
717
- mockOverrides: hasOverrides ? resolvedOverrides : void 0,
718
- fetchSpanOutput,
719
- dbBranchLease: lease,
720
- pendingPersistence
721
- },
722
- () => fn(...inputs)
723
- );
724
- result = maybePromise instanceof Promise ? await maybePromise : maybePromise;
725
- } catch (e) {
726
- error = e instanceof Error ? e.message : String(e);
727
- } finally {
728
- await Promise.allSettled(pendingPersistence);
729
- if (lease) {
730
- try {
731
- await httpClient.releaseDbBranchLease(lease.neonBranchId);
732
- } catch (e) {
733
- try {
734
- console.warn(
735
- `Bitfab: failed to release DB branch ${lease.neonBranchId} (TTL janitor will catch it): ${e instanceof Error ? e.message : String(e)}`
736
- );
737
- } catch {
738
- }
739
- }
740
- }
404
+ function spanToOtlp(span) {
405
+ const spanContext = span.spanContext();
406
+ const result = {
407
+ traceId: spanContext.traceId,
408
+ spanId: spanContext.spanId,
409
+ name: span.name,
410
+ kind: span.kind + 1,
411
+ startTimeUnixNano: hrTimeToNanoString(span.startTime),
412
+ endTimeUnixNano: hrTimeToNanoString(span.endTime),
413
+ attributes: otlpAttributes(span.attributes),
414
+ droppedAttributesCount: span.droppedAttributesCount,
415
+ droppedEventsCount: span.droppedEventsCount,
416
+ droppedLinksCount: span.droppedLinksCount,
417
+ status: {
418
+ code: span.status.code,
419
+ ...span.status.message ? { message: span.status.message } : {}
420
+ },
421
+ flags: spanContext.traceFlags
422
+ };
423
+ const parentSpanId = span.parentSpanContext?.spanId;
424
+ if (parentSpanId) {
425
+ result.parentSpanId = parentSpanId;
426
+ }
427
+ if (spanContext.traceState) {
428
+ result.traceState = spanContext.traceState.serialize();
741
429
  }
430
+ return result;
431
+ }
432
+ function buildOtlpRequest(first, spans) {
433
+ const scope = first.instrumentationScope;
742
434
  return {
743
- // Written in by replay() from the complete-replay response once the server
744
- // has minted this replay trace's row. Null until then: the client-side
745
- // correlation id (replayedTraceId) is never surfaced as the item's traceId.
746
- traceId: null,
747
- originalTraceId,
748
- originalSpanId,
749
- // Deprecated aliases for originalTraceId/originalSpanId.
750
- sourceTraceId: originalTraceId,
751
- sourceSpanId: originalSpanId,
752
- input: inputs,
753
- result,
754
- originalOutput,
755
- error,
756
- durationMs: serverItem.durationMs ?? null,
757
- // Filled in by replay() from the complete-replay response once the
758
- // replay traces are persisted and their spans aggregated server-side.
759
- // Null here (and on older servers) means "replay tokens not known".
760
- tokens: null,
761
- model: serverItem.model ?? null,
762
- dbSnapshotRef: dbSnapshotRef ?? null
435
+ resourceSpans: [
436
+ {
437
+ resource: {
438
+ attributes: otlpAttributes(
439
+ first.resource.attributes
440
+ )
441
+ },
442
+ scopeSpans: [
443
+ {
444
+ scope: { name: scope.name, version: scope.version ?? "" },
445
+ spans
446
+ }
447
+ ]
448
+ }
449
+ ]
763
450
  };
764
451
  }
765
- async function mapWithConcurrency(tasks, maxConcurrency, onSettled) {
766
- const results = new Array(tasks.length);
767
- let nextIndex = 0;
768
- async function worker() {
769
- while (nextIndex < tasks.length) {
770
- const index = nextIndex++;
771
- const result = await tasks[index]();
772
- results[index] = result;
773
- onSettled?.(result, index);
452
+ function encodedSize(value) {
453
+ const json = JSON.stringify(value);
454
+ if (typeof TextEncoder !== "undefined") {
455
+ return new TextEncoder().encode(json).length;
456
+ }
457
+ return json.length;
458
+ }
459
+ function delay(ms) {
460
+ return new Promise((resolve) => {
461
+ const timer = setTimeout(resolve, ms);
462
+ unrefTimer(timer);
463
+ });
464
+ }
465
+ async function withDeadline(work, timeoutMs) {
466
+ let timer;
467
+ try {
468
+ return await Promise.race([
469
+ work,
470
+ new Promise((resolve) => {
471
+ timer = setTimeout(() => resolve(false), Math.max(0, timeoutMs));
472
+ unrefTimer(timer);
473
+ })
474
+ ]);
475
+ } finally {
476
+ if (timer) {
477
+ clearTimeout(timer);
774
478
  }
775
479
  }
480
+ }
481
+ async function mapWithConcurrency(items, limit, task) {
482
+ const results = new Array(items.length);
483
+ let next = 0;
776
484
  const workers = Array.from(
777
- { length: Math.min(maxConcurrency, tasks.length) },
778
- () => worker()
485
+ { length: Math.min(Math.max(limit, 1), items.length) },
486
+ async () => {
487
+ while (next < items.length) {
488
+ const index = next;
489
+ next += 1;
490
+ results[index] = await task(items[index]);
491
+ }
492
+ }
779
493
  );
780
494
  await Promise.all(workers);
781
495
  return results;
782
496
  }
783
- async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, registeredOverrides = []) {
784
- if (options?.traceIds !== void 0) {
785
- if (options.traceIds.length === 0) {
786
- throw new BitfabError("traceIds must contain at least one trace ID.");
787
- }
788
- if (options.traceIds.length > 100) {
789
- throw new BitfabError(
790
- `traceIds supports at most 100 trace IDs per replay (got ${options.traceIds.length}).`
791
- );
792
- }
497
+ function responseStatus(error) {
498
+ return error instanceof BitfabError ? error.status : void 0;
499
+ }
500
+ function isRetryable(error) {
501
+ const status = responseStatus(error);
502
+ if (status === void 0) {
503
+ return true;
793
504
  }
794
- if (options?.limit !== void 0 && options?.traceIds !== void 0) {
795
- try {
796
- console.warn(
797
- "Bitfab: limit is ignored when traceIds is passed: the explicit trace ID list already determines how many traces replay."
798
- );
799
- } catch {
505
+ return RETRYABLE_STATUSES.has(status) || status >= 500;
506
+ }
507
+ function normalizeCollectorEndpoint(endpoint) {
508
+ const trimmed = endpoint.replace(/\/+$/, "");
509
+ return trimmed.endsWith("/v1/traces") ? trimmed : `${trimmed}/v1/traces`;
510
+ }
511
+ function endSpan(span, endTime) {
512
+ span.end(endTime);
513
+ }
514
+ function spanName(operation, payload) {
515
+ if (operation === "external_span") {
516
+ const spanData = asRecord(asRecord(payload.rawSpan)?.span_data);
517
+ if (typeof spanData?.name === "string") {
518
+ return spanData.name;
800
519
  }
801
520
  }
802
- await replayContextReady;
803
- let codeChangeDescription = options?.codeChangeDescription;
804
- let codeChangeFiles = options?.codeChangeFiles;
805
- if (codeChangeFiles === void 0) {
806
- const captured = await resolveAutoCodeChange(options?.name);
807
- if (captured) {
808
- codeChangeFiles = captured.files;
809
- if (codeChangeDescription === void 0) {
810
- codeChangeDescription = captured.description;
811
- }
812
- }
521
+ if (typeof payload.traceFunctionKey === "string") {
522
+ return payload.traceFunctionKey;
813
523
  }
814
- const {
815
- testRunId,
816
- testRunUrl,
817
- items: serverItems
818
- } = await httpClient.startReplay(
819
- traceFunctionKey,
820
- // limit is meaningless with explicit traceIds (the ID list determines
821
- // the count), so it's omitted from the request entirely.
822
- options?.traceIds ? void 0 : options?.limit ?? 5,
823
- options?.traceIds,
824
- options?.name,
825
- codeChangeDescription,
826
- codeChangeFiles,
827
- dbBranchEnabled(options?.dbBranch),
828
- // includeDbBranchLease
829
- options?.experimentGroupId,
830
- options?.datasetId,
831
- options?.graderIds,
832
- resolveDbBranchSettings(options?.dbBranch)
833
- );
834
- const mockStrategy = options?.mock ?? "marked";
835
- const maxConcurrency = options?.maxConcurrency ?? 10;
836
- const resolvedOverrides = [
837
- ...normalizeMockOverrides(options?.mockOverride),
838
- ...registeredOverrides
839
- ];
840
- const replayedTraceIds = serverItems.map(() => randomUuid());
841
- const tasks = serverItems.map(
842
- (serverItem, index) => () => processItem(
843
- httpClient,
844
- serverItem,
845
- fn,
846
- testRunId,
847
- mockStrategy,
848
- resolvedOverrides,
849
- replayedTraceIds[index],
850
- dbBranchEnabled(options?.dbBranch),
851
- resolveDbBranchSettings(options?.dbBranch),
852
- options?.adaptInputs
524
+ return `bitfab.${operation}`;
525
+ }
526
+ function payloadTimestamp(payload, field) {
527
+ const rawSpan = asRecord(payload.rawSpan);
528
+ const rawTrace = asRecord(payload.externalTrace) ?? asRecord(payload.rawTrace);
529
+ const raw = rawSpan?.[field] ?? rawTrace?.[field];
530
+ if (typeof raw !== "string") {
531
+ return void 0;
532
+ }
533
+ const parsed = Date.parse(raw);
534
+ return Number.isNaN(parsed) ? void 0 : parsed;
535
+ }
536
+ function hasError(payload) {
537
+ const spanData = asRecord(asRecord(payload.rawSpan)?.span_data);
538
+ if (spanData?.error != null) {
539
+ return true;
540
+ }
541
+ const errors = payload.errors;
542
+ return Array.isArray(errors) ? errors.length > 0 : Boolean(errors);
543
+ }
544
+ function createOtelTransport(options) {
545
+ return new OtelBatchTransport({
546
+ ...options,
547
+ collectorEndpoint: readEnv(COLLECTOR_ENDPOINT_ENV) || void 0,
548
+ exportConcurrency: readBoundedIntEnv(
549
+ EXPORT_CONCURRENCY_ENV,
550
+ MAX_EXPORT_CONCURRENCY,
551
+ DEFAULT_EXPORT_CONCURRENCY,
552
+ "otel-export-concurrency-invalid"
553
+ ),
554
+ maxRequestBytes: readBoundedIntEnv(
555
+ MAX_REQUEST_BYTES_ENV,
556
+ MAX_EXPORT_REQUEST_BYTES,
557
+ MAX_EXPORT_REQUEST_BYTES,
558
+ "otel-max-request-bytes-invalid"
853
559
  )
560
+ });
561
+ }
562
+ async function forEachLiveTransport(timeoutMs, run) {
563
+ const deadline = Date.now() + Math.max(timeoutMs, 0);
564
+ let succeeded = true;
565
+ for (const transport of [...liveTransports]) {
566
+ succeeded = await run(transport, Math.max(0, deadline - Date.now())) && succeeded;
567
+ }
568
+ return succeeded;
569
+ }
570
+ function flushOtelTransports(timeoutMs = DEFAULT_LIFECYCLE_TIMEOUT_MS) {
571
+ return forEachLiveTransport(
572
+ timeoutMs,
573
+ (transport, remaining) => transport.flush(remaining)
854
574
  );
855
- const total = tasks.length;
856
- let completed = 0;
857
- let succeeded = 0;
858
- let errored = 0;
859
- const resultItems = await mapWithConcurrency(
860
- tasks,
861
- maxConcurrency,
862
- options?.onProgress ? (item) => {
863
- completed += 1;
864
- if (item.error === null) {
865
- succeeded += 1;
866
- } else {
867
- errored += 1;
575
+ }
576
+ function shutdownOtelTransports(timeoutMs = DEFAULT_LIFECYCLE_TIMEOUT_MS) {
577
+ return forEachLiveTransport(
578
+ timeoutMs,
579
+ (transport, remaining) => transport.shutdown(remaining)
580
+ );
581
+ }
582
+ var import_api, import_core, import_resources, import_sdk_trace_base, OPERATION_ATTRIBUTE, PAYLOAD_ATTRIBUTE, OTLP_TRACES_ENDPOINT, MAX_EXPORT_REQUEST_BYTES, MAX_REQUEST_BYTES_ENV, EXPORT_CONCURRENCY_ENV, COLLECTOR_ENDPOINT_ENV, MAX_QUEUE_SIZE, DIRECT_MAX_EXPORT_BATCH_SIZE, COLLECTOR_MAX_EXPORT_BATCH_SIZE, DIRECT_MAX_REQUEST_BATCH_SIZE, DEFAULT_EXPORT_CONCURRENCY, MAX_EXPORT_CONCURRENCY, SCHEDULE_DELAY_MILLIS, EXPORT_TIMEOUT_MILLIS, RETRY_DELAY_MILLIS, MAX_SEND_ATTEMPTS, DEFAULT_LIFECYCLE_TIMEOUT_MS, RETRYABLE_STATUSES, liveTransports, traceSubmissionSpanIds, replayTraceSubmissions, submissionCounter, OtlpPayloadTooLargeError, OtlpPartialSuccessError, BitfabSpanExporter, CollectorSpanExporter, DeliveryTrackingExporter, OtelBatchTransport;
583
+ var init_otel = __esm({
584
+ "src/otel.ts"() {
585
+ "use strict";
586
+ import_api = require("@opentelemetry/api");
587
+ import_core = require("@opentelemetry/core");
588
+ import_resources = require("@opentelemetry/resources");
589
+ import_sdk_trace_base = require("@opentelemetry/sdk-trace-base");
590
+ init_constants();
591
+ init_errors();
592
+ init_readEnv();
593
+ init_serializePayload();
594
+ init_unrefTimer();
595
+ init_warnOnce();
596
+ OPERATION_ATTRIBUTE = "bitfab.operation";
597
+ PAYLOAD_ATTRIBUTE = "bitfab.payload";
598
+ OTLP_TRACES_ENDPOINT = "/api/sdk/otel/v1/traces";
599
+ MAX_EXPORT_REQUEST_BYTES = 3e6;
600
+ MAX_REQUEST_BYTES_ENV = "BITFAB_OTEL_MAX_REQUEST_BYTES";
601
+ EXPORT_CONCURRENCY_ENV = "BITFAB_OTEL_EXPORT_CONCURRENCY";
602
+ COLLECTOR_ENDPOINT_ENV = "BITFAB_OTEL_EXPORTER_ENDPOINT";
603
+ MAX_QUEUE_SIZE = 8192;
604
+ DIRECT_MAX_EXPORT_BATCH_SIZE = 512;
605
+ COLLECTOR_MAX_EXPORT_BATCH_SIZE = 32;
606
+ DIRECT_MAX_REQUEST_BATCH_SIZE = 8;
607
+ DEFAULT_EXPORT_CONCURRENCY = 32;
608
+ MAX_EXPORT_CONCURRENCY = 64;
609
+ SCHEDULE_DELAY_MILLIS = 5e3;
610
+ EXPORT_TIMEOUT_MILLIS = 3e4;
611
+ RETRY_DELAY_MILLIS = 100;
612
+ MAX_SEND_ATTEMPTS = 3;
613
+ DEFAULT_LIFECYCLE_TIMEOUT_MS = 3e4;
614
+ RETRYABLE_STATUSES = /* @__PURE__ */ new Set([408, 425, 429]);
615
+ liveTransports = /* @__PURE__ */ new Set();
616
+ traceSubmissionSpanIds = /* @__PURE__ */ new Map();
617
+ replayTraceSubmissions = /* @__PURE__ */ new Set();
618
+ submissionCounter = 0;
619
+ OtlpPayloadTooLargeError = class extends Error {
620
+ };
621
+ OtlpPartialSuccessError = class extends Error {
622
+ };
623
+ BitfabSpanExporter = class {
624
+ constructor(directSender, maxRequestBytes, maxRequestBatchSize, exportConcurrency) {
625
+ this.directSender = directSender;
626
+ this.maxRequestBytes = maxRequestBytes;
627
+ this.maxRequestBatchSize = maxRequestBatchSize;
628
+ this.exportConcurrency = exportConcurrency;
868
629
  }
869
- try {
870
- options?.onProgress?.({
871
- testRunId,
872
- completed,
873
- total,
874
- succeeded,
875
- errored,
876
- item: {
877
- // The server replay trace id isn't known until completeReplay
878
- // runs (below), so it can't be reported mid-run and we never
879
- // emit the client-side placeholder. originalTraceId (the
880
- // historical trace) is known now and is what a UI keys on to
881
- // identify what just settled.
882
- traceId: null,
883
- originalTraceId: item.originalTraceId ?? null,
884
- originalSpanId: item.originalSpanId ?? null,
885
- // Deprecated aliases for originalTraceId/originalSpanId.
886
- sourceTraceId: item.originalTraceId ?? null,
887
- sourceSpanId: item.originalSpanId ?? null,
888
- input: item.input,
889
- result: item.result,
890
- originalOutput: item.originalOutput,
891
- error: item.error,
892
- durationMs: item.durationMs,
893
- tokens: item.tokens,
894
- model: item.model,
895
- dbSnapshotRef: item.dbSnapshotRef
630
+ export(spans, resultCallback) {
631
+ void this.exportAsync(spans).then(
632
+ (succeeded) => {
633
+ resultCallback({
634
+ code: succeeded ? import_core.ExportResultCode.SUCCESS : import_core.ExportResultCode.FAILED
635
+ });
636
+ },
637
+ (error) => {
638
+ resultCallback({ code: import_core.ExportResultCode.FAILED, error });
639
+ }
640
+ );
641
+ }
642
+ async exportAsync(spans) {
643
+ if (spans.length === 0) {
644
+ return true;
645
+ }
646
+ let encoded;
647
+ try {
648
+ encoded = spans.map(spanToOtlp);
649
+ } catch (error) {
650
+ logError("failed to encode an OpenTelemetry span batch", error);
651
+ return false;
652
+ }
653
+ const first = spans[0];
654
+ const batches = this.buildRequestBatches(first, encoded);
655
+ const results = await mapWithConcurrency(
656
+ batches,
657
+ this.exportConcurrency,
658
+ (batch) => this.send(first, batch)
659
+ );
660
+ return results.every(Boolean);
661
+ }
662
+ buildRequestBatches(first, spans) {
663
+ const batches = [];
664
+ let current = [];
665
+ for (const span of spans) {
666
+ if (current.length >= this.maxRequestBatchSize) {
667
+ batches.push(current);
668
+ current = [];
669
+ }
670
+ const candidate = [...current, span];
671
+ if (current.length > 0 && encodedSize(buildOtlpRequest(first, candidate)) > this.maxRequestBytes) {
672
+ batches.push(current);
673
+ current = [span];
674
+ } else {
675
+ current = candidate;
676
+ }
677
+ }
678
+ if (current.length > 0) {
679
+ batches.push(current);
680
+ }
681
+ return batches;
682
+ }
683
+ async send(first, spans) {
684
+ const payload = buildOtlpRequest(first, spans);
685
+ if (encodedSize(payload) > this.maxRequestBytes) {
686
+ logError(
687
+ "a single OpenTelemetry span exceeded the configured request-size target and could not be exported"
688
+ );
689
+ return false;
690
+ }
691
+ try {
692
+ await this.sendWithRetries(payload);
693
+ return true;
694
+ } catch (error) {
695
+ if (error instanceof OtlpPayloadTooLargeError) {
696
+ logError(
697
+ 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"
698
+ );
699
+ return false;
700
+ }
701
+ if (error instanceof OtlpPartialSuccessError) {
702
+ return false;
703
+ }
704
+ logError("failed to export an OpenTelemetry span batch", error);
705
+ return false;
706
+ }
707
+ }
708
+ /**
709
+ * Retries transient failures. Span and trace-completion carriers are safe to
710
+ * retry: the server keys them idempotently on `sourceSpanId`/`sourceTraceId`,
711
+ * so a duplicate delivery cannot create a duplicate row.
712
+ *
713
+ * KNOWN LIMITATION: an `internal_trace` (a `call()` BAML trace) carries no
714
+ * such key, so retrying a batch that holds one can create a duplicate trace -
715
+ * including when a request times out client-side but the server goes on to
716
+ * persist it. Accepted deliberately for now, matching the other SDKs, rather
717
+ * than skipping retries for a whole batch or inventing an idempotency scheme
718
+ * the server does not yet understand. The fix is a client-supplied
719
+ * idempotency key that ingestion dedupes on.
720
+ */
721
+ async sendWithRetries(payload) {
722
+ for (let attempt = 0; attempt < MAX_SEND_ATTEMPTS; attempt += 1) {
723
+ try {
724
+ const response = await this.directSender(
725
+ OTLP_TRACES_ENDPOINT,
726
+ payload,
727
+ EXPORT_TIMEOUT_MILLIS
728
+ );
729
+ const partialSuccess = asRecord(response?.partialSuccess);
730
+ const rejected = partialSuccess?.rejectedSpans;
731
+ if (rejected !== void 0 && rejected !== "0" && rejected !== 0) {
732
+ logError(
733
+ `OTLP ingestion rejected ${rejected} span(s): ${partialSuccess?.errorMessage ?? "no reason provided"}`
734
+ );
735
+ throw new OtlpPartialSuccessError();
736
+ }
737
+ return;
738
+ } catch (error) {
739
+ if (error instanceof OtlpPartialSuccessError) {
740
+ throw error;
741
+ }
742
+ if (responseStatus(error) === 413) {
743
+ throw new OtlpPayloadTooLargeError();
744
+ }
745
+ if (attempt === MAX_SEND_ATTEMPTS - 1 || !isRetryable(error)) {
746
+ throw error;
747
+ }
748
+ await delay(RETRY_DELAY_MILLIS);
749
+ }
750
+ }
751
+ }
752
+ async shutdown() {
753
+ }
754
+ async forceFlush() {
755
+ }
756
+ };
757
+ CollectorSpanExporter = class {
758
+ constructor(endpoint, apiKey, maxRequestBytes) {
759
+ this.endpoint = endpoint;
760
+ this.apiKey = apiKey;
761
+ this.maxRequestBytes = maxRequestBytes;
762
+ }
763
+ /**
764
+ * Loaded through a dynamic import rather than a top-level one so bundlers
765
+ * code-split it: Collector delivery is opt-in, and a consumer who never sets
766
+ * an endpoint should not pay for the exporter in their initial bundle. It is
767
+ * a hard dependency, so this cannot fail for want of the package.
768
+ */
769
+ loadExporterModule() {
770
+ if (!this.pendingModule) {
771
+ this.pendingModule = import("@opentelemetry/exporter-trace-otlp-proto");
772
+ }
773
+ return this.pendingModule;
774
+ }
775
+ export(spans, resultCallback) {
776
+ void this.exportAsync(spans).then(
777
+ (succeeded) => {
778
+ resultCallback({
779
+ code: succeeded ? import_core.ExportResultCode.SUCCESS : import_core.ExportResultCode.FAILED
780
+ });
781
+ },
782
+ (error) => {
783
+ resultCallback({ code: import_core.ExportResultCode.FAILED, error });
784
+ }
785
+ );
786
+ }
787
+ async exportAsync(spans) {
788
+ if (spans.length === 0) {
789
+ return true;
790
+ }
791
+ let delegate;
792
+ try {
793
+ delegate = await this.resolveDelegate();
794
+ } catch (error) {
795
+ logError("failed to build the OTLP Collector exporter", error);
796
+ return false;
797
+ }
798
+ const results = await Promise.all(
799
+ this.partition(spans).map(
800
+ (batch) => new Promise((resolve) => {
801
+ try {
802
+ delegate.export(batch, (result) => {
803
+ resolve(result.code === import_core.ExportResultCode.SUCCESS);
804
+ });
805
+ } catch (error) {
806
+ logError("Collector export threw", error);
807
+ resolve(false);
808
+ }
809
+ })
810
+ )
811
+ );
812
+ return results.every(Boolean);
813
+ }
814
+ /**
815
+ * Partition by the encoded JSON size of each carrier rather than its encoded
816
+ * protobuf size. Protobuf is strictly smaller than the equivalent JSON for
817
+ * these payloads, so the JSON figure is a conservative bound that keeps every
818
+ * request under the target without pulling `@opentelemetry/otlp-transformer`
819
+ * into the dependency set purely to measure bytes.
820
+ */
821
+ partition(spans) {
822
+ const batches = [];
823
+ let current = [];
824
+ let currentSize = 0;
825
+ for (const span of spans) {
826
+ const size = encodedSize(spanToOtlp(span));
827
+ if (current.length > 0 && currentSize + size > this.maxRequestBytes) {
828
+ batches.push(current);
829
+ current = [];
830
+ currentSize = 0;
896
831
  }
832
+ current.push(span);
833
+ currentSize += size;
834
+ }
835
+ if (current.length > 0) {
836
+ batches.push(current);
837
+ }
838
+ return batches;
839
+ }
840
+ async resolveDelegate() {
841
+ const apiKey = this.apiKey() ?? "";
842
+ if (this.delegate && this.delegateApiKey === apiKey) {
843
+ return this.delegate;
844
+ }
845
+ const { OTLPTraceExporter } = await this.loadExporterModule();
846
+ const previous = this.delegate;
847
+ this.delegate = new OTLPTraceExporter({
848
+ url: this.endpoint,
849
+ headers: { Authorization: `Bearer ${apiKey}` },
850
+ timeoutMillis: EXPORT_TIMEOUT_MILLIS
897
851
  });
898
- } catch {
852
+ this.delegateApiKey = apiKey;
853
+ if (previous) {
854
+ void previous.shutdown().catch(() => {
855
+ });
856
+ }
857
+ return this.delegate;
899
858
  }
900
- } : void 0
901
- );
902
- const completeResult = await httpClient.completeReplay(testRunId);
903
- const serverTraceIds = completeResult.traceIds;
904
- const replayTokens = completeResult.tokens;
905
- if (serverTraceIds !== void 0) {
906
- const missing = [];
907
- let completedCount = 0;
908
- for (let index = 0; index < resultItems.length; index += 1) {
909
- const item = resultItems[index];
910
- const localId = replayedTraceIds[index];
911
- const mapped = localId ? serverTraceIds[localId] : void 0;
912
- item.traceId = mapped ?? null;
913
- if (item.error === null) {
914
- completedCount += 1;
915
- if (mapped === void 0) {
916
- missing.push(localId ?? item.originalTraceId);
859
+ async shutdown() {
860
+ await this.delegate?.shutdown();
861
+ }
862
+ async forceFlush() {
863
+ await this.delegate?.forceFlush?.();
864
+ }
865
+ };
866
+ DeliveryTrackingExporter = class {
867
+ constructor(exporter) {
868
+ this.exporter = exporter;
869
+ // Deliberately unscoped, matching the Python SDK. An export can outlive
870
+ // OTel's export timeout and report failure after the flush that was waiting
871
+ // on it already returned, so that failure surfaces on the NEXT flush instead.
872
+ // That over-reports: a good flush can inherit an older failure. The
873
+ // alternative - discarding failures from completed flush windows - under-
874
+ // reports, and `BatchSpanProcessor` also runs scheduled exports that belong
875
+ // to no flush at all, so their failures would vanish entirely. For a
876
+ // telemetry SDK a false "flush failed" is investigable; a false "flush
877
+ // succeeded" silently loses traces. We take the noisy direction on purpose.
878
+ this.failedExports = 0;
879
+ }
880
+ export(spans, resultCallback) {
881
+ try {
882
+ this.exporter.export(spans, (result) => {
883
+ if (result.code !== import_core.ExportResultCode.SUCCESS) {
884
+ this.failedExports += 1;
885
+ }
886
+ resultCallback(result);
887
+ });
888
+ } catch (error) {
889
+ this.failedExports += 1;
890
+ resultCallback({ code: import_core.ExportResultCode.FAILED, error });
917
891
  }
918
892
  }
919
- if (mapped !== void 0) {
920
- item.tokens = replayTokens?.[mapped] ?? null;
893
+ takeFailedExports() {
894
+ const failed = this.failedExports;
895
+ this.failedExports = 0;
896
+ return failed;
921
897
  }
922
- }
923
- if (completedCount > 0 && missing.length === completedCount) {
924
- const serverCount = completeResult.traceCount !== void 0 ? ` The server persisted ${completeResult.traceCount} trace(s) for this run.` : "";
925
- throw new BitfabError(
926
- `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.`
927
- );
928
- }
929
- if (missing.length > 0) {
930
- try {
931
- console.error(
932
- `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.`
898
+ shutdown() {
899
+ return this.exporter.shutdown();
900
+ }
901
+ forceFlush() {
902
+ return this.exporter.forceFlush?.() ?? Promise.resolve();
903
+ }
904
+ };
905
+ OtelBatchTransport = class {
906
+ constructor(options) {
907
+ this.closed = false;
908
+ const collectorEndpoint = options.collectorEndpoint;
909
+ const maxRequestBytes = options.maxRequestBytes ?? MAX_EXPORT_REQUEST_BYTES;
910
+ const maxRequestBatchSize = options.maxRequestBatchSize ?? DIRECT_MAX_REQUEST_BATCH_SIZE;
911
+ if (maxRequestBatchSize <= 0) {
912
+ throw new BitfabError("maxRequestBatchSize must be a positive integer");
913
+ }
914
+ this.deliveryTracker = new DeliveryTrackingExporter(
915
+ collectorEndpoint === void 0 ? new BitfabSpanExporter(
916
+ options.directSender,
917
+ maxRequestBytes,
918
+ maxRequestBatchSize,
919
+ options.exportConcurrency ?? DEFAULT_EXPORT_CONCURRENCY
920
+ ) : new CollectorSpanExporter(
921
+ normalizeCollectorEndpoint(collectorEndpoint),
922
+ options.apiKey,
923
+ maxRequestBytes
924
+ )
933
925
  );
934
- } catch {
926
+ this.processor = new import_sdk_trace_base.BatchSpanProcessor(this.deliveryTracker, {
927
+ maxQueueSize: options.maxQueueSize ?? MAX_QUEUE_SIZE,
928
+ maxExportBatchSize: options.maxExportBatchSize ?? (collectorEndpoint === void 0 ? DIRECT_MAX_EXPORT_BATCH_SIZE : COLLECTOR_MAX_EXPORT_BATCH_SIZE),
929
+ scheduledDelayMillis: SCHEDULE_DELAY_MILLIS,
930
+ exportTimeoutMillis: options.exportTimeoutMillis ?? EXPORT_TIMEOUT_MILLIS
931
+ });
932
+ this.provider = new import_sdk_trace_base.BasicTracerProvider({
933
+ sampler: new import_sdk_trace_base.AlwaysOnSampler(),
934
+ resource: (0, import_resources.resourceFromAttributes)({
935
+ "service.name": "bitfab-typescript-sdk",
936
+ "service.version": __version__
937
+ }),
938
+ spanLimits: {
939
+ attributeCountLimit: 2,
940
+ attributeValueLengthLimit: Number.POSITIVE_INFINITY
941
+ },
942
+ spanProcessors: [this.processor]
943
+ });
944
+ this.tracer = this.provider.getTracer("bitfab", __version__);
945
+ liveTransports.add(this);
935
946
  }
936
- }
947
+ submit(operation, payload) {
948
+ recordTraceSubmission(operation, payload);
949
+ if (this.closed) {
950
+ warnOnce(
951
+ "otel-submit-after-shutdown",
952
+ "OpenTelemetry transport is shut down; dropping spans"
953
+ );
954
+ return;
955
+ }
956
+ try {
957
+ const { body, dropped } = serializePayloadBody(payload);
958
+ if (dropped.length > 0) {
959
+ warnOnce(
960
+ "otel-carrier-payload-stubbed",
961
+ `a span payload held non-serializable value(s) (${[
962
+ ...new Set(dropped)
963
+ ].join(", ")}); they were stubbed so the span still ships, but the trace may be incomplete or not replayable.`
964
+ );
965
+ }
966
+ const span = this.tracer.startSpan(spanName(operation, payload), {
967
+ attributes: {
968
+ [OPERATION_ATTRIBUTE]: operation,
969
+ [PAYLOAD_ATTRIBUTE]: body
970
+ },
971
+ startTime: payloadTimestamp(payload, "started_at")
972
+ });
973
+ if (hasError(payload)) {
974
+ span.setStatus({ code: import_api.SpanStatusCode.ERROR });
975
+ }
976
+ endSpan(span, payloadTimestamp(payload, "ended_at"));
977
+ } catch (error) {
978
+ logError("failed to queue an OpenTelemetry span", error);
979
+ }
980
+ }
981
+ async flush(timeoutMs = DEFAULT_LIFECYCLE_TIMEOUT_MS) {
982
+ const pending = (this.pendingFlush ?? Promise.resolve(true)).then(
983
+ () => this.forceFlushOnce()
984
+ );
985
+ this.pendingFlush = pending.catch(() => false);
986
+ return withDeadline(pending, timeoutMs);
987
+ }
988
+ async forceFlushOnce() {
989
+ try {
990
+ await this.processor.forceFlush();
991
+ } catch (error) {
992
+ logError("failed to flush OpenTelemetry spans", error);
993
+ this.deliveryTracker.takeFailedExports();
994
+ return false;
995
+ }
996
+ return this.deliveryTracker.takeFailedExports() === 0;
997
+ }
998
+ async shutdown(timeoutMs = DEFAULT_LIFECYCLE_TIMEOUT_MS) {
999
+ const deadline = Date.now() + Math.max(timeoutMs, 0);
1000
+ this.closed = true;
1001
+ const flushed = await this.flush(Math.max(0, deadline - Date.now()));
1002
+ liveTransports.delete(this);
1003
+ const shutdownCompleted = await withDeadline(
1004
+ this.provider.shutdown().then(() => true).catch((error) => {
1005
+ logError("failed to shut down the OpenTelemetry transport", error);
1006
+ return false;
1007
+ }),
1008
+ Math.max(0, deadline - Date.now())
1009
+ );
1010
+ return flushed && shutdownCompleted;
1011
+ }
1012
+ };
937
1013
  }
938
- const result = {
939
- items: resultItems,
940
- testRunId,
941
- testRunUrl: `${serviceUrl}${testRunUrl}`
942
- };
943
- await writeReplayResultFile(result);
944
- try {
945
- options?.onProgress?.({
946
- type: "complete",
947
- testRunId,
948
- completed: total,
949
- total,
950
- succeeded,
951
- errored,
952
- result
953
- });
954
- } catch {
1014
+ });
1015
+
1016
+ // src/transport.ts
1017
+ function createTraceTransport(options) {
1018
+ return createOtelTransport(options);
1019
+ }
1020
+ function flushTraceTransports(timeoutMs) {
1021
+ return flushOtelTransports(timeoutMs);
1022
+ }
1023
+ function shutdownTraceTransports(timeoutMs) {
1024
+ return shutdownOtelTransports(timeoutMs);
1025
+ }
1026
+ function takeReplaySpanCounts2(traceIds) {
1027
+ return takeReplaySpanCounts(traceIds);
1028
+ }
1029
+ var init_transport = __esm({
1030
+ "src/transport.ts"() {
1031
+ "use strict";
1032
+ init_otel();
955
1033
  }
956
- return result;
1034
+ });
1035
+
1036
+ // src/http.ts
1037
+ function awaitOnExit(promise) {
1038
+ pendingTracePromises.add(promise);
1039
+ void promise.finally(() => {
1040
+ pendingTracePromises.delete(promise);
1041
+ }).catch(() => {
1042
+ });
1043
+ return promise;
957
1044
  }
958
- async function writeReplayResultFile(result) {
959
- const resultPath = typeof process !== "undefined" ? process.env?.BITFAB_REPLAY_RESULT_PATH : void 0;
960
- if (!resultPath) {
961
- return;
1045
+ async function flushTraces(timeoutMs = 5e3) {
1046
+ const deadline = Date.now() + Math.max(timeoutMs, 0);
1047
+ const requestsFlushed = await awaitPendingRequests(timeoutMs);
1048
+ const transportsFlushed = await flushTraceTransports(
1049
+ Math.max(0, deadline - Date.now())
1050
+ );
1051
+ return requestsFlushed && transportsFlushed;
1052
+ }
1053
+ async function awaitPendingRequests(timeoutMs = DEFAULT_LIFECYCLE_TIMEOUT_MS2) {
1054
+ await replayContextReady.catch(() => {
1055
+ });
1056
+ return waitForPromises(Array.from(pendingTracePromises), timeoutMs);
1057
+ }
1058
+ async function waitForPromises(promises, timeoutMs) {
1059
+ if (promises.length === 0) {
1060
+ return true;
962
1061
  }
1062
+ let timer;
963
1063
  try {
964
- const [{ dirname }, { mkdir, writeFile }] = await Promise.all([
965
- import("path"),
966
- import("fs/promises")
1064
+ return await Promise.race([
1065
+ Promise.allSettled(promises).then(() => true),
1066
+ new Promise((resolve) => {
1067
+ timer = setTimeout(() => resolve(false), timeoutMs);
1068
+ unrefTimer(timer);
1069
+ })
967
1070
  ]);
968
- await mkdir(dirname(resultPath), { recursive: true });
969
- await writeFile(resultPath, `${JSON.stringify(result, null, 2)}
970
- `);
971
- } catch (err) {
972
- try {
973
- console.warn(
974
- `Bitfab: failed to write replay result to BITFAB_REPLAY_RESULT_PATH (${resultPath}): ${err instanceof Error ? err.message : String(err)}`
975
- );
976
- } catch {
1071
+ } finally {
1072
+ if (timer) {
1073
+ clearTimeout(timer);
977
1074
  }
978
1075
  }
979
1076
  }
980
- var BITFAB_PROGRESS_PREFIX;
981
- var init_replay = __esm({
982
- "src/replay.ts"() {
1077
+ var REPLAY_DB_BRANCH_REQUEST_TIMEOUT_MS, EXIT_FLUSH_TIMEOUT_MS, DEFAULT_LIFECYCLE_TIMEOUT_MS2, pendingTracePromises, HttpClient;
1078
+ var init_http = __esm({
1079
+ "src/http.ts"() {
983
1080
  "use strict";
984
- init_codeChange();
1081
+ init_constants();
985
1082
  init_errors();
986
- init_mockOverride();
987
- init_randomUuid();
988
1083
  init_replayContext();
989
- init_serialize();
990
- BITFAB_PROGRESS_PREFIX = "@@bitfab:progress ";
991
- }
992
- });
993
-
994
- // src/index.ts
995
- var index_exports = {};
996
- __export(index_exports, {
997
- BITFAB_PROGRESS_PREFIX: () => BITFAB_PROGRESS_PREFIX,
998
- Bitfab: () => Bitfab,
999
- BitfabClaudeAgentHandler: () => BitfabClaudeAgentHandler,
1000
- BitfabError: () => BitfabError,
1001
- BitfabFunction: () => BitfabFunction,
1002
- BitfabLangChainCallbackHandler: () => BitfabLangGraphCallbackHandler,
1003
- BitfabLangGraphCallbackHandler: () => BitfabLangGraphCallbackHandler,
1004
- BitfabOpenAIAgentHandler: () => BitfabOpenAIAgentHandler,
1005
- BitfabOpenAITracingProcessor: () => BitfabOpenAITracingProcessor,
1006
- BitfabVercelAiHandler: () => BitfabVercelAiHandler,
1007
- DEFAULT_SERVICE_URL: () => DEFAULT_SERVICE_URL,
1008
- SUPPORTED_PROVIDERS: () => SUPPORTED_PROVIDERS,
1009
- __version__: () => __version__,
1010
- finalizers: () => finalizers,
1011
- flushTraces: () => flushTraces,
1012
- getCurrentReplayBranch: () => getCurrentReplayBranch,
1013
- getCurrentSpan: () => getCurrentSpan,
1014
- getCurrentTrace: () => getCurrentTrace,
1015
- reportReplayProgress: () => reportReplayProgress
1016
- });
1017
- module.exports = __toCommonJS(index_exports);
1018
-
1019
- // src/version.generated.ts
1020
- var __version__ = "0.33.7";
1021
-
1022
- // src/constants.ts
1023
- var DEFAULT_SERVICE_URL = "https://bitfab.ai";
1024
-
1025
- // src/http.ts
1026
- init_errors();
1027
-
1028
- // src/unrefTimer.ts
1029
- function unrefTimer(timer) {
1030
- const handle = timer;
1031
- if (typeof handle.unref === "function") {
1032
- handle.unref();
1033
- }
1034
- }
1035
-
1036
- // src/http.ts
1037
- init_warnOnce();
1038
- var REPLAY_DB_BRANCH_REQUEST_TIMEOUT_MS = 3e5;
1039
- function serializePayloadBody(payload) {
1040
- try {
1041
- return { body: JSON.stringify(payload), dropped: [] };
1042
- } catch {
1043
- const dropped = [];
1044
- const sanitize = (value, seen) => {
1045
- const t = typeof value;
1046
- if (value === null || t === "string" || t === "number" || t === "boolean") {
1047
- return value;
1084
+ init_serializePayload();
1085
+ init_transport();
1086
+ init_unrefTimer();
1087
+ init_warnOnce();
1088
+ REPLAY_DB_BRANCH_REQUEST_TIMEOUT_MS = 3e5;
1089
+ EXIT_FLUSH_TIMEOUT_MS = 5e3;
1090
+ DEFAULT_LIFECYCLE_TIMEOUT_MS2 = 3e4;
1091
+ pendingTracePromises = /* @__PURE__ */ new Set();
1092
+ if (typeof process !== "undefined" && process.versions != null && process.versions.node != null) {
1093
+ let isFlushing = false;
1094
+ process.on("beforeExit", () => {
1095
+ if (isFlushing) {
1096
+ return;
1097
+ }
1098
+ isFlushing = true;
1099
+ void Promise.allSettled([
1100
+ ...Array.from(pendingTracePromises).map((p) => p.catch(() => {
1101
+ })),
1102
+ shutdownTraceTransports(EXIT_FLUSH_TIMEOUT_MS).catch(() => false)
1103
+ ]).then(() => {
1104
+ isFlushing = false;
1105
+ });
1106
+ });
1107
+ }
1108
+ HttpClient = class {
1109
+ constructor(config) {
1110
+ // Deferred span work owned by THIS client. The module-global set backs the
1111
+ // process-wide `flushTraces()` and the exit hook, but per-client lifecycle
1112
+ // must not wait on another client's slow finalize: a false `close()` failure
1113
+ // caused by unrelated work is worse than no signal at all.
1114
+ this.deferredWork = /* @__PURE__ */ new Set();
1115
+ this.closed = false;
1116
+ this.apiKey = config.apiKey;
1117
+ this.serviceUrl = config.serviceUrl;
1118
+ this.timeout = config.timeout ?? 12e4;
1048
1119
  }
1049
- if (t === "bigint") {
1050
- dropped.push("BigInt");
1051
- return "<unserializable: BigInt>";
1120
+ /**
1121
+ * Resolve the API key at the moment it is needed (request time), invoking
1122
+ * the function form if one was supplied. Never read at construction.
1123
+ */
1124
+ resolveApiKey() {
1125
+ return typeof this.apiKey === "function" ? this.apiKey() : this.apiKey;
1052
1126
  }
1053
- if (t === "function") {
1054
- const name = value.name || "Function";
1055
- dropped.push(name);
1056
- return `<unserializable: ${name}>`;
1127
+ /**
1128
+ * This client's span transport, built on first use.
1129
+ *
1130
+ * Lazy on purpose: a client that never sends a span must never start a batch
1131
+ * worker. Every framework integration created from a `Bitfab` client shares
1132
+ * the owning client's `HttpClient`, so handlers reuse this one worker instead
1133
+ * of each spinning up their own.
1134
+ */
1135
+ getTraceTransport() {
1136
+ if (this.closed) {
1137
+ warnOnce(
1138
+ "http-client-closed",
1139
+ "the Bitfab client is closed; dropping spans"
1140
+ );
1141
+ return void 0;
1142
+ }
1143
+ if (!this.traceTransport) {
1144
+ this.traceTransport = createTraceTransport({
1145
+ apiKey: () => this.resolveApiKey(),
1146
+ directSender: (endpoint, payload, timeoutMs) => this.request(endpoint, payload, {
1147
+ timeout: timeoutMs
1148
+ })
1149
+ });
1150
+ }
1151
+ return this.traceTransport;
1057
1152
  }
1058
- if (t === "symbol") {
1059
- dropped.push("Symbol");
1060
- return "<unserializable: Symbol>";
1153
+ /**
1154
+ * Track deferred span work so this client's own lifecycle waits for it, and
1155
+ * so the process-wide flush and exit hook do too.
1156
+ */
1157
+ trackDeferred(promise) {
1158
+ this.deferredWork.add(promise);
1159
+ void promise.finally(() => this.deferredWork.delete(promise)).catch(() => {
1160
+ });
1161
+ return awaitOnExit(promise);
1061
1162
  }
1062
- if (t !== "object") {
1063
- return void 0;
1163
+ /**
1164
+ * Settle only THIS client's deferred span work. Scoped deliberately: the
1165
+ * global set can contain another client's long-running finalize, and
1166
+ * attributing its timeout here would fail a client whose own work succeeded.
1167
+ */
1168
+ async settleDeferredWork(timeoutMs = DEFAULT_LIFECYCLE_TIMEOUT_MS2) {
1169
+ await replayContextReady.catch(() => {
1170
+ });
1171
+ return waitForPromises(Array.from(this.deferredWork), timeoutMs);
1064
1172
  }
1065
- const obj = value;
1066
- const className = obj.constructor?.name || "object";
1067
- if (seen.has(obj)) {
1068
- dropped.push(className);
1069
- return `<cycle: ${className}>`;
1173
+ /**
1174
+ * Wait for spans queued by this client to be delivered, within one deadline.
1175
+ * Returns false on delivery failure or timeout.
1176
+ */
1177
+ async waitForPendingRequests(timeoutMs = DEFAULT_LIFECYCLE_TIMEOUT_MS2) {
1178
+ const deadline = Date.now() + Math.max(timeoutMs, 0);
1179
+ const settled = await this.settleDeferredWork(timeoutMs);
1180
+ const flushed = await this.traceTransport?.flush(Math.max(0, deadline - Date.now())) ?? true;
1181
+ return settled && flushed;
1070
1182
  }
1071
- seen.add(obj);
1072
- let result;
1073
- if (Array.isArray(obj)) {
1074
- result = obj.map((item) => sanitize(item, seen));
1075
- } else if (typeof obj.toJSON === "function") {
1183
+ /**
1184
+ * Flush and permanently close this client's tracing transport. Idempotent:
1185
+ * a second call joins the first rather than tearing down a pipeline the
1186
+ * first call already owns.
1187
+ */
1188
+ close(timeoutMs = DEFAULT_LIFECYCLE_TIMEOUT_MS2) {
1189
+ if (this.closing) {
1190
+ return this.closing;
1191
+ }
1192
+ const deadline = Date.now() + Math.max(timeoutMs, 0);
1193
+ this.closing = (async () => {
1194
+ const settled = await this.settleDeferredWork(
1195
+ Math.max(0, deadline - Date.now())
1196
+ );
1197
+ this.closed = true;
1198
+ const transport = this.traceTransport;
1199
+ this.traceTransport = void 0;
1200
+ const shutdownOk = await transport?.shutdown(Math.max(0, deadline - Date.now())) ?? true;
1201
+ return settled && shutdownOk;
1202
+ })();
1203
+ return this.closing;
1204
+ }
1205
+ /**
1206
+ * Make an HTTP request to the Bitfab API. Defaults to POST; pass
1207
+ * `options.method` to use a different verb (e.g. "PATCH").
1208
+ *
1209
+ * @param endpoint - The API endpoint (without base URL)
1210
+ * @param payload - The request body
1211
+ * @param options - Optional request options
1212
+ * @returns The parsed JSON response
1213
+ * @throws {BitfabError} If the request fails
1214
+ */
1215
+ async request(endpoint, payload, options) {
1216
+ const url = `${this.serviceUrl}${endpoint}`;
1217
+ const timeout = options?.timeout ?? this.timeout;
1218
+ const method = options?.method ?? "POST";
1219
+ const controller = new AbortController();
1220
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
1221
+ const { body, dropped } = serializePayloadBody(payload);
1222
+ if (dropped.length > 0) {
1223
+ try {
1224
+ console.warn(
1225
+ `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.`
1226
+ );
1227
+ } catch {
1228
+ }
1229
+ }
1076
1230
  try {
1077
- result = sanitize(obj.toJSON(), seen);
1078
- } catch {
1079
- dropped.push(className);
1080
- result = `<unserializable: ${className}>`;
1231
+ const response = await fetch(url, {
1232
+ method,
1233
+ headers: {
1234
+ "Content-Type": "application/json",
1235
+ Authorization: `Bearer ${this.resolveApiKey() ?? ""}`
1236
+ },
1237
+ body,
1238
+ signal: controller.signal
1239
+ });
1240
+ if (!response.ok) {
1241
+ const errorText = await response.text();
1242
+ throw new BitfabError(
1243
+ `HTTP ${response.status}: ${errorText.slice(0, 500)}`,
1244
+ void 0,
1245
+ response.status
1246
+ );
1247
+ }
1248
+ const result = await response.json();
1249
+ if (result.error) {
1250
+ if (result.url) {
1251
+ throw new BitfabError(
1252
+ `${result.error} Configure it at: ${this.serviceUrl}${result.url}`,
1253
+ result.url
1254
+ );
1255
+ }
1256
+ throw new BitfabError(result.error);
1257
+ }
1258
+ return result;
1259
+ } catch (error) {
1260
+ if (error instanceof BitfabError) {
1261
+ throw error;
1262
+ }
1263
+ if (error instanceof Error) {
1264
+ if (error.name === "AbortError") {
1265
+ throw new BitfabError(`Request timed out after ${timeout}ms`);
1266
+ }
1267
+ throw new BitfabError(error.message);
1268
+ }
1269
+ throw new BitfabError("Unknown error occurred");
1270
+ } finally {
1271
+ clearTimeout(timeoutId);
1081
1272
  }
1082
- } else {
1273
+ }
1274
+ /**
1275
+ * Look up a function by name.
1276
+ * Blocks until complete - needed for function execution.
1277
+ */
1278
+ async lookupFunction(name) {
1279
+ return this.request("/api/sdk/functions/lookup", { name });
1280
+ }
1281
+ async getTraceSpan(traceId, lookup) {
1282
+ const searchParams = new URLSearchParams();
1283
+ if (lookup.id !== void 0) {
1284
+ searchParams.set("id", lookup.id);
1285
+ } else {
1286
+ searchParams.set("name", lookup.name);
1287
+ searchParams.set("occurrence", String(lookup.occurrence ?? "last"));
1288
+ }
1289
+ const endpoint = `/api/sdk/traces/${encodeURIComponent(traceId)}/span?${searchParams.toString()}`;
1290
+ const response = await this.get(endpoint);
1291
+ return response.span;
1292
+ }
1293
+ async get(endpoint) {
1294
+ const url = `${this.serviceUrl}${endpoint}`;
1295
+ const controller = new AbortController();
1296
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
1297
+ try {
1298
+ const response = await fetch(url, {
1299
+ method: "GET",
1300
+ headers: { Authorization: `Bearer ${this.resolveApiKey() ?? ""}` },
1301
+ signal: controller.signal
1302
+ });
1303
+ if (!response.ok) {
1304
+ const errorText = await response.text();
1305
+ throw new BitfabError(
1306
+ `HTTP ${response.status}: ${errorText.slice(0, 500)}`
1307
+ );
1308
+ }
1309
+ return await response.json();
1310
+ } catch (error) {
1311
+ if (error instanceof BitfabError) {
1312
+ throw error;
1313
+ }
1314
+ if (error instanceof Error) {
1315
+ if (error.name === "AbortError") {
1316
+ throw new BitfabError(`Request timed out after ${this.timeout}ms`);
1317
+ }
1318
+ throw new BitfabError(error.message);
1319
+ }
1320
+ throw new BitfabError("Unknown error occurred");
1321
+ } finally {
1322
+ clearTimeout(timeoutId);
1323
+ }
1324
+ }
1325
+ /**
1326
+ * Queue an internal trace (from local BAML execution via `call()`) onto this
1327
+ * client's batching transport. `functionId` moves into the payload because
1328
+ * the OTLP carrier has no path to carry it.
1329
+ */
1330
+ sendInternalTrace(functionId, payload) {
1331
+ this.getTraceTransport()?.submit("internal_trace", {
1332
+ ...payload,
1333
+ functionId,
1334
+ sdkVersion: __version__
1335
+ });
1336
+ }
1337
+ /**
1338
+ * Queue an external span (from withSpan wrapper or OpenAI tracing) onto this
1339
+ * client's batching transport. Fire-and-forget: the transport owns delivery,
1340
+ * so callers await `flushTraces()` or `close()` rather than a per-span
1341
+ * promise.
1342
+ */
1343
+ sendExternalSpan(payload) {
1344
+ this.getTraceTransport()?.submit("external_span", {
1345
+ ...payload,
1346
+ sdkVersion: __version__
1347
+ });
1348
+ }
1349
+ /**
1350
+ * Queue an external trace completion (from OpenAI tracing) onto this
1351
+ * client's batching transport. Fire-and-forget for the same reason as
1352
+ * {@link HttpClient.sendExternalSpan}; replay confirms persistence with the
1353
+ * server-authoritative barrier in `replay.ts`, not by awaiting this call.
1354
+ */
1355
+ sendExternalTrace(payload) {
1356
+ this.getTraceTransport()?.submit("external_trace", {
1357
+ ...payload,
1358
+ sdkVersion: __version__
1359
+ });
1360
+ }
1361
+ /**
1362
+ * Partial update of an existing trace identified by its Bitfab trace ID.
1363
+ * Used by the detached `client.getTrace(id)` handle.
1364
+ *
1365
+ * Blocking, like the other trace-API calls: it resolves once the server has
1366
+ * applied the change and rejects if the server refused it. A patch targets a
1367
+ * trace that is already closed, so there is no batch for it to ride along
1368
+ * with and no later signal that would reveal a silent failure.
1369
+ */
1370
+ async patchTrace(traceId, payload) {
1371
+ const endpoint = `/api/sdk/traces/${encodeURIComponent(traceId)}`;
1372
+ await this.request(endpoint, payload, { method: "PATCH" });
1373
+ }
1374
+ /**
1375
+ * Start a replay session by fetching historical traces.
1376
+ * Blocking call - creates a test run and returns lightweight item references.
1377
+ */
1378
+ async startReplay(traceFunctionKey, limit, traceIds, name, codeChangeDescription, codeChangeFiles, includeDbBranchLease, experimentGroupId, datasetId, graderIds, dbBranchSettings) {
1379
+ const payload = { traceFunctionKey };
1380
+ if (limit !== void 0) {
1381
+ payload.limit = limit;
1382
+ }
1383
+ if (traceIds) {
1384
+ payload.traceIds = traceIds;
1385
+ }
1386
+ if (name !== void 0) {
1387
+ payload.name = name;
1388
+ }
1389
+ if (codeChangeDescription !== void 0) {
1390
+ payload.codeChangeDescription = codeChangeDescription;
1391
+ }
1392
+ if (codeChangeFiles !== void 0) {
1393
+ payload.codeChangeFiles = codeChangeFiles;
1394
+ }
1395
+ if (includeDbBranchLease) {
1396
+ payload.includeDbBranchLease = true;
1397
+ payload.lazyDbBranchLease = true;
1398
+ }
1399
+ if (experimentGroupId !== void 0) {
1400
+ payload.experimentGroupId = experimentGroupId;
1401
+ }
1402
+ if (datasetId !== void 0) {
1403
+ payload.datasetId = datasetId;
1404
+ }
1405
+ if (graderIds !== void 0) {
1406
+ payload.graderIds = graderIds;
1407
+ }
1408
+ if (dbBranchSettings !== void 0) {
1409
+ payload.dbBranchSettings = dbBranchSettings;
1410
+ }
1411
+ const timeout = includeDbBranchLease ? REPLAY_DB_BRANCH_REQUEST_TIMEOUT_MS : 3e4;
1412
+ return this.request("/api/sdk/replay/start", payload, {
1413
+ timeout
1414
+ });
1415
+ }
1416
+ /**
1417
+ * Fetch an external span by ID.
1418
+ * Blocking GET request.
1419
+ */
1420
+ async getExternalSpan(spanId) {
1421
+ const url = `${this.serviceUrl}/api/sdk/externalSpans/${spanId}`;
1422
+ const controller = new AbortController();
1423
+ const timeoutId = setTimeout(() => controller.abort(), 3e4);
1424
+ try {
1425
+ const response = await fetch(url, {
1426
+ method: "GET",
1427
+ headers: { Authorization: `Bearer ${this.resolveApiKey() ?? ""}` },
1428
+ signal: controller.signal
1429
+ });
1430
+ if (!response.ok) {
1431
+ const errorText = await response.text();
1432
+ throw new BitfabError(
1433
+ `HTTP ${response.status}: ${errorText.slice(0, 500)}`
1434
+ );
1435
+ }
1436
+ return await response.json();
1437
+ } catch (error) {
1438
+ if (error instanceof BitfabError) {
1439
+ throw error;
1440
+ }
1441
+ if (error instanceof Error) {
1442
+ if (error.name === "AbortError") {
1443
+ throw new BitfabError("Request timed out after 30000ms");
1444
+ }
1445
+ throw new BitfabError(error.message);
1446
+ }
1447
+ throw new BitfabError("Unknown error occurred");
1448
+ } finally {
1449
+ clearTimeout(timeoutId);
1450
+ }
1451
+ }
1452
+ /**
1453
+ * Fetch the span tree for a root span.
1454
+ * Blocking GET request.
1455
+ *
1456
+ * Pass `includeOutputs: false` for a payload-free tree (structure +
1457
+ * `externalSpanId` only), so recorded outputs are fetched lazily per mocked
1458
+ * span instead of all up front. Omit it (default eager) for `mock: "all"`.
1459
+ */
1460
+ async getSpanTree(externalSpanId, options) {
1461
+ const query = options?.includeOutputs === false ? "?includeOutputs=false" : "";
1462
+ const url = `${this.serviceUrl}/api/sdk/replay/spanTree/${externalSpanId}${query}`;
1463
+ const controller = new AbortController();
1464
+ const timeoutId = setTimeout(() => controller.abort(), 3e4);
1465
+ try {
1466
+ const response = await fetch(url, {
1467
+ method: "GET",
1468
+ headers: { Authorization: `Bearer ${this.resolveApiKey() ?? ""}` },
1469
+ signal: controller.signal
1470
+ });
1471
+ if (!response.ok) {
1472
+ const errorText = await response.text();
1473
+ throw new BitfabError(
1474
+ `HTTP ${response.status}: ${errorText.slice(0, 500)}`
1475
+ );
1476
+ }
1477
+ return await response.json();
1478
+ } catch (error) {
1479
+ if (error instanceof BitfabError) {
1480
+ throw error;
1481
+ }
1482
+ if (error instanceof Error) {
1483
+ if (error.name === "AbortError") {
1484
+ throw new BitfabError("Request timed out after 30000ms");
1485
+ }
1486
+ throw new BitfabError(error.message);
1487
+ }
1488
+ throw new BitfabError("Unknown error occurred");
1489
+ } finally {
1490
+ clearTimeout(timeoutId);
1491
+ }
1492
+ }
1493
+ /**
1494
+ * Read which of a replay run's traces the server has fully persisted.
1495
+ *
1496
+ * With `expectedSpanCounts`, a trace appears in the response only once it
1497
+ * has a final status AND at least that many persisted spans, which is what
1498
+ * makes this a real barrier rather than a "the row exists" check.
1499
+ */
1500
+ async getReplayStatus(testRunId, expectedSpanCounts) {
1501
+ return this.request(
1502
+ "/api/sdk/replay/status",
1503
+ { testRunId, expectedSpanCounts },
1504
+ { timeout: 3e4 }
1505
+ );
1506
+ }
1507
+ /**
1508
+ * Mark a replay test run as completed.
1509
+ * Blocking call.
1510
+ */
1511
+ async completeReplay(testRunId) {
1512
+ return this.request(
1513
+ "/api/sdk/replay/complete",
1514
+ { testRunId },
1515
+ { timeout: 3e4 }
1516
+ );
1517
+ }
1518
+ /**
1519
+ * Ask the server to materialize a per-trace DB branch lease from a
1520
+ * captured `dbSnapshotRef`. Blocking - the resolver creates a Neon
1521
+ * snapshot + preview branch and polls operations to readiness, which
1522
+ * can take seconds.
1523
+ */
1524
+ async resolveDbBranchLease(testRunId, traceId, dbBranchSettings) {
1525
+ return this.request(
1526
+ "/api/sdk/replay/resolveDbBranchLease",
1527
+ { testRunId, traceId, dbBranchSettings },
1528
+ { timeout: REPLAY_DB_BRANCH_REQUEST_TIMEOUT_MS }
1529
+ );
1530
+ }
1531
+ /** Release a previously-resolved DB branch by deleting its Neon branch. Idempotent server-side. */
1532
+ async releaseDbBranchLease(neonBranchId) {
1533
+ await this.request(
1534
+ "/api/sdk/replay/releaseDbBranchLease",
1535
+ { neonBranchId },
1536
+ { timeout: 3e4 }
1537
+ );
1538
+ }
1539
+ };
1540
+ }
1541
+ });
1542
+
1543
+ // src/serialize.ts
1544
+ function describeValue(value) {
1545
+ try {
1546
+ const ctorName = value?.constructor?.name;
1547
+ if (ctorName && ctorName !== "Object") {
1548
+ return ctorName;
1549
+ }
1550
+ } catch {
1551
+ }
1552
+ return typeof value;
1553
+ }
1554
+ function unserializableStub(value, reason) {
1555
+ warnOnce(
1556
+ `serialize:${reason.replace(/\d+/g, "N")}`,
1557
+ `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.`
1558
+ );
1559
+ let summary;
1560
+ try {
1561
+ summary = `<unserializable: ${describeValue(value)} (${reason})>`;
1562
+ } catch {
1563
+ summary = `<unserializable (${reason})>`;
1564
+ }
1565
+ return { json: summary };
1566
+ }
1567
+ function serializeValue(value) {
1568
+ try {
1569
+ const { json, meta } = import_superjson.default.serialize(value);
1570
+ let size;
1571
+ try {
1572
+ size = JSON.stringify(json).length;
1573
+ } catch {
1574
+ return unserializableStub(value, "stringify_failed_after_superjson");
1575
+ }
1576
+ if (size > MAX_SERIALIZED_BYTES) {
1577
+ return unserializableStub(value, `too_large_${size}_bytes`);
1578
+ }
1579
+ return meta ? { json, meta } : { json };
1580
+ } catch {
1581
+ try {
1582
+ return { json: JSON.parse(JSON.stringify(value)) };
1583
+ } catch {
1584
+ return unserializableStub(value, "json_stringify_failed");
1585
+ }
1586
+ }
1587
+ }
1588
+ function deserializeValue(serialized) {
1589
+ if (serialized.meta === void 0) {
1590
+ return serialized.json;
1591
+ }
1592
+ return import_superjson.default.deserialize({
1593
+ json: serialized.json,
1594
+ meta: serialized.meta
1595
+ });
1596
+ }
1597
+ function toJsonSafe(value) {
1598
+ return toJsonSafeReport(value).safe;
1599
+ }
1600
+ function toJsonSafeReport(value) {
1601
+ const dropped = [];
1602
+ const safe = toJsonSafeInner(value, 0, /* @__PURE__ */ new WeakSet(), dropped);
1603
+ try {
1604
+ const size = JSON.stringify(safe)?.length ?? 0;
1605
+ if (size > MAX_FRAMEWORK_SERIALIZED_BYTES) {
1606
+ warnOnce(
1607
+ "toJsonSafe:too_large",
1608
+ `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.`
1609
+ );
1610
+ return {
1611
+ safe: `<unserializable: too_large_${size}_bytes>`,
1612
+ dropped: [...dropped, `too_large_${size}_bytes`]
1613
+ };
1614
+ }
1615
+ } catch {
1616
+ }
1617
+ return { safe, dropped };
1618
+ }
1619
+ function toJsonSafeInner(value, depth, seen, dropped) {
1620
+ if (value === null || value === void 0) {
1621
+ return value;
1622
+ }
1623
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
1624
+ return value;
1625
+ }
1626
+ const className = value?.constructor?.name ?? typeof value;
1627
+ if (depth > MAX_SAFE_DEPTH) {
1628
+ dropped.push(className);
1629
+ return `<${className}>`;
1630
+ }
1631
+ if (typeof value !== "object") {
1632
+ if (typeof value === "function" || typeof value === "symbol") {
1633
+ dropped.push(className);
1634
+ }
1635
+ try {
1636
+ return String(value);
1637
+ } catch {
1638
+ dropped.push(className);
1639
+ return `<${className}>`;
1640
+ }
1641
+ }
1642
+ if (seen.has(value)) {
1643
+ dropped.push(className);
1644
+ return `<cycle ${className}>`;
1645
+ }
1646
+ seen.add(value);
1647
+ let result;
1648
+ if (Array.isArray(value)) {
1649
+ result = value.map(
1650
+ (item) => toJsonSafeInner(item, depth + 1, seen, dropped)
1651
+ );
1652
+ } else if (typeof value.toJSON === "function") {
1653
+ try {
1654
+ result = toJsonSafeInner(
1655
+ value.toJSON(),
1656
+ depth + 1,
1657
+ seen,
1658
+ dropped
1659
+ );
1660
+ } catch {
1661
+ dropped.push(className);
1662
+ result = `<${className}>`;
1663
+ }
1664
+ } else {
1665
+ try {
1666
+ const obj = {};
1667
+ for (const [k, v] of Object.entries(value)) {
1668
+ if (!k.startsWith("_")) {
1669
+ obj[k] = toJsonSafeInner(v, depth + 1, seen, dropped);
1670
+ }
1671
+ }
1672
+ result = obj;
1673
+ } catch {
1674
+ dropped.push(className);
1675
+ result = `<${className}>`;
1676
+ }
1677
+ }
1678
+ seen.delete(value);
1679
+ return result;
1680
+ }
1681
+ var import_superjson, MAX_SERIALIZED_BYTES, MAX_FRAMEWORK_SERIALIZED_BYTES, MAX_SAFE_DEPTH;
1682
+ var init_serialize = __esm({
1683
+ "src/serialize.ts"() {
1684
+ "use strict";
1685
+ import_superjson = __toESM(require("superjson"), 1);
1686
+ init_warnOnce();
1687
+ MAX_SERIALIZED_BYTES = 512e3;
1688
+ MAX_FRAMEWORK_SERIALIZED_BYTES = 2e6;
1689
+ MAX_SAFE_DEPTH = 6;
1690
+ }
1691
+ });
1692
+
1693
+ // src/randomUuid.ts
1694
+ function randomUuid() {
1695
+ const globalCrypto = globalThis.crypto;
1696
+ if (typeof globalCrypto?.randomUUID === "function") {
1697
+ try {
1698
+ return globalCrypto.randomUUID();
1699
+ } catch {
1700
+ }
1701
+ }
1702
+ warnOnce(
1703
+ "crypto-unavailable",
1704
+ "global crypto.randomUUID is unavailable; using a non-cryptographic fallback for trace/span ids. Tracing works normally (ids are correlation-only, not security-sensitive)."
1705
+ );
1706
+ return fallbackUuidV4();
1707
+ }
1708
+ function fallbackUuidV4() {
1709
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (char) => {
1710
+ const rand = Math.random() * 16 | 0;
1711
+ const value = char === "x" ? rand : rand & 3 | 8;
1712
+ return value.toString(16);
1713
+ });
1714
+ }
1715
+ var init_randomUuid = __esm({
1716
+ "src/randomUuid.ts"() {
1717
+ "use strict";
1718
+ init_warnOnce();
1719
+ }
1720
+ });
1721
+
1722
+ // src/mockOverride.ts
1723
+ function resolveMockValue(value, ctx) {
1724
+ return typeof value === "function" ? value(ctx) : value;
1725
+ }
1726
+ function normalizeMockOverrides(mockOverride) {
1727
+ if (mockOverride === void 0) {
1728
+ return [];
1729
+ }
1730
+ return Array.isArray(mockOverride) ? mockOverride : [mockOverride];
1731
+ }
1732
+ var init_mockOverride = __esm({
1733
+ "src/mockOverride.ts"() {
1734
+ "use strict";
1735
+ }
1736
+ });
1737
+
1738
+ // src/codeChange.ts
1739
+ async function resolveAutoCodeChange(label) {
1740
+ if (typeof process === "undefined") {
1741
+ return null;
1742
+ }
1743
+ if (process.env?.BITFAB_DISABLE_CODE_CHANGE_CAPTURE) {
1744
+ return null;
1745
+ }
1746
+ const fromEnv = await readCodeChangeFile();
1747
+ if (fromEnv) {
1748
+ return fromEnv;
1749
+ }
1750
+ return captureCodeChangeFromGit(process.cwd?.() ?? ".", label);
1751
+ }
1752
+ async function readCodeChangeFile() {
1753
+ const path = process.env?.BITFAB_CODE_CHANGE_PATH;
1754
+ if (!path) {
1755
+ return null;
1756
+ }
1757
+ try {
1758
+ const { readFile } = await import("fs/promises");
1759
+ const parsed = JSON.parse(await readFile(path, "utf8"));
1760
+ const files = Array.isArray(parsed?.files) && parsed.files.every(
1761
+ (f) => typeof f === "object" && f !== null && !Array.isArray(f)
1762
+ ) ? parsed.files : void 0;
1763
+ const description = typeof parsed?.description === "string" ? parsed.description : void 0;
1764
+ if (!files && description === void 0) {
1765
+ return null;
1766
+ }
1767
+ return { description, files };
1768
+ } catch {
1769
+ return null;
1770
+ }
1771
+ }
1772
+ async function captureCodeChangeFromGit(cwd, label) {
1773
+ let execFile;
1774
+ let readFile;
1775
+ try {
1776
+ ;
1777
+ ({ execFile } = await import("child_process"));
1778
+ ({ readFile } = await import("fs/promises"));
1779
+ } catch {
1780
+ return null;
1781
+ }
1782
+ const git = (dir, args) => new Promise((resolve) => {
1783
+ execFile(
1784
+ "git",
1785
+ args,
1786
+ // 30s timeout so a hung git (e.g. a network-touching ref op) can't
1787
+ // block the whole replay indefinitely.
1788
+ { cwd: dir, maxBuffer: 64 * 1024 * 1024, timeout: 3e4 },
1789
+ (err, stdout) => resolve(err ? null : stdout)
1790
+ );
1791
+ });
1792
+ try {
1793
+ const root = (await git(cwd, ["rev-parse", "--show-toplevel"]))?.trim();
1794
+ if (!root) {
1795
+ return null;
1796
+ }
1797
+ const resolved = await resolveBase(git, root);
1798
+ if (!resolved) {
1799
+ return null;
1800
+ }
1801
+ const { base, fromTrunk } = resolved;
1802
+ const blobBytes = async (ref, path) => {
1803
+ const out = await git(root, ["cat-file", "-s", `${ref}:${path}`]);
1804
+ const n = out ? Number.parseInt(out.trim(), 10) : Number.NaN;
1805
+ return Number.isFinite(n) ? n : 0;
1806
+ };
1807
+ const workingBytes = async (path) => {
1808
+ try {
1809
+ const { stat } = await import("fs/promises");
1810
+ const { join } = await import("path");
1811
+ return (await stat(join(root, path))).size;
1812
+ } catch {
1813
+ return 0;
1814
+ }
1815
+ };
1816
+ const tracked = await git(root, [
1817
+ "diff",
1818
+ "--name-status",
1819
+ "--no-renames",
1820
+ "-z",
1821
+ base,
1822
+ "--",
1823
+ ":!.bitfab"
1824
+ ]);
1825
+ const untracked = await git(root, [
1826
+ "ls-files",
1827
+ "--others",
1828
+ "--exclude-standard",
1829
+ "-z",
1830
+ "--",
1831
+ ":!.bitfab"
1832
+ ]);
1833
+ const entries = [
1834
+ ...parseNameStatusZ(tracked ?? ""),
1835
+ ...(untracked ?? "").split(NUL).filter((p) => p.length > 0).map((path) => ({ status: "A", path }))
1836
+ ];
1837
+ if (entries.length === 0) {
1838
+ return null;
1839
+ }
1840
+ const files = [];
1841
+ let totalBytes = 0;
1842
+ for (const { status, path } of entries) {
1843
+ if (files.length >= MAX_FILES) {
1844
+ break;
1845
+ }
1846
+ const beforeBytes = status === "A" ? 0 : await blobBytes(base, path);
1847
+ const afterBytes = status === "D" ? 0 : await workingBytes(path);
1848
+ if (beforeBytes > MAX_FILE_BYTES || afterBytes > MAX_FILE_BYTES) {
1849
+ continue;
1850
+ }
1851
+ const before = (status === "A" ? "" : await git(root, ["show", `${base}:${path}`]) ?? "").replace(/\r\n/g, "\n");
1852
+ const after = (status === "D" ? "" : await readWorkingFile(readFile, root, path)).replace(/\r\n/g, "\n");
1853
+ if (before === after) {
1854
+ continue;
1855
+ }
1856
+ const size = Buffer.byteLength(before, "utf8") + Buffer.byteLength(after, "utf8");
1857
+ if (totalBytes + size > MAX_TOTAL_BYTES || looksBinary(before) || looksBinary(after)) {
1858
+ continue;
1859
+ }
1860
+ totalBytes += size;
1861
+ files.push({ path, before, after });
1862
+ }
1863
+ if (files.length === 0) {
1864
+ return null;
1865
+ }
1866
+ const subject = (await git(root, ["log", "-1", "--format=%s", "HEAD"]))?.trim();
1867
+ const fileWord = files.length === 1 ? "file" : "files";
1868
+ const head = label?.trim() || subject || "Working-tree change";
1869
+ const against = fromTrunk ? "vs trunk" : "uncommitted (vs HEAD)";
1870
+ return {
1871
+ description: `${head} (${files.length} ${fileWord} changed ${against})`,
1872
+ files
1873
+ };
1874
+ } catch {
1875
+ return null;
1876
+ }
1877
+ }
1878
+ async function resolveBase(git, root) {
1879
+ const forced = process.env?.BITFAB_CODE_CHANGE_BASE;
1880
+ if (forced && await refExists(git, root, forced)) {
1881
+ const base = (await git(root, ["merge-base", "HEAD", forced]))?.trim() || (await git(root, ["rev-parse", "--verify", forced]))?.trim() || null;
1882
+ return base ? { base, fromTrunk: true } : null;
1883
+ }
1884
+ for (const candidate of TRUNK_CANDIDATES) {
1885
+ if (!await refExists(git, root, candidate)) {
1886
+ continue;
1887
+ }
1888
+ const mb = (await git(root, ["merge-base", "HEAD", candidate]))?.trim();
1889
+ if (mb) {
1890
+ return { base: mb, fromTrunk: true };
1891
+ }
1892
+ }
1893
+ return await refExists(git, root, "HEAD") ? { base: "HEAD", fromTrunk: false } : null;
1894
+ }
1895
+ async function refExists(git, root, ref) {
1896
+ return await git(root, ["rev-parse", "--verify", `${ref}^{object}`]) !== null;
1897
+ }
1898
+ async function readWorkingFile(readFile, root, path) {
1899
+ try {
1900
+ const { join } = await import("path");
1901
+ return await readFile(join(root, path), "utf8");
1902
+ } catch {
1903
+ return "";
1904
+ }
1905
+ }
1906
+ function parseNameStatusZ(raw) {
1907
+ const parts = raw.split(NUL).filter((p) => p.length > 0);
1908
+ const out = [];
1909
+ for (let i = 0; i + 1 < parts.length; i += 2) {
1910
+ out.push({ status: parts[i].charAt(0), path: parts[i + 1] });
1911
+ }
1912
+ return out;
1913
+ }
1914
+ function looksBinary(s) {
1915
+ return s.slice(0, 8e3).includes(NUL);
1916
+ }
1917
+ var MAX_FILES, MAX_FILE_BYTES, MAX_TOTAL_BYTES, TRUNK_CANDIDATES, NUL;
1918
+ var init_codeChange = __esm({
1919
+ "src/codeChange.ts"() {
1920
+ "use strict";
1921
+ MAX_FILES = 60;
1922
+ MAX_FILE_BYTES = 5e5;
1923
+ MAX_TOTAL_BYTES = 2e6;
1924
+ TRUNK_CANDIDATES = [
1925
+ "origin/HEAD",
1926
+ "origin/main",
1927
+ "origin/master",
1928
+ "main",
1929
+ "master"
1930
+ ];
1931
+ NUL = String.fromCharCode(0);
1932
+ }
1933
+ });
1934
+
1935
+ // src/replay.ts
1936
+ var replay_exports = {};
1937
+ __export(replay_exports, {
1938
+ BITFAB_PROGRESS_PREFIX: () => BITFAB_PROGRESS_PREFIX,
1939
+ replay: () => replay,
1940
+ reportReplayProgress: () => reportReplayProgress
1941
+ });
1942
+ function dbBranchEnabled(dbBranch) {
1943
+ return dbBranch !== void 0 && dbBranch !== false;
1944
+ }
1945
+ function resolveDbBranchSettings(dbBranch) {
1946
+ if (!dbBranch || dbBranch === true) {
1947
+ return void 0;
1948
+ }
1949
+ const { minCu, maxCu, warmupSql } = dbBranch;
1950
+ const settings = {
1951
+ ...minCu === void 0 ? {} : { minCu },
1952
+ ...maxCu === void 0 ? {} : { maxCu },
1953
+ ...warmupSql === void 0 ? {} : { warmupSql }
1954
+ };
1955
+ return Object.keys(settings).length === 0 ? void 0 : settings;
1956
+ }
1957
+ function reportReplayProgress(progress) {
1958
+ const stderr = typeof process !== "undefined" ? process.stderr : void 0;
1959
+ if (!stderr) {
1960
+ return;
1961
+ }
1962
+ try {
1963
+ stderr.write(`${BITFAB_PROGRESS_PREFIX}${JSON.stringify(progress)}
1964
+ `);
1965
+ } catch {
1966
+ }
1967
+ }
1968
+ function deserializeInputs(spanData) {
1969
+ const inputMeta = spanData.input_meta;
1970
+ const rawInput = spanData.input;
1971
+ if (inputMeta !== void 0 && inputMeta !== null) {
1972
+ const deserialized = deserializeValue({ json: rawInput, meta: inputMeta });
1973
+ if (Array.isArray(deserialized)) {
1974
+ return deserialized;
1975
+ }
1976
+ return deserialized !== void 0 && deserialized !== null ? [deserialized] : [];
1977
+ }
1978
+ if (Array.isArray(rawInput)) {
1979
+ return rawInput;
1980
+ }
1981
+ return rawInput !== void 0 && rawInput !== null ? [rawInput] : [];
1982
+ }
1983
+ function deserializeOutput(spanData) {
1984
+ const outputMeta = spanData.output_meta;
1985
+ const rawOutput = spanData.output;
1986
+ if (outputMeta !== void 0 && outputMeta !== null) {
1987
+ return deserializeValue({ json: rawOutput, meta: outputMeta });
1988
+ }
1989
+ return rawOutput;
1990
+ }
1991
+ function buildMockTree(rootNode) {
1992
+ const spans = /* @__PURE__ */ new Map();
1993
+ const counters = /* @__PURE__ */ new Map();
1994
+ function walk(node) {
1995
+ const key = node.traceFunctionKey;
1996
+ if (key) {
1997
+ const name = node.spanName || key;
1998
+ const counterKey = `${key}:${name}`;
1999
+ const index = counters.get(counterKey) ?? 0;
2000
+ counters.set(counterKey, index + 1);
2001
+ spans.set(`${counterKey}:${index}`, {
2002
+ sourceSpanId: node.sourceSpanId,
2003
+ externalSpanId: node.externalSpanId,
2004
+ output: node.output,
2005
+ outputMeta: node.outputMeta
2006
+ });
2007
+ }
2008
+ for (const child of node.children) {
2009
+ walk(child);
2010
+ }
2011
+ }
2012
+ for (const child of rootNode.children) {
2013
+ walk(child);
2014
+ }
2015
+ return { spans };
2016
+ }
2017
+ async function processItem(httpClient, serverItem, fn, testRunId, mockStrategy, resolvedOverrides, replayedTraceId, includeDbBranchLease, dbBranchSettings, adaptInputs) {
2018
+ let lease = includeDbBranchLease ? serverItem.dbBranchLease : void 0;
2019
+ let leaseError = includeDbBranchLease ? serverItem.dbBranchLeaseError : void 0;
2020
+ let dbSnapshotRef = serverItem.dbSnapshotRef;
2021
+ let inputs = [];
2022
+ let originalOutput;
2023
+ let result;
2024
+ let error = null;
2025
+ const originalTraceId = serverItem.originalTraceId ?? serverItem.sourceTraceId;
2026
+ const originalSpanId = serverItem.originalSpanId ?? serverItem.sourceSpanId;
2027
+ try {
2028
+ if (includeDbBranchLease && !lease && !leaseError) {
2029
+ const resolved = await httpClient.resolveDbBranchLease(
2030
+ testRunId,
2031
+ originalTraceId,
2032
+ dbBranchSettings
2033
+ );
2034
+ lease = resolved.lease ?? void 0;
2035
+ leaseError = resolved.leaseError ?? void 0;
2036
+ dbSnapshotRef = resolved.dbSnapshotRef ?? dbSnapshotRef;
2037
+ }
2038
+ if (leaseError) {
2039
+ throw new BitfabError(
2040
+ `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.`
2041
+ );
2042
+ }
2043
+ const span = await httpClient.getExternalSpan(originalSpanId);
2044
+ const spanData = span.rawData?.span_data ?? {};
2045
+ inputs = deserializeInputs(spanData);
2046
+ originalOutput = deserializeOutput(spanData);
2047
+ if (adaptInputs) {
2048
+ inputs = adaptInputs(inputs, {
2049
+ originalTraceId,
2050
+ originalSpanId,
2051
+ // Deprecated aliases for originalTraceId/originalSpanId.
2052
+ sourceTraceId: originalTraceId,
2053
+ sourceSpanId: originalSpanId
2054
+ });
2055
+ }
2056
+ const hasOverrides = resolvedOverrides.length > 0;
2057
+ const needTree = mockStrategy === "all" || mockStrategy === "marked" || hasOverrides;
2058
+ const includeOutputs = mockStrategy === "all";
2059
+ let mockTree;
2060
+ if (needTree) {
2061
+ try {
2062
+ const treeResponse = await httpClient.getSpanTree(originalSpanId, {
2063
+ includeOutputs
2064
+ });
2065
+ if (treeResponse.root) {
2066
+ mockTree = buildMockTree(treeResponse.root);
2067
+ } else if (mockStrategy === "all" || hasOverrides) {
2068
+ throw new BitfabError(
2069
+ `Replay mock strategy "${mockStrategy}"${hasOverrides ? " with overrides" : ""} requires a span tree root for original span ${originalSpanId}.`
2070
+ );
2071
+ } else {
2072
+ mockTree = void 0;
2073
+ }
2074
+ } catch (e) {
2075
+ if (mockStrategy === "all" || hasOverrides) {
2076
+ throw e;
2077
+ }
2078
+ mockTree = void 0;
2079
+ }
2080
+ }
2081
+ const outputCache = /* @__PURE__ */ new Map();
2082
+ const fetchSpanOutput = mockTree && !includeOutputs ? (externalSpanId) => {
2083
+ let pending = outputCache.get(externalSpanId);
2084
+ if (!pending) {
2085
+ pending = httpClient.getExternalSpan(externalSpanId).then(
2086
+ (s) => deserializeOutput(
2087
+ s.rawData?.span_data ?? {}
2088
+ )
2089
+ );
2090
+ outputCache.set(externalSpanId, pending);
2091
+ }
2092
+ return pending;
2093
+ } : void 0;
2094
+ const maybePromise = runWithReplayContext(
2095
+ {
2096
+ testRunId,
2097
+ traceId: replayedTraceId,
2098
+ inputSourceSpanId: span.id,
2099
+ inputSourceTraceId: span.externalTraceId,
2100
+ sourceBitfabTraceId: originalTraceId,
2101
+ mockTree,
2102
+ callCounters: mockTree ? /* @__PURE__ */ new Map() : void 0,
2103
+ mockStrategy,
2104
+ mockOverrides: hasOverrides ? resolvedOverrides : void 0,
2105
+ fetchSpanOutput,
2106
+ dbBranchLease: lease
2107
+ },
2108
+ () => fn(...inputs)
2109
+ );
2110
+ result = maybePromise instanceof Promise ? await maybePromise : maybePromise;
2111
+ } catch (e) {
2112
+ error = e instanceof Error ? e.message : String(e);
2113
+ } finally {
2114
+ if (lease) {
2115
+ try {
2116
+ await httpClient.releaseDbBranchLease(lease.neonBranchId);
2117
+ } catch (e) {
1083
2118
  try {
1084
- const out = {};
1085
- for (const [k, v] of Object.entries(obj)) {
1086
- out[k] = sanitize(v, seen);
1087
- }
1088
- result = out;
1089
- } catch {
1090
- warnOnce(
1091
- "payload:field-getter-threw",
1092
- "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."
2119
+ console.warn(
2120
+ `Bitfab: failed to release DB branch ${lease.neonBranchId} (TTL janitor will catch it): ${e instanceof Error ? e.message : String(e)}`
1093
2121
  );
1094
- dropped.push(className);
1095
- result = `<unserializable: ${className}>`;
2122
+ } catch {
1096
2123
  }
1097
2124
  }
1098
- seen.delete(obj);
1099
- return result;
1100
- };
1101
- let sanitized;
1102
- try {
1103
- sanitized = sanitize(payload, /* @__PURE__ */ new WeakSet());
1104
- } catch (error) {
1105
- const message = error instanceof Error ? error.message : String(error);
1106
- return {
1107
- body: JSON.stringify({ error: `payload_serialize_failed: ${message}` }),
1108
- dropped
1109
- };
1110
2125
  }
1111
- if (dropped.length > 0 && typeof sanitized === "object" && sanitized !== null && !Array.isArray(sanitized)) {
1112
- const obj = sanitized;
1113
- const existing = Array.isArray(obj.errors) ? obj.errors : [];
1114
- obj.errors = [
1115
- ...existing,
1116
- {
1117
- source: "sdk",
1118
- step: "json_serialize",
1119
- error: `stubbed non-serializable value(s): ${[
1120
- ...new Set(dropped)
1121
- ].join(", ")}`
1122
- }
1123
- ];
1124
- }
1125
- return { body: JSON.stringify(sanitized), dropped };
1126
2126
  }
2127
+ return {
2128
+ // Written in by replay() from the complete-replay response once the server
2129
+ // has minted this replay trace's row. Null until then: the client-side
2130
+ // correlation id (replayedTraceId) is never surfaced as the item's traceId.
2131
+ traceId: null,
2132
+ originalTraceId,
2133
+ originalSpanId,
2134
+ // Deprecated aliases for originalTraceId/originalSpanId.
2135
+ sourceTraceId: originalTraceId,
2136
+ sourceSpanId: originalSpanId,
2137
+ input: inputs,
2138
+ result,
2139
+ originalOutput,
2140
+ error,
2141
+ durationMs: serverItem.durationMs ?? null,
2142
+ // Filled in by replay() from the complete-replay response once the
2143
+ // replay traces are persisted and their spans aggregated server-side.
2144
+ // Null here (and on older servers) means "replay tokens not known".
2145
+ tokens: null,
2146
+ model: serverItem.model ?? null,
2147
+ dbSnapshotRef: dbSnapshotRef ?? null
2148
+ };
1127
2149
  }
1128
- var pendingTracePromises = /* @__PURE__ */ new Set();
1129
- function awaitOnExit(promise) {
1130
- pendingTracePromises.add(promise);
1131
- void promise.finally(() => {
1132
- pendingTracePromises.delete(promise);
1133
- }).catch(() => {
1134
- });
1135
- return promise;
1136
- }
1137
- async function flushTraces(timeoutMs = 5e3) {
1138
- if (pendingTracePromises.size === 0) {
2150
+ async function waitForReplayPersistence(httpClient, testRunId, replayedTraceIds) {
2151
+ const deferredSettled = await httpClient.settleDeferredWork(
2152
+ REPLAY_PERSISTENCE_TIMEOUT_MS
2153
+ );
2154
+ if (!deferredSettled) {
2155
+ throw new BitfabError(
2156
+ `Replay could not settle deferred span work before the deadline, so the expected span counts are incomplete (testRunId ${testRunId}).`
2157
+ );
2158
+ }
2159
+ const expectedSpanCounts = takeReplaySpanCounts2(replayedTraceIds);
2160
+ if (Object.keys(expectedSpanCounts).length === 0) {
1139
2161
  return;
1140
2162
  }
1141
- let timer;
1142
- try {
1143
- await Promise.race([
1144
- Promise.allSettled(Array.from(pendingTracePromises)),
1145
- new Promise((resolve) => {
1146
- timer = setTimeout(resolve, timeoutMs);
1147
- unrefTimer(timer);
1148
- })
1149
- ]);
1150
- } finally {
1151
- if (timer) {
1152
- clearTimeout(timer);
2163
+ const flushed = await flushTraces(REPLAY_PERSISTENCE_TIMEOUT_MS);
2164
+ const deadline = Date.now() + REPLAY_PERSISTENCE_TIMEOUT_MS;
2165
+ let missing = Object.keys(expectedSpanCounts).length;
2166
+ while (true) {
2167
+ const status = await httpClient.getReplayStatus(
2168
+ testRunId,
2169
+ expectedSpanCounts
2170
+ );
2171
+ const ready = status.traceIds ?? {};
2172
+ missing = Object.keys(expectedSpanCounts).filter(
2173
+ (traceId) => ready[traceId] === void 0
2174
+ ).length;
2175
+ if (missing === 0) {
2176
+ return;
1153
2177
  }
2178
+ if (Date.now() >= deadline) {
2179
+ break;
2180
+ }
2181
+ await sleep(Math.min(100, Math.max(0, deadline - Date.now())));
1154
2182
  }
2183
+ const cause = flushed ? "" : " Delivery was also not confirmed before the flush deadline, so the spans likely never reached the server.";
2184
+ throw new BitfabError(
2185
+ `Replay traces were not fully persisted before the delivery deadline (testRunId ${testRunId}, missing ${missing} of ${Object.keys(expectedSpanCounts).length} trace(s)).${cause}`
2186
+ );
1155
2187
  }
1156
- if (typeof process !== "undefined" && process.versions != null && process.versions.node != null) {
1157
- let isFlushing = false;
1158
- process.on("beforeExit", () => {
1159
- if (pendingTracePromises.size > 0 && !isFlushing) {
1160
- isFlushing = true;
1161
- Promise.allSettled(
1162
- Array.from(pendingTracePromises).map(
1163
- (p) => p.catch(() => {
1164
- })
1165
- )
1166
- ).then(() => {
1167
- isFlushing = false;
1168
- }).catch(() => {
1169
- isFlushing = false;
1170
- });
1171
- }
2188
+ function sleep(ms) {
2189
+ return new Promise((resolve) => {
2190
+ const timer = setTimeout(resolve, ms);
2191
+ unrefTimer(timer);
1172
2192
  });
1173
2193
  }
1174
- var HttpClient = class {
1175
- constructor(config) {
1176
- this.apiKey = config.apiKey;
1177
- this.serviceUrl = config.serviceUrl;
1178
- this.timeout = config.timeout ?? 12e4;
2194
+ async function mapWithConcurrency2(tasks, maxConcurrency, onSettled) {
2195
+ const results = new Array(tasks.length);
2196
+ let nextIndex = 0;
2197
+ async function worker() {
2198
+ while (nextIndex < tasks.length) {
2199
+ const index = nextIndex++;
2200
+ const result = await tasks[index]();
2201
+ results[index] = result;
2202
+ onSettled?.(result, index);
2203
+ }
1179
2204
  }
1180
- /**
1181
- * Resolve the API key at the moment it is needed (request time), invoking
1182
- * the function form if one was supplied. Never read at construction.
1183
- */
1184
- resolveApiKey() {
1185
- return typeof this.apiKey === "function" ? this.apiKey() : this.apiKey;
2205
+ const workers = Array.from(
2206
+ { length: Math.min(maxConcurrency, tasks.length) },
2207
+ () => worker()
2208
+ );
2209
+ await Promise.all(workers);
2210
+ return results;
2211
+ }
2212
+ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, registeredOverrides = []) {
2213
+ if (options?.traceIds !== void 0) {
2214
+ if (options.traceIds.length === 0) {
2215
+ throw new BitfabError("traceIds must contain at least one trace ID.");
2216
+ }
2217
+ if (options.traceIds.length > 100) {
2218
+ throw new BitfabError(
2219
+ `traceIds supports at most 100 trace IDs per replay (got ${options.traceIds.length}).`
2220
+ );
2221
+ }
1186
2222
  }
1187
- /**
1188
- * Make an HTTP request to the Bitfab API. Defaults to POST; pass
1189
- * `options.method` to use a different verb (e.g. "PATCH").
1190
- *
1191
- * @param endpoint - The API endpoint (without base URL)
1192
- * @param payload - The request body
1193
- * @param options - Optional request options
1194
- * @returns The parsed JSON response
1195
- * @throws {BitfabError} If the request fails
1196
- */
1197
- async request(endpoint, payload, options) {
1198
- const url = `${this.serviceUrl}${endpoint}`;
1199
- const timeout = options?.timeout ?? this.timeout;
1200
- const method = options?.method ?? "POST";
1201
- const controller = new AbortController();
1202
- const timeoutId = setTimeout(() => controller.abort(), timeout);
1203
- const { body, dropped } = serializePayloadBody(payload);
1204
- if (dropped.length > 0) {
1205
- try {
1206
- console.warn(
1207
- `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.`
1208
- );
1209
- } catch {
2223
+ if (options?.limit !== void 0 && options?.traceIds !== void 0) {
2224
+ try {
2225
+ console.warn(
2226
+ "Bitfab: limit is ignored when traceIds is passed: the explicit trace ID list already determines how many traces replay."
2227
+ );
2228
+ } catch {
2229
+ }
2230
+ }
2231
+ await replayContextReady;
2232
+ let codeChangeDescription = options?.codeChangeDescription;
2233
+ let codeChangeFiles = options?.codeChangeFiles;
2234
+ if (codeChangeFiles === void 0) {
2235
+ const captured = await resolveAutoCodeChange(options?.name);
2236
+ if (captured) {
2237
+ codeChangeFiles = captured.files;
2238
+ if (codeChangeDescription === void 0) {
2239
+ codeChangeDescription = captured.description;
1210
2240
  }
1211
2241
  }
1212
- try {
1213
- const response = await fetch(url, {
1214
- method,
1215
- headers: {
1216
- "Content-Type": "application/json",
1217
- Authorization: `Bearer ${this.resolveApiKey() ?? ""}`
1218
- },
1219
- body,
1220
- signal: controller.signal
1221
- });
1222
- if (!response.ok) {
1223
- const errorText = await response.text();
1224
- throw new BitfabError(
1225
- `HTTP ${response.status}: ${errorText.slice(0, 500)}`
1226
- );
2242
+ }
2243
+ const {
2244
+ testRunId,
2245
+ testRunUrl,
2246
+ items: serverItems
2247
+ } = await httpClient.startReplay(
2248
+ traceFunctionKey,
2249
+ // limit is meaningless with explicit traceIds (the ID list determines
2250
+ // the count), so it's omitted from the request entirely.
2251
+ options?.traceIds ? void 0 : options?.limit ?? 5,
2252
+ options?.traceIds,
2253
+ options?.name,
2254
+ codeChangeDescription,
2255
+ codeChangeFiles,
2256
+ dbBranchEnabled(options?.dbBranch),
2257
+ // includeDbBranchLease
2258
+ options?.experimentGroupId,
2259
+ options?.datasetId,
2260
+ options?.graderIds,
2261
+ resolveDbBranchSettings(options?.dbBranch)
2262
+ );
2263
+ const mockStrategy = options?.mock ?? "marked";
2264
+ const maxConcurrency = options?.maxConcurrency ?? 10;
2265
+ const resolvedOverrides = [
2266
+ ...normalizeMockOverrides(options?.mockOverride),
2267
+ ...registeredOverrides
2268
+ ];
2269
+ const replayedTraceIds = serverItems.map(() => randomUuid());
2270
+ const tasks = serverItems.map(
2271
+ (serverItem, index) => () => processItem(
2272
+ httpClient,
2273
+ serverItem,
2274
+ fn,
2275
+ testRunId,
2276
+ mockStrategy,
2277
+ resolvedOverrides,
2278
+ replayedTraceIds[index],
2279
+ dbBranchEnabled(options?.dbBranch),
2280
+ resolveDbBranchSettings(options?.dbBranch),
2281
+ options?.adaptInputs
2282
+ )
2283
+ );
2284
+ const total = tasks.length;
2285
+ let completed = 0;
2286
+ let succeeded = 0;
2287
+ let errored = 0;
2288
+ const resultItems = await mapWithConcurrency2(
2289
+ tasks,
2290
+ maxConcurrency,
2291
+ options?.onProgress ? (item) => {
2292
+ completed += 1;
2293
+ if (item.error === null) {
2294
+ succeeded += 1;
2295
+ } else {
2296
+ errored += 1;
1227
2297
  }
1228
- const result = await response.json();
1229
- if (result.error) {
1230
- if (result.url) {
1231
- throw new BitfabError(
1232
- `${result.error} Configure it at: ${this.serviceUrl}${result.url}`,
1233
- result.url
1234
- );
2298
+ try {
2299
+ options?.onProgress?.({
2300
+ testRunId,
2301
+ completed,
2302
+ total,
2303
+ succeeded,
2304
+ errored,
2305
+ item: {
2306
+ // The server replay trace id isn't known until completeReplay
2307
+ // runs (below), so it can't be reported mid-run and we never
2308
+ // emit the client-side placeholder. originalTraceId (the
2309
+ // historical trace) is known now and is what a UI keys on to
2310
+ // identify what just settled.
2311
+ traceId: null,
2312
+ originalTraceId: item.originalTraceId ?? null,
2313
+ originalSpanId: item.originalSpanId ?? null,
2314
+ // Deprecated aliases for originalTraceId/originalSpanId.
2315
+ sourceTraceId: item.originalTraceId ?? null,
2316
+ sourceSpanId: item.originalSpanId ?? null,
2317
+ input: item.input,
2318
+ result: item.result,
2319
+ originalOutput: item.originalOutput,
2320
+ error: item.error,
2321
+ durationMs: item.durationMs,
2322
+ tokens: item.tokens,
2323
+ model: item.model,
2324
+ dbSnapshotRef: item.dbSnapshotRef
2325
+ }
2326
+ });
2327
+ } catch {
2328
+ }
2329
+ } : void 0
2330
+ );
2331
+ await waitForReplayPersistence(httpClient, testRunId, replayedTraceIds);
2332
+ const completeResult = await httpClient.completeReplay(testRunId);
2333
+ const serverTraceIds = completeResult.traceIds;
2334
+ const replayTokens = completeResult.tokens;
2335
+ if (serverTraceIds !== void 0) {
2336
+ const missing = [];
2337
+ let completedCount = 0;
2338
+ for (let index = 0; index < resultItems.length; index += 1) {
2339
+ const item = resultItems[index];
2340
+ const localId = replayedTraceIds[index];
2341
+ const mapped = localId ? serverTraceIds[localId] : void 0;
2342
+ item.traceId = mapped ?? null;
2343
+ if (item.error === null) {
2344
+ completedCount += 1;
2345
+ if (mapped === void 0) {
2346
+ missing.push(localId ?? item.originalTraceId);
1235
2347
  }
1236
- throw new BitfabError(result.error);
1237
- }
1238
- return result;
1239
- } catch (error) {
1240
- if (error instanceof BitfabError) {
1241
- throw error;
1242
2348
  }
1243
- if (error instanceof Error) {
1244
- if (error.name === "AbortError") {
1245
- throw new BitfabError(`Request timed out after ${timeout}ms`);
1246
- }
1247
- throw new BitfabError(error.message);
2349
+ if (mapped !== void 0) {
2350
+ item.tokens = replayTokens?.[mapped] ?? null;
1248
2351
  }
1249
- throw new BitfabError("Unknown error occurred");
1250
- } finally {
1251
- clearTimeout(timeoutId);
1252
2352
  }
1253
- }
1254
- /**
1255
- * Look up a function by name.
1256
- * Blocks until complete - needed for function execution.
1257
- */
1258
- async lookupFunction(name) {
1259
- return this.request("/api/sdk/functions/lookup", { name });
1260
- }
1261
- async getTraceSpan(traceId, lookup) {
1262
- const searchParams = new URLSearchParams();
1263
- if (lookup.id !== void 0) {
1264
- searchParams.set("id", lookup.id);
1265
- } else {
1266
- searchParams.set("name", lookup.name);
1267
- searchParams.set("occurrence", String(lookup.occurrence ?? "last"));
1268
- }
1269
- const endpoint = `/api/sdk/traces/${encodeURIComponent(traceId)}/span?${searchParams.toString()}`;
1270
- const response = await this.get(endpoint);
1271
- return response.span;
1272
- }
1273
- async get(endpoint) {
1274
- const url = `${this.serviceUrl}${endpoint}`;
1275
- const controller = new AbortController();
1276
- const timeoutId = setTimeout(() => controller.abort(), this.timeout);
1277
- try {
1278
- const response = await fetch(url, {
1279
- method: "GET",
1280
- headers: { Authorization: `Bearer ${this.resolveApiKey() ?? ""}` },
1281
- signal: controller.signal
1282
- });
1283
- if (!response.ok) {
1284
- const errorText = await response.text();
1285
- throw new BitfabError(
1286
- `HTTP ${response.status}: ${errorText.slice(0, 500)}`
1287
- );
1288
- }
1289
- return await response.json();
1290
- } catch (error) {
1291
- if (error instanceof BitfabError) {
1292
- throw error;
1293
- }
1294
- if (error instanceof Error) {
1295
- if (error.name === "AbortError") {
1296
- throw new BitfabError(`Request timed out after ${this.timeout}ms`);
1297
- }
1298
- throw new BitfabError(error.message);
1299
- }
1300
- throw new BitfabError("Unknown error occurred");
1301
- } finally {
1302
- clearTimeout(timeoutId);
2353
+ if (completedCount > 0 && missing.length === completedCount) {
2354
+ const serverCount = completeResult.traceCount !== void 0 ? ` The server persisted ${completeResult.traceCount} trace(s) for this run.` : "";
2355
+ throw new BitfabError(
2356
+ `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.`
2357
+ );
1303
2358
  }
1304
- }
1305
- /**
1306
- * Send an internal trace (from BAML execution).
1307
- * Fire-and-forget with awaitOnExit - doesn't block the caller.
1308
- */
1309
- sendInternalTrace(functionId, payload) {
1310
- void awaitOnExit(
1311
- this.request(`/api/sdk/functions/${functionId}/traces`, {
1312
- ...payload,
1313
- sdkVersion: __version__
1314
- })
1315
- ).catch((error) => {
1316
- try {
1317
- console.error("Bitfab: Failed to create trace:", error);
1318
- } catch {
1319
- }
1320
- });
1321
- }
1322
- /**
1323
- * Send an external span (from withSpan wrapper or OpenAI tracing).
1324
- * Fire-and-forget with awaitOnExit - doesn't block the caller.
1325
- * Returns the tracked promise so callers can optionally await it.
1326
- */
1327
- sendExternalSpan(payload) {
1328
- return awaitOnExit(
1329
- this.request("/api/sdk/externalSpans", {
1330
- ...payload,
1331
- sdkVersion: __version__
1332
- })
1333
- ).catch((error) => {
1334
- try {
1335
- console.error("Bitfab: Failed to create external span:", error);
1336
- } catch {
1337
- }
1338
- });
1339
- }
1340
- /**
1341
- * Send an external trace (from OpenAI tracing).
1342
- * Fire-and-forget with awaitOnExit - doesn't block the caller.
1343
- * Returns the tracked promise so callers can optionally await it
1344
- * (the replay path does, so trace completions are persisted before
1345
- * `completeReplay` builds the trace-ID mapping).
1346
- */
1347
- sendExternalTrace(payload) {
1348
- return awaitOnExit(
1349
- this.request("/api/sdk/externalTraces", {
1350
- ...payload,
1351
- sdkVersion: __version__
1352
- })
1353
- ).catch((error) => {
1354
- try {
1355
- console.error("Bitfab: Failed to create external trace:", error);
1356
- } catch {
1357
- }
1358
- });
1359
- }
1360
- /**
1361
- * Partial update of an existing trace identified by its Bitfab trace ID.
1362
- * Used by the detached `client.getTrace(id)` handle. Fire-and-forget;
1363
- * returns a tracked promise that callers may optionally await.
1364
- */
1365
- patchTrace(traceId, payload) {
1366
- const endpoint = `/api/sdk/traces/${encodeURIComponent(traceId)}`;
1367
- return awaitOnExit(
1368
- this.request(endpoint, payload, { method: "PATCH" })
1369
- ).catch((error) => {
2359
+ if (missing.length > 0) {
1370
2360
  try {
1371
- console.error("Bitfab: Failed to patch trace:", error);
2361
+ console.error(
2362
+ `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.`
2363
+ );
1372
2364
  } catch {
1373
2365
  }
1374
- });
1375
- }
1376
- /**
1377
- * Start a replay session by fetching historical traces.
1378
- * Blocking call - creates a test run and returns lightweight item references.
1379
- */
1380
- async startReplay(traceFunctionKey, limit, traceIds, name, codeChangeDescription, codeChangeFiles, includeDbBranchLease, experimentGroupId, datasetId, graderIds, dbBranchSettings) {
1381
- const payload = { traceFunctionKey };
1382
- if (limit !== void 0) {
1383
- payload.limit = limit;
1384
- }
1385
- if (traceIds) {
1386
- payload.traceIds = traceIds;
1387
- }
1388
- if (name !== void 0) {
1389
- payload.name = name;
1390
2366
  }
1391
- if (codeChangeDescription !== void 0) {
1392
- payload.codeChangeDescription = codeChangeDescription;
1393
- }
1394
- if (codeChangeFiles !== void 0) {
1395
- payload.codeChangeFiles = codeChangeFiles;
1396
- }
1397
- if (includeDbBranchLease) {
1398
- payload.includeDbBranchLease = true;
1399
- payload.lazyDbBranchLease = true;
1400
- }
1401
- if (experimentGroupId !== void 0) {
1402
- payload.experimentGroupId = experimentGroupId;
1403
- }
1404
- if (datasetId !== void 0) {
1405
- payload.datasetId = datasetId;
1406
- }
1407
- if (graderIds !== void 0) {
1408
- payload.graderIds = graderIds;
1409
- }
1410
- if (dbBranchSettings !== void 0) {
1411
- payload.dbBranchSettings = dbBranchSettings;
1412
- }
1413
- const timeout = includeDbBranchLease ? REPLAY_DB_BRANCH_REQUEST_TIMEOUT_MS : 3e4;
1414
- return this.request("/api/sdk/replay/start", payload, {
1415
- timeout
2367
+ }
2368
+ const result = {
2369
+ items: resultItems,
2370
+ testRunId,
2371
+ testRunUrl: `${serviceUrl}${testRunUrl}`
2372
+ };
2373
+ await writeReplayResultFile(result);
2374
+ try {
2375
+ options?.onProgress?.({
2376
+ type: "complete",
2377
+ testRunId,
2378
+ completed: total,
2379
+ total,
2380
+ succeeded,
2381
+ errored,
2382
+ result
1416
2383
  });
2384
+ } catch {
1417
2385
  }
1418
- /**
1419
- * Fetch an external span by ID.
1420
- * Blocking GET request.
1421
- */
1422
- async getExternalSpan(spanId) {
1423
- const url = `${this.serviceUrl}/api/sdk/externalSpans/${spanId}`;
1424
- const controller = new AbortController();
1425
- const timeoutId = setTimeout(() => controller.abort(), 3e4);
1426
- try {
1427
- const response = await fetch(url, {
1428
- method: "GET",
1429
- headers: { Authorization: `Bearer ${this.resolveApiKey() ?? ""}` },
1430
- signal: controller.signal
1431
- });
1432
- if (!response.ok) {
1433
- const errorText = await response.text();
1434
- throw new BitfabError(
1435
- `HTTP ${response.status}: ${errorText.slice(0, 500)}`
1436
- );
1437
- }
1438
- return await response.json();
1439
- } catch (error) {
1440
- if (error instanceof BitfabError) {
1441
- throw error;
1442
- }
1443
- if (error instanceof Error) {
1444
- if (error.name === "AbortError") {
1445
- throw new BitfabError("Request timed out after 30000ms");
1446
- }
1447
- throw new BitfabError(error.message);
1448
- }
1449
- throw new BitfabError("Unknown error occurred");
1450
- } finally {
1451
- clearTimeout(timeoutId);
1452
- }
2386
+ return result;
2387
+ }
2388
+ async function writeReplayResultFile(result) {
2389
+ const resultPath = typeof process !== "undefined" ? process.env?.BITFAB_REPLAY_RESULT_PATH : void 0;
2390
+ if (!resultPath) {
2391
+ return;
1453
2392
  }
1454
- /**
1455
- * Fetch the span tree for a root span.
1456
- * Blocking GET request.
1457
- *
1458
- * Pass `includeOutputs: false` for a payload-free tree (structure +
1459
- * `externalSpanId` only), so recorded outputs are fetched lazily per mocked
1460
- * span instead of all up front. Omit it (default eager) for `mock: "all"`.
1461
- */
1462
- async getSpanTree(externalSpanId, options) {
1463
- const query = options?.includeOutputs === false ? "?includeOutputs=false" : "";
1464
- const url = `${this.serviceUrl}/api/sdk/replay/spanTree/${externalSpanId}${query}`;
1465
- const controller = new AbortController();
1466
- const timeoutId = setTimeout(() => controller.abort(), 3e4);
2393
+ try {
2394
+ const [{ dirname }, { mkdir, writeFile }] = await Promise.all([
2395
+ import("path"),
2396
+ import("fs/promises")
2397
+ ]);
2398
+ await mkdir(dirname(resultPath), { recursive: true });
2399
+ await writeFile(resultPath, `${JSON.stringify(result, null, 2)}
2400
+ `);
2401
+ } catch (err) {
1467
2402
  try {
1468
- const response = await fetch(url, {
1469
- method: "GET",
1470
- headers: { Authorization: `Bearer ${this.resolveApiKey() ?? ""}` },
1471
- signal: controller.signal
1472
- });
1473
- if (!response.ok) {
1474
- const errorText = await response.text();
1475
- throw new BitfabError(
1476
- `HTTP ${response.status}: ${errorText.slice(0, 500)}`
1477
- );
1478
- }
1479
- return await response.json();
1480
- } catch (error) {
1481
- if (error instanceof BitfabError) {
1482
- throw error;
1483
- }
1484
- if (error instanceof Error) {
1485
- if (error.name === "AbortError") {
1486
- throw new BitfabError("Request timed out after 30000ms");
1487
- }
1488
- throw new BitfabError(error.message);
1489
- }
1490
- throw new BitfabError("Unknown error occurred");
1491
- } finally {
1492
- clearTimeout(timeoutId);
2403
+ console.warn(
2404
+ `Bitfab: failed to write replay result to BITFAB_REPLAY_RESULT_PATH (${resultPath}): ${err instanceof Error ? err.message : String(err)}`
2405
+ );
2406
+ } catch {
1493
2407
  }
1494
2408
  }
1495
- /**
1496
- * Mark a replay test run as completed.
1497
- * Blocking call.
1498
- */
1499
- async completeReplay(testRunId) {
1500
- return this.request(
1501
- "/api/sdk/replay/complete",
1502
- { testRunId },
1503
- { timeout: 3e4 }
1504
- );
1505
- }
1506
- /**
1507
- * Ask the server to materialize a per-trace DB branch lease from a
1508
- * captured `dbSnapshotRef`. Blocking - the resolver creates a Neon
1509
- * snapshot + preview branch and polls operations to readiness, which
1510
- * can take seconds.
1511
- */
1512
- async resolveDbBranchLease(testRunId, traceId, dbBranchSettings) {
1513
- return this.request(
1514
- "/api/sdk/replay/resolveDbBranchLease",
1515
- { testRunId, traceId, dbBranchSettings },
1516
- { timeout: REPLAY_DB_BRANCH_REQUEST_TIMEOUT_MS }
1517
- );
1518
- }
1519
- /** Release a previously-resolved DB branch by deleting its Neon branch. Idempotent server-side. */
1520
- async releaseDbBranchLease(neonBranchId) {
1521
- await this.request(
1522
- "/api/sdk/replay/releaseDbBranchLease",
1523
- { neonBranchId },
1524
- { timeout: 3e4 }
1525
- );
2409
+ }
2410
+ var REPLAY_PERSISTENCE_TIMEOUT_MS, BITFAB_PROGRESS_PREFIX;
2411
+ var init_replay = __esm({
2412
+ "src/replay.ts"() {
2413
+ "use strict";
2414
+ init_codeChange();
2415
+ init_errors();
2416
+ init_http();
2417
+ init_mockOverride();
2418
+ init_randomUuid();
2419
+ init_replayContext();
2420
+ init_serialize();
2421
+ init_transport();
2422
+ init_unrefTimer();
2423
+ REPLAY_PERSISTENCE_TIMEOUT_MS = 3e4;
2424
+ BITFAB_PROGRESS_PREFIX = "@@bitfab:progress ";
1526
2425
  }
1527
- };
2426
+ });
2427
+
2428
+ // src/index.ts
2429
+ var index_exports = {};
2430
+ __export(index_exports, {
2431
+ BITFAB_PROGRESS_PREFIX: () => BITFAB_PROGRESS_PREFIX,
2432
+ Bitfab: () => Bitfab,
2433
+ BitfabClaudeAgentHandler: () => BitfabClaudeAgentHandler,
2434
+ BitfabError: () => BitfabError,
2435
+ BitfabFunction: () => BitfabFunction,
2436
+ BitfabLangChainCallbackHandler: () => BitfabLangGraphCallbackHandler,
2437
+ BitfabLangGraphCallbackHandler: () => BitfabLangGraphCallbackHandler,
2438
+ BitfabOpenAIAgentHandler: () => BitfabOpenAIAgentHandler,
2439
+ BitfabOpenAITracingProcessor: () => BitfabOpenAITracingProcessor,
2440
+ BitfabVercelAiHandler: () => BitfabVercelAiHandler,
2441
+ DEFAULT_SERVICE_URL: () => DEFAULT_SERVICE_URL,
2442
+ SUPPORTED_PROVIDERS: () => SUPPORTED_PROVIDERS,
2443
+ __version__: () => __version__,
2444
+ finalizers: () => finalizers,
2445
+ flushTraces: () => flushTraces,
2446
+ getCurrentReplayBranch: () => getCurrentReplayBranch,
2447
+ getCurrentSpan: () => getCurrentSpan,
2448
+ getCurrentTrace: () => getCurrentTrace,
2449
+ reportReplayProgress: () => reportReplayProgress
2450
+ });
2451
+ module.exports = __toCommonJS(index_exports);
2452
+
2453
+ // src/claudeAgentSdk.ts
2454
+ init_constants();
2455
+ init_http();
1528
2456
 
1529
2457
  // src/processorPayload.ts
1530
2458
  init_serialize();
@@ -1649,7 +2577,8 @@ var BitfabClaudeAgentHandler = class {
1649
2577
  // its root. The prompt is not present anywhere in the message stream, so it
1650
2578
  // must be handed in explicitly.
1651
2579
  this.hasRootInput = false;
1652
- this.httpClient = new HttpClient({
2580
+ this.ownsHttpClient = config._httpClient === void 0;
2581
+ this.httpClient = config._httpClient ?? new HttpClient({
1653
2582
  apiKey: config.apiKey,
1654
2583
  serviceUrl: config.serviceUrl ?? DEFAULT_SERVICE_URL,
1655
2584
  timeout: config.timeout ?? 1e4
@@ -1662,6 +2591,14 @@ var BitfabClaudeAgentHandler = class {
1662
2591
  this.subagentStartHook = this.subagentStartHook.bind(this);
1663
2592
  this.subagentStopHook = this.subagentStopHook.bind(this);
1664
2593
  }
2594
+ /**
2595
+ * Flush and release the span transport this handler started. A no-op when
2596
+ * the handler borrowed a `Bitfab` client's HTTP client: that client's
2597
+ * `close()` owns the worker's lifetime.
2598
+ */
2599
+ async close(timeoutMs) {
2600
+ return this.ownsHttpClient ? this.httpClient.close(timeoutMs) : true;
2601
+ }
1665
2602
  // ── trace lifecycle ──────────────────────────────────────────
1666
2603
  ensureTrace() {
1667
2604
  if (this.traceId !== null) {
@@ -2432,6 +3369,9 @@ async function runFunctionWithBaml(bamlSource, inputs, providers, envVars) {
2432
3369
  };
2433
3370
  }
2434
3371
 
3372
+ // src/client.ts
3373
+ init_constants();
3374
+
2435
3375
  // src/dbSnapshot.ts
2436
3376
  init_errors();
2437
3377
  var SUPPORTED_PROVIDERS = ["neon"];
@@ -2449,7 +3389,12 @@ function buildSnapshotRef(config, sdkWallClockBeforeFn) {
2449
3389
  };
2450
3390
  }
2451
3391
 
3392
+ // src/client.ts
3393
+ init_http();
3394
+
2452
3395
  // src/langgraph.ts
3396
+ init_constants();
3397
+ init_http();
2453
3398
  init_randomUuid();
2454
3399
  init_serialize();
2455
3400
  var LANGSMITH_HIDDEN_TAG = "langsmith:hidden";
@@ -2673,7 +3618,8 @@ var BitfabLangGraphCallbackHandler = class {
2673
3618
  this.ignoreCustomEvent = true;
2674
3619
  this.runToSpan = /* @__PURE__ */ new Map();
2675
3620
  this.invocations = /* @__PURE__ */ new Map();
2676
- this.httpClient = new HttpClient({
3621
+ this.ownsHttpClient = config._httpClient === void 0;
3622
+ this.httpClient = config._httpClient ?? new HttpClient({
2677
3623
  apiKey: config.apiKey,
2678
3624
  serviceUrl: config.serviceUrl ?? DEFAULT_SERVICE_URL,
2679
3625
  timeout: config.timeout ?? 1e4
@@ -2681,6 +3627,14 @@ var BitfabLangGraphCallbackHandler = class {
2681
3627
  this.traceFunctionKey = config.traceFunctionKey;
2682
3628
  this.getActiveSpanContext = config.getActiveSpanContext ?? null;
2683
3629
  }
3630
+ /**
3631
+ * Flush and release the span transport this handler started. A no-op when
3632
+ * the handler borrowed a `Bitfab` client's HTTP client: that client's
3633
+ * `close()` owns the worker's lifetime.
3634
+ */
3635
+ async close(timeoutMs) {
3636
+ return this.ownsHttpClient ? this.httpClient.close(timeoutMs) : true;
3637
+ }
2684
3638
  // ── lifecycle helpers ──────────────────────────────────────────
2685
3639
  startSpan(runId, parentRunId, name, spanType, inputData, metadata, tags) {
2686
3640
  const parentSpan = parentRunId ? this.runToSpan.get(parentRunId) : void 0;
@@ -3152,6 +4106,8 @@ init_replayContext();
3152
4106
  init_serialize();
3153
4107
 
3154
4108
  // src/tracing.ts
4109
+ init_constants();
4110
+ init_http();
3155
4111
  init_randomUuid();
3156
4112
  var BitfabOpenAITracingProcessor = class {
3157
4113
  /**
@@ -3163,7 +4119,8 @@ var BitfabOpenAITracingProcessor = class {
3163
4119
  this.activeTraces = {};
3164
4120
  this.activeSpanMappings = {};
3165
4121
  this.canonicalTraceIds = {};
3166
- this.httpClient = new HttpClient({
4122
+ this.ownsHttpClient = config._httpClient === void 0;
4123
+ this.httpClient = config._httpClient ?? new HttpClient({
3167
4124
  apiKey: config.apiKey,
3168
4125
  serviceUrl: config.serviceUrl ?? DEFAULT_SERVICE_URL,
3169
4126
  timeout: config.timeout ?? 1e4
@@ -3179,6 +4136,14 @@ var BitfabOpenAITracingProcessor = class {
3179
4136
  this.canonicalTraceIds[sourceTraceId] = created;
3180
4137
  return created;
3181
4138
  }
4139
+ /**
4140
+ * Flush and release the span transport this processor started. A no-op when
4141
+ * the processor borrowed a `Bitfab` client's HTTP client: that client's
4142
+ * `close()` owns the worker's lifetime.
4143
+ */
4144
+ async close(timeoutMs) {
4145
+ return this.ownsHttpClient ? this.httpClient.close(timeoutMs) : true;
4146
+ }
3182
4147
  /**
3183
4148
  * Called when a trace is started.
3184
4149
  * If there's an active withSpan context, the trace ID is remapped to the
@@ -3233,14 +4198,16 @@ var BitfabOpenAITracingProcessor = class {
3233
4198
  * Called when a trace is being flushed.
3234
4199
  */
3235
4200
  async forceFlush() {
4201
+ await this.httpClient.waitForPendingRequests();
3236
4202
  }
3237
4203
  /**
3238
4204
  * Called when the trace processor is shutting down.
3239
4205
  */
3240
- async shutdown(_timeout) {
4206
+ async shutdown(timeout) {
3241
4207
  this.activeTraces = {};
3242
4208
  this.activeSpanMappings = {};
3243
4209
  this.canonicalTraceIds = {};
4210
+ await this.close(timeout);
3244
4211
  }
3245
4212
  /**
3246
4213
  * Send trace to Bitfab API (fire-and-forget).
@@ -3507,7 +4474,6 @@ var BitfabVercelAiHandler = class {
3507
4474
  // src/client.ts
3508
4475
  init_warnOnce();
3509
4476
  var activeTraceStates = /* @__PURE__ */ new Map();
3510
- var pendingSpanPromises = /* @__PURE__ */ new Map();
3511
4477
  var asyncLocalStorage = null;
3512
4478
  var SPAN_CONTEXT_STORAGE_SYMBOL = /* @__PURE__ */ Symbol.for("bitfab.spanContextStorage");
3513
4479
  var initializeAsyncContext = () => {
@@ -3825,7 +4791,7 @@ function getCurrentTrace() {
3825
4791
  }
3826
4792
  };
3827
4793
  }
3828
- function readEnv(name) {
4794
+ function readEnv2(name) {
3829
4795
  if (typeof process !== "undefined" && process.env) {
3830
4796
  return process.env[name];
3831
4797
  }
@@ -3863,6 +4829,23 @@ var Bitfab = class {
3863
4829
  timeout: this.timeout
3864
4830
  });
3865
4831
  }
4832
+ /**
4833
+ * Flush and permanently close this client's tracing resources: its pending
4834
+ * requests and the single span-transport worker shared by its decorators and
4835
+ * framework handlers.
4836
+ *
4837
+ * Resolves `false` when delivery failed or the deadline expired. Long-lived
4838
+ * processes never need this (the transport batches in the background and the
4839
+ * exit hook drains it); scripts and tests that want a hard guarantee should
4840
+ * await it.
4841
+ *
4842
+ * Deliberately not a `Symbol.asyncDispose` method: the SDK targets runtimes
4843
+ * where that symbol may be absent, and a computed key on a missing symbol
4844
+ * throws at class-definition time, taking the whole SDK down on load.
4845
+ */
4846
+ close(timeoutMs) {
4847
+ return this.httpClient.close(timeoutMs);
4848
+ }
3866
4849
  /**
3867
4850
  * Resolve the API key lazily, the first time a span actually needs it.
3868
4851
  *
@@ -3883,7 +4866,7 @@ var Bitfab = class {
3883
4866
  return this.resolvedApiKey;
3884
4867
  }
3885
4868
  const fromConfig = typeof this.apiKeyConfig === "function" ? this.apiKeyConfig() : this.apiKeyConfig;
3886
- const candidate = fromConfig && fromConfig.trim() !== "" ? fromConfig : readEnv("BITFAB_API_KEY");
4869
+ const candidate = fromConfig && fromConfig.trim() !== "" ? fromConfig : readEnv2("BITFAB_API_KEY");
3887
4870
  const key = candidate && candidate.trim() !== "" ? candidate : void 0;
3888
4871
  if (key) {
3889
4872
  this.resolvedApiKey = key;
@@ -4012,7 +4995,8 @@ var Bitfab = class {
4012
4995
  getActiveSpanContext: () => {
4013
4996
  const stack = getSpanStack();
4014
4997
  return stack[stack.length - 1] ?? null;
4015
- }
4998
+ },
4999
+ _httpClient: this.httpClient
4016
5000
  });
4017
5001
  }
4018
5002
  /**
@@ -4072,7 +5056,8 @@ var Bitfab = class {
4072
5056
  getActiveSpanContext: () => {
4073
5057
  const stack = getSpanStack();
4074
5058
  return stack[stack.length - 1] ?? null;
4075
- }
5059
+ },
5060
+ _httpClient: this.httpClient
4076
5061
  });
4077
5062
  }
4078
5063
  /**
@@ -4124,7 +5109,8 @@ var Bitfab = class {
4124
5109
  getActiveSpanContext: () => {
4125
5110
  const stack = getSpanStack();
4126
5111
  return stack[stack.length - 1] ?? null;
4127
- }
5112
+ },
5113
+ _httpClient: this.httpClient
4128
5114
  });
4129
5115
  }
4130
5116
  /**
@@ -4377,7 +5363,6 @@ var Bitfab = class {
4377
5363
  },
4378
5364
  dbSnapshotRef
4379
5365
  });
4380
- pendingSpanPromises.set(traceId, []);
4381
5366
  registeredTraceId = traceId;
4382
5367
  }
4383
5368
  const functionName = fn.name !== "" ? fn.name : void 0;
@@ -4392,57 +5377,29 @@ var Bitfab = class {
4392
5377
  startedAt,
4393
5378
  spanType: options.type ?? "custom"
4394
5379
  };
4395
- const sendSpan = async (params, spanOpts) => {
5380
+ const sendSpan = async (params) => {
4396
5381
  const replayCtx = getReplayContext();
4397
- const persistenceCollector = isRootSpan ? replayCtx?.pendingPersistence : void 0;
4398
- let resolvePersistence;
4399
- if (persistenceCollector && !spanOpts?.skipPersistenceRegistration) {
4400
- persistenceCollector.push(
4401
- new Promise((resolve) => {
4402
- resolvePersistence = resolve;
4403
- })
4404
- );
4405
- }
4406
5382
  try {
4407
5383
  const endedAt = (/* @__PURE__ */ new Date()).toISOString();
4408
5384
  const traceDropped = activeTraceStates.get(traceId)?.dropped === true;
4409
- const spanPromise = traceDropped ? Promise.resolve() : self.sendWrapperSpan({
4410
- ...baseSpanParams,
4411
- ...params,
4412
- contexts: newContext.contexts,
4413
- prompt: newContext.prompt,
4414
- endedAt,
4415
- ...replayCtx?.testRunId && {
4416
- testRunId: replayCtx.testRunId
4417
- },
4418
- ...replayCtx?.inputSourceSpanId && {
4419
- inputSourceSpanId: replayCtx.inputSourceSpanId
4420
- }
4421
- });
4422
- if (isRootSpan) {
4423
- const pending = pendingSpanPromises.get(traceId) ?? [];
4424
- pending.push(spanPromise);
4425
- if (persistenceCollector) {
4426
- await Promise.allSettled(pending);
4427
- } else {
4428
- let raceTimer;
4429
- try {
4430
- await Promise.race([
4431
- Promise.allSettled(pending),
4432
- new Promise((resolve) => {
4433
- raceTimer = setTimeout(resolve, 5e3);
4434
- unrefTimer(raceTimer);
4435
- })
4436
- ]);
4437
- } finally {
4438
- if (raceTimer) {
4439
- clearTimeout(raceTimer);
4440
- }
5385
+ if (!traceDropped) {
5386
+ self.sendWrapperSpan({
5387
+ ...baseSpanParams,
5388
+ ...params,
5389
+ contexts: newContext.contexts,
5390
+ prompt: newContext.prompt,
5391
+ endedAt,
5392
+ ...replayCtx?.testRunId && {
5393
+ testRunId: replayCtx.testRunId
5394
+ },
5395
+ ...replayCtx?.inputSourceSpanId && {
5396
+ inputSourceSpanId: replayCtx.inputSourceSpanId
4441
5397
  }
4442
- }
4443
- pendingSpanPromises.delete(traceId);
5398
+ });
5399
+ }
5400
+ if (isRootSpan) {
4444
5401
  const traceState = activeTraceStates.get(traceId);
4445
- const completionPromise = self.sendTraceCompletion({
5402
+ self.sendTraceCompletion({
4446
5403
  traceFunctionKey,
4447
5404
  traceId,
4448
5405
  startedAt: traceState?.startedAt ?? startedAt,
@@ -4468,20 +5425,8 @@ var Bitfab = class {
4468
5425
  }
4469
5426
  });
4470
5427
  activeTraceStates.delete(traceId);
4471
- if (persistenceCollector) {
4472
- await completionPromise;
4473
- }
4474
- } else {
4475
- const pending = pendingSpanPromises.get(traceId);
4476
- if (pending) {
4477
- pending.push(spanPromise);
4478
- } else {
4479
- pendingSpanPromises.set(traceId, [spanPromise]);
4480
- }
4481
5428
  }
4482
5429
  } catch {
4483
- } finally {
4484
- resolvePersistence?.();
4485
5430
  }
4486
5431
  };
4487
5432
  const replayCtxForMock = getReplayContext();
@@ -4565,30 +5510,14 @@ var Bitfab = class {
4565
5510
  }
4566
5511
  const recordSpan = (result) => {
4567
5512
  if (options.finalize) {
4568
- const replayCtx = getReplayContext();
4569
- const persistenceCollector = isRootSpan ? replayCtx?.pendingPersistence : void 0;
4570
- let resolvePersistence;
4571
- if (persistenceCollector) {
4572
- persistenceCollector.push(
4573
- new Promise((resolve) => {
4574
- resolvePersistence = resolve;
4575
- })
4576
- );
4577
- }
4578
- void Promise.resolve().then(() => options.finalize(result)).then(
4579
- (output) => sendSpan(
4580
- { result: output },
4581
- { skipPersistenceRegistration: true }
4582
- )
4583
- ).catch(
4584
- (error) => sendSpan(
4585
- {
5513
+ void self.httpClient.trackDeferred(
5514
+ Promise.resolve().then(() => options.finalize(result)).then((output) => sendSpan({ result: output })).catch(
5515
+ (error) => sendSpan({
4586
5516
  result: void 0,
4587
5517
  error: error instanceof Error ? `finalize failed: ${error.message}` : `finalize failed: ${String(error)}`
4588
- },
4589
- { skipPersistenceRegistration: true }
5518
+ })
4590
5519
  )
4591
- ).finally(() => resolvePersistence?.());
5520
+ );
4592
5521
  } else {
4593
5522
  void sendSpan({ result });
4594
5523
  }
@@ -4616,7 +5545,6 @@ var Bitfab = class {
4616
5545
  } catch (setupError) {
4617
5546
  if (registeredTraceId) {
4618
5547
  activeTraceStates.delete(registeredTraceId);
4619
- pendingSpanPromises.delete(registeredTraceId);
4620
5548
  }
4621
5549
  if (getReplayContext()) {
4622
5550
  throw setupError;
@@ -4739,7 +5667,7 @@ var Bitfab = class {
4739
5667
  /**
4740
5668
  * Send trace completion when a root span ends.
4741
5669
  * Internal method to record trace completion with end time.
4742
- * Fire-and-forget - sends to externalTraces endpoint via httpClient.
5670
+ * Queued on the client's span transport; delivery is the transport's job.
4743
5671
  */
4744
5672
  sendTraceCompletion(params) {
4745
5673
  const rawTrace = {
@@ -4775,7 +5703,7 @@ var Bitfab = class {
4775
5703
  accessed: params.dbSnapshotUsage.accessed
4776
5704
  };
4777
5705
  }
4778
- return this.httpClient.sendExternalTrace({
5706
+ this.httpClient.sendExternalTrace({
4779
5707
  id: params.traceId,
4780
5708
  type: "sdk-function",
4781
5709
  source: "typescript-sdk-function",
@@ -4790,7 +5718,7 @@ var Bitfab = class {
4790
5718
  /**
4791
5719
  * Send a wrapper span from function execution.
4792
5720
  * Internal method to record spans when using withSpan.
4793
- * Fire-and-forget - sends to externalSpans endpoint via httpClient.
5721
+ * Queued on the client's span transport; delivery is the transport's job.
4794
5722
  */
4795
5723
  sendWrapperSpan(params) {
4796
5724
  const serializedInputs = serializeValue(params.inputs);
@@ -4831,7 +5759,7 @@ var Bitfab = class {
4831
5759
  if (params.inputSourceSpanId) {
4832
5760
  externalSpan.input_source_span_id = params.inputSourceSpanId;
4833
5761
  }
4834
- return this.httpClient.sendExternalSpan({
5762
+ this.httpClient.sendExternalSpan({
4835
5763
  id: params.spanId,
4836
5764
  traceId: params.traceId,
4837
5765
  type: "sdk-function",
@@ -5020,6 +5948,9 @@ var BitfabFunction = class {
5020
5948
  }
5021
5949
  };
5022
5950
 
5951
+ // src/index.ts
5952
+ init_constants();
5953
+
5023
5954
  // src/finalizers.ts
5024
5955
  async function settle(value) {
5025
5956
  try {
@@ -5069,6 +6000,7 @@ var finalizers = {
5069
6000
  };
5070
6001
 
5071
6002
  // src/index.ts
6003
+ init_http();
5072
6004
  init_replay();
5073
6005
  // Annotate the CommonJS export names for ESM import in node:
5074
6006
  0 && (module.exports = {