@bitfab/sdk 0.13.3 → 0.13.5

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
@@ -81,7 +81,7 @@ var __version__;
81
81
  var init_version_generated = __esm({
82
82
  "src/version.generated.ts"() {
83
83
  "use strict";
84
- __version__ = "0.13.3";
84
+ __version__ = "0.13.5";
85
85
  }
86
86
  });
87
87
 
@@ -95,6 +95,21 @@ var init_constants = __esm({
95
95
  }
96
96
  });
97
97
 
98
+ // src/errors.ts
99
+ var BitfabError;
100
+ var init_errors = __esm({
101
+ "src/errors.ts"() {
102
+ "use strict";
103
+ BitfabError = class extends Error {
104
+ constructor(message, url) {
105
+ super(message);
106
+ this.url = url;
107
+ this.name = "BitfabError";
108
+ }
109
+ };
110
+ }
111
+ });
112
+
98
113
  // src/http.ts
99
114
  function awaitOnExit(promise) {
100
115
  pendingTracePromises.add(promise);
@@ -113,18 +128,12 @@ async function flushTraces(timeoutMs = 5e3) {
113
128
  new Promise((resolve) => setTimeout(resolve, timeoutMs))
114
129
  ]);
115
130
  }
116
- var BitfabError, pendingTracePromises, HttpClient;
131
+ var pendingTracePromises, HttpClient;
117
132
  var init_http = __esm({
118
133
  "src/http.ts"() {
119
134
  "use strict";
120
135
  init_constants();
121
- BitfabError = class extends Error {
122
- constructor(message, url) {
123
- super(message);
124
- this.url = url;
125
- this.name = "BitfabError";
126
- }
127
- };
136
+ init_errors();
128
137
  pendingTracePromises = /* @__PURE__ */ new Set();
129
138
  if (typeof process !== "undefined" && process.versions != null && process.versions.node != null) {
130
139
  let isFlushing = false;
@@ -303,7 +312,7 @@ var init_http = __esm({
303
312
  * Start a replay session by fetching historical traces.
304
313
  * Blocking call — creates a test run and returns lightweight item references.
305
314
  */
306
- async startReplay(traceFunctionKey, limit, traceIds, codeChangeDescription, codeChangeFiles) {
315
+ async startReplay(traceFunctionKey, limit, traceIds, codeChangeDescription, codeChangeFiles, includeDbBranchLease) {
307
316
  const payload = { traceFunctionKey, limit };
308
317
  if (traceIds) {
309
318
  payload.traceIds = traceIds;
@@ -314,8 +323,12 @@ var init_http = __esm({
314
323
  if (codeChangeFiles !== void 0) {
315
324
  payload.codeChangeFiles = codeChangeFiles;
316
325
  }
326
+ if (includeDbBranchLease) {
327
+ payload.includeDbBranchLease = true;
328
+ }
329
+ const timeout = includeDbBranchLease ? 18e4 : 3e4;
317
330
  return this.request("/api/sdk/replay/start", payload, {
318
- timeout: 3e4
331
+ timeout
319
332
  });
320
333
  }
321
334
  /**
@@ -401,6 +414,27 @@ var init_http = __esm({
401
414
  { timeout: 3e4 }
402
415
  );
403
416
  }
417
+ /**
418
+ * Ask the server to materialize a per-trace DB branch lease from a
419
+ * captured `dbSnapshotRef`. Blocking — the resolver creates a Neon
420
+ * snapshot + preview branch and polls operations to readiness, which
421
+ * can take seconds.
422
+ */
423
+ async resolveDbBranchLease(testRunId, traceId, dbSnapshotRef) {
424
+ return this.request(
425
+ "/api/sdk/replay/resolveDbBranchLease",
426
+ { testRunId, traceId, dbSnapshotRef },
427
+ { timeout: 9e4 }
428
+ );
429
+ }
430
+ /** Release a previously-resolved DB branch lease. Idempotent server-side. */
431
+ async releaseDbBranchLease(leaseId) {
432
+ await this.request(
433
+ "/api/sdk/replay/releaseDbBranchLease",
434
+ { leaseId },
435
+ { timeout: 3e4 }
436
+ );
437
+ }
404
438
  };
405
439
  }
406
440
  });
@@ -539,42 +573,66 @@ function buildMockTree(rootNode) {
539
573
  }
540
574
  return { spans };
541
575
  }
542
- async function processItem(httpClient, serverItem, fn, testRunId, mockStrategy) {
543
- const span = await httpClient.getExternalSpan(serverItem.externalSpanId);
544
- const spanData = span.rawData?.span_data ?? {};
545
- const inputs = deserializeInputs(spanData);
546
- const originalOutput = deserializeOutput(spanData);
547
- let mockTree;
548
- if (mockStrategy === "all" || mockStrategy === "marked") {
549
- const treeResponse = await httpClient.getSpanTree(serverItem.externalSpanId);
550
- mockTree = buildMockTree(treeResponse.root);
551
- }
576
+ async function processItem(httpClient, serverItem, fn, testRunId, mockStrategy, environment) {
577
+ const lease = environment ? serverItem.dbBranchLease : void 0;
578
+ let inputs = [];
579
+ let originalOutput;
552
580
  let result;
553
581
  let error = null;
582
+ const replayedTraceId = crypto.randomUUID();
554
583
  try {
584
+ const span = await httpClient.getExternalSpan(serverItem.externalSpanId);
585
+ const spanData = span.rawData?.span_data ?? {};
586
+ inputs = deserializeInputs(spanData);
587
+ originalOutput = deserializeOutput(spanData);
588
+ let mockTree;
589
+ if (mockStrategy === "all" || mockStrategy === "marked") {
590
+ const treeResponse = await httpClient.getSpanTree(
591
+ serverItem.externalSpanId
592
+ );
593
+ mockTree = buildMockTree(treeResponse.root);
594
+ }
555
595
  const maybePromise = runWithReplayContext(
556
596
  {
557
597
  testRunId,
598
+ traceId: replayedTraceId,
558
599
  inputSourceSpanId: span.id,
559
600
  inputSourceTraceId: span.externalTraceId,
601
+ sourceBitfabTraceId: serverItem.traceId,
560
602
  mockTree,
561
603
  callCounters: mockTree ? /* @__PURE__ */ new Map() : void 0,
562
- mockStrategy
604
+ mockStrategy,
605
+ dbBranchLease: lease
563
606
  },
564
607
  () => fn(...inputs)
565
608
  );
566
609
  result = maybePromise instanceof Promise ? await maybePromise : maybePromise;
567
610
  } catch (e) {
568
611
  error = e instanceof Error ? e.message : String(e);
612
+ } finally {
613
+ if (lease) {
614
+ try {
615
+ await httpClient.releaseDbBranchLease(lease.leaseId);
616
+ } catch (e) {
617
+ try {
618
+ console.warn(
619
+ `Bitfab: failed to release DB branch lease ${lease.leaseId} (TTL janitor will catch it): ${e instanceof Error ? e.message : String(e)}`
620
+ );
621
+ } catch {
622
+ }
623
+ }
624
+ }
569
625
  }
570
626
  return {
627
+ traceId: replayedTraceId,
571
628
  input: inputs,
572
629
  result,
573
630
  originalOutput,
574
631
  error,
575
632
  durationMs: serverItem.durationMs ?? null,
576
633
  tokens: serverItem.tokens ?? null,
577
- model: serverItem.model ?? null
634
+ model: serverItem.model ?? null,
635
+ dbSnapshotRef: serverItem.dbSnapshotRef ?? null
578
636
  };
579
637
  }
580
638
  async function mapWithConcurrency(tasks, maxConcurrency) {
@@ -604,23 +662,39 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options) {
604
662
  options?.limit ?? 5,
605
663
  options?.traceIds,
606
664
  options?.codeChangeDescription,
607
- options?.codeChangeFiles
665
+ options?.codeChangeFiles,
666
+ options?.environment !== void 0
667
+ // includeDbBranchLease
608
668
  );
609
669
  const mockStrategy = options?.mock ?? "none";
610
670
  const maxConcurrency = options?.maxConcurrency ?? 10;
611
671
  const tasks = serverItems.map(
612
- (serverItem) => () => processItem(httpClient, serverItem, fn, testRunId, mockStrategy)
672
+ (serverItem) => () => processItem(
673
+ httpClient,
674
+ serverItem,
675
+ fn,
676
+ testRunId,
677
+ mockStrategy,
678
+ options?.environment
679
+ )
613
680
  );
614
681
  const resultItems = await mapWithConcurrency(tasks, maxConcurrency);
615
682
  await flushTraces();
683
+ let serverTraceIds = {};
616
684
  try {
617
- await httpClient.completeReplay(testRunId);
685
+ const completeResult = await httpClient.completeReplay(testRunId);
686
+ serverTraceIds = completeResult.traceIds ?? {};
618
687
  } catch (e) {
619
688
  try {
620
689
  console.error("Bitfab: Failed to complete replay:", e);
621
690
  } catch {
622
691
  }
623
692
  }
693
+ for (const item of resultItems) {
694
+ if (item.traceId) {
695
+ item.traceId = serverTraceIds[item.traceId] ?? null;
696
+ }
697
+ }
624
698
  return {
625
699
  items: resultItems,
626
700
  testRunId,
@@ -646,6 +720,8 @@ __export(node_exports, {
646
720
  BitfabLangGraphCallbackHandler: () => BitfabLangGraphCallbackHandler,
647
721
  BitfabOpenAITracingProcessor: () => BitfabOpenAITracingProcessor,
648
722
  DEFAULT_SERVICE_URL: () => DEFAULT_SERVICE_URL,
723
+ ReplayEnvironment: () => ReplayEnvironment,
724
+ SUPPORTED_PROVIDERS: () => SUPPORTED_PROVIDERS,
649
725
  __version__: () => __version__,
650
726
  flushTraces: () => flushTraces,
651
727
  getCurrentSpan: () => getCurrentSpan,
@@ -1429,6 +1505,34 @@ async function runFunctionWithBaml(bamlSource, inputs, providers, envVars) {
1429
1505
 
1430
1506
  // src/client.ts
1431
1507
  init_constants();
1508
+
1509
+ // src/dbSnapshot.ts
1510
+ init_errors();
1511
+ var SUPPORTED_PROVIDERS = [
1512
+ "neon",
1513
+ "ardent",
1514
+ "dolt",
1515
+ "gfs",
1516
+ "mongodb-atlas",
1517
+ "dynamodb",
1518
+ "snowflake",
1519
+ "bigquery"
1520
+ ];
1521
+ function validateDbSnapshotConfig(config) {
1522
+ if (!SUPPORTED_PROVIDERS.includes(config.provider)) {
1523
+ throw new BitfabError(
1524
+ `dbSnapshot.provider "${config.provider}" is not supported. Supported providers: ${SUPPORTED_PROVIDERS.join(", ")}.`
1525
+ );
1526
+ }
1527
+ }
1528
+ function buildSnapshotRef(config, sdkWallClockBeforeFn) {
1529
+ return {
1530
+ provider: config.provider,
1531
+ sdkWallClockBeforeFn
1532
+ };
1533
+ }
1534
+
1535
+ // src/client.ts
1432
1536
  init_http();
1433
1537
 
1434
1538
  // src/langgraph.ts
@@ -1909,6 +2013,84 @@ var BitfabLangGraphCallbackHandler = class {
1909
2013
 
1910
2014
  // src/client.ts
1911
2015
  init_replayContext();
2016
+
2017
+ // src/replayEnvironment.ts
2018
+ init_replayContext();
2019
+ var ReplayEnvironment = class {
2020
+ /**
2021
+ * The per-trace branch URL for the item currently being replayed.
2022
+ * Throws if read outside a replay item.
2023
+ */
2024
+ get databaseUrl() {
2025
+ return this.require().databaseUrl;
2026
+ }
2027
+ /** When the per-trace branch URL stops being valid. ISO-8601. */
2028
+ get expiresAt() {
2029
+ return this.require().expiresAt;
2030
+ }
2031
+ /** Deep link to the branch in the provider console, if available. */
2032
+ get providerConsoleUrl() {
2033
+ return this.require().providerConsoleUrl;
2034
+ }
2035
+ /**
2036
+ * True if the branch is read-only. Customer code can use this to skip
2037
+ * write operations during replay when the provider returned a read-only
2038
+ * lease.
2039
+ */
2040
+ get readOnly() {
2041
+ return this.require().readOnly;
2042
+ }
2043
+ /** The historical trace ID that produced the input for this replay item. */
2044
+ get traceId() {
2045
+ return this.require().traceId;
2046
+ }
2047
+ /**
2048
+ * How the resolver pinned this branch.
2049
+ * - "timestamp": snapshot at SDK wall clock; bounded by replication lag.
2050
+ * - "lsn": customer LSN mapped to a replica snapshot (future).
2051
+ */
2052
+ get precision() {
2053
+ return this.require().precision;
2054
+ }
2055
+ /** True when read inside a replay item that has a resolved branch. */
2056
+ get active() {
2057
+ return this.read() !== null;
2058
+ }
2059
+ /** Non-throwing variant for callers that handle the inactive case. */
2060
+ snapshot() {
2061
+ return this.read();
2062
+ }
2063
+ read() {
2064
+ const ctx = getReplayContext();
2065
+ if (!ctx?.dbBranchLease) {
2066
+ return null;
2067
+ }
2068
+ const traceId = ctx.sourceBitfabTraceId ?? ctx.inputSourceTraceId;
2069
+ if (!traceId) {
2070
+ return null;
2071
+ }
2072
+ const lease = ctx.dbBranchLease;
2073
+ return {
2074
+ databaseUrl: lease.databaseUrl,
2075
+ expiresAt: lease.expiresAt,
2076
+ providerConsoleUrl: lease.providerConsoleUrl,
2077
+ readOnly: lease.readOnly,
2078
+ traceId,
2079
+ precision: lease.precision
2080
+ };
2081
+ }
2082
+ require() {
2083
+ const snapshot = this.read();
2084
+ if (!snapshot) {
2085
+ throw new Error(
2086
+ "ReplayEnvironment accessed outside of a replay item. Pass it to bitfab.replay({ environment }) and only read it inside the replayed function."
2087
+ );
2088
+ }
2089
+ return snapshot;
2090
+ }
2091
+ };
2092
+
2093
+ // src/client.ts
1912
2094
  init_serialize();
1913
2095
 
1914
2096
  // src/tracing.ts
@@ -2404,6 +2586,10 @@ var Bitfab = class {
2404
2586
  this.enabled = enabled;
2405
2587
  }
2406
2588
  this.bamlClient = config.bamlClient ?? null;
2589
+ if (config.dbSnapshot) {
2590
+ validateDbSnapshotConfig(config.dbSnapshot);
2591
+ }
2592
+ this.dbSnapshot = config.dbSnapshot;
2407
2593
  this.httpClient = new HttpClient({
2408
2594
  apiKey: this.apiKey,
2409
2595
  serviceUrl: this.serviceUrl,
@@ -2700,7 +2886,8 @@ var Bitfab = class {
2700
2886
  }
2701
2887
  const currentStack = getSpanStack();
2702
2888
  const parentContext = currentStack[currentStack.length - 1];
2703
- const traceId = parentContext?.traceId ?? crypto.randomUUID();
2889
+ const replayCtxForTraceId = parentContext ? null : getReplayContext();
2890
+ const traceId = parentContext?.traceId ?? replayCtxForTraceId?.traceId ?? crypto.randomUUID();
2704
2891
  const spanId = crypto.randomUUID();
2705
2892
  const parentSpanId = parentContext?.spanId ?? null;
2706
2893
  const isRootSpan = parentSpanId === null;
@@ -2710,6 +2897,7 @@ var Bitfab = class {
2710
2897
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
2711
2898
  if (isRootSpan && !activeTraceStates.has(traceId)) {
2712
2899
  const replayCtxAtRoot = getReplayContext();
2900
+ const dbSnapshotRef = self.dbSnapshot ? buildSnapshotRef(self.dbSnapshot, startedAt) : void 0;
2713
2901
  activeTraceStates.set(traceId, {
2714
2902
  traceId,
2715
2903
  startedAt,
@@ -2719,7 +2907,8 @@ var Bitfab = class {
2719
2907
  },
2720
2908
  ...replayCtxAtRoot?.inputSourceTraceId && {
2721
2909
  inputSourceTraceId: replayCtxAtRoot.inputSourceTraceId
2722
- }
2910
+ },
2911
+ ...dbSnapshotRef && { dbSnapshotRef }
2723
2912
  });
2724
2913
  pendingSpanPromises.set(traceId, []);
2725
2914
  }
@@ -2739,6 +2928,7 @@ var Bitfab = class {
2739
2928
  try {
2740
2929
  const endedAt = (/* @__PURE__ */ new Date()).toISOString();
2741
2930
  const replayCtx = getReplayContext();
2931
+ const dbSnapshotRefForSpan = isRootSpan ? activeTraceStates.get(traceId)?.dbSnapshotRef : void 0;
2742
2932
  const spanPromise = self.sendWrapperSpan({
2743
2933
  ...baseSpanParams,
2744
2934
  ...params,
@@ -2748,6 +2938,9 @@ var Bitfab = class {
2748
2938
  ...replayCtx?.testRunId && { testRunId: replayCtx.testRunId },
2749
2939
  ...replayCtx?.inputSourceSpanId && {
2750
2940
  inputSourceSpanId: replayCtx.inputSourceSpanId
2941
+ },
2942
+ ...dbSnapshotRefForSpan && {
2943
+ dbSnapshotRef: dbSnapshotRefForSpan
2751
2944
  }
2752
2945
  });
2753
2946
  if (isRootSpan) {
@@ -2768,7 +2961,8 @@ var Bitfab = class {
2768
2961
  metadata: traceState?.metadata,
2769
2962
  contexts: traceState?.contexts ?? [],
2770
2963
  testRunId: traceState?.testRunId,
2771
- inputSourceTraceId: traceState?.inputSourceTraceId
2964
+ inputSourceTraceId: traceState?.inputSourceTraceId,
2965
+ dbSnapshotRef: traceState?.dbSnapshotRef
2772
2966
  });
2773
2967
  activeTraceStates.delete(traceId);
2774
2968
  } else {
@@ -2929,6 +3123,9 @@ var Bitfab = class {
2929
3123
  if (params.inputSourceTraceId) {
2930
3124
  rawTrace.input_source_trace_id = params.inputSourceTraceId;
2931
3125
  }
3126
+ if (params.dbSnapshotRef) {
3127
+ rawTrace.db_snapshot_ref = params.dbSnapshotRef;
3128
+ }
2932
3129
  this.httpClient.sendExternalTrace({
2933
3130
  type: "sdk-function",
2934
3131
  source: "typescript-sdk-function",
@@ -2971,7 +3168,10 @@ var Bitfab = class {
2971
3168
  ...params.contexts && params.contexts.length > 0 && {
2972
3169
  contexts: params.contexts
2973
3170
  },
2974
- ...params.prompt !== void 0 && { prompt: params.prompt }
3171
+ ...params.prompt !== void 0 && { prompt: params.prompt },
3172
+ ...params.dbSnapshotRef && {
3173
+ db_snapshot_ref: params.dbSnapshotRef
3174
+ }
2975
3175
  }
2976
3176
  };
2977
3177
  if (params.parentSpanId) {
@@ -3014,6 +3214,12 @@ var Bitfab = class {
3014
3214
  );
3015
3215
  }
3016
3216
  };
3217
+ /**
3218
+ * Per-trace environment for `replay({ environment })`. Construct one,
3219
+ * pass it to replay, and read `env.databaseUrl` inside the replayed
3220
+ * function to pick up the per-trace branch URL.
3221
+ */
3222
+ Bitfab.ReplayEnvironment = ReplayEnvironment;
3017
3223
  var BitfabFunction = class {
3018
3224
  constructor(client, traceFunctionKey) {
3019
3225
  this.client = client;
@@ -3079,6 +3285,8 @@ assertAsyncStorageRegistered();
3079
3285
  BitfabLangGraphCallbackHandler,
3080
3286
  BitfabOpenAITracingProcessor,
3081
3287
  DEFAULT_SERVICE_URL,
3288
+ ReplayEnvironment,
3289
+ SUPPORTED_PROVIDERS,
3082
3290
  __version__,
3083
3291
  flushTraces,
3084
3292
  getCurrentSpan,