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/node.cjs CHANGED
@@ -100,6 +100,54 @@ var init_errors = __esm({
100
100
  }
101
101
  });
102
102
 
103
+ // src/warnOnce.ts
104
+ function warnOnce(key, message) {
105
+ if (warned.has(key)) {
106
+ return;
107
+ }
108
+ warned.add(key);
109
+ try {
110
+ console.warn(`[bitfab] ${message}`);
111
+ } catch {
112
+ }
113
+ }
114
+ var warned;
115
+ var init_warnOnce = __esm({
116
+ "src/warnOnce.ts"() {
117
+ "use strict";
118
+ warned = /* @__PURE__ */ new Set();
119
+ }
120
+ });
121
+
122
+ // src/randomUuid.ts
123
+ function randomUuid() {
124
+ const globalCrypto = globalThis.crypto;
125
+ if (typeof globalCrypto?.randomUUID === "function") {
126
+ try {
127
+ return globalCrypto.randomUUID();
128
+ } catch {
129
+ }
130
+ }
131
+ warnOnce(
132
+ "crypto-unavailable",
133
+ "global crypto.randomUUID is unavailable; using a non-cryptographic fallback for trace/span ids. Tracing works normally (ids are correlation-only, not security-sensitive)."
134
+ );
135
+ return fallbackUuidV4();
136
+ }
137
+ function fallbackUuidV4() {
138
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (char) => {
139
+ const rand = Math.random() * 16 | 0;
140
+ const value = char === "x" ? rand : rand & 3 | 8;
141
+ return value.toString(16);
142
+ });
143
+ }
144
+ var init_randomUuid = __esm({
145
+ "src/randomUuid.ts"() {
146
+ "use strict";
147
+ init_warnOnce();
148
+ }
149
+ });
150
+
103
151
  // src/serialize.ts
104
152
  function describeValue(value) {
105
153
  try {
@@ -112,6 +160,10 @@ function describeValue(value) {
112
160
  return typeof value;
113
161
  }
114
162
  function unserializableStub(value, reason) {
163
+ warnOnce(
164
+ `serialize:${reason.replace(/\d+/g, "N")}`,
165
+ `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.`
166
+ );
115
167
  let summary;
116
168
  try {
117
169
  summary = `<unserializable: ${describeValue(value)} (${reason})>`;
@@ -151,7 +203,19 @@ function deserializeValue(serialized) {
151
203
  });
152
204
  }
153
205
  function toJsonSafe(value) {
154
- return toJsonSafeInner(value, 0, /* @__PURE__ */ new WeakSet());
206
+ const safe = toJsonSafeInner(value, 0, /* @__PURE__ */ new WeakSet());
207
+ try {
208
+ const size = JSON.stringify(safe)?.length ?? 0;
209
+ if (size > MAX_FRAMEWORK_SERIALIZED_BYTES) {
210
+ warnOnce(
211
+ "toJsonSafe:too_large",
212
+ `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.`
213
+ );
214
+ return `<unserializable: too_large_${size}_bytes>`;
215
+ }
216
+ } catch {
217
+ }
218
+ return safe;
155
219
  }
156
220
  function toJsonSafeInner(value, depth, seen) {
157
221
  if (value === null || value === void 0) {
@@ -204,12 +268,14 @@ function toJsonSafeInner(value, depth, seen) {
204
268
  seen.delete(value);
205
269
  return result;
206
270
  }
207
- var import_superjson, MAX_SERIALIZED_BYTES, MAX_SAFE_DEPTH;
271
+ var import_superjson, MAX_SERIALIZED_BYTES, MAX_FRAMEWORK_SERIALIZED_BYTES, MAX_SAFE_DEPTH;
208
272
  var init_serialize = __esm({
209
273
  "src/serialize.ts"() {
210
274
  "use strict";
211
275
  import_superjson = __toESM(require("superjson"), 1);
276
+ init_warnOnce();
212
277
  MAX_SERIALIZED_BYTES = 512e3;
278
+ MAX_FRAMEWORK_SERIALIZED_BYTES = 2e6;
213
279
  MAX_SAFE_DEPTH = 6;
214
280
  }
215
281
  });
@@ -295,7 +361,7 @@ async function processItem(httpClient, serverItem, fn, testRunId, mockStrategy,
295
361
  let originalOutput;
296
362
  let result;
297
363
  let error = null;
298
- const replayedTraceId = crypto.randomUUID();
364
+ const replayedTraceId = randomUuid();
299
365
  const pendingPersistence = [];
300
366
  try {
301
367
  const span = await httpClient.getExternalSpan(serverItem.externalSpanId);
@@ -355,7 +421,10 @@ async function processItem(httpClient, serverItem, fn, testRunId, mockStrategy,
355
421
  originalOutput,
356
422
  error,
357
423
  durationMs: serverItem.durationMs ?? null,
358
- tokens: serverItem.tokens ?? null,
424
+ // Filled in by replay() from the complete-replay response once the
425
+ // replay traces are persisted and their spans aggregated server-side.
426
+ // Null here (and on older servers) means "replay tokens not known".
427
+ tokens: null,
359
428
  model: serverItem.model ?? null,
360
429
  dbSnapshotRef: serverItem.dbSnapshotRef ?? null
361
430
  };
@@ -429,6 +498,7 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options) {
429
498
  const resultItems = await mapWithConcurrency(tasks, maxConcurrency);
430
499
  const completeResult = await httpClient.completeReplay(testRunId);
431
500
  const serverTraceIds = completeResult.traceIds;
501
+ const replayTokens = completeResult.tokens;
432
502
  if (serverTraceIds === void 0) {
433
503
  try {
434
504
  console.warn(
@@ -451,6 +521,9 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options) {
451
521
  missing.push(item.traceId);
452
522
  }
453
523
  }
524
+ if (mapped !== void 0) {
525
+ item.tokens = replayTokens?.[mapped] ?? null;
526
+ }
454
527
  item.traceId = mapped ?? null;
455
528
  }
456
529
  }
@@ -479,6 +552,7 @@ var init_replay = __esm({
479
552
  "src/replay.ts"() {
480
553
  "use strict";
481
554
  init_errors();
555
+ init_randomUuid();
482
556
  init_replayContext();
483
557
  init_serialize();
484
558
  }
@@ -515,13 +589,24 @@ registerAsyncLocalStorageClass(
515
589
  );
516
590
 
517
591
  // src/version.generated.ts
518
- var __version__ = "0.23.3";
592
+ var __version__ = "0.24.1";
519
593
 
520
594
  // src/constants.ts
521
595
  var DEFAULT_SERVICE_URL = "https://bitfab.ai";
522
596
 
523
597
  // src/http.ts
524
598
  init_errors();
599
+
600
+ // src/unrefTimer.ts
601
+ function unrefTimer(timer) {
602
+ const handle = timer;
603
+ if (typeof handle.unref === "function") {
604
+ handle.unref();
605
+ }
606
+ }
607
+
608
+ // src/http.ts
609
+ init_warnOnce();
525
610
  function serializePayloadBody(payload) {
526
611
  try {
527
612
  return { body: JSON.stringify(payload), dropped: [] };
@@ -566,11 +651,20 @@ function serializePayloadBody(payload) {
566
651
  result = `<unserializable: ${className}>`;
567
652
  }
568
653
  } else {
569
- const out = {};
570
- for (const [k, v] of Object.entries(obj)) {
571
- out[k] = sanitize(v, seen);
654
+ try {
655
+ const out = {};
656
+ for (const [k, v] of Object.entries(obj)) {
657
+ out[k] = sanitize(v, seen);
658
+ }
659
+ result = out;
660
+ } catch {
661
+ warnOnce(
662
+ "payload:field-getter-threw",
663
+ "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."
664
+ );
665
+ dropped.push(className);
666
+ result = `<unserializable: ${className}>`;
572
667
  }
573
- result = out;
574
668
  }
575
669
  seen.delete(obj);
576
670
  return result;
@@ -615,10 +709,20 @@ async function flushTraces(timeoutMs = 5e3) {
615
709
  if (pendingTracePromises.size === 0) {
616
710
  return;
617
711
  }
618
- await Promise.race([
619
- Promise.allSettled(Array.from(pendingTracePromises)),
620
- new Promise((resolve) => setTimeout(resolve, timeoutMs))
621
- ]);
712
+ let timer;
713
+ try {
714
+ await Promise.race([
715
+ Promise.allSettled(Array.from(pendingTracePromises)),
716
+ new Promise((resolve) => {
717
+ timer = setTimeout(resolve, timeoutMs);
718
+ unrefTimer(timer);
719
+ })
720
+ ]);
721
+ } finally {
722
+ if (timer) {
723
+ clearTimeout(timer);
724
+ }
725
+ }
622
726
  }
623
727
  if (typeof process !== "undefined" && process.versions != null && process.versions.node != null) {
624
728
  let isFlushing = false;
@@ -928,6 +1032,7 @@ var HttpClient = class {
928
1032
  };
929
1033
 
930
1034
  // src/claudeAgentSdk.ts
1035
+ init_randomUuid();
931
1036
  init_serialize();
932
1037
  function nowIso() {
933
1038
  return (/* @__PURE__ */ new Date()).toISOString();
@@ -1015,7 +1120,7 @@ var BitfabClaudeAgentHandler = class {
1015
1120
  if (this.activeContext) {
1016
1121
  this.traceId = this.activeContext.traceId;
1017
1122
  } else {
1018
- this.traceId = crypto.randomUUID();
1123
+ this.traceId = randomUuid();
1019
1124
  }
1020
1125
  this.traceStartedAt = nowIso();
1021
1126
  return this.traceId;
@@ -1040,7 +1145,7 @@ var BitfabClaudeAgentHandler = class {
1040
1145
  if (this.activeContext !== null) {
1041
1146
  return;
1042
1147
  }
1043
- const spanId = crypto.randomUUID();
1148
+ const spanId = randomUuid();
1044
1149
  this.startSpan(spanId, this.traceFunctionKey, "agent", this.rootInput, null);
1045
1150
  this.rootSpanId = spanId;
1046
1151
  }
@@ -1154,7 +1259,7 @@ var BitfabClaudeAgentHandler = class {
1154
1259
  // ── hook callbacks ───────────────────────────────────────────
1155
1260
  async preToolUseHook(inputData, toolUseId, _context) {
1156
1261
  try {
1157
- const sid = inputData.tool_use_id ?? toolUseId ?? crypto.randomUUID();
1262
+ const sid = inputData.tool_use_id ?? toolUseId ?? randomUuid();
1158
1263
  const toolName = inputData.tool_name ?? "tool";
1159
1264
  const toolInput = inputData.tool_input ?? {};
1160
1265
  const agentId = inputData.agent_id;
@@ -1184,10 +1289,10 @@ var BitfabClaudeAgentHandler = class {
1184
1289
  }
1185
1290
  async subagentStartHook(inputData, _toolUseId, _context) {
1186
1291
  try {
1187
- const agentId = inputData.agent_id ?? crypto.randomUUID();
1292
+ const agentId = inputData.agent_id ?? randomUuid();
1188
1293
  const agentType = inputData.agent_type ?? "subagent";
1189
1294
  const parentId = this.getParentId();
1190
- const spanId = crypto.randomUUID();
1295
+ const spanId = randomUuid();
1191
1296
  this.activeSubagentSpans.set(agentId, spanId);
1192
1297
  this.startSpan(
1193
1298
  spanId,
@@ -1328,7 +1433,7 @@ var BitfabClaudeAgentHandler = class {
1328
1433
  this.flushLlmTurn();
1329
1434
  this.conversationHistory.push(...this.pendingMessages);
1330
1435
  this.pendingMessages = [];
1331
- this.currentLlmSpanId = crypto.randomUUID();
1436
+ this.currentLlmSpanId = randomUuid();
1332
1437
  this.currentLlmMessageId = messageId ?? null;
1333
1438
  this.currentLlmContent = [];
1334
1439
  this.currentLlmModel = inner.model ?? null;
@@ -1772,6 +1877,7 @@ function buildSnapshotRef(config, sdkWallClockBeforeFn) {
1772
1877
  }
1773
1878
 
1774
1879
  // src/langgraph.ts
1880
+ init_randomUuid();
1775
1881
  init_serialize();
1776
1882
  var LANGSMITH_HIDDEN_TAG = "langsmith:hidden";
1777
1883
  var LANGGRAPH_METADATA_KEYS = [
@@ -2020,7 +2126,7 @@ var BitfabLangGraphCallbackHandler = class {
2020
2126
  } else {
2021
2127
  const activeContext = this.getActiveSpanContext?.() ?? null;
2022
2128
  invocation = {
2023
- traceId: activeContext ? activeContext.traceId : crypto.randomUUID(),
2129
+ traceId: activeContext ? activeContext.traceId : randomUuid(),
2024
2130
  activeContext,
2025
2131
  rootRunId: runId
2026
2132
  };
@@ -2315,6 +2421,20 @@ var BitfabOpenAIAgentHandler = class {
2315
2421
  this.withSpanFn = config.withSpan;
2316
2422
  this.getActiveSpanContext = config.getActiveSpanContext;
2317
2423
  }
2424
+ /**
2425
+ * Drop-in replacement for the OpenAI Agents SDK's `run()` that records a
2426
+ * replayable root `agent` span.
2427
+ *
2428
+ * The `input` is captured as the root span's input (as a single positional
2429
+ * argument, so `replay(key, fn)` re-feeds it), and the run's `finalOutput`
2430
+ * is recorded as the root output. For streaming runs (`{ stream: true }`),
2431
+ * the result is handed back immediately and the final output is recorded
2432
+ * once the stream completes — first-byte latency is untouched.
2433
+ *
2434
+ * The process-wide tracing processor (`getOpenAiTracingProcessor`) must still
2435
+ * be registered: it captures the LLM/tool/handoff spans that nest beneath
2436
+ * this root.
2437
+ */
2318
2438
  async wrapRun(agent, input, options) {
2319
2439
  const { run } = await importOptionalPeer([
2320
2440
  "@openai",
@@ -2353,6 +2473,7 @@ var BitfabOpenAIAgentHandler = class {
2353
2473
  };
2354
2474
 
2355
2475
  // src/client.ts
2476
+ init_randomUuid();
2356
2477
  init_replayContext();
2357
2478
 
2358
2479
  // src/replayEnvironment.ts
@@ -2772,6 +2893,7 @@ var BitfabVercelAiHandler = class {
2772
2893
  };
2773
2894
 
2774
2895
  // src/client.ts
2896
+ init_warnOnce();
2775
2897
  var activeTraceStates = /* @__PURE__ */ new Map();
2776
2898
  var pendingSpanPromises = /* @__PURE__ */ new Map();
2777
2899
  var asyncLocalStorage = null;
@@ -3130,7 +3252,20 @@ var Bitfab = class {
3130
3252
  functionVersion.providers,
3131
3253
  this.envVars
3132
3254
  );
3133
- const resultStr = typeof executionResult.result === "string" ? executionResult.result : JSON.stringify(executionResult.result);
3255
+ let resultStr;
3256
+ if (typeof executionResult.result === "string") {
3257
+ resultStr = executionResult.result;
3258
+ } else {
3259
+ try {
3260
+ resultStr = JSON.stringify(executionResult.result) ?? String(executionResult.result);
3261
+ } catch {
3262
+ warnOnce(
3263
+ "call-result-serialize",
3264
+ "a local execution result could not be JSON-serialized; storing its String() form instead. The call still returns its real value."
3265
+ );
3266
+ resultStr = String(executionResult.result);
3267
+ }
3268
+ }
3134
3269
  this.httpClient.sendInternalTrace(functionVersion.id, {
3135
3270
  result: resultStr,
3136
3271
  source: "typescript-sdk",
@@ -3379,11 +3514,26 @@ var Bitfab = class {
3379
3514
  return await bamlClient[methodName](...args);
3380
3515
  }
3381
3516
  const collector = new CollectorClass("bitfab-baml-tracing");
3382
- const trackedClient = bamlClient.withOptions({ collector });
3383
- const trackedMethod = trackedClient[methodName];
3384
- const result = await trackedMethod.bind(
3385
- trackedClient
3386
- )(...args);
3517
+ let trackedClient;
3518
+ let trackedMethod;
3519
+ try {
3520
+ trackedClient = bamlClient.withOptions({ collector });
3521
+ const method2 = trackedClient[methodName];
3522
+ if (typeof method2 !== "function") {
3523
+ throw new BitfabError(
3524
+ "bamlClient.withOptions did not return the wrapped method"
3525
+ );
3526
+ }
3527
+ trackedMethod = method2;
3528
+ } catch {
3529
+ warnOnce(
3530
+ `wrapBAML-setup:${methodName}`,
3531
+ `BAML tracing setup failed for "${methodName}" (incompatible bamlClient or BAML version); calling it untraced. The call still runs; no span is recorded.`
3532
+ );
3533
+ wrappedFn.collector = null;
3534
+ return await bamlClient[methodName](...args);
3535
+ }
3536
+ const result = await trackedMethod.bind(trackedClient)(...args);
3387
3537
  wrappedFn.collector = collector;
3388
3538
  try {
3389
3539
  const prompt = extractPromptFromCollector(collector);
@@ -3459,200 +3609,229 @@ var Bitfab = class {
3459
3609
  () => wrappedFn.apply(this, args)
3460
3610
  );
3461
3611
  }
3462
- const currentStack = getSpanStack();
3463
- const parentContext = currentStack[currentStack.length - 1];
3464
- const replayCtxForTraceId = parentContext ? null : getReplayContext();
3465
- const traceId = parentContext?.traceId ?? replayCtxForTraceId?.traceId ?? crypto.randomUUID();
3466
- const spanId = crypto.randomUUID();
3467
- const parentSpanId = parentContext?.spanId ?? null;
3468
- const isRootSpan = parentSpanId === null;
3469
- const newContext = { traceId, spanId, contexts: [] };
3470
- const newStack = [...currentStack, newContext];
3471
- const inputs = args;
3472
- const startedAt = (/* @__PURE__ */ new Date()).toISOString();
3473
- if (isRootSpan && !activeTraceStates.has(traceId)) {
3474
- const replayCtxAtRoot = getReplayContext();
3475
- const dbSnapshotRef = buildSnapshotRef(self.dbSnapshot, startedAt);
3476
- activeTraceStates.set(traceId, {
3612
+ let newStack;
3613
+ let executeWithContext;
3614
+ let registeredTraceId;
3615
+ try {
3616
+ const currentStack = getSpanStack();
3617
+ const parentContext = currentStack[currentStack.length - 1];
3618
+ const replayCtxForTraceId = parentContext ? null : getReplayContext();
3619
+ const traceId = parentContext?.traceId ?? replayCtxForTraceId?.traceId ?? randomUuid();
3620
+ const spanId = randomUuid();
3621
+ const parentSpanId = parentContext?.spanId ?? null;
3622
+ const isRootSpan = parentSpanId === null;
3623
+ const newContext = { traceId, spanId, contexts: [] };
3624
+ newStack = [...currentStack, newContext];
3625
+ const inputs = args;
3626
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
3627
+ if (isRootSpan && !activeTraceStates.has(traceId)) {
3628
+ const replayCtxAtRoot = getReplayContext();
3629
+ const dbSnapshotRef = buildSnapshotRef(self.dbSnapshot, startedAt);
3630
+ activeTraceStates.set(traceId, {
3631
+ traceId,
3632
+ startedAt,
3633
+ contexts: [],
3634
+ ...replayCtxAtRoot?.testRunId && {
3635
+ testRunId: replayCtxAtRoot.testRunId
3636
+ },
3637
+ ...replayCtxAtRoot?.inputSourceTraceId && {
3638
+ inputSourceTraceId: replayCtxAtRoot.inputSourceTraceId
3639
+ },
3640
+ dbSnapshotRef
3641
+ });
3642
+ pendingSpanPromises.set(traceId, []);
3643
+ registeredTraceId = traceId;
3644
+ }
3645
+ const functionName = fn.name !== "" ? fn.name : void 0;
3646
+ const baseSpanParams = {
3647
+ traceFunctionKey,
3648
+ functionName,
3649
+ spanName: options.name ?? functionName ?? traceFunctionKey,
3477
3650
  traceId,
3651
+ spanId,
3652
+ parentSpanId,
3653
+ inputs,
3478
3654
  startedAt,
3479
- contexts: [],
3480
- ...replayCtxAtRoot?.testRunId && {
3481
- testRunId: replayCtxAtRoot.testRunId
3482
- },
3483
- ...replayCtxAtRoot?.inputSourceTraceId && {
3484
- inputSourceTraceId: replayCtxAtRoot.inputSourceTraceId
3485
- },
3486
- dbSnapshotRef
3487
- });
3488
- pendingSpanPromises.set(traceId, []);
3489
- }
3490
- const functionName = fn.name !== "" ? fn.name : void 0;
3491
- const baseSpanParams = {
3492
- traceFunctionKey,
3493
- functionName,
3494
- spanName: options.name ?? functionName ?? traceFunctionKey,
3495
- traceId,
3496
- spanId,
3497
- parentSpanId,
3498
- inputs,
3499
- startedAt,
3500
- spanType: options.type ?? "custom"
3501
- };
3502
- const sendSpan = async (params, spanOpts) => {
3503
- const replayCtx = getReplayContext();
3504
- const persistenceCollector = isRootSpan ? replayCtx?.pendingPersistence : void 0;
3505
- let resolvePersistence;
3506
- if (persistenceCollector && !spanOpts?.skipPersistenceRegistration) {
3507
- persistenceCollector.push(
3508
- new Promise((resolve) => {
3509
- resolvePersistence = resolve;
3510
- })
3511
- );
3512
- }
3513
- try {
3514
- const endedAt = (/* @__PURE__ */ new Date()).toISOString();
3515
- const spanPromise = self.sendWrapperSpan({
3516
- ...baseSpanParams,
3517
- ...params,
3518
- contexts: newContext.contexts,
3519
- prompt: newContext.prompt,
3520
- endedAt,
3521
- ...replayCtx?.testRunId && { testRunId: replayCtx.testRunId },
3522
- ...replayCtx?.inputSourceSpanId && {
3523
- inputSourceSpanId: replayCtx.inputSourceSpanId
3524
- }
3525
- });
3526
- if (isRootSpan) {
3527
- const pending = pendingSpanPromises.get(traceId) ?? [];
3528
- pending.push(spanPromise);
3529
- if (persistenceCollector) {
3530
- await Promise.allSettled(pending);
3531
- } else {
3532
- await Promise.race([
3533
- Promise.allSettled(pending),
3534
- new Promise((resolve) => setTimeout(resolve, 5e3))
3535
- ]);
3536
- }
3537
- pendingSpanPromises.delete(traceId);
3538
- const traceState = activeTraceStates.get(traceId);
3539
- const completionPromise = self.sendTraceCompletion({
3540
- traceFunctionKey,
3541
- traceId,
3542
- startedAt: traceState?.startedAt ?? startedAt,
3655
+ spanType: options.type ?? "custom"
3656
+ };
3657
+ const sendSpan = async (params, spanOpts) => {
3658
+ const replayCtx = getReplayContext();
3659
+ const persistenceCollector = isRootSpan ? replayCtx?.pendingPersistence : void 0;
3660
+ let resolvePersistence;
3661
+ if (persistenceCollector && !spanOpts?.skipPersistenceRegistration) {
3662
+ persistenceCollector.push(
3663
+ new Promise((resolve) => {
3664
+ resolvePersistence = resolve;
3665
+ })
3666
+ );
3667
+ }
3668
+ try {
3669
+ const endedAt = (/* @__PURE__ */ new Date()).toISOString();
3670
+ const spanPromise = self.sendWrapperSpan({
3671
+ ...baseSpanParams,
3672
+ ...params,
3673
+ contexts: newContext.contexts,
3674
+ prompt: newContext.prompt,
3543
3675
  endedAt,
3544
- sessionId: traceState?.sessionId,
3545
- metadata: traceState?.metadata,
3546
- contexts: traceState?.contexts ?? [],
3547
- testRunId: traceState?.testRunId,
3548
- inputSourceTraceId: traceState?.inputSourceTraceId,
3549
- dbSnapshotRef: traceState?.dbSnapshotRef,
3550
- // Built AFTER the wrapped fn finished, so `accessed` reflects
3551
- // whether customer code obtained the branch URL during this
3552
- // item. Omitted entirely when no lease was attached, so the
3553
- // server can distinguish "no branch" from "branch ignored".
3554
- ...replayCtx?.dbBranchLease && {
3555
- dbSnapshotUsage: {
3556
- neonBranchId: replayCtx.dbBranchLease.neonBranchId,
3557
- snapshotTimestamp: replayCtx.dbBranchLease.snapshotTimestamp,
3558
- sourceTraceId: replayCtx.sourceBitfabTraceId,
3559
- accessed: replayCtx.dbSnapshotAccessed === true
3560
- }
3676
+ ...replayCtx?.testRunId && { testRunId: replayCtx.testRunId },
3677
+ ...replayCtx?.inputSourceSpanId && {
3678
+ inputSourceSpanId: replayCtx.inputSourceSpanId
3561
3679
  }
3562
3680
  });
3563
- activeTraceStates.delete(traceId);
3564
- if (persistenceCollector) {
3565
- await completionPromise;
3566
- }
3567
- } else {
3568
- const pending = pendingSpanPromises.get(traceId);
3569
- if (pending) {
3681
+ if (isRootSpan) {
3682
+ const pending = pendingSpanPromises.get(traceId) ?? [];
3570
3683
  pending.push(spanPromise);
3684
+ if (persistenceCollector) {
3685
+ await Promise.allSettled(pending);
3686
+ } else {
3687
+ let raceTimer;
3688
+ try {
3689
+ await Promise.race([
3690
+ Promise.allSettled(pending),
3691
+ new Promise((resolve) => {
3692
+ raceTimer = setTimeout(resolve, 5e3);
3693
+ unrefTimer(raceTimer);
3694
+ })
3695
+ ]);
3696
+ } finally {
3697
+ if (raceTimer) {
3698
+ clearTimeout(raceTimer);
3699
+ }
3700
+ }
3701
+ }
3702
+ pendingSpanPromises.delete(traceId);
3703
+ const traceState = activeTraceStates.get(traceId);
3704
+ const completionPromise = self.sendTraceCompletion({
3705
+ traceFunctionKey,
3706
+ traceId,
3707
+ startedAt: traceState?.startedAt ?? startedAt,
3708
+ endedAt,
3709
+ sessionId: traceState?.sessionId,
3710
+ metadata: traceState?.metadata,
3711
+ contexts: traceState?.contexts ?? [],
3712
+ testRunId: traceState?.testRunId,
3713
+ inputSourceTraceId: traceState?.inputSourceTraceId,
3714
+ dbSnapshotRef: traceState?.dbSnapshotRef,
3715
+ // Built AFTER the wrapped fn finished, so `accessed` reflects
3716
+ // whether customer code obtained the branch URL during this
3717
+ // item. Omitted entirely when no lease was attached, so the
3718
+ // server can distinguish "no branch" from "branch ignored".
3719
+ ...replayCtx?.dbBranchLease && {
3720
+ dbSnapshotUsage: {
3721
+ neonBranchId: replayCtx.dbBranchLease.neonBranchId,
3722
+ snapshotTimestamp: replayCtx.dbBranchLease.snapshotTimestamp,
3723
+ sourceTraceId: replayCtx.sourceBitfabTraceId,
3724
+ accessed: replayCtx.dbSnapshotAccessed === true
3725
+ }
3726
+ }
3727
+ });
3728
+ activeTraceStates.delete(traceId);
3729
+ if (persistenceCollector) {
3730
+ await completionPromise;
3731
+ }
3571
3732
  } else {
3572
- pendingSpanPromises.set(traceId, [spanPromise]);
3733
+ const pending = pendingSpanPromises.get(traceId);
3734
+ if (pending) {
3735
+ pending.push(spanPromise);
3736
+ } else {
3737
+ pendingSpanPromises.set(traceId, [spanPromise]);
3738
+ }
3573
3739
  }
3740
+ } catch {
3741
+ } finally {
3742
+ resolvePersistence?.();
3574
3743
  }
3575
- } catch {
3576
- } finally {
3577
- resolvePersistence?.();
3578
- }
3579
- };
3580
- const replayCtxForMock = getReplayContext();
3581
- if (replayCtxForMock?.mockTree && !isRootSpan) {
3582
- const counters = replayCtxForMock.callCounters;
3583
- const counterKey = `${traceFunctionKey}:${baseSpanParams.spanName}`;
3584
- const callIndex = counters.get(counterKey) ?? 0;
3585
- counters.set(counterKey, callIndex + 1);
3586
- const shouldMock = replayCtxForMock.mockStrategy === "all" || replayCtxForMock.mockStrategy === "marked" && options.mockOnReplay === true;
3587
- if (shouldMock) {
3588
- const mockKey = `${counterKey}:${callIndex}`;
3589
- const mockSpan = replayCtxForMock.mockTree.spans.get(mockKey);
3590
- if (mockSpan) {
3591
- let output = mockSpan.output;
3592
- if (mockSpan.outputMeta !== void 0 && mockSpan.outputMeta !== null) {
3593
- output = deserializeValue({
3594
- json: mockSpan.output,
3595
- meta: mockSpan.outputMeta
3596
- });
3597
- }
3598
- void sendSpan({ result: output });
3599
- if (fnReturnsPromise) {
3600
- return Promise.resolve(output);
3744
+ };
3745
+ const replayCtxForMock = getReplayContext();
3746
+ if (replayCtxForMock?.mockTree && !isRootSpan) {
3747
+ const counters = replayCtxForMock.callCounters;
3748
+ const counterKey = `${traceFunctionKey}:${baseSpanParams.spanName}`;
3749
+ const callIndex = counters.get(counterKey) ?? 0;
3750
+ counters.set(counterKey, callIndex + 1);
3751
+ const shouldMock = replayCtxForMock.mockStrategy === "all" || replayCtxForMock.mockStrategy === "marked" && options.mockOnReplay === true;
3752
+ if (shouldMock) {
3753
+ const mockKey = `${counterKey}:${callIndex}`;
3754
+ const mockSpan = replayCtxForMock.mockTree.spans.get(mockKey);
3755
+ if (mockSpan) {
3756
+ let output = mockSpan.output;
3757
+ if (mockSpan.outputMeta !== void 0 && mockSpan.outputMeta !== null) {
3758
+ output = deserializeValue({
3759
+ json: mockSpan.output,
3760
+ meta: mockSpan.outputMeta
3761
+ });
3762
+ }
3763
+ void sendSpan({ result: output });
3764
+ if (fnReturnsPromise) {
3765
+ return Promise.resolve(output);
3766
+ }
3767
+ return output;
3601
3768
  }
3602
- return output;
3603
3769
  }
3604
3770
  }
3605
- }
3606
- const recordSpan = (result) => {
3607
- if (options.finalize) {
3608
- const replayCtx = getReplayContext();
3609
- const persistenceCollector = isRootSpan ? replayCtx?.pendingPersistence : void 0;
3610
- let resolvePersistence;
3611
- if (persistenceCollector) {
3612
- persistenceCollector.push(
3613
- new Promise((resolve) => {
3614
- resolvePersistence = resolve;
3615
- })
3616
- );
3771
+ const recordSpan = (result) => {
3772
+ if (options.finalize) {
3773
+ const replayCtx = getReplayContext();
3774
+ const persistenceCollector = isRootSpan ? replayCtx?.pendingPersistence : void 0;
3775
+ let resolvePersistence;
3776
+ if (persistenceCollector) {
3777
+ persistenceCollector.push(
3778
+ new Promise((resolve) => {
3779
+ resolvePersistence = resolve;
3780
+ })
3781
+ );
3782
+ }
3783
+ void Promise.resolve().then(() => options.finalize(result)).then(
3784
+ (output) => sendSpan(
3785
+ { result: output },
3786
+ { skipPersistenceRegistration: true }
3787
+ )
3788
+ ).catch(
3789
+ (error) => sendSpan(
3790
+ {
3791
+ result: void 0,
3792
+ error: error instanceof Error ? `finalize failed: ${error.message}` : `finalize failed: ${String(error)}`
3793
+ },
3794
+ { skipPersistenceRegistration: true }
3795
+ )
3796
+ ).finally(() => resolvePersistence?.());
3797
+ } else {
3798
+ void sendSpan({ result });
3617
3799
  }
3618
- void Promise.resolve().then(() => options.finalize(result)).then(
3619
- (output) => sendSpan(
3620
- { result: output },
3621
- { skipPersistenceRegistration: true }
3622
- )
3623
- ).catch(
3624
- (error) => sendSpan(
3625
- {
3800
+ };
3801
+ executeWithContext = () => {
3802
+ const result = fn(...args);
3803
+ if (result instanceof Promise) {
3804
+ return result.then((resolvedResult) => {
3805
+ recordSpan(resolvedResult);
3806
+ return resolvedResult;
3807
+ }).catch((error) => {
3808
+ void sendSpan({
3626
3809
  result: void 0,
3627
- error: error instanceof Error ? `finalize failed: ${error.message}` : `finalize failed: ${String(error)}`
3628
- },
3629
- { skipPersistenceRegistration: true }
3630
- )
3631
- ).finally(() => resolvePersistence?.());
3632
- } else {
3633
- void sendSpan({ result });
3634
- }
3635
- };
3636
- const executeWithContext = () => {
3637
- const result = fn(...args);
3638
- if (result instanceof Promise) {
3639
- return result.then((resolvedResult) => {
3640
- recordSpan(resolvedResult);
3641
- return resolvedResult;
3642
- }).catch((error) => {
3643
- void sendSpan({
3644
- result: void 0,
3645
- error: error instanceof Error ? error.message : String(error)
3810
+ error: error instanceof Error ? error.message : String(error)
3811
+ });
3812
+ throw error;
3646
3813
  });
3647
- throw error;
3648
- });
3814
+ }
3815
+ if (isAsyncGenerator(result)) {
3816
+ return wrapAsyncGenerator(result, newStack, sendSpan);
3817
+ }
3818
+ recordSpan(result);
3819
+ return result;
3820
+ };
3821
+ } catch (setupError) {
3822
+ if (registeredTraceId) {
3823
+ activeTraceStates.delete(registeredTraceId);
3824
+ pendingSpanPromises.delete(registeredTraceId);
3649
3825
  }
3650
- if (isAsyncGenerator(result)) {
3651
- return wrapAsyncGenerator(result, newStack, sendSpan);
3826
+ if (getReplayContext()) {
3827
+ throw setupError;
3652
3828
  }
3653
- recordSpan(result);
3654
- return result;
3655
- };
3829
+ warnOnce(
3830
+ `withSpan-setup:${traceFunctionKey}`,
3831
+ `tracing setup failed for "${traceFunctionKey}"; running it untraced. The function still runs and returns normally; no span is recorded.`
3832
+ );
3833
+ return fn(...args);
3834
+ }
3656
3835
  return runWithSpanStack(newStack, executeWithContext);
3657
3836
  };
3658
3837
  Object.defineProperty(wrappedFn, "_bitfabTraceFunctionKey", {