@bitfab/sdk 0.33.6 → 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.6";
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
- }
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
2116
  }
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;
2168
+ }
2169
+ if (Date.now() >= deadline) {
2170
+ break;
1144
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
- }
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
2343
  }
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
- }
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
2357
  }
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;
@@ -2756,7 +3710,9 @@ var BitfabLangGraphCallbackHandler = class {
2756
3710
  if (extraContexts && Object.keys(extraContexts).length > 0) {
2757
3711
  spanInfo.contexts.push(extraContexts);
2758
3712
  }
2759
- this.sendSpan(spanInfo);
3713
+ if (spanInfo.hidden !== true) {
3714
+ this.sendSpan(spanInfo);
3715
+ }
2760
3716
  if (runId === spanInfo.rootRunId) {
2761
3717
  const invocation = this.invocations.get(runId);
2762
3718
  this.sendTraceCompletion(spanInfo, invocation?.activeContext ?? null);
@@ -2780,9 +3736,6 @@ var BitfabLangGraphCallbackHandler = class {
2780
3736
  if (spanInfo.contexts.length > 0) {
2781
3737
  spanData.contexts = spanInfo.contexts;
2782
3738
  }
2783
- if (spanInfo.hidden) {
2784
- spanData.hidden = true;
2785
- }
2786
3739
  const rawSpan = {
2787
3740
  id: spanInfo.spanId,
2788
3741
  trace_id: spanInfo.traceId,
@@ -3144,6 +4097,8 @@ init_replayContext();
3144
4097
  init_serialize();
3145
4098
 
3146
4099
  // src/tracing.ts
4100
+ init_constants();
4101
+ init_http();
3147
4102
  init_randomUuid();
3148
4103
  var BitfabOpenAITracingProcessor = class {
3149
4104
  /**
@@ -3155,7 +4110,8 @@ var BitfabOpenAITracingProcessor = class {
3155
4110
  this.activeTraces = {};
3156
4111
  this.activeSpanMappings = {};
3157
4112
  this.canonicalTraceIds = {};
3158
- this.httpClient = new HttpClient({
4113
+ this.ownsHttpClient = config._httpClient === void 0;
4114
+ this.httpClient = config._httpClient ?? new HttpClient({
3159
4115
  apiKey: config.apiKey,
3160
4116
  serviceUrl: config.serviceUrl ?? DEFAULT_SERVICE_URL,
3161
4117
  timeout: config.timeout ?? 1e4
@@ -3171,6 +4127,14 @@ var BitfabOpenAITracingProcessor = class {
3171
4127
  this.canonicalTraceIds[sourceTraceId] = created;
3172
4128
  return created;
3173
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
+ }
3174
4138
  /**
3175
4139
  * Called when a trace is started.
3176
4140
  * If there's an active withSpan context, the trace ID is remapped to the
@@ -3225,14 +4189,16 @@ var BitfabOpenAITracingProcessor = class {
3225
4189
  * Called when a trace is being flushed.
3226
4190
  */
3227
4191
  async forceFlush() {
4192
+ await this.httpClient.waitForPendingRequests();
3228
4193
  }
3229
4194
  /**
3230
4195
  * Called when the trace processor is shutting down.
3231
4196
  */
3232
- async shutdown(_timeout) {
4197
+ async shutdown(timeout) {
3233
4198
  this.activeTraces = {};
3234
4199
  this.activeSpanMappings = {};
3235
4200
  this.canonicalTraceIds = {};
4201
+ await this.close(timeout);
3236
4202
  }
3237
4203
  /**
3238
4204
  * Send trace to Bitfab API (fire-and-forget).
@@ -3499,7 +4465,6 @@ var BitfabVercelAiHandler = class {
3499
4465
  // src/client.ts
3500
4466
  init_warnOnce();
3501
4467
  var activeTraceStates = /* @__PURE__ */ new Map();
3502
- var pendingSpanPromises = /* @__PURE__ */ new Map();
3503
4468
  var asyncLocalStorage = null;
3504
4469
  var SPAN_CONTEXT_STORAGE_SYMBOL = /* @__PURE__ */ Symbol.for("bitfab.spanContextStorage");
3505
4470
  var initializeAsyncContext = () => {
@@ -3817,7 +4782,7 @@ function getCurrentTrace() {
3817
4782
  }
3818
4783
  };
3819
4784
  }
3820
- function readEnv(name) {
4785
+ function readEnv2(name) {
3821
4786
  if (typeof process !== "undefined" && process.env) {
3822
4787
  return process.env[name];
3823
4788
  }
@@ -3855,6 +4820,23 @@ var Bitfab = class {
3855
4820
  timeout: this.timeout
3856
4821
  });
3857
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
+ }
3858
4840
  /**
3859
4841
  * Resolve the API key lazily, the first time a span actually needs it.
3860
4842
  *
@@ -3875,7 +4857,7 @@ var Bitfab = class {
3875
4857
  return this.resolvedApiKey;
3876
4858
  }
3877
4859
  const fromConfig = typeof this.apiKeyConfig === "function" ? this.apiKeyConfig() : this.apiKeyConfig;
3878
- const candidate = fromConfig && fromConfig.trim() !== "" ? fromConfig : readEnv("BITFAB_API_KEY");
4860
+ const candidate = fromConfig && fromConfig.trim() !== "" ? fromConfig : readEnv2("BITFAB_API_KEY");
3879
4861
  const key = candidate && candidate.trim() !== "" ? candidate : void 0;
3880
4862
  if (key) {
3881
4863
  this.resolvedApiKey = key;
@@ -4004,7 +4986,8 @@ var Bitfab = class {
4004
4986
  getActiveSpanContext: () => {
4005
4987
  const stack = getSpanStack();
4006
4988
  return stack[stack.length - 1] ?? null;
4007
- }
4989
+ },
4990
+ _httpClient: this.httpClient
4008
4991
  });
4009
4992
  }
4010
4993
  /**
@@ -4064,7 +5047,8 @@ var Bitfab = class {
4064
5047
  getActiveSpanContext: () => {
4065
5048
  const stack = getSpanStack();
4066
5049
  return stack[stack.length - 1] ?? null;
4067
- }
5050
+ },
5051
+ _httpClient: this.httpClient
4068
5052
  });
4069
5053
  }
4070
5054
  /**
@@ -4116,7 +5100,8 @@ var Bitfab = class {
4116
5100
  getActiveSpanContext: () => {
4117
5101
  const stack = getSpanStack();
4118
5102
  return stack[stack.length - 1] ?? null;
4119
- }
5103
+ },
5104
+ _httpClient: this.httpClient
4120
5105
  });
4121
5106
  }
4122
5107
  /**
@@ -4369,7 +5354,6 @@ var Bitfab = class {
4369
5354
  },
4370
5355
  dbSnapshotRef
4371
5356
  });
4372
- pendingSpanPromises.set(traceId, []);
4373
5357
  registeredTraceId = traceId;
4374
5358
  }
4375
5359
  const functionName = fn.name !== "" ? fn.name : void 0;
@@ -4384,57 +5368,29 @@ var Bitfab = class {
4384
5368
  startedAt,
4385
5369
  spanType: options.type ?? "custom"
4386
5370
  };
4387
- const sendSpan = async (params, spanOpts) => {
5371
+ const sendSpan = async (params) => {
4388
5372
  const replayCtx = getReplayContext();
4389
- const persistenceCollector = isRootSpan ? replayCtx?.pendingPersistence : void 0;
4390
- let resolvePersistence;
4391
- if (persistenceCollector && !spanOpts?.skipPersistenceRegistration) {
4392
- persistenceCollector.push(
4393
- new Promise((resolve) => {
4394
- resolvePersistence = resolve;
4395
- })
4396
- );
4397
- }
4398
5373
  try {
4399
5374
  const endedAt = (/* @__PURE__ */ new Date()).toISOString();
4400
5375
  const traceDropped = activeTraceStates.get(traceId)?.dropped === true;
4401
- const spanPromise = traceDropped ? Promise.resolve() : self.sendWrapperSpan({
4402
- ...baseSpanParams,
4403
- ...params,
4404
- contexts: newContext.contexts,
4405
- prompt: newContext.prompt,
4406
- endedAt,
4407
- ...replayCtx?.testRunId && {
4408
- testRunId: replayCtx.testRunId
4409
- },
4410
- ...replayCtx?.inputSourceSpanId && {
4411
- inputSourceSpanId: replayCtx.inputSourceSpanId
4412
- }
4413
- });
4414
- if (isRootSpan) {
4415
- const pending = pendingSpanPromises.get(traceId) ?? [];
4416
- pending.push(spanPromise);
4417
- if (persistenceCollector) {
4418
- await Promise.allSettled(pending);
4419
- } else {
4420
- let raceTimer;
4421
- try {
4422
- await Promise.race([
4423
- Promise.allSettled(pending),
4424
- new Promise((resolve) => {
4425
- raceTimer = setTimeout(resolve, 5e3);
4426
- unrefTimer(raceTimer);
4427
- })
4428
- ]);
4429
- } finally {
4430
- if (raceTimer) {
4431
- clearTimeout(raceTimer);
4432
- }
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
4433
5388
  }
4434
- }
4435
- pendingSpanPromises.delete(traceId);
5389
+ });
5390
+ }
5391
+ if (isRootSpan) {
4436
5392
  const traceState = activeTraceStates.get(traceId);
4437
- const completionPromise = self.sendTraceCompletion({
5393
+ self.sendTraceCompletion({
4438
5394
  traceFunctionKey,
4439
5395
  traceId,
4440
5396
  startedAt: traceState?.startedAt ?? startedAt,
@@ -4460,20 +5416,8 @@ var Bitfab = class {
4460
5416
  }
4461
5417
  });
4462
5418
  activeTraceStates.delete(traceId);
4463
- if (persistenceCollector) {
4464
- await completionPromise;
4465
- }
4466
- } else {
4467
- const pending = pendingSpanPromises.get(traceId);
4468
- if (pending) {
4469
- pending.push(spanPromise);
4470
- } else {
4471
- pendingSpanPromises.set(traceId, [spanPromise]);
4472
- }
4473
5419
  }
4474
5420
  } catch {
4475
- } finally {
4476
- resolvePersistence?.();
4477
5421
  }
4478
5422
  };
4479
5423
  const replayCtxForMock = getReplayContext();
@@ -4557,30 +5501,14 @@ var Bitfab = class {
4557
5501
  }
4558
5502
  const recordSpan = (result) => {
4559
5503
  if (options.finalize) {
4560
- const replayCtx = getReplayContext();
4561
- const persistenceCollector = isRootSpan ? replayCtx?.pendingPersistence : void 0;
4562
- let resolvePersistence;
4563
- if (persistenceCollector) {
4564
- persistenceCollector.push(
4565
- new Promise((resolve) => {
4566
- resolvePersistence = resolve;
4567
- })
4568
- );
4569
- }
4570
- void Promise.resolve().then(() => options.finalize(result)).then(
4571
- (output) => sendSpan(
4572
- { result: output },
4573
- { skipPersistenceRegistration: true }
4574
- )
4575
- ).catch(
4576
- (error) => sendSpan(
4577
- {
5504
+ void self.httpClient.trackDeferred(
5505
+ Promise.resolve().then(() => options.finalize(result)).then((output) => sendSpan({ result: output })).catch(
5506
+ (error) => sendSpan({
4578
5507
  result: void 0,
4579
5508
  error: error instanceof Error ? `finalize failed: ${error.message}` : `finalize failed: ${String(error)}`
4580
- },
4581
- { skipPersistenceRegistration: true }
5509
+ })
4582
5510
  )
4583
- ).finally(() => resolvePersistence?.());
5511
+ );
4584
5512
  } else {
4585
5513
  void sendSpan({ result });
4586
5514
  }
@@ -4608,7 +5536,6 @@ var Bitfab = class {
4608
5536
  } catch (setupError) {
4609
5537
  if (registeredTraceId) {
4610
5538
  activeTraceStates.delete(registeredTraceId);
4611
- pendingSpanPromises.delete(registeredTraceId);
4612
5539
  }
4613
5540
  if (getReplayContext()) {
4614
5541
  throw setupError;
@@ -4731,7 +5658,7 @@ var Bitfab = class {
4731
5658
  /**
4732
5659
  * Send trace completion when a root span ends.
4733
5660
  * Internal method to record trace completion with end time.
4734
- * Fire-and-forget - sends to externalTraces endpoint via httpClient.
5661
+ * Queued on the client's span transport; delivery is the transport's job.
4735
5662
  */
4736
5663
  sendTraceCompletion(params) {
4737
5664
  const rawTrace = {
@@ -4767,7 +5694,7 @@ var Bitfab = class {
4767
5694
  accessed: params.dbSnapshotUsage.accessed
4768
5695
  };
4769
5696
  }
4770
- return this.httpClient.sendExternalTrace({
5697
+ this.httpClient.sendExternalTrace({
4771
5698
  id: params.traceId,
4772
5699
  type: "sdk-function",
4773
5700
  source: "typescript-sdk-function",
@@ -4782,7 +5709,7 @@ var Bitfab = class {
4782
5709
  /**
4783
5710
  * Send a wrapper span from function execution.
4784
5711
  * Internal method to record spans when using withSpan.
4785
- * Fire-and-forget - sends to externalSpans endpoint via httpClient.
5712
+ * Queued on the client's span transport; delivery is the transport's job.
4786
5713
  */
4787
5714
  sendWrapperSpan(params) {
4788
5715
  const serializedInputs = serializeValue(params.inputs);
@@ -4823,7 +5750,7 @@ var Bitfab = class {
4823
5750
  if (params.inputSourceSpanId) {
4824
5751
  externalSpan.input_source_span_id = params.inputSourceSpanId;
4825
5752
  }
4826
- return this.httpClient.sendExternalSpan({
5753
+ this.httpClient.sendExternalSpan({
4827
5754
  id: params.spanId,
4828
5755
  traceId: params.traceId,
4829
5756
  type: "sdk-function",
@@ -5012,6 +5939,9 @@ var BitfabFunction = class {
5012
5939
  }
5013
5940
  };
5014
5941
 
5942
+ // src/index.ts
5943
+ init_constants();
5944
+
5015
5945
  // src/finalizers.ts
5016
5946
  async function settle(value) {
5017
5947
  try {
@@ -5061,6 +5991,7 @@ var finalizers = {
5061
5991
  };
5062
5992
 
5063
5993
  // src/index.ts
5994
+ init_http();
5064
5995
  init_replay();
5065
5996
  // Annotate the CommonJS export names for ESM import in node:
5066
5997
  0 && (module.exports = {