bitfab 0.23.2 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -54,6 +54,54 @@ var init_errors = __esm({
54
54
  }
55
55
  });
56
56
 
57
+ // src/warnOnce.ts
58
+ function warnOnce(key, message) {
59
+ if (warned.has(key)) {
60
+ return;
61
+ }
62
+ warned.add(key);
63
+ try {
64
+ console.warn(`[bitfab] ${message}`);
65
+ } catch {
66
+ }
67
+ }
68
+ var warned;
69
+ var init_warnOnce = __esm({
70
+ "src/warnOnce.ts"() {
71
+ "use strict";
72
+ warned = /* @__PURE__ */ new Set();
73
+ }
74
+ });
75
+
76
+ // src/randomUuid.ts
77
+ function randomUuid() {
78
+ const globalCrypto = globalThis.crypto;
79
+ if (typeof globalCrypto?.randomUUID === "function") {
80
+ try {
81
+ return globalCrypto.randomUUID();
82
+ } catch {
83
+ }
84
+ }
85
+ warnOnce(
86
+ "crypto-unavailable",
87
+ "global crypto.randomUUID is unavailable; using a non-cryptographic fallback for trace/span ids. Tracing works normally (ids are correlation-only, not security-sensitive)."
88
+ );
89
+ return fallbackUuidV4();
90
+ }
91
+ function fallbackUuidV4() {
92
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (char) => {
93
+ const rand = Math.random() * 16 | 0;
94
+ const value = char === "x" ? rand : rand & 3 | 8;
95
+ return value.toString(16);
96
+ });
97
+ }
98
+ var init_randomUuid = __esm({
99
+ "src/randomUuid.ts"() {
100
+ "use strict";
101
+ init_warnOnce();
102
+ }
103
+ });
104
+
57
105
  // src/serialize.ts
58
106
  function describeValue(value) {
59
107
  try {
@@ -66,6 +114,10 @@ function describeValue(value) {
66
114
  return typeof value;
67
115
  }
68
116
  function unserializableStub(value, reason) {
117
+ warnOnce(
118
+ `serialize:${reason.replace(/\d+/g, "N")}`,
119
+ `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.`
120
+ );
69
121
  let summary;
70
122
  try {
71
123
  summary = `<unserializable: ${describeValue(value)} (${reason})>`;
@@ -105,7 +157,19 @@ function deserializeValue(serialized) {
105
157
  });
106
158
  }
107
159
  function toJsonSafe(value) {
108
- return toJsonSafeInner(value, 0, /* @__PURE__ */ new WeakSet());
160
+ const safe = toJsonSafeInner(value, 0, /* @__PURE__ */ new WeakSet());
161
+ try {
162
+ const size = JSON.stringify(safe)?.length ?? 0;
163
+ if (size > MAX_FRAMEWORK_SERIALIZED_BYTES) {
164
+ warnOnce(
165
+ "toJsonSafe:too_large",
166
+ `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.`
167
+ );
168
+ return `<unserializable: too_large_${size}_bytes>`;
169
+ }
170
+ } catch {
171
+ }
172
+ return safe;
109
173
  }
110
174
  function toJsonSafeInner(value, depth, seen) {
111
175
  if (value === null || value === void 0) {
@@ -158,12 +222,14 @@ function toJsonSafeInner(value, depth, seen) {
158
222
  seen.delete(value);
159
223
  return result;
160
224
  }
161
- var import_superjson, MAX_SERIALIZED_BYTES, MAX_SAFE_DEPTH;
225
+ var import_superjson, MAX_SERIALIZED_BYTES, MAX_FRAMEWORK_SERIALIZED_BYTES, MAX_SAFE_DEPTH;
162
226
  var init_serialize = __esm({
163
227
  "src/serialize.ts"() {
164
228
  "use strict";
165
229
  import_superjson = __toESM(require("superjson"), 1);
230
+ init_warnOnce();
166
231
  MAX_SERIALIZED_BYTES = 512e3;
232
+ MAX_FRAMEWORK_SERIALIZED_BYTES = 2e6;
167
233
  MAX_SAFE_DEPTH = 6;
168
234
  }
169
235
  });
@@ -288,7 +354,7 @@ async function processItem(httpClient, serverItem, fn, testRunId, mockStrategy,
288
354
  let originalOutput;
289
355
  let result;
290
356
  let error = null;
291
- const replayedTraceId = crypto.randomUUID();
357
+ const replayedTraceId = randomUuid();
292
358
  const pendingPersistence = [];
293
359
  try {
294
360
  const span = await httpClient.getExternalSpan(serverItem.externalSpanId);
@@ -472,6 +538,7 @@ var init_replay = __esm({
472
538
  "src/replay.ts"() {
473
539
  "use strict";
474
540
  init_errors();
541
+ init_randomUuid();
475
542
  init_replayContext();
476
543
  init_serialize();
477
544
  }
@@ -501,13 +568,24 @@ __export(index_exports, {
501
568
  module.exports = __toCommonJS(index_exports);
502
569
 
503
570
  // src/version.generated.ts
504
- var __version__ = "0.23.2";
571
+ var __version__ = "0.24.0";
505
572
 
506
573
  // src/constants.ts
507
574
  var DEFAULT_SERVICE_URL = "https://bitfab.ai";
508
575
 
509
576
  // src/http.ts
510
577
  init_errors();
578
+
579
+ // src/unrefTimer.ts
580
+ function unrefTimer(timer) {
581
+ const handle = timer;
582
+ if (typeof handle.unref === "function") {
583
+ handle.unref();
584
+ }
585
+ }
586
+
587
+ // src/http.ts
588
+ init_warnOnce();
511
589
  function serializePayloadBody(payload) {
512
590
  try {
513
591
  return { body: JSON.stringify(payload), dropped: [] };
@@ -552,11 +630,20 @@ function serializePayloadBody(payload) {
552
630
  result = `<unserializable: ${className}>`;
553
631
  }
554
632
  } else {
555
- const out = {};
556
- for (const [k, v] of Object.entries(obj)) {
557
- out[k] = sanitize(v, seen);
633
+ try {
634
+ const out = {};
635
+ for (const [k, v] of Object.entries(obj)) {
636
+ out[k] = sanitize(v, seen);
637
+ }
638
+ result = out;
639
+ } catch {
640
+ warnOnce(
641
+ "payload:field-getter-threw",
642
+ "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."
643
+ );
644
+ dropped.push(className);
645
+ result = `<unserializable: ${className}>`;
558
646
  }
559
- result = out;
560
647
  }
561
648
  seen.delete(obj);
562
649
  return result;
@@ -601,10 +688,20 @@ async function flushTraces(timeoutMs = 5e3) {
601
688
  if (pendingTracePromises.size === 0) {
602
689
  return;
603
690
  }
604
- await Promise.race([
605
- Promise.allSettled(Array.from(pendingTracePromises)),
606
- new Promise((resolve) => setTimeout(resolve, timeoutMs))
607
- ]);
691
+ let timer;
692
+ try {
693
+ await Promise.race([
694
+ Promise.allSettled(Array.from(pendingTracePromises)),
695
+ new Promise((resolve) => {
696
+ timer = setTimeout(resolve, timeoutMs);
697
+ unrefTimer(timer);
698
+ })
699
+ ]);
700
+ } finally {
701
+ if (timer) {
702
+ clearTimeout(timer);
703
+ }
704
+ }
608
705
  }
609
706
  if (typeof process !== "undefined" && process.versions != null && process.versions.node != null) {
610
707
  let isFlushing = false;
@@ -914,6 +1011,7 @@ var HttpClient = class {
914
1011
  };
915
1012
 
916
1013
  // src/claudeAgentSdk.ts
1014
+ init_randomUuid();
917
1015
  init_serialize();
918
1016
  function nowIso() {
919
1017
  return (/* @__PURE__ */ new Date()).toISOString();
@@ -1001,7 +1099,7 @@ var BitfabClaudeAgentHandler = class {
1001
1099
  if (this.activeContext) {
1002
1100
  this.traceId = this.activeContext.traceId;
1003
1101
  } else {
1004
- this.traceId = crypto.randomUUID();
1102
+ this.traceId = randomUuid();
1005
1103
  }
1006
1104
  this.traceStartedAt = nowIso();
1007
1105
  return this.traceId;
@@ -1026,7 +1124,7 @@ var BitfabClaudeAgentHandler = class {
1026
1124
  if (this.activeContext !== null) {
1027
1125
  return;
1028
1126
  }
1029
- const spanId = crypto.randomUUID();
1127
+ const spanId = randomUuid();
1030
1128
  this.startSpan(spanId, this.traceFunctionKey, "agent", this.rootInput, null);
1031
1129
  this.rootSpanId = spanId;
1032
1130
  }
@@ -1140,7 +1238,7 @@ var BitfabClaudeAgentHandler = class {
1140
1238
  // ── hook callbacks ───────────────────────────────────────────
1141
1239
  async preToolUseHook(inputData, toolUseId, _context) {
1142
1240
  try {
1143
- const sid = inputData.tool_use_id ?? toolUseId ?? crypto.randomUUID();
1241
+ const sid = inputData.tool_use_id ?? toolUseId ?? randomUuid();
1144
1242
  const toolName = inputData.tool_name ?? "tool";
1145
1243
  const toolInput = inputData.tool_input ?? {};
1146
1244
  const agentId = inputData.agent_id;
@@ -1170,10 +1268,10 @@ var BitfabClaudeAgentHandler = class {
1170
1268
  }
1171
1269
  async subagentStartHook(inputData, _toolUseId, _context) {
1172
1270
  try {
1173
- const agentId = inputData.agent_id ?? crypto.randomUUID();
1271
+ const agentId = inputData.agent_id ?? randomUuid();
1174
1272
  const agentType = inputData.agent_type ?? "subagent";
1175
1273
  const parentId = this.getParentId();
1176
- const spanId = crypto.randomUUID();
1274
+ const spanId = randomUuid();
1177
1275
  this.activeSubagentSpans.set(agentId, spanId);
1178
1276
  this.startSpan(
1179
1277
  spanId,
@@ -1314,7 +1412,7 @@ var BitfabClaudeAgentHandler = class {
1314
1412
  this.flushLlmTurn();
1315
1413
  this.conversationHistory.push(...this.pendingMessages);
1316
1414
  this.pendingMessages = [];
1317
- this.currentLlmSpanId = crypto.randomUUID();
1415
+ this.currentLlmSpanId = randomUuid();
1318
1416
  this.currentLlmMessageId = messageId ?? null;
1319
1417
  this.currentLlmContent = [];
1320
1418
  this.currentLlmModel = inner.model ?? null;
@@ -1442,6 +1540,16 @@ var BitfabClaudeAgentHandler = class {
1442
1540
  // src/client.ts
1443
1541
  init_asyncStorage();
1444
1542
 
1543
+ // src/optionalPeer.ts
1544
+ function importOptionalPeer(specifierParts) {
1545
+ const specifier = specifierParts.join("/");
1546
+ return import(
1547
+ /* webpackIgnore: true */
1548
+ /* @vite-ignore */
1549
+ specifier
1550
+ );
1551
+ }
1552
+
1445
1553
  // src/baml.ts
1446
1554
  var cachedBaml = null;
1447
1555
  async function loadBaml() {
@@ -1449,7 +1557,7 @@ async function loadBaml() {
1449
1557
  return cachedBaml;
1450
1558
  }
1451
1559
  try {
1452
- cachedBaml = await import("@boundaryml/baml");
1560
+ cachedBaml = await importOptionalPeer(["@boundaryml", "baml"]);
1453
1561
  return cachedBaml;
1454
1562
  } catch {
1455
1563
  throw new Error(
@@ -1748,6 +1856,7 @@ function buildSnapshotRef(config, sdkWallClockBeforeFn) {
1748
1856
  }
1749
1857
 
1750
1858
  // src/langgraph.ts
1859
+ init_randomUuid();
1751
1860
  init_serialize();
1752
1861
  var LANGSMITH_HIDDEN_TAG = "langsmith:hidden";
1753
1862
  var LANGGRAPH_METADATA_KEYS = [
@@ -1996,7 +2105,7 @@ var BitfabLangGraphCallbackHandler = class {
1996
2105
  } else {
1997
2106
  const activeContext = this.getActiveSpanContext?.() ?? null;
1998
2107
  invocation = {
1999
- traceId: activeContext ? activeContext.traceId : crypto.randomUUID(),
2108
+ traceId: activeContext ? activeContext.traceId : randomUuid(),
2000
2109
  activeContext,
2001
2110
  rootRunId: runId
2002
2111
  };
@@ -2291,8 +2400,25 @@ var BitfabOpenAIAgentHandler = class {
2291
2400
  this.withSpanFn = config.withSpan;
2292
2401
  this.getActiveSpanContext = config.getActiveSpanContext;
2293
2402
  }
2403
+ /**
2404
+ * Drop-in replacement for the OpenAI Agents SDK's `run()` that records a
2405
+ * replayable root `agent` span.
2406
+ *
2407
+ * The `input` is captured as the root span's input (as a single positional
2408
+ * argument, so `replay(key, fn)` re-feeds it), and the run's `finalOutput`
2409
+ * is recorded as the root output. For streaming runs (`{ stream: true }`),
2410
+ * the result is handed back immediately and the final output is recorded
2411
+ * once the stream completes — first-byte latency is untouched.
2412
+ *
2413
+ * The process-wide tracing processor (`getOpenAiTracingProcessor`) must still
2414
+ * be registered: it captures the LLM/tool/handoff spans that nest beneath
2415
+ * this root.
2416
+ */
2294
2417
  async wrapRun(agent, input, options) {
2295
- const { run } = await import("@openai/agents");
2418
+ const { run } = await importOptionalPeer([
2419
+ "@openai",
2420
+ "agents"
2421
+ ]);
2296
2422
  if (this.getActiveSpanContext?.() != null) {
2297
2423
  return run(
2298
2424
  agent,
@@ -2326,6 +2452,7 @@ var BitfabOpenAIAgentHandler = class {
2326
2452
  };
2327
2453
 
2328
2454
  // src/client.ts
2455
+ init_randomUuid();
2329
2456
  init_replayContext();
2330
2457
 
2331
2458
  // src/replayEnvironment.ts
@@ -2745,6 +2872,7 @@ var BitfabVercelAiHandler = class {
2745
2872
  };
2746
2873
 
2747
2874
  // src/client.ts
2875
+ init_warnOnce();
2748
2876
  var activeTraceStates = /* @__PURE__ */ new Map();
2749
2877
  var pendingSpanPromises = /* @__PURE__ */ new Map();
2750
2878
  var asyncLocalStorage = null;
@@ -2843,7 +2971,10 @@ async function loadCollectorClass() {
2843
2971
  return cachedCollectorClass;
2844
2972
  }
2845
2973
  try {
2846
- const baml = await import("@boundaryml/baml");
2974
+ const baml = await importOptionalPeer([
2975
+ "@boundaryml",
2976
+ "baml"
2977
+ ]);
2847
2978
  cachedCollectorClass = baml.Collector;
2848
2979
  return cachedCollectorClass;
2849
2980
  } catch {
@@ -3100,7 +3231,20 @@ var Bitfab = class {
3100
3231
  functionVersion.providers,
3101
3232
  this.envVars
3102
3233
  );
3103
- const resultStr = typeof executionResult.result === "string" ? executionResult.result : JSON.stringify(executionResult.result);
3234
+ let resultStr;
3235
+ if (typeof executionResult.result === "string") {
3236
+ resultStr = executionResult.result;
3237
+ } else {
3238
+ try {
3239
+ resultStr = JSON.stringify(executionResult.result) ?? String(executionResult.result);
3240
+ } catch {
3241
+ warnOnce(
3242
+ "call-result-serialize",
3243
+ "a local execution result could not be JSON-serialized; storing its String() form instead. The call still returns its real value."
3244
+ );
3245
+ resultStr = String(executionResult.result);
3246
+ }
3247
+ }
3104
3248
  this.httpClient.sendInternalTrace(functionVersion.id, {
3105
3249
  result: resultStr,
3106
3250
  source: "typescript-sdk",
@@ -3349,11 +3493,26 @@ var Bitfab = class {
3349
3493
  return await bamlClient[methodName](...args);
3350
3494
  }
3351
3495
  const collector = new CollectorClass("bitfab-baml-tracing");
3352
- const trackedClient = bamlClient.withOptions({ collector });
3353
- const trackedMethod = trackedClient[methodName];
3354
- const result = await trackedMethod.bind(
3355
- trackedClient
3356
- )(...args);
3496
+ let trackedClient;
3497
+ let trackedMethod;
3498
+ try {
3499
+ trackedClient = bamlClient.withOptions({ collector });
3500
+ const method2 = trackedClient[methodName];
3501
+ if (typeof method2 !== "function") {
3502
+ throw new BitfabError(
3503
+ "bamlClient.withOptions did not return the wrapped method"
3504
+ );
3505
+ }
3506
+ trackedMethod = method2;
3507
+ } catch {
3508
+ warnOnce(
3509
+ `wrapBAML-setup:${methodName}`,
3510
+ `BAML tracing setup failed for "${methodName}" (incompatible bamlClient or BAML version); calling it untraced. The call still runs; no span is recorded.`
3511
+ );
3512
+ wrappedFn.collector = null;
3513
+ return await bamlClient[methodName](...args);
3514
+ }
3515
+ const result = await trackedMethod.bind(trackedClient)(...args);
3357
3516
  wrappedFn.collector = collector;
3358
3517
  try {
3359
3518
  const prompt = extractPromptFromCollector(collector);
@@ -3429,200 +3588,229 @@ var Bitfab = class {
3429
3588
  () => wrappedFn.apply(this, args)
3430
3589
  );
3431
3590
  }
3432
- const currentStack = getSpanStack();
3433
- const parentContext = currentStack[currentStack.length - 1];
3434
- const replayCtxForTraceId = parentContext ? null : getReplayContext();
3435
- const traceId = parentContext?.traceId ?? replayCtxForTraceId?.traceId ?? crypto.randomUUID();
3436
- const spanId = crypto.randomUUID();
3437
- const parentSpanId = parentContext?.spanId ?? null;
3438
- const isRootSpan = parentSpanId === null;
3439
- const newContext = { traceId, spanId, contexts: [] };
3440
- const newStack = [...currentStack, newContext];
3441
- const inputs = args;
3442
- const startedAt = (/* @__PURE__ */ new Date()).toISOString();
3443
- if (isRootSpan && !activeTraceStates.has(traceId)) {
3444
- const replayCtxAtRoot = getReplayContext();
3445
- const dbSnapshotRef = buildSnapshotRef(self.dbSnapshot, startedAt);
3446
- activeTraceStates.set(traceId, {
3591
+ let newStack;
3592
+ let executeWithContext;
3593
+ let registeredTraceId;
3594
+ try {
3595
+ const currentStack = getSpanStack();
3596
+ const parentContext = currentStack[currentStack.length - 1];
3597
+ const replayCtxForTraceId = parentContext ? null : getReplayContext();
3598
+ const traceId = parentContext?.traceId ?? replayCtxForTraceId?.traceId ?? randomUuid();
3599
+ const spanId = randomUuid();
3600
+ const parentSpanId = parentContext?.spanId ?? null;
3601
+ const isRootSpan = parentSpanId === null;
3602
+ const newContext = { traceId, spanId, contexts: [] };
3603
+ newStack = [...currentStack, newContext];
3604
+ const inputs = args;
3605
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
3606
+ if (isRootSpan && !activeTraceStates.has(traceId)) {
3607
+ const replayCtxAtRoot = getReplayContext();
3608
+ const dbSnapshotRef = buildSnapshotRef(self.dbSnapshot, startedAt);
3609
+ activeTraceStates.set(traceId, {
3610
+ traceId,
3611
+ startedAt,
3612
+ contexts: [],
3613
+ ...replayCtxAtRoot?.testRunId && {
3614
+ testRunId: replayCtxAtRoot.testRunId
3615
+ },
3616
+ ...replayCtxAtRoot?.inputSourceTraceId && {
3617
+ inputSourceTraceId: replayCtxAtRoot.inputSourceTraceId
3618
+ },
3619
+ dbSnapshotRef
3620
+ });
3621
+ pendingSpanPromises.set(traceId, []);
3622
+ registeredTraceId = traceId;
3623
+ }
3624
+ const functionName = fn.name !== "" ? fn.name : void 0;
3625
+ const baseSpanParams = {
3626
+ traceFunctionKey,
3627
+ functionName,
3628
+ spanName: options.name ?? functionName ?? traceFunctionKey,
3447
3629
  traceId,
3630
+ spanId,
3631
+ parentSpanId,
3632
+ inputs,
3448
3633
  startedAt,
3449
- contexts: [],
3450
- ...replayCtxAtRoot?.testRunId && {
3451
- testRunId: replayCtxAtRoot.testRunId
3452
- },
3453
- ...replayCtxAtRoot?.inputSourceTraceId && {
3454
- inputSourceTraceId: replayCtxAtRoot.inputSourceTraceId
3455
- },
3456
- dbSnapshotRef
3457
- });
3458
- pendingSpanPromises.set(traceId, []);
3459
- }
3460
- const functionName = fn.name !== "" ? fn.name : void 0;
3461
- const baseSpanParams = {
3462
- traceFunctionKey,
3463
- functionName,
3464
- spanName: options.name ?? functionName ?? traceFunctionKey,
3465
- traceId,
3466
- spanId,
3467
- parentSpanId,
3468
- inputs,
3469
- startedAt,
3470
- spanType: options.type ?? "custom"
3471
- };
3472
- const sendSpan = async (params, spanOpts) => {
3473
- const replayCtx = getReplayContext();
3474
- const persistenceCollector = isRootSpan ? replayCtx?.pendingPersistence : void 0;
3475
- let resolvePersistence;
3476
- if (persistenceCollector && !spanOpts?.skipPersistenceRegistration) {
3477
- persistenceCollector.push(
3478
- new Promise((resolve) => {
3479
- resolvePersistence = resolve;
3480
- })
3481
- );
3482
- }
3483
- try {
3484
- const endedAt = (/* @__PURE__ */ new Date()).toISOString();
3485
- const spanPromise = self.sendWrapperSpan({
3486
- ...baseSpanParams,
3487
- ...params,
3488
- contexts: newContext.contexts,
3489
- prompt: newContext.prompt,
3490
- endedAt,
3491
- ...replayCtx?.testRunId && { testRunId: replayCtx.testRunId },
3492
- ...replayCtx?.inputSourceSpanId && {
3493
- inputSourceSpanId: replayCtx.inputSourceSpanId
3494
- }
3495
- });
3496
- if (isRootSpan) {
3497
- const pending = pendingSpanPromises.get(traceId) ?? [];
3498
- pending.push(spanPromise);
3499
- if (persistenceCollector) {
3500
- await Promise.allSettled(pending);
3501
- } else {
3502
- await Promise.race([
3503
- Promise.allSettled(pending),
3504
- new Promise((resolve) => setTimeout(resolve, 5e3))
3505
- ]);
3506
- }
3507
- pendingSpanPromises.delete(traceId);
3508
- const traceState = activeTraceStates.get(traceId);
3509
- const completionPromise = self.sendTraceCompletion({
3510
- traceFunctionKey,
3511
- traceId,
3512
- startedAt: traceState?.startedAt ?? startedAt,
3634
+ spanType: options.type ?? "custom"
3635
+ };
3636
+ const sendSpan = async (params, spanOpts) => {
3637
+ const replayCtx = getReplayContext();
3638
+ const persistenceCollector = isRootSpan ? replayCtx?.pendingPersistence : void 0;
3639
+ let resolvePersistence;
3640
+ if (persistenceCollector && !spanOpts?.skipPersistenceRegistration) {
3641
+ persistenceCollector.push(
3642
+ new Promise((resolve) => {
3643
+ resolvePersistence = resolve;
3644
+ })
3645
+ );
3646
+ }
3647
+ try {
3648
+ const endedAt = (/* @__PURE__ */ new Date()).toISOString();
3649
+ const spanPromise = self.sendWrapperSpan({
3650
+ ...baseSpanParams,
3651
+ ...params,
3652
+ contexts: newContext.contexts,
3653
+ prompt: newContext.prompt,
3513
3654
  endedAt,
3514
- sessionId: traceState?.sessionId,
3515
- metadata: traceState?.metadata,
3516
- contexts: traceState?.contexts ?? [],
3517
- testRunId: traceState?.testRunId,
3518
- inputSourceTraceId: traceState?.inputSourceTraceId,
3519
- dbSnapshotRef: traceState?.dbSnapshotRef,
3520
- // Built AFTER the wrapped fn finished, so `accessed` reflects
3521
- // whether customer code obtained the branch URL during this
3522
- // item. Omitted entirely when no lease was attached, so the
3523
- // server can distinguish "no branch" from "branch ignored".
3524
- ...replayCtx?.dbBranchLease && {
3525
- dbSnapshotUsage: {
3526
- neonBranchId: replayCtx.dbBranchLease.neonBranchId,
3527
- snapshotTimestamp: replayCtx.dbBranchLease.snapshotTimestamp,
3528
- sourceTraceId: replayCtx.sourceBitfabTraceId,
3529
- accessed: replayCtx.dbSnapshotAccessed === true
3530
- }
3655
+ ...replayCtx?.testRunId && { testRunId: replayCtx.testRunId },
3656
+ ...replayCtx?.inputSourceSpanId && {
3657
+ inputSourceSpanId: replayCtx.inputSourceSpanId
3531
3658
  }
3532
3659
  });
3533
- activeTraceStates.delete(traceId);
3534
- if (persistenceCollector) {
3535
- await completionPromise;
3536
- }
3537
- } else {
3538
- const pending = pendingSpanPromises.get(traceId);
3539
- if (pending) {
3660
+ if (isRootSpan) {
3661
+ const pending = pendingSpanPromises.get(traceId) ?? [];
3540
3662
  pending.push(spanPromise);
3663
+ if (persistenceCollector) {
3664
+ await Promise.allSettled(pending);
3665
+ } else {
3666
+ let raceTimer;
3667
+ try {
3668
+ await Promise.race([
3669
+ Promise.allSettled(pending),
3670
+ new Promise((resolve) => {
3671
+ raceTimer = setTimeout(resolve, 5e3);
3672
+ unrefTimer(raceTimer);
3673
+ })
3674
+ ]);
3675
+ } finally {
3676
+ if (raceTimer) {
3677
+ clearTimeout(raceTimer);
3678
+ }
3679
+ }
3680
+ }
3681
+ pendingSpanPromises.delete(traceId);
3682
+ const traceState = activeTraceStates.get(traceId);
3683
+ const completionPromise = self.sendTraceCompletion({
3684
+ traceFunctionKey,
3685
+ traceId,
3686
+ startedAt: traceState?.startedAt ?? startedAt,
3687
+ endedAt,
3688
+ sessionId: traceState?.sessionId,
3689
+ metadata: traceState?.metadata,
3690
+ contexts: traceState?.contexts ?? [],
3691
+ testRunId: traceState?.testRunId,
3692
+ inputSourceTraceId: traceState?.inputSourceTraceId,
3693
+ dbSnapshotRef: traceState?.dbSnapshotRef,
3694
+ // Built AFTER the wrapped fn finished, so `accessed` reflects
3695
+ // whether customer code obtained the branch URL during this
3696
+ // item. Omitted entirely when no lease was attached, so the
3697
+ // server can distinguish "no branch" from "branch ignored".
3698
+ ...replayCtx?.dbBranchLease && {
3699
+ dbSnapshotUsage: {
3700
+ neonBranchId: replayCtx.dbBranchLease.neonBranchId,
3701
+ snapshotTimestamp: replayCtx.dbBranchLease.snapshotTimestamp,
3702
+ sourceTraceId: replayCtx.sourceBitfabTraceId,
3703
+ accessed: replayCtx.dbSnapshotAccessed === true
3704
+ }
3705
+ }
3706
+ });
3707
+ activeTraceStates.delete(traceId);
3708
+ if (persistenceCollector) {
3709
+ await completionPromise;
3710
+ }
3541
3711
  } else {
3542
- pendingSpanPromises.set(traceId, [spanPromise]);
3712
+ const pending = pendingSpanPromises.get(traceId);
3713
+ if (pending) {
3714
+ pending.push(spanPromise);
3715
+ } else {
3716
+ pendingSpanPromises.set(traceId, [spanPromise]);
3717
+ }
3543
3718
  }
3719
+ } catch {
3720
+ } finally {
3721
+ resolvePersistence?.();
3544
3722
  }
3545
- } catch {
3546
- } finally {
3547
- resolvePersistence?.();
3548
- }
3549
- };
3550
- const replayCtxForMock = getReplayContext();
3551
- if (replayCtxForMock?.mockTree && !isRootSpan) {
3552
- const counters = replayCtxForMock.callCounters;
3553
- const counterKey = `${traceFunctionKey}:${baseSpanParams.spanName}`;
3554
- const callIndex = counters.get(counterKey) ?? 0;
3555
- counters.set(counterKey, callIndex + 1);
3556
- const shouldMock = replayCtxForMock.mockStrategy === "all" || replayCtxForMock.mockStrategy === "marked" && options.mockOnReplay === true;
3557
- if (shouldMock) {
3558
- const mockKey = `${counterKey}:${callIndex}`;
3559
- const mockSpan = replayCtxForMock.mockTree.spans.get(mockKey);
3560
- if (mockSpan) {
3561
- let output = mockSpan.output;
3562
- if (mockSpan.outputMeta !== void 0 && mockSpan.outputMeta !== null) {
3563
- output = deserializeValue({
3564
- json: mockSpan.output,
3565
- meta: mockSpan.outputMeta
3566
- });
3567
- }
3568
- void sendSpan({ result: output });
3569
- if (fnReturnsPromise) {
3570
- return Promise.resolve(output);
3723
+ };
3724
+ const replayCtxForMock = getReplayContext();
3725
+ if (replayCtxForMock?.mockTree && !isRootSpan) {
3726
+ const counters = replayCtxForMock.callCounters;
3727
+ const counterKey = `${traceFunctionKey}:${baseSpanParams.spanName}`;
3728
+ const callIndex = counters.get(counterKey) ?? 0;
3729
+ counters.set(counterKey, callIndex + 1);
3730
+ const shouldMock = replayCtxForMock.mockStrategy === "all" || replayCtxForMock.mockStrategy === "marked" && options.mockOnReplay === true;
3731
+ if (shouldMock) {
3732
+ const mockKey = `${counterKey}:${callIndex}`;
3733
+ const mockSpan = replayCtxForMock.mockTree.spans.get(mockKey);
3734
+ if (mockSpan) {
3735
+ let output = mockSpan.output;
3736
+ if (mockSpan.outputMeta !== void 0 && mockSpan.outputMeta !== null) {
3737
+ output = deserializeValue({
3738
+ json: mockSpan.output,
3739
+ meta: mockSpan.outputMeta
3740
+ });
3741
+ }
3742
+ void sendSpan({ result: output });
3743
+ if (fnReturnsPromise) {
3744
+ return Promise.resolve(output);
3745
+ }
3746
+ return output;
3571
3747
  }
3572
- return output;
3573
3748
  }
3574
3749
  }
3575
- }
3576
- const recordSpan = (result) => {
3577
- if (options.finalize) {
3578
- const replayCtx = getReplayContext();
3579
- const persistenceCollector = isRootSpan ? replayCtx?.pendingPersistence : void 0;
3580
- let resolvePersistence;
3581
- if (persistenceCollector) {
3582
- persistenceCollector.push(
3583
- new Promise((resolve) => {
3584
- resolvePersistence = resolve;
3585
- })
3586
- );
3750
+ const recordSpan = (result) => {
3751
+ if (options.finalize) {
3752
+ const replayCtx = getReplayContext();
3753
+ const persistenceCollector = isRootSpan ? replayCtx?.pendingPersistence : void 0;
3754
+ let resolvePersistence;
3755
+ if (persistenceCollector) {
3756
+ persistenceCollector.push(
3757
+ new Promise((resolve) => {
3758
+ resolvePersistence = resolve;
3759
+ })
3760
+ );
3761
+ }
3762
+ void Promise.resolve().then(() => options.finalize(result)).then(
3763
+ (output) => sendSpan(
3764
+ { result: output },
3765
+ { skipPersistenceRegistration: true }
3766
+ )
3767
+ ).catch(
3768
+ (error) => sendSpan(
3769
+ {
3770
+ result: void 0,
3771
+ error: error instanceof Error ? `finalize failed: ${error.message}` : `finalize failed: ${String(error)}`
3772
+ },
3773
+ { skipPersistenceRegistration: true }
3774
+ )
3775
+ ).finally(() => resolvePersistence?.());
3776
+ } else {
3777
+ void sendSpan({ result });
3587
3778
  }
3588
- void Promise.resolve().then(() => options.finalize(result)).then(
3589
- (output) => sendSpan(
3590
- { result: output },
3591
- { skipPersistenceRegistration: true }
3592
- )
3593
- ).catch(
3594
- (error) => sendSpan(
3595
- {
3779
+ };
3780
+ executeWithContext = () => {
3781
+ const result = fn(...args);
3782
+ if (result instanceof Promise) {
3783
+ return result.then((resolvedResult) => {
3784
+ recordSpan(resolvedResult);
3785
+ return resolvedResult;
3786
+ }).catch((error) => {
3787
+ void sendSpan({
3596
3788
  result: void 0,
3597
- error: error instanceof Error ? `finalize failed: ${error.message}` : `finalize failed: ${String(error)}`
3598
- },
3599
- { skipPersistenceRegistration: true }
3600
- )
3601
- ).finally(() => resolvePersistence?.());
3602
- } else {
3603
- void sendSpan({ result });
3604
- }
3605
- };
3606
- const executeWithContext = () => {
3607
- const result = fn(...args);
3608
- if (result instanceof Promise) {
3609
- return result.then((resolvedResult) => {
3610
- recordSpan(resolvedResult);
3611
- return resolvedResult;
3612
- }).catch((error) => {
3613
- void sendSpan({
3614
- result: void 0,
3615
- error: error instanceof Error ? error.message : String(error)
3789
+ error: error instanceof Error ? error.message : String(error)
3790
+ });
3791
+ throw error;
3616
3792
  });
3617
- throw error;
3618
- });
3793
+ }
3794
+ if (isAsyncGenerator(result)) {
3795
+ return wrapAsyncGenerator(result, newStack, sendSpan);
3796
+ }
3797
+ recordSpan(result);
3798
+ return result;
3799
+ };
3800
+ } catch (setupError) {
3801
+ if (registeredTraceId) {
3802
+ activeTraceStates.delete(registeredTraceId);
3803
+ pendingSpanPromises.delete(registeredTraceId);
3619
3804
  }
3620
- if (isAsyncGenerator(result)) {
3621
- return wrapAsyncGenerator(result, newStack, sendSpan);
3805
+ if (getReplayContext()) {
3806
+ throw setupError;
3622
3807
  }
3623
- recordSpan(result);
3624
- return result;
3625
- };
3808
+ warnOnce(
3809
+ `withSpan-setup:${traceFunctionKey}`,
3810
+ `tracing setup failed for "${traceFunctionKey}"; running it untraced. The function still runs and returns normally; no span is recorded.`
3811
+ );
3812
+ return fn(...args);
3813
+ }
3626
3814
  return runWithSpanStack(newStack, executeWithContext);
3627
3815
  };
3628
3816
  Object.defineProperty(wrappedFn, "_bitfabTraceFunctionKey", {