bitfab 0.33.6 → 0.34.0

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