@bitfab/sdk 0.33.7 → 0.34.0

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