@bitfab/sdk 0.33.7 → 0.34.0

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