@bitfab/sdk 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
@@ -91,6 +91,54 @@ var init_errors = __esm({
91
91
  }
92
92
  });
93
93
 
94
+ // src/warnOnce.ts
95
+ function warnOnce(key, message) {
96
+ if (warned.has(key)) {
97
+ return;
98
+ }
99
+ warned.add(key);
100
+ try {
101
+ console.warn(`[bitfab] ${message}`);
102
+ } catch {
103
+ }
104
+ }
105
+ var warned;
106
+ var init_warnOnce = __esm({
107
+ "src/warnOnce.ts"() {
108
+ "use strict";
109
+ warned = /* @__PURE__ */ new Set();
110
+ }
111
+ });
112
+
113
+ // src/randomUuid.ts
114
+ function randomUuid() {
115
+ const globalCrypto = globalThis.crypto;
116
+ if (typeof globalCrypto?.randomUUID === "function") {
117
+ try {
118
+ return globalCrypto.randomUUID();
119
+ } catch {
120
+ }
121
+ }
122
+ warnOnce(
123
+ "crypto-unavailable",
124
+ "global crypto.randomUUID is unavailable; using a non-cryptographic fallback for trace/span ids. Tracing works normally (ids are correlation-only, not security-sensitive)."
125
+ );
126
+ return fallbackUuidV4();
127
+ }
128
+ function fallbackUuidV4() {
129
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (char) => {
130
+ const rand = Math.random() * 16 | 0;
131
+ const value = char === "x" ? rand : rand & 3 | 8;
132
+ return value.toString(16);
133
+ });
134
+ }
135
+ var init_randomUuid = __esm({
136
+ "src/randomUuid.ts"() {
137
+ "use strict";
138
+ init_warnOnce();
139
+ }
140
+ });
141
+
94
142
  // src/serialize.ts
95
143
  function describeValue(value) {
96
144
  try {
@@ -103,6 +151,10 @@ function describeValue(value) {
103
151
  return typeof value;
104
152
  }
105
153
  function unserializableStub(value, reason) {
154
+ warnOnce(
155
+ `serialize:${reason.replace(/\d+/g, "N")}`,
156
+ `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.`
157
+ );
106
158
  let summary;
107
159
  try {
108
160
  summary = `<unserializable: ${describeValue(value)} (${reason})>`;
@@ -142,7 +194,19 @@ function deserializeValue(serialized) {
142
194
  });
143
195
  }
144
196
  function toJsonSafe(value) {
145
- return toJsonSafeInner(value, 0, /* @__PURE__ */ new WeakSet());
197
+ const safe = toJsonSafeInner(value, 0, /* @__PURE__ */ new WeakSet());
198
+ try {
199
+ const size = JSON.stringify(safe)?.length ?? 0;
200
+ if (size > MAX_FRAMEWORK_SERIALIZED_BYTES) {
201
+ warnOnce(
202
+ "toJsonSafe:too_large",
203
+ `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.`
204
+ );
205
+ return `<unserializable: too_large_${size}_bytes>`;
206
+ }
207
+ } catch {
208
+ }
209
+ return safe;
146
210
  }
147
211
  function toJsonSafeInner(value, depth, seen) {
148
212
  if (value === null || value === void 0) {
@@ -195,12 +259,14 @@ function toJsonSafeInner(value, depth, seen) {
195
259
  seen.delete(value);
196
260
  return result;
197
261
  }
198
- var import_superjson, MAX_SERIALIZED_BYTES, MAX_SAFE_DEPTH;
262
+ var import_superjson, MAX_SERIALIZED_BYTES, MAX_FRAMEWORK_SERIALIZED_BYTES, MAX_SAFE_DEPTH;
199
263
  var init_serialize = __esm({
200
264
  "src/serialize.ts"() {
201
265
  "use strict";
202
266
  import_superjson = __toESM(require("superjson"), 1);
267
+ init_warnOnce();
203
268
  MAX_SERIALIZED_BYTES = 512e3;
269
+ MAX_FRAMEWORK_SERIALIZED_BYTES = 2e6;
204
270
  MAX_SAFE_DEPTH = 6;
205
271
  }
206
272
  });
@@ -286,7 +352,7 @@ async function processItem(httpClient, serverItem, fn, testRunId, mockStrategy,
286
352
  let originalOutput;
287
353
  let result;
288
354
  let error = null;
289
- const replayedTraceId = crypto.randomUUID();
355
+ const replayedTraceId = randomUuid();
290
356
  const pendingPersistence = [];
291
357
  try {
292
358
  const span = await httpClient.getExternalSpan(serverItem.externalSpanId);
@@ -346,7 +412,10 @@ async function processItem(httpClient, serverItem, fn, testRunId, mockStrategy,
346
412
  originalOutput,
347
413
  error,
348
414
  durationMs: serverItem.durationMs ?? null,
349
- tokens: serverItem.tokens ?? null,
415
+ // Filled in by replay() from the complete-replay response once the
416
+ // replay traces are persisted and their spans aggregated server-side.
417
+ // Null here (and on older servers) means "replay tokens not known".
418
+ tokens: null,
350
419
  model: serverItem.model ?? null,
351
420
  dbSnapshotRef: serverItem.dbSnapshotRef ?? null
352
421
  };
@@ -420,6 +489,7 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options) {
420
489
  const resultItems = await mapWithConcurrency(tasks, maxConcurrency);
421
490
  const completeResult = await httpClient.completeReplay(testRunId);
422
491
  const serverTraceIds = completeResult.traceIds;
492
+ const replayTokens = completeResult.tokens;
423
493
  if (serverTraceIds === void 0) {
424
494
  try {
425
495
  console.warn(
@@ -442,6 +512,9 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options) {
442
512
  missing.push(item.traceId);
443
513
  }
444
514
  }
515
+ if (mapped !== void 0) {
516
+ item.tokens = replayTokens?.[mapped] ?? null;
517
+ }
445
518
  item.traceId = mapped ?? null;
446
519
  }
447
520
  }
@@ -470,6 +543,7 @@ var init_replay = __esm({
470
543
  "src/replay.ts"() {
471
544
  "use strict";
472
545
  init_errors();
546
+ init_randomUuid();
473
547
  init_replayContext();
474
548
  init_serialize();
475
549
  }
@@ -506,13 +580,24 @@ registerAsyncLocalStorageClass(
506
580
  );
507
581
 
508
582
  // src/version.generated.ts
509
- var __version__ = "0.23.3";
583
+ var __version__ = "0.24.1";
510
584
 
511
585
  // src/constants.ts
512
586
  var DEFAULT_SERVICE_URL = "https://bitfab.ai";
513
587
 
514
588
  // src/http.ts
515
589
  init_errors();
590
+
591
+ // src/unrefTimer.ts
592
+ function unrefTimer(timer) {
593
+ const handle = timer;
594
+ if (typeof handle.unref === "function") {
595
+ handle.unref();
596
+ }
597
+ }
598
+
599
+ // src/http.ts
600
+ init_warnOnce();
516
601
  function serializePayloadBody(payload) {
517
602
  try {
518
603
  return { body: JSON.stringify(payload), dropped: [] };
@@ -557,11 +642,20 @@ function serializePayloadBody(payload) {
557
642
  result = `<unserializable: ${className}>`;
558
643
  }
559
644
  } else {
560
- const out = {};
561
- for (const [k, v] of Object.entries(obj)) {
562
- out[k] = sanitize(v, seen);
645
+ try {
646
+ const out = {};
647
+ for (const [k, v] of Object.entries(obj)) {
648
+ out[k] = sanitize(v, seen);
649
+ }
650
+ result = out;
651
+ } catch {
652
+ warnOnce(
653
+ "payload:field-getter-threw",
654
+ "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."
655
+ );
656
+ dropped.push(className);
657
+ result = `<unserializable: ${className}>`;
563
658
  }
564
- result = out;
565
659
  }
566
660
  seen.delete(obj);
567
661
  return result;
@@ -606,10 +700,20 @@ async function flushTraces(timeoutMs = 5e3) {
606
700
  if (pendingTracePromises.size === 0) {
607
701
  return;
608
702
  }
609
- await Promise.race([
610
- Promise.allSettled(Array.from(pendingTracePromises)),
611
- new Promise((resolve) => setTimeout(resolve, timeoutMs))
612
- ]);
703
+ let timer;
704
+ try {
705
+ await Promise.race([
706
+ Promise.allSettled(Array.from(pendingTracePromises)),
707
+ new Promise((resolve) => {
708
+ timer = setTimeout(resolve, timeoutMs);
709
+ unrefTimer(timer);
710
+ })
711
+ ]);
712
+ } finally {
713
+ if (timer) {
714
+ clearTimeout(timer);
715
+ }
716
+ }
613
717
  }
614
718
  if (typeof process !== "undefined" && process.versions != null && process.versions.node != null) {
615
719
  let isFlushing = false;
@@ -919,6 +1023,7 @@ var HttpClient = class {
919
1023
  };
920
1024
 
921
1025
  // src/claudeAgentSdk.ts
1026
+ init_randomUuid();
922
1027
  init_serialize();
923
1028
  function nowIso() {
924
1029
  return (/* @__PURE__ */ new Date()).toISOString();
@@ -1006,7 +1111,7 @@ var BitfabClaudeAgentHandler = class {
1006
1111
  if (this.activeContext) {
1007
1112
  this.traceId = this.activeContext.traceId;
1008
1113
  } else {
1009
- this.traceId = crypto.randomUUID();
1114
+ this.traceId = randomUuid();
1010
1115
  }
1011
1116
  this.traceStartedAt = nowIso();
1012
1117
  return this.traceId;
@@ -1031,7 +1136,7 @@ var BitfabClaudeAgentHandler = class {
1031
1136
  if (this.activeContext !== null) {
1032
1137
  return;
1033
1138
  }
1034
- const spanId = crypto.randomUUID();
1139
+ const spanId = randomUuid();
1035
1140
  this.startSpan(spanId, this.traceFunctionKey, "agent", this.rootInput, null);
1036
1141
  this.rootSpanId = spanId;
1037
1142
  }
@@ -1145,7 +1250,7 @@ var BitfabClaudeAgentHandler = class {
1145
1250
  // ── hook callbacks ───────────────────────────────────────────
1146
1251
  async preToolUseHook(inputData, toolUseId, _context) {
1147
1252
  try {
1148
- const sid = inputData.tool_use_id ?? toolUseId ?? crypto.randomUUID();
1253
+ const sid = inputData.tool_use_id ?? toolUseId ?? randomUuid();
1149
1254
  const toolName = inputData.tool_name ?? "tool";
1150
1255
  const toolInput = inputData.tool_input ?? {};
1151
1256
  const agentId = inputData.agent_id;
@@ -1175,10 +1280,10 @@ var BitfabClaudeAgentHandler = class {
1175
1280
  }
1176
1281
  async subagentStartHook(inputData, _toolUseId, _context) {
1177
1282
  try {
1178
- const agentId = inputData.agent_id ?? crypto.randomUUID();
1283
+ const agentId = inputData.agent_id ?? randomUuid();
1179
1284
  const agentType = inputData.agent_type ?? "subagent";
1180
1285
  const parentId = this.getParentId();
1181
- const spanId = crypto.randomUUID();
1286
+ const spanId = randomUuid();
1182
1287
  this.activeSubagentSpans.set(agentId, spanId);
1183
1288
  this.startSpan(
1184
1289
  spanId,
@@ -1319,7 +1424,7 @@ var BitfabClaudeAgentHandler = class {
1319
1424
  this.flushLlmTurn();
1320
1425
  this.conversationHistory.push(...this.pendingMessages);
1321
1426
  this.pendingMessages = [];
1322
- this.currentLlmSpanId = crypto.randomUUID();
1427
+ this.currentLlmSpanId = randomUuid();
1323
1428
  this.currentLlmMessageId = messageId ?? null;
1324
1429
  this.currentLlmContent = [];
1325
1430
  this.currentLlmModel = inner.model ?? null;
@@ -1763,6 +1868,7 @@ function buildSnapshotRef(config, sdkWallClockBeforeFn) {
1763
1868
  }
1764
1869
 
1765
1870
  // src/langgraph.ts
1871
+ init_randomUuid();
1766
1872
  init_serialize();
1767
1873
  var LANGSMITH_HIDDEN_TAG = "langsmith:hidden";
1768
1874
  var LANGGRAPH_METADATA_KEYS = [
@@ -2011,7 +2117,7 @@ var BitfabLangGraphCallbackHandler = class {
2011
2117
  } else {
2012
2118
  const activeContext = this.getActiveSpanContext?.() ?? null;
2013
2119
  invocation = {
2014
- traceId: activeContext ? activeContext.traceId : crypto.randomUUID(),
2120
+ traceId: activeContext ? activeContext.traceId : randomUuid(),
2015
2121
  activeContext,
2016
2122
  rootRunId: runId
2017
2123
  };
@@ -2306,6 +2412,20 @@ var BitfabOpenAIAgentHandler = class {
2306
2412
  this.withSpanFn = config.withSpan;
2307
2413
  this.getActiveSpanContext = config.getActiveSpanContext;
2308
2414
  }
2415
+ /**
2416
+ * Drop-in replacement for the OpenAI Agents SDK's `run()` that records a
2417
+ * replayable root `agent` span.
2418
+ *
2419
+ * The `input` is captured as the root span's input (as a single positional
2420
+ * argument, so `replay(key, fn)` re-feeds it), and the run's `finalOutput`
2421
+ * is recorded as the root output. For streaming runs (`{ stream: true }`),
2422
+ * the result is handed back immediately and the final output is recorded
2423
+ * once the stream completes — first-byte latency is untouched.
2424
+ *
2425
+ * The process-wide tracing processor (`getOpenAiTracingProcessor`) must still
2426
+ * be registered: it captures the LLM/tool/handoff spans that nest beneath
2427
+ * this root.
2428
+ */
2309
2429
  async wrapRun(agent, input, options) {
2310
2430
  const { run } = await importOptionalPeer([
2311
2431
  "@openai",
@@ -2344,6 +2464,7 @@ var BitfabOpenAIAgentHandler = class {
2344
2464
  };
2345
2465
 
2346
2466
  // src/client.ts
2467
+ init_randomUuid();
2347
2468
  init_replayContext();
2348
2469
 
2349
2470
  // src/replayEnvironment.ts
@@ -2763,6 +2884,7 @@ var BitfabVercelAiHandler = class {
2763
2884
  };
2764
2885
 
2765
2886
  // src/client.ts
2887
+ init_warnOnce();
2766
2888
  var activeTraceStates = /* @__PURE__ */ new Map();
2767
2889
  var pendingSpanPromises = /* @__PURE__ */ new Map();
2768
2890
  var asyncLocalStorage = null;
@@ -3121,7 +3243,20 @@ var Bitfab = class {
3121
3243
  functionVersion.providers,
3122
3244
  this.envVars
3123
3245
  );
3124
- const resultStr = typeof executionResult.result === "string" ? executionResult.result : JSON.stringify(executionResult.result);
3246
+ let resultStr;
3247
+ if (typeof executionResult.result === "string") {
3248
+ resultStr = executionResult.result;
3249
+ } else {
3250
+ try {
3251
+ resultStr = JSON.stringify(executionResult.result) ?? String(executionResult.result);
3252
+ } catch {
3253
+ warnOnce(
3254
+ "call-result-serialize",
3255
+ "a local execution result could not be JSON-serialized; storing its String() form instead. The call still returns its real value."
3256
+ );
3257
+ resultStr = String(executionResult.result);
3258
+ }
3259
+ }
3125
3260
  this.httpClient.sendInternalTrace(functionVersion.id, {
3126
3261
  result: resultStr,
3127
3262
  source: "typescript-sdk",
@@ -3370,11 +3505,26 @@ var Bitfab = class {
3370
3505
  return await bamlClient[methodName](...args);
3371
3506
  }
3372
3507
  const collector = new CollectorClass("bitfab-baml-tracing");
3373
- const trackedClient = bamlClient.withOptions({ collector });
3374
- const trackedMethod = trackedClient[methodName];
3375
- const result = await trackedMethod.bind(
3376
- trackedClient
3377
- )(...args);
3508
+ let trackedClient;
3509
+ let trackedMethod;
3510
+ try {
3511
+ trackedClient = bamlClient.withOptions({ collector });
3512
+ const method2 = trackedClient[methodName];
3513
+ if (typeof method2 !== "function") {
3514
+ throw new BitfabError(
3515
+ "bamlClient.withOptions did not return the wrapped method"
3516
+ );
3517
+ }
3518
+ trackedMethod = method2;
3519
+ } catch {
3520
+ warnOnce(
3521
+ `wrapBAML-setup:${methodName}`,
3522
+ `BAML tracing setup failed for "${methodName}" (incompatible bamlClient or BAML version); calling it untraced. The call still runs; no span is recorded.`
3523
+ );
3524
+ wrappedFn.collector = null;
3525
+ return await bamlClient[methodName](...args);
3526
+ }
3527
+ const result = await trackedMethod.bind(trackedClient)(...args);
3378
3528
  wrappedFn.collector = collector;
3379
3529
  try {
3380
3530
  const prompt = extractPromptFromCollector(collector);
@@ -3450,200 +3600,229 @@ var Bitfab = class {
3450
3600
  () => wrappedFn.apply(this, args)
3451
3601
  );
3452
3602
  }
3453
- const currentStack = getSpanStack();
3454
- const parentContext = currentStack[currentStack.length - 1];
3455
- const replayCtxForTraceId = parentContext ? null : getReplayContext();
3456
- const traceId = parentContext?.traceId ?? replayCtxForTraceId?.traceId ?? crypto.randomUUID();
3457
- const spanId = crypto.randomUUID();
3458
- const parentSpanId = parentContext?.spanId ?? null;
3459
- const isRootSpan = parentSpanId === null;
3460
- const newContext = { traceId, spanId, contexts: [] };
3461
- const newStack = [...currentStack, newContext];
3462
- const inputs = args;
3463
- const startedAt = (/* @__PURE__ */ new Date()).toISOString();
3464
- if (isRootSpan && !activeTraceStates.has(traceId)) {
3465
- const replayCtxAtRoot = getReplayContext();
3466
- const dbSnapshotRef = buildSnapshotRef(self.dbSnapshot, startedAt);
3467
- activeTraceStates.set(traceId, {
3603
+ let newStack;
3604
+ let executeWithContext;
3605
+ let registeredTraceId;
3606
+ try {
3607
+ const currentStack = getSpanStack();
3608
+ const parentContext = currentStack[currentStack.length - 1];
3609
+ const replayCtxForTraceId = parentContext ? null : getReplayContext();
3610
+ const traceId = parentContext?.traceId ?? replayCtxForTraceId?.traceId ?? randomUuid();
3611
+ const spanId = randomUuid();
3612
+ const parentSpanId = parentContext?.spanId ?? null;
3613
+ const isRootSpan = parentSpanId === null;
3614
+ const newContext = { traceId, spanId, contexts: [] };
3615
+ newStack = [...currentStack, newContext];
3616
+ const inputs = args;
3617
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
3618
+ if (isRootSpan && !activeTraceStates.has(traceId)) {
3619
+ const replayCtxAtRoot = getReplayContext();
3620
+ const dbSnapshotRef = buildSnapshotRef(self.dbSnapshot, startedAt);
3621
+ activeTraceStates.set(traceId, {
3622
+ traceId,
3623
+ startedAt,
3624
+ contexts: [],
3625
+ ...replayCtxAtRoot?.testRunId && {
3626
+ testRunId: replayCtxAtRoot.testRunId
3627
+ },
3628
+ ...replayCtxAtRoot?.inputSourceTraceId && {
3629
+ inputSourceTraceId: replayCtxAtRoot.inputSourceTraceId
3630
+ },
3631
+ dbSnapshotRef
3632
+ });
3633
+ pendingSpanPromises.set(traceId, []);
3634
+ registeredTraceId = traceId;
3635
+ }
3636
+ const functionName = fn.name !== "" ? fn.name : void 0;
3637
+ const baseSpanParams = {
3638
+ traceFunctionKey,
3639
+ functionName,
3640
+ spanName: options.name ?? functionName ?? traceFunctionKey,
3468
3641
  traceId,
3642
+ spanId,
3643
+ parentSpanId,
3644
+ inputs,
3469
3645
  startedAt,
3470
- contexts: [],
3471
- ...replayCtxAtRoot?.testRunId && {
3472
- testRunId: replayCtxAtRoot.testRunId
3473
- },
3474
- ...replayCtxAtRoot?.inputSourceTraceId && {
3475
- inputSourceTraceId: replayCtxAtRoot.inputSourceTraceId
3476
- },
3477
- dbSnapshotRef
3478
- });
3479
- pendingSpanPromises.set(traceId, []);
3480
- }
3481
- const functionName = fn.name !== "" ? fn.name : void 0;
3482
- const baseSpanParams = {
3483
- traceFunctionKey,
3484
- functionName,
3485
- spanName: options.name ?? functionName ?? traceFunctionKey,
3486
- traceId,
3487
- spanId,
3488
- parentSpanId,
3489
- inputs,
3490
- startedAt,
3491
- spanType: options.type ?? "custom"
3492
- };
3493
- const sendSpan = async (params, spanOpts) => {
3494
- const replayCtx = getReplayContext();
3495
- const persistenceCollector = isRootSpan ? replayCtx?.pendingPersistence : void 0;
3496
- let resolvePersistence;
3497
- if (persistenceCollector && !spanOpts?.skipPersistenceRegistration) {
3498
- persistenceCollector.push(
3499
- new Promise((resolve) => {
3500
- resolvePersistence = resolve;
3501
- })
3502
- );
3503
- }
3504
- try {
3505
- const endedAt = (/* @__PURE__ */ new Date()).toISOString();
3506
- const spanPromise = self.sendWrapperSpan({
3507
- ...baseSpanParams,
3508
- ...params,
3509
- contexts: newContext.contexts,
3510
- prompt: newContext.prompt,
3511
- endedAt,
3512
- ...replayCtx?.testRunId && { testRunId: replayCtx.testRunId },
3513
- ...replayCtx?.inputSourceSpanId && {
3514
- inputSourceSpanId: replayCtx.inputSourceSpanId
3515
- }
3516
- });
3517
- if (isRootSpan) {
3518
- const pending = pendingSpanPromises.get(traceId) ?? [];
3519
- pending.push(spanPromise);
3520
- if (persistenceCollector) {
3521
- await Promise.allSettled(pending);
3522
- } else {
3523
- await Promise.race([
3524
- Promise.allSettled(pending),
3525
- new Promise((resolve) => setTimeout(resolve, 5e3))
3526
- ]);
3527
- }
3528
- pendingSpanPromises.delete(traceId);
3529
- const traceState = activeTraceStates.get(traceId);
3530
- const completionPromise = self.sendTraceCompletion({
3531
- traceFunctionKey,
3532
- traceId,
3533
- startedAt: traceState?.startedAt ?? startedAt,
3646
+ spanType: options.type ?? "custom"
3647
+ };
3648
+ const sendSpan = async (params, spanOpts) => {
3649
+ const replayCtx = getReplayContext();
3650
+ const persistenceCollector = isRootSpan ? replayCtx?.pendingPersistence : void 0;
3651
+ let resolvePersistence;
3652
+ if (persistenceCollector && !spanOpts?.skipPersistenceRegistration) {
3653
+ persistenceCollector.push(
3654
+ new Promise((resolve) => {
3655
+ resolvePersistence = resolve;
3656
+ })
3657
+ );
3658
+ }
3659
+ try {
3660
+ const endedAt = (/* @__PURE__ */ new Date()).toISOString();
3661
+ const spanPromise = self.sendWrapperSpan({
3662
+ ...baseSpanParams,
3663
+ ...params,
3664
+ contexts: newContext.contexts,
3665
+ prompt: newContext.prompt,
3534
3666
  endedAt,
3535
- sessionId: traceState?.sessionId,
3536
- metadata: traceState?.metadata,
3537
- contexts: traceState?.contexts ?? [],
3538
- testRunId: traceState?.testRunId,
3539
- inputSourceTraceId: traceState?.inputSourceTraceId,
3540
- dbSnapshotRef: traceState?.dbSnapshotRef,
3541
- // Built AFTER the wrapped fn finished, so `accessed` reflects
3542
- // whether customer code obtained the branch URL during this
3543
- // item. Omitted entirely when no lease was attached, so the
3544
- // server can distinguish "no branch" from "branch ignored".
3545
- ...replayCtx?.dbBranchLease && {
3546
- dbSnapshotUsage: {
3547
- neonBranchId: replayCtx.dbBranchLease.neonBranchId,
3548
- snapshotTimestamp: replayCtx.dbBranchLease.snapshotTimestamp,
3549
- sourceTraceId: replayCtx.sourceBitfabTraceId,
3550
- accessed: replayCtx.dbSnapshotAccessed === true
3551
- }
3667
+ ...replayCtx?.testRunId && { testRunId: replayCtx.testRunId },
3668
+ ...replayCtx?.inputSourceSpanId && {
3669
+ inputSourceSpanId: replayCtx.inputSourceSpanId
3552
3670
  }
3553
3671
  });
3554
- activeTraceStates.delete(traceId);
3555
- if (persistenceCollector) {
3556
- await completionPromise;
3557
- }
3558
- } else {
3559
- const pending = pendingSpanPromises.get(traceId);
3560
- if (pending) {
3672
+ if (isRootSpan) {
3673
+ const pending = pendingSpanPromises.get(traceId) ?? [];
3561
3674
  pending.push(spanPromise);
3675
+ if (persistenceCollector) {
3676
+ await Promise.allSettled(pending);
3677
+ } else {
3678
+ let raceTimer;
3679
+ try {
3680
+ await Promise.race([
3681
+ Promise.allSettled(pending),
3682
+ new Promise((resolve) => {
3683
+ raceTimer = setTimeout(resolve, 5e3);
3684
+ unrefTimer(raceTimer);
3685
+ })
3686
+ ]);
3687
+ } finally {
3688
+ if (raceTimer) {
3689
+ clearTimeout(raceTimer);
3690
+ }
3691
+ }
3692
+ }
3693
+ pendingSpanPromises.delete(traceId);
3694
+ const traceState = activeTraceStates.get(traceId);
3695
+ const completionPromise = self.sendTraceCompletion({
3696
+ traceFunctionKey,
3697
+ traceId,
3698
+ startedAt: traceState?.startedAt ?? startedAt,
3699
+ endedAt,
3700
+ sessionId: traceState?.sessionId,
3701
+ metadata: traceState?.metadata,
3702
+ contexts: traceState?.contexts ?? [],
3703
+ testRunId: traceState?.testRunId,
3704
+ inputSourceTraceId: traceState?.inputSourceTraceId,
3705
+ dbSnapshotRef: traceState?.dbSnapshotRef,
3706
+ // Built AFTER the wrapped fn finished, so `accessed` reflects
3707
+ // whether customer code obtained the branch URL during this
3708
+ // item. Omitted entirely when no lease was attached, so the
3709
+ // server can distinguish "no branch" from "branch ignored".
3710
+ ...replayCtx?.dbBranchLease && {
3711
+ dbSnapshotUsage: {
3712
+ neonBranchId: replayCtx.dbBranchLease.neonBranchId,
3713
+ snapshotTimestamp: replayCtx.dbBranchLease.snapshotTimestamp,
3714
+ sourceTraceId: replayCtx.sourceBitfabTraceId,
3715
+ accessed: replayCtx.dbSnapshotAccessed === true
3716
+ }
3717
+ }
3718
+ });
3719
+ activeTraceStates.delete(traceId);
3720
+ if (persistenceCollector) {
3721
+ await completionPromise;
3722
+ }
3562
3723
  } else {
3563
- pendingSpanPromises.set(traceId, [spanPromise]);
3724
+ const pending = pendingSpanPromises.get(traceId);
3725
+ if (pending) {
3726
+ pending.push(spanPromise);
3727
+ } else {
3728
+ pendingSpanPromises.set(traceId, [spanPromise]);
3729
+ }
3564
3730
  }
3731
+ } catch {
3732
+ } finally {
3733
+ resolvePersistence?.();
3565
3734
  }
3566
- } catch {
3567
- } finally {
3568
- resolvePersistence?.();
3569
- }
3570
- };
3571
- const replayCtxForMock = getReplayContext();
3572
- if (replayCtxForMock?.mockTree && !isRootSpan) {
3573
- const counters = replayCtxForMock.callCounters;
3574
- const counterKey = `${traceFunctionKey}:${baseSpanParams.spanName}`;
3575
- const callIndex = counters.get(counterKey) ?? 0;
3576
- counters.set(counterKey, callIndex + 1);
3577
- const shouldMock = replayCtxForMock.mockStrategy === "all" || replayCtxForMock.mockStrategy === "marked" && options.mockOnReplay === true;
3578
- if (shouldMock) {
3579
- const mockKey = `${counterKey}:${callIndex}`;
3580
- const mockSpan = replayCtxForMock.mockTree.spans.get(mockKey);
3581
- if (mockSpan) {
3582
- let output = mockSpan.output;
3583
- if (mockSpan.outputMeta !== void 0 && mockSpan.outputMeta !== null) {
3584
- output = deserializeValue({
3585
- json: mockSpan.output,
3586
- meta: mockSpan.outputMeta
3587
- });
3588
- }
3589
- void sendSpan({ result: output });
3590
- if (fnReturnsPromise) {
3591
- return Promise.resolve(output);
3735
+ };
3736
+ const replayCtxForMock = getReplayContext();
3737
+ if (replayCtxForMock?.mockTree && !isRootSpan) {
3738
+ const counters = replayCtxForMock.callCounters;
3739
+ const counterKey = `${traceFunctionKey}:${baseSpanParams.spanName}`;
3740
+ const callIndex = counters.get(counterKey) ?? 0;
3741
+ counters.set(counterKey, callIndex + 1);
3742
+ const shouldMock = replayCtxForMock.mockStrategy === "all" || replayCtxForMock.mockStrategy === "marked" && options.mockOnReplay === true;
3743
+ if (shouldMock) {
3744
+ const mockKey = `${counterKey}:${callIndex}`;
3745
+ const mockSpan = replayCtxForMock.mockTree.spans.get(mockKey);
3746
+ if (mockSpan) {
3747
+ let output = mockSpan.output;
3748
+ if (mockSpan.outputMeta !== void 0 && mockSpan.outputMeta !== null) {
3749
+ output = deserializeValue({
3750
+ json: mockSpan.output,
3751
+ meta: mockSpan.outputMeta
3752
+ });
3753
+ }
3754
+ void sendSpan({ result: output });
3755
+ if (fnReturnsPromise) {
3756
+ return Promise.resolve(output);
3757
+ }
3758
+ return output;
3592
3759
  }
3593
- return output;
3594
3760
  }
3595
3761
  }
3596
- }
3597
- const recordSpan = (result) => {
3598
- if (options.finalize) {
3599
- const replayCtx = getReplayContext();
3600
- const persistenceCollector = isRootSpan ? replayCtx?.pendingPersistence : void 0;
3601
- let resolvePersistence;
3602
- if (persistenceCollector) {
3603
- persistenceCollector.push(
3604
- new Promise((resolve) => {
3605
- resolvePersistence = resolve;
3606
- })
3607
- );
3762
+ const recordSpan = (result) => {
3763
+ if (options.finalize) {
3764
+ const replayCtx = getReplayContext();
3765
+ const persistenceCollector = isRootSpan ? replayCtx?.pendingPersistence : void 0;
3766
+ let resolvePersistence;
3767
+ if (persistenceCollector) {
3768
+ persistenceCollector.push(
3769
+ new Promise((resolve) => {
3770
+ resolvePersistence = resolve;
3771
+ })
3772
+ );
3773
+ }
3774
+ void Promise.resolve().then(() => options.finalize(result)).then(
3775
+ (output) => sendSpan(
3776
+ { result: output },
3777
+ { skipPersistenceRegistration: true }
3778
+ )
3779
+ ).catch(
3780
+ (error) => sendSpan(
3781
+ {
3782
+ result: void 0,
3783
+ error: error instanceof Error ? `finalize failed: ${error.message}` : `finalize failed: ${String(error)}`
3784
+ },
3785
+ { skipPersistenceRegistration: true }
3786
+ )
3787
+ ).finally(() => resolvePersistence?.());
3788
+ } else {
3789
+ void sendSpan({ result });
3608
3790
  }
3609
- void Promise.resolve().then(() => options.finalize(result)).then(
3610
- (output) => sendSpan(
3611
- { result: output },
3612
- { skipPersistenceRegistration: true }
3613
- )
3614
- ).catch(
3615
- (error) => sendSpan(
3616
- {
3791
+ };
3792
+ executeWithContext = () => {
3793
+ const result = fn(...args);
3794
+ if (result instanceof Promise) {
3795
+ return result.then((resolvedResult) => {
3796
+ recordSpan(resolvedResult);
3797
+ return resolvedResult;
3798
+ }).catch((error) => {
3799
+ void sendSpan({
3617
3800
  result: void 0,
3618
- error: error instanceof Error ? `finalize failed: ${error.message}` : `finalize failed: ${String(error)}`
3619
- },
3620
- { skipPersistenceRegistration: true }
3621
- )
3622
- ).finally(() => resolvePersistence?.());
3623
- } else {
3624
- void sendSpan({ result });
3625
- }
3626
- };
3627
- const executeWithContext = () => {
3628
- const result = fn(...args);
3629
- if (result instanceof Promise) {
3630
- return result.then((resolvedResult) => {
3631
- recordSpan(resolvedResult);
3632
- return resolvedResult;
3633
- }).catch((error) => {
3634
- void sendSpan({
3635
- result: void 0,
3636
- error: error instanceof Error ? error.message : String(error)
3801
+ error: error instanceof Error ? error.message : String(error)
3802
+ });
3803
+ throw error;
3637
3804
  });
3638
- throw error;
3639
- });
3805
+ }
3806
+ if (isAsyncGenerator(result)) {
3807
+ return wrapAsyncGenerator(result, newStack, sendSpan);
3808
+ }
3809
+ recordSpan(result);
3810
+ return result;
3811
+ };
3812
+ } catch (setupError) {
3813
+ if (registeredTraceId) {
3814
+ activeTraceStates.delete(registeredTraceId);
3815
+ pendingSpanPromises.delete(registeredTraceId);
3640
3816
  }
3641
- if (isAsyncGenerator(result)) {
3642
- return wrapAsyncGenerator(result, newStack, sendSpan);
3817
+ if (getReplayContext()) {
3818
+ throw setupError;
3643
3819
  }
3644
- recordSpan(result);
3645
- return result;
3646
- };
3820
+ warnOnce(
3821
+ `withSpan-setup:${traceFunctionKey}`,
3822
+ `tracing setup failed for "${traceFunctionKey}"; running it untraced. The function still runs and returns normally; no span is recorded.`
3823
+ );
3824
+ return fn(...args);
3825
+ }
3647
3826
  return runWithSpanStack(newStack, executeWithContext);
3648
3827
  };
3649
3828
  Object.defineProperty(wrappedFn, "_bitfabTraceFunctionKey", {