bitfab 0.23.3 → 0.24.1

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);
@@ -348,7 +414,10 @@ async function processItem(httpClient, serverItem, fn, testRunId, mockStrategy,
348
414
  originalOutput,
349
415
  error,
350
416
  durationMs: serverItem.durationMs ?? null,
351
- tokens: serverItem.tokens ?? null,
417
+ // Filled in by replay() from the complete-replay response once the
418
+ // replay traces are persisted and their spans aggregated server-side.
419
+ // Null here (and on older servers) means "replay tokens not known".
420
+ tokens: null,
352
421
  model: serverItem.model ?? null,
353
422
  dbSnapshotRef: serverItem.dbSnapshotRef ?? null
354
423
  };
@@ -422,6 +491,7 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options) {
422
491
  const resultItems = await mapWithConcurrency(tasks, maxConcurrency);
423
492
  const completeResult = await httpClient.completeReplay(testRunId);
424
493
  const serverTraceIds = completeResult.traceIds;
494
+ const replayTokens = completeResult.tokens;
425
495
  if (serverTraceIds === void 0) {
426
496
  try {
427
497
  console.warn(
@@ -444,6 +514,9 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options) {
444
514
  missing.push(item.traceId);
445
515
  }
446
516
  }
517
+ if (mapped !== void 0) {
518
+ item.tokens = replayTokens?.[mapped] ?? null;
519
+ }
447
520
  item.traceId = mapped ?? null;
448
521
  }
449
522
  }
@@ -472,6 +545,7 @@ var init_replay = __esm({
472
545
  "src/replay.ts"() {
473
546
  "use strict";
474
547
  init_errors();
548
+ init_randomUuid();
475
549
  init_replayContext();
476
550
  init_serialize();
477
551
  }
@@ -501,13 +575,24 @@ __export(index_exports, {
501
575
  module.exports = __toCommonJS(index_exports);
502
576
 
503
577
  // src/version.generated.ts
504
- var __version__ = "0.23.3";
578
+ var __version__ = "0.24.1";
505
579
 
506
580
  // src/constants.ts
507
581
  var DEFAULT_SERVICE_URL = "https://bitfab.ai";
508
582
 
509
583
  // src/http.ts
510
584
  init_errors();
585
+
586
+ // src/unrefTimer.ts
587
+ function unrefTimer(timer) {
588
+ const handle = timer;
589
+ if (typeof handle.unref === "function") {
590
+ handle.unref();
591
+ }
592
+ }
593
+
594
+ // src/http.ts
595
+ init_warnOnce();
511
596
  function serializePayloadBody(payload) {
512
597
  try {
513
598
  return { body: JSON.stringify(payload), dropped: [] };
@@ -552,11 +637,20 @@ function serializePayloadBody(payload) {
552
637
  result = `<unserializable: ${className}>`;
553
638
  }
554
639
  } else {
555
- const out = {};
556
- for (const [k, v] of Object.entries(obj)) {
557
- out[k] = sanitize(v, seen);
640
+ try {
641
+ const out = {};
642
+ for (const [k, v] of Object.entries(obj)) {
643
+ out[k] = sanitize(v, seen);
644
+ }
645
+ result = out;
646
+ } catch {
647
+ warnOnce(
648
+ "payload:field-getter-threw",
649
+ "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."
650
+ );
651
+ dropped.push(className);
652
+ result = `<unserializable: ${className}>`;
558
653
  }
559
- result = out;
560
654
  }
561
655
  seen.delete(obj);
562
656
  return result;
@@ -601,10 +695,20 @@ async function flushTraces(timeoutMs = 5e3) {
601
695
  if (pendingTracePromises.size === 0) {
602
696
  return;
603
697
  }
604
- await Promise.race([
605
- Promise.allSettled(Array.from(pendingTracePromises)),
606
- new Promise((resolve) => setTimeout(resolve, timeoutMs))
607
- ]);
698
+ let timer;
699
+ try {
700
+ await Promise.race([
701
+ Promise.allSettled(Array.from(pendingTracePromises)),
702
+ new Promise((resolve) => {
703
+ timer = setTimeout(resolve, timeoutMs);
704
+ unrefTimer(timer);
705
+ })
706
+ ]);
707
+ } finally {
708
+ if (timer) {
709
+ clearTimeout(timer);
710
+ }
711
+ }
608
712
  }
609
713
  if (typeof process !== "undefined" && process.versions != null && process.versions.node != null) {
610
714
  let isFlushing = false;
@@ -914,6 +1018,7 @@ var HttpClient = class {
914
1018
  };
915
1019
 
916
1020
  // src/claudeAgentSdk.ts
1021
+ init_randomUuid();
917
1022
  init_serialize();
918
1023
  function nowIso() {
919
1024
  return (/* @__PURE__ */ new Date()).toISOString();
@@ -1001,7 +1106,7 @@ var BitfabClaudeAgentHandler = class {
1001
1106
  if (this.activeContext) {
1002
1107
  this.traceId = this.activeContext.traceId;
1003
1108
  } else {
1004
- this.traceId = crypto.randomUUID();
1109
+ this.traceId = randomUuid();
1005
1110
  }
1006
1111
  this.traceStartedAt = nowIso();
1007
1112
  return this.traceId;
@@ -1026,7 +1131,7 @@ var BitfabClaudeAgentHandler = class {
1026
1131
  if (this.activeContext !== null) {
1027
1132
  return;
1028
1133
  }
1029
- const spanId = crypto.randomUUID();
1134
+ const spanId = randomUuid();
1030
1135
  this.startSpan(spanId, this.traceFunctionKey, "agent", this.rootInput, null);
1031
1136
  this.rootSpanId = spanId;
1032
1137
  }
@@ -1140,7 +1245,7 @@ var BitfabClaudeAgentHandler = class {
1140
1245
  // ── hook callbacks ───────────────────────────────────────────
1141
1246
  async preToolUseHook(inputData, toolUseId, _context) {
1142
1247
  try {
1143
- const sid = inputData.tool_use_id ?? toolUseId ?? crypto.randomUUID();
1248
+ const sid = inputData.tool_use_id ?? toolUseId ?? randomUuid();
1144
1249
  const toolName = inputData.tool_name ?? "tool";
1145
1250
  const toolInput = inputData.tool_input ?? {};
1146
1251
  const agentId = inputData.agent_id;
@@ -1170,10 +1275,10 @@ var BitfabClaudeAgentHandler = class {
1170
1275
  }
1171
1276
  async subagentStartHook(inputData, _toolUseId, _context) {
1172
1277
  try {
1173
- const agentId = inputData.agent_id ?? crypto.randomUUID();
1278
+ const agentId = inputData.agent_id ?? randomUuid();
1174
1279
  const agentType = inputData.agent_type ?? "subagent";
1175
1280
  const parentId = this.getParentId();
1176
- const spanId = crypto.randomUUID();
1281
+ const spanId = randomUuid();
1177
1282
  this.activeSubagentSpans.set(agentId, spanId);
1178
1283
  this.startSpan(
1179
1284
  spanId,
@@ -1314,7 +1419,7 @@ var BitfabClaudeAgentHandler = class {
1314
1419
  this.flushLlmTurn();
1315
1420
  this.conversationHistory.push(...this.pendingMessages);
1316
1421
  this.pendingMessages = [];
1317
- this.currentLlmSpanId = crypto.randomUUID();
1422
+ this.currentLlmSpanId = randomUuid();
1318
1423
  this.currentLlmMessageId = messageId ?? null;
1319
1424
  this.currentLlmContent = [];
1320
1425
  this.currentLlmModel = inner.model ?? null;
@@ -1758,6 +1863,7 @@ function buildSnapshotRef(config, sdkWallClockBeforeFn) {
1758
1863
  }
1759
1864
 
1760
1865
  // src/langgraph.ts
1866
+ init_randomUuid();
1761
1867
  init_serialize();
1762
1868
  var LANGSMITH_HIDDEN_TAG = "langsmith:hidden";
1763
1869
  var LANGGRAPH_METADATA_KEYS = [
@@ -2006,7 +2112,7 @@ var BitfabLangGraphCallbackHandler = class {
2006
2112
  } else {
2007
2113
  const activeContext = this.getActiveSpanContext?.() ?? null;
2008
2114
  invocation = {
2009
- traceId: activeContext ? activeContext.traceId : crypto.randomUUID(),
2115
+ traceId: activeContext ? activeContext.traceId : randomUuid(),
2010
2116
  activeContext,
2011
2117
  rootRunId: runId
2012
2118
  };
@@ -2301,6 +2407,20 @@ var BitfabOpenAIAgentHandler = class {
2301
2407
  this.withSpanFn = config.withSpan;
2302
2408
  this.getActiveSpanContext = config.getActiveSpanContext;
2303
2409
  }
2410
+ /**
2411
+ * Drop-in replacement for the OpenAI Agents SDK's `run()` that records a
2412
+ * replayable root `agent` span.
2413
+ *
2414
+ * The `input` is captured as the root span's input (as a single positional
2415
+ * argument, so `replay(key, fn)` re-feeds it), and the run's `finalOutput`
2416
+ * is recorded as the root output. For streaming runs (`{ stream: true }`),
2417
+ * the result is handed back immediately and the final output is recorded
2418
+ * once the stream completes — first-byte latency is untouched.
2419
+ *
2420
+ * The process-wide tracing processor (`getOpenAiTracingProcessor`) must still
2421
+ * be registered: it captures the LLM/tool/handoff spans that nest beneath
2422
+ * this root.
2423
+ */
2304
2424
  async wrapRun(agent, input, options) {
2305
2425
  const { run } = await importOptionalPeer([
2306
2426
  "@openai",
@@ -2339,6 +2459,7 @@ var BitfabOpenAIAgentHandler = class {
2339
2459
  };
2340
2460
 
2341
2461
  // src/client.ts
2462
+ init_randomUuid();
2342
2463
  init_replayContext();
2343
2464
 
2344
2465
  // src/replayEnvironment.ts
@@ -2758,6 +2879,7 @@ var BitfabVercelAiHandler = class {
2758
2879
  };
2759
2880
 
2760
2881
  // src/client.ts
2882
+ init_warnOnce();
2761
2883
  var activeTraceStates = /* @__PURE__ */ new Map();
2762
2884
  var pendingSpanPromises = /* @__PURE__ */ new Map();
2763
2885
  var asyncLocalStorage = null;
@@ -3116,7 +3238,20 @@ var Bitfab = class {
3116
3238
  functionVersion.providers,
3117
3239
  this.envVars
3118
3240
  );
3119
- const resultStr = typeof executionResult.result === "string" ? executionResult.result : JSON.stringify(executionResult.result);
3241
+ let resultStr;
3242
+ if (typeof executionResult.result === "string") {
3243
+ resultStr = executionResult.result;
3244
+ } else {
3245
+ try {
3246
+ resultStr = JSON.stringify(executionResult.result) ?? String(executionResult.result);
3247
+ } catch {
3248
+ warnOnce(
3249
+ "call-result-serialize",
3250
+ "a local execution result could not be JSON-serialized; storing its String() form instead. The call still returns its real value."
3251
+ );
3252
+ resultStr = String(executionResult.result);
3253
+ }
3254
+ }
3120
3255
  this.httpClient.sendInternalTrace(functionVersion.id, {
3121
3256
  result: resultStr,
3122
3257
  source: "typescript-sdk",
@@ -3365,11 +3500,26 @@ var Bitfab = class {
3365
3500
  return await bamlClient[methodName](...args);
3366
3501
  }
3367
3502
  const collector = new CollectorClass("bitfab-baml-tracing");
3368
- const trackedClient = bamlClient.withOptions({ collector });
3369
- const trackedMethod = trackedClient[methodName];
3370
- const result = await trackedMethod.bind(
3371
- trackedClient
3372
- )(...args);
3503
+ let trackedClient;
3504
+ let trackedMethod;
3505
+ try {
3506
+ trackedClient = bamlClient.withOptions({ collector });
3507
+ const method2 = trackedClient[methodName];
3508
+ if (typeof method2 !== "function") {
3509
+ throw new BitfabError(
3510
+ "bamlClient.withOptions did not return the wrapped method"
3511
+ );
3512
+ }
3513
+ trackedMethod = method2;
3514
+ } catch {
3515
+ warnOnce(
3516
+ `wrapBAML-setup:${methodName}`,
3517
+ `BAML tracing setup failed for "${methodName}" (incompatible bamlClient or BAML version); calling it untraced. The call still runs; no span is recorded.`
3518
+ );
3519
+ wrappedFn.collector = null;
3520
+ return await bamlClient[methodName](...args);
3521
+ }
3522
+ const result = await trackedMethod.bind(trackedClient)(...args);
3373
3523
  wrappedFn.collector = collector;
3374
3524
  try {
3375
3525
  const prompt = extractPromptFromCollector(collector);
@@ -3445,200 +3595,229 @@ var Bitfab = class {
3445
3595
  () => wrappedFn.apply(this, args)
3446
3596
  );
3447
3597
  }
3448
- const currentStack = getSpanStack();
3449
- const parentContext = currentStack[currentStack.length - 1];
3450
- const replayCtxForTraceId = parentContext ? null : getReplayContext();
3451
- const traceId = parentContext?.traceId ?? replayCtxForTraceId?.traceId ?? crypto.randomUUID();
3452
- const spanId = crypto.randomUUID();
3453
- const parentSpanId = parentContext?.spanId ?? null;
3454
- const isRootSpan = parentSpanId === null;
3455
- const newContext = { traceId, spanId, contexts: [] };
3456
- const newStack = [...currentStack, newContext];
3457
- const inputs = args;
3458
- const startedAt = (/* @__PURE__ */ new Date()).toISOString();
3459
- if (isRootSpan && !activeTraceStates.has(traceId)) {
3460
- const replayCtxAtRoot = getReplayContext();
3461
- const dbSnapshotRef = buildSnapshotRef(self.dbSnapshot, startedAt);
3462
- activeTraceStates.set(traceId, {
3598
+ let newStack;
3599
+ let executeWithContext;
3600
+ let registeredTraceId;
3601
+ try {
3602
+ const currentStack = getSpanStack();
3603
+ const parentContext = currentStack[currentStack.length - 1];
3604
+ const replayCtxForTraceId = parentContext ? null : getReplayContext();
3605
+ const traceId = parentContext?.traceId ?? replayCtxForTraceId?.traceId ?? randomUuid();
3606
+ const spanId = randomUuid();
3607
+ const parentSpanId = parentContext?.spanId ?? null;
3608
+ const isRootSpan = parentSpanId === null;
3609
+ const newContext = { traceId, spanId, contexts: [] };
3610
+ newStack = [...currentStack, newContext];
3611
+ const inputs = args;
3612
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
3613
+ if (isRootSpan && !activeTraceStates.has(traceId)) {
3614
+ const replayCtxAtRoot = getReplayContext();
3615
+ const dbSnapshotRef = buildSnapshotRef(self.dbSnapshot, startedAt);
3616
+ activeTraceStates.set(traceId, {
3617
+ traceId,
3618
+ startedAt,
3619
+ contexts: [],
3620
+ ...replayCtxAtRoot?.testRunId && {
3621
+ testRunId: replayCtxAtRoot.testRunId
3622
+ },
3623
+ ...replayCtxAtRoot?.inputSourceTraceId && {
3624
+ inputSourceTraceId: replayCtxAtRoot.inputSourceTraceId
3625
+ },
3626
+ dbSnapshotRef
3627
+ });
3628
+ pendingSpanPromises.set(traceId, []);
3629
+ registeredTraceId = traceId;
3630
+ }
3631
+ const functionName = fn.name !== "" ? fn.name : void 0;
3632
+ const baseSpanParams = {
3633
+ traceFunctionKey,
3634
+ functionName,
3635
+ spanName: options.name ?? functionName ?? traceFunctionKey,
3463
3636
  traceId,
3637
+ spanId,
3638
+ parentSpanId,
3639
+ inputs,
3464
3640
  startedAt,
3465
- contexts: [],
3466
- ...replayCtxAtRoot?.testRunId && {
3467
- testRunId: replayCtxAtRoot.testRunId
3468
- },
3469
- ...replayCtxAtRoot?.inputSourceTraceId && {
3470
- inputSourceTraceId: replayCtxAtRoot.inputSourceTraceId
3471
- },
3472
- dbSnapshotRef
3473
- });
3474
- pendingSpanPromises.set(traceId, []);
3475
- }
3476
- const functionName = fn.name !== "" ? fn.name : void 0;
3477
- const baseSpanParams = {
3478
- traceFunctionKey,
3479
- functionName,
3480
- spanName: options.name ?? functionName ?? traceFunctionKey,
3481
- traceId,
3482
- spanId,
3483
- parentSpanId,
3484
- inputs,
3485
- startedAt,
3486
- spanType: options.type ?? "custom"
3487
- };
3488
- const sendSpan = async (params, spanOpts) => {
3489
- const replayCtx = getReplayContext();
3490
- const persistenceCollector = isRootSpan ? replayCtx?.pendingPersistence : void 0;
3491
- let resolvePersistence;
3492
- if (persistenceCollector && !spanOpts?.skipPersistenceRegistration) {
3493
- persistenceCollector.push(
3494
- new Promise((resolve) => {
3495
- resolvePersistence = resolve;
3496
- })
3497
- );
3498
- }
3499
- try {
3500
- const endedAt = (/* @__PURE__ */ new Date()).toISOString();
3501
- const spanPromise = self.sendWrapperSpan({
3502
- ...baseSpanParams,
3503
- ...params,
3504
- contexts: newContext.contexts,
3505
- prompt: newContext.prompt,
3506
- endedAt,
3507
- ...replayCtx?.testRunId && { testRunId: replayCtx.testRunId },
3508
- ...replayCtx?.inputSourceSpanId && {
3509
- inputSourceSpanId: replayCtx.inputSourceSpanId
3510
- }
3511
- });
3512
- if (isRootSpan) {
3513
- const pending = pendingSpanPromises.get(traceId) ?? [];
3514
- pending.push(spanPromise);
3515
- if (persistenceCollector) {
3516
- await Promise.allSettled(pending);
3517
- } else {
3518
- await Promise.race([
3519
- Promise.allSettled(pending),
3520
- new Promise((resolve) => setTimeout(resolve, 5e3))
3521
- ]);
3522
- }
3523
- pendingSpanPromises.delete(traceId);
3524
- const traceState = activeTraceStates.get(traceId);
3525
- const completionPromise = self.sendTraceCompletion({
3526
- traceFunctionKey,
3527
- traceId,
3528
- startedAt: traceState?.startedAt ?? startedAt,
3641
+ spanType: options.type ?? "custom"
3642
+ };
3643
+ const sendSpan = async (params, spanOpts) => {
3644
+ const replayCtx = getReplayContext();
3645
+ const persistenceCollector = isRootSpan ? replayCtx?.pendingPersistence : void 0;
3646
+ let resolvePersistence;
3647
+ if (persistenceCollector && !spanOpts?.skipPersistenceRegistration) {
3648
+ persistenceCollector.push(
3649
+ new Promise((resolve) => {
3650
+ resolvePersistence = resolve;
3651
+ })
3652
+ );
3653
+ }
3654
+ try {
3655
+ const endedAt = (/* @__PURE__ */ new Date()).toISOString();
3656
+ const spanPromise = self.sendWrapperSpan({
3657
+ ...baseSpanParams,
3658
+ ...params,
3659
+ contexts: newContext.contexts,
3660
+ prompt: newContext.prompt,
3529
3661
  endedAt,
3530
- sessionId: traceState?.sessionId,
3531
- metadata: traceState?.metadata,
3532
- contexts: traceState?.contexts ?? [],
3533
- testRunId: traceState?.testRunId,
3534
- inputSourceTraceId: traceState?.inputSourceTraceId,
3535
- dbSnapshotRef: traceState?.dbSnapshotRef,
3536
- // Built AFTER the wrapped fn finished, so `accessed` reflects
3537
- // whether customer code obtained the branch URL during this
3538
- // item. Omitted entirely when no lease was attached, so the
3539
- // server can distinguish "no branch" from "branch ignored".
3540
- ...replayCtx?.dbBranchLease && {
3541
- dbSnapshotUsage: {
3542
- neonBranchId: replayCtx.dbBranchLease.neonBranchId,
3543
- snapshotTimestamp: replayCtx.dbBranchLease.snapshotTimestamp,
3544
- sourceTraceId: replayCtx.sourceBitfabTraceId,
3545
- accessed: replayCtx.dbSnapshotAccessed === true
3546
- }
3662
+ ...replayCtx?.testRunId && { testRunId: replayCtx.testRunId },
3663
+ ...replayCtx?.inputSourceSpanId && {
3664
+ inputSourceSpanId: replayCtx.inputSourceSpanId
3547
3665
  }
3548
3666
  });
3549
- activeTraceStates.delete(traceId);
3550
- if (persistenceCollector) {
3551
- await completionPromise;
3552
- }
3553
- } else {
3554
- const pending = pendingSpanPromises.get(traceId);
3555
- if (pending) {
3667
+ if (isRootSpan) {
3668
+ const pending = pendingSpanPromises.get(traceId) ?? [];
3556
3669
  pending.push(spanPromise);
3670
+ if (persistenceCollector) {
3671
+ await Promise.allSettled(pending);
3672
+ } else {
3673
+ let raceTimer;
3674
+ try {
3675
+ await Promise.race([
3676
+ Promise.allSettled(pending),
3677
+ new Promise((resolve) => {
3678
+ raceTimer = setTimeout(resolve, 5e3);
3679
+ unrefTimer(raceTimer);
3680
+ })
3681
+ ]);
3682
+ } finally {
3683
+ if (raceTimer) {
3684
+ clearTimeout(raceTimer);
3685
+ }
3686
+ }
3687
+ }
3688
+ pendingSpanPromises.delete(traceId);
3689
+ const traceState = activeTraceStates.get(traceId);
3690
+ const completionPromise = self.sendTraceCompletion({
3691
+ traceFunctionKey,
3692
+ traceId,
3693
+ startedAt: traceState?.startedAt ?? startedAt,
3694
+ endedAt,
3695
+ sessionId: traceState?.sessionId,
3696
+ metadata: traceState?.metadata,
3697
+ contexts: traceState?.contexts ?? [],
3698
+ testRunId: traceState?.testRunId,
3699
+ inputSourceTraceId: traceState?.inputSourceTraceId,
3700
+ dbSnapshotRef: traceState?.dbSnapshotRef,
3701
+ // Built AFTER the wrapped fn finished, so `accessed` reflects
3702
+ // whether customer code obtained the branch URL during this
3703
+ // item. Omitted entirely when no lease was attached, so the
3704
+ // server can distinguish "no branch" from "branch ignored".
3705
+ ...replayCtx?.dbBranchLease && {
3706
+ dbSnapshotUsage: {
3707
+ neonBranchId: replayCtx.dbBranchLease.neonBranchId,
3708
+ snapshotTimestamp: replayCtx.dbBranchLease.snapshotTimestamp,
3709
+ sourceTraceId: replayCtx.sourceBitfabTraceId,
3710
+ accessed: replayCtx.dbSnapshotAccessed === true
3711
+ }
3712
+ }
3713
+ });
3714
+ activeTraceStates.delete(traceId);
3715
+ if (persistenceCollector) {
3716
+ await completionPromise;
3717
+ }
3557
3718
  } else {
3558
- pendingSpanPromises.set(traceId, [spanPromise]);
3719
+ const pending = pendingSpanPromises.get(traceId);
3720
+ if (pending) {
3721
+ pending.push(spanPromise);
3722
+ } else {
3723
+ pendingSpanPromises.set(traceId, [spanPromise]);
3724
+ }
3559
3725
  }
3726
+ } catch {
3727
+ } finally {
3728
+ resolvePersistence?.();
3560
3729
  }
3561
- } catch {
3562
- } finally {
3563
- resolvePersistence?.();
3564
- }
3565
- };
3566
- const replayCtxForMock = getReplayContext();
3567
- if (replayCtxForMock?.mockTree && !isRootSpan) {
3568
- const counters = replayCtxForMock.callCounters;
3569
- const counterKey = `${traceFunctionKey}:${baseSpanParams.spanName}`;
3570
- const callIndex = counters.get(counterKey) ?? 0;
3571
- counters.set(counterKey, callIndex + 1);
3572
- const shouldMock = replayCtxForMock.mockStrategy === "all" || replayCtxForMock.mockStrategy === "marked" && options.mockOnReplay === true;
3573
- if (shouldMock) {
3574
- const mockKey = `${counterKey}:${callIndex}`;
3575
- const mockSpan = replayCtxForMock.mockTree.spans.get(mockKey);
3576
- if (mockSpan) {
3577
- let output = mockSpan.output;
3578
- if (mockSpan.outputMeta !== void 0 && mockSpan.outputMeta !== null) {
3579
- output = deserializeValue({
3580
- json: mockSpan.output,
3581
- meta: mockSpan.outputMeta
3582
- });
3583
- }
3584
- void sendSpan({ result: output });
3585
- if (fnReturnsPromise) {
3586
- return Promise.resolve(output);
3730
+ };
3731
+ const replayCtxForMock = getReplayContext();
3732
+ if (replayCtxForMock?.mockTree && !isRootSpan) {
3733
+ const counters = replayCtxForMock.callCounters;
3734
+ const counterKey = `${traceFunctionKey}:${baseSpanParams.spanName}`;
3735
+ const callIndex = counters.get(counterKey) ?? 0;
3736
+ counters.set(counterKey, callIndex + 1);
3737
+ const shouldMock = replayCtxForMock.mockStrategy === "all" || replayCtxForMock.mockStrategy === "marked" && options.mockOnReplay === true;
3738
+ if (shouldMock) {
3739
+ const mockKey = `${counterKey}:${callIndex}`;
3740
+ const mockSpan = replayCtxForMock.mockTree.spans.get(mockKey);
3741
+ if (mockSpan) {
3742
+ let output = mockSpan.output;
3743
+ if (mockSpan.outputMeta !== void 0 && mockSpan.outputMeta !== null) {
3744
+ output = deserializeValue({
3745
+ json: mockSpan.output,
3746
+ meta: mockSpan.outputMeta
3747
+ });
3748
+ }
3749
+ void sendSpan({ result: output });
3750
+ if (fnReturnsPromise) {
3751
+ return Promise.resolve(output);
3752
+ }
3753
+ return output;
3587
3754
  }
3588
- return output;
3589
3755
  }
3590
3756
  }
3591
- }
3592
- const recordSpan = (result) => {
3593
- if (options.finalize) {
3594
- const replayCtx = getReplayContext();
3595
- const persistenceCollector = isRootSpan ? replayCtx?.pendingPersistence : void 0;
3596
- let resolvePersistence;
3597
- if (persistenceCollector) {
3598
- persistenceCollector.push(
3599
- new Promise((resolve) => {
3600
- resolvePersistence = resolve;
3601
- })
3602
- );
3757
+ const recordSpan = (result) => {
3758
+ if (options.finalize) {
3759
+ const replayCtx = getReplayContext();
3760
+ const persistenceCollector = isRootSpan ? replayCtx?.pendingPersistence : void 0;
3761
+ let resolvePersistence;
3762
+ if (persistenceCollector) {
3763
+ persistenceCollector.push(
3764
+ new Promise((resolve) => {
3765
+ resolvePersistence = resolve;
3766
+ })
3767
+ );
3768
+ }
3769
+ void Promise.resolve().then(() => options.finalize(result)).then(
3770
+ (output) => sendSpan(
3771
+ { result: output },
3772
+ { skipPersistenceRegistration: true }
3773
+ )
3774
+ ).catch(
3775
+ (error) => sendSpan(
3776
+ {
3777
+ result: void 0,
3778
+ error: error instanceof Error ? `finalize failed: ${error.message}` : `finalize failed: ${String(error)}`
3779
+ },
3780
+ { skipPersistenceRegistration: true }
3781
+ )
3782
+ ).finally(() => resolvePersistence?.());
3783
+ } else {
3784
+ void sendSpan({ result });
3603
3785
  }
3604
- void Promise.resolve().then(() => options.finalize(result)).then(
3605
- (output) => sendSpan(
3606
- { result: output },
3607
- { skipPersistenceRegistration: true }
3608
- )
3609
- ).catch(
3610
- (error) => sendSpan(
3611
- {
3786
+ };
3787
+ executeWithContext = () => {
3788
+ const result = fn(...args);
3789
+ if (result instanceof Promise) {
3790
+ return result.then((resolvedResult) => {
3791
+ recordSpan(resolvedResult);
3792
+ return resolvedResult;
3793
+ }).catch((error) => {
3794
+ void sendSpan({
3612
3795
  result: void 0,
3613
- error: error instanceof Error ? `finalize failed: ${error.message}` : `finalize failed: ${String(error)}`
3614
- },
3615
- { skipPersistenceRegistration: true }
3616
- )
3617
- ).finally(() => resolvePersistence?.());
3618
- } else {
3619
- void sendSpan({ result });
3620
- }
3621
- };
3622
- const executeWithContext = () => {
3623
- const result = fn(...args);
3624
- if (result instanceof Promise) {
3625
- return result.then((resolvedResult) => {
3626
- recordSpan(resolvedResult);
3627
- return resolvedResult;
3628
- }).catch((error) => {
3629
- void sendSpan({
3630
- result: void 0,
3631
- error: error instanceof Error ? error.message : String(error)
3796
+ error: error instanceof Error ? error.message : String(error)
3797
+ });
3798
+ throw error;
3632
3799
  });
3633
- throw error;
3634
- });
3800
+ }
3801
+ if (isAsyncGenerator(result)) {
3802
+ return wrapAsyncGenerator(result, newStack, sendSpan);
3803
+ }
3804
+ recordSpan(result);
3805
+ return result;
3806
+ };
3807
+ } catch (setupError) {
3808
+ if (registeredTraceId) {
3809
+ activeTraceStates.delete(registeredTraceId);
3810
+ pendingSpanPromises.delete(registeredTraceId);
3635
3811
  }
3636
- if (isAsyncGenerator(result)) {
3637
- return wrapAsyncGenerator(result, newStack, sendSpan);
3812
+ if (getReplayContext()) {
3813
+ throw setupError;
3638
3814
  }
3639
- recordSpan(result);
3640
- return result;
3641
- };
3815
+ warnOnce(
3816
+ `withSpan-setup:${traceFunctionKey}`,
3817
+ `tracing setup failed for "${traceFunctionKey}"; running it untraced. The function still runs and returns normally; no span is recorded.`
3818
+ );
3819
+ return fn(...args);
3820
+ }
3642
3821
  return runWithSpanStack(newStack, executeWithContext);
3643
3822
  };
3644
3823
  Object.defineProperty(wrappedFn, "_bitfabTraceFunctionKey", {