@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/index.cjs CHANGED
@@ -35,7 +35,7 @@ var __version__;
35
35
  var init_version_generated = __esm({
36
36
  "src/version.generated.ts"() {
37
37
  "use strict";
38
- __version__ = "0.13.3";
38
+ __version__ = "0.13.5";
39
39
  }
40
40
  });
41
41
 
@@ -49,6 +49,21 @@ var init_constants = __esm({
49
49
  }
50
50
  });
51
51
 
52
+ // src/errors.ts
53
+ var BitfabError;
54
+ var init_errors = __esm({
55
+ "src/errors.ts"() {
56
+ "use strict";
57
+ BitfabError = class extends Error {
58
+ constructor(message, url) {
59
+ super(message);
60
+ this.url = url;
61
+ this.name = "BitfabError";
62
+ }
63
+ };
64
+ }
65
+ });
66
+
52
67
  // src/http.ts
53
68
  function awaitOnExit(promise) {
54
69
  pendingTracePromises.add(promise);
@@ -67,18 +82,12 @@ async function flushTraces(timeoutMs = 5e3) {
67
82
  new Promise((resolve) => setTimeout(resolve, timeoutMs))
68
83
  ]);
69
84
  }
70
- var BitfabError, pendingTracePromises, HttpClient;
85
+ var pendingTracePromises, HttpClient;
71
86
  var init_http = __esm({
72
87
  "src/http.ts"() {
73
88
  "use strict";
74
89
  init_constants();
75
- BitfabError = class extends Error {
76
- constructor(message, url) {
77
- super(message);
78
- this.url = url;
79
- this.name = "BitfabError";
80
- }
81
- };
90
+ init_errors();
82
91
  pendingTracePromises = /* @__PURE__ */ new Set();
83
92
  if (typeof process !== "undefined" && process.versions != null && process.versions.node != null) {
84
93
  let isFlushing = false;
@@ -257,7 +266,7 @@ var init_http = __esm({
257
266
  * Start a replay session by fetching historical traces.
258
267
  * Blocking call — creates a test run and returns lightweight item references.
259
268
  */
260
- async startReplay(traceFunctionKey, limit, traceIds, codeChangeDescription, codeChangeFiles) {
269
+ async startReplay(traceFunctionKey, limit, traceIds, codeChangeDescription, codeChangeFiles, includeDbBranchLease) {
261
270
  const payload = { traceFunctionKey, limit };
262
271
  if (traceIds) {
263
272
  payload.traceIds = traceIds;
@@ -268,8 +277,12 @@ var init_http = __esm({
268
277
  if (codeChangeFiles !== void 0) {
269
278
  payload.codeChangeFiles = codeChangeFiles;
270
279
  }
280
+ if (includeDbBranchLease) {
281
+ payload.includeDbBranchLease = true;
282
+ }
283
+ const timeout = includeDbBranchLease ? 18e4 : 3e4;
271
284
  return this.request("/api/sdk/replay/start", payload, {
272
- timeout: 3e4
285
+ timeout
273
286
  });
274
287
  }
275
288
  /**
@@ -355,6 +368,27 @@ var init_http = __esm({
355
368
  { timeout: 3e4 }
356
369
  );
357
370
  }
371
+ /**
372
+ * Ask the server to materialize a per-trace DB branch lease from a
373
+ * captured `dbSnapshotRef`. Blocking — the resolver creates a Neon
374
+ * snapshot + preview branch and polls operations to readiness, which
375
+ * can take seconds.
376
+ */
377
+ async resolveDbBranchLease(testRunId, traceId, dbSnapshotRef) {
378
+ return this.request(
379
+ "/api/sdk/replay/resolveDbBranchLease",
380
+ { testRunId, traceId, dbSnapshotRef },
381
+ { timeout: 9e4 }
382
+ );
383
+ }
384
+ /** Release a previously-resolved DB branch lease. Idempotent server-side. */
385
+ async releaseDbBranchLease(leaseId) {
386
+ await this.request(
387
+ "/api/sdk/replay/releaseDbBranchLease",
388
+ { leaseId },
389
+ { timeout: 3e4 }
390
+ );
391
+ }
358
392
  };
359
393
  }
360
394
  });
@@ -532,42 +566,66 @@ function buildMockTree(rootNode) {
532
566
  }
533
567
  return { spans };
534
568
  }
535
- async function processItem(httpClient, serverItem, fn, testRunId, mockStrategy) {
536
- const span = await httpClient.getExternalSpan(serverItem.externalSpanId);
537
- const spanData = span.rawData?.span_data ?? {};
538
- const inputs = deserializeInputs(spanData);
539
- const originalOutput = deserializeOutput(spanData);
540
- let mockTree;
541
- if (mockStrategy === "all" || mockStrategy === "marked") {
542
- const treeResponse = await httpClient.getSpanTree(serverItem.externalSpanId);
543
- mockTree = buildMockTree(treeResponse.root);
544
- }
569
+ async function processItem(httpClient, serverItem, fn, testRunId, mockStrategy, environment) {
570
+ const lease = environment ? serverItem.dbBranchLease : void 0;
571
+ let inputs = [];
572
+ let originalOutput;
545
573
  let result;
546
574
  let error = null;
575
+ const replayedTraceId = crypto.randomUUID();
547
576
  try {
577
+ const span = await httpClient.getExternalSpan(serverItem.externalSpanId);
578
+ const spanData = span.rawData?.span_data ?? {};
579
+ inputs = deserializeInputs(spanData);
580
+ originalOutput = deserializeOutput(spanData);
581
+ let mockTree;
582
+ if (mockStrategy === "all" || mockStrategy === "marked") {
583
+ const treeResponse = await httpClient.getSpanTree(
584
+ serverItem.externalSpanId
585
+ );
586
+ mockTree = buildMockTree(treeResponse.root);
587
+ }
548
588
  const maybePromise = runWithReplayContext(
549
589
  {
550
590
  testRunId,
591
+ traceId: replayedTraceId,
551
592
  inputSourceSpanId: span.id,
552
593
  inputSourceTraceId: span.externalTraceId,
594
+ sourceBitfabTraceId: serverItem.traceId,
553
595
  mockTree,
554
596
  callCounters: mockTree ? /* @__PURE__ */ new Map() : void 0,
555
- mockStrategy
597
+ mockStrategy,
598
+ dbBranchLease: lease
556
599
  },
557
600
  () => fn(...inputs)
558
601
  );
559
602
  result = maybePromise instanceof Promise ? await maybePromise : maybePromise;
560
603
  } catch (e) {
561
604
  error = e instanceof Error ? e.message : String(e);
605
+ } finally {
606
+ if (lease) {
607
+ try {
608
+ await httpClient.releaseDbBranchLease(lease.leaseId);
609
+ } catch (e) {
610
+ try {
611
+ console.warn(
612
+ `Bitfab: failed to release DB branch lease ${lease.leaseId} (TTL janitor will catch it): ${e instanceof Error ? e.message : String(e)}`
613
+ );
614
+ } catch {
615
+ }
616
+ }
617
+ }
562
618
  }
563
619
  return {
620
+ traceId: replayedTraceId,
564
621
  input: inputs,
565
622
  result,
566
623
  originalOutput,
567
624
  error,
568
625
  durationMs: serverItem.durationMs ?? null,
569
626
  tokens: serverItem.tokens ?? null,
570
- model: serverItem.model ?? null
627
+ model: serverItem.model ?? null,
628
+ dbSnapshotRef: serverItem.dbSnapshotRef ?? null
571
629
  };
572
630
  }
573
631
  async function mapWithConcurrency(tasks, maxConcurrency) {
@@ -597,23 +655,39 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options) {
597
655
  options?.limit ?? 5,
598
656
  options?.traceIds,
599
657
  options?.codeChangeDescription,
600
- options?.codeChangeFiles
658
+ options?.codeChangeFiles,
659
+ options?.environment !== void 0
660
+ // includeDbBranchLease
601
661
  );
602
662
  const mockStrategy = options?.mock ?? "none";
603
663
  const maxConcurrency = options?.maxConcurrency ?? 10;
604
664
  const tasks = serverItems.map(
605
- (serverItem) => () => processItem(httpClient, serverItem, fn, testRunId, mockStrategy)
665
+ (serverItem) => () => processItem(
666
+ httpClient,
667
+ serverItem,
668
+ fn,
669
+ testRunId,
670
+ mockStrategy,
671
+ options?.environment
672
+ )
606
673
  );
607
674
  const resultItems = await mapWithConcurrency(tasks, maxConcurrency);
608
675
  await flushTraces();
676
+ let serverTraceIds = {};
609
677
  try {
610
- await httpClient.completeReplay(testRunId);
678
+ const completeResult = await httpClient.completeReplay(testRunId);
679
+ serverTraceIds = completeResult.traceIds ?? {};
611
680
  } catch (e) {
612
681
  try {
613
682
  console.error("Bitfab: Failed to complete replay:", e);
614
683
  } catch {
615
684
  }
616
685
  }
686
+ for (const item of resultItems) {
687
+ if (item.traceId) {
688
+ item.traceId = serverTraceIds[item.traceId] ?? null;
689
+ }
690
+ }
617
691
  return {
618
692
  items: resultItems,
619
693
  testRunId,
@@ -639,6 +713,8 @@ __export(index_exports, {
639
713
  BitfabLangGraphCallbackHandler: () => BitfabLangGraphCallbackHandler,
640
714
  BitfabOpenAITracingProcessor: () => BitfabOpenAITracingProcessor,
641
715
  DEFAULT_SERVICE_URL: () => DEFAULT_SERVICE_URL,
716
+ ReplayEnvironment: () => ReplayEnvironment,
717
+ SUPPORTED_PROVIDERS: () => SUPPORTED_PROVIDERS,
642
718
  __version__: () => __version__,
643
719
  flushTraces: () => flushTraces,
644
720
  getCurrentSpan: () => getCurrentSpan,
@@ -1415,6 +1491,34 @@ async function runFunctionWithBaml(bamlSource, inputs, providers, envVars) {
1415
1491
 
1416
1492
  // src/client.ts
1417
1493
  init_constants();
1494
+
1495
+ // src/dbSnapshot.ts
1496
+ init_errors();
1497
+ var SUPPORTED_PROVIDERS = [
1498
+ "neon",
1499
+ "ardent",
1500
+ "dolt",
1501
+ "gfs",
1502
+ "mongodb-atlas",
1503
+ "dynamodb",
1504
+ "snowflake",
1505
+ "bigquery"
1506
+ ];
1507
+ function validateDbSnapshotConfig(config) {
1508
+ if (!SUPPORTED_PROVIDERS.includes(config.provider)) {
1509
+ throw new BitfabError(
1510
+ `dbSnapshot.provider "${config.provider}" is not supported. Supported providers: ${SUPPORTED_PROVIDERS.join(", ")}.`
1511
+ );
1512
+ }
1513
+ }
1514
+ function buildSnapshotRef(config, sdkWallClockBeforeFn) {
1515
+ return {
1516
+ provider: config.provider,
1517
+ sdkWallClockBeforeFn
1518
+ };
1519
+ }
1520
+
1521
+ // src/client.ts
1418
1522
  init_http();
1419
1523
 
1420
1524
  // src/langgraph.ts
@@ -1895,6 +1999,84 @@ var BitfabLangGraphCallbackHandler = class {
1895
1999
 
1896
2000
  // src/client.ts
1897
2001
  init_replayContext();
2002
+
2003
+ // src/replayEnvironment.ts
2004
+ init_replayContext();
2005
+ var ReplayEnvironment = class {
2006
+ /**
2007
+ * The per-trace branch URL for the item currently being replayed.
2008
+ * Throws if read outside a replay item.
2009
+ */
2010
+ get databaseUrl() {
2011
+ return this.require().databaseUrl;
2012
+ }
2013
+ /** When the per-trace branch URL stops being valid. ISO-8601. */
2014
+ get expiresAt() {
2015
+ return this.require().expiresAt;
2016
+ }
2017
+ /** Deep link to the branch in the provider console, if available. */
2018
+ get providerConsoleUrl() {
2019
+ return this.require().providerConsoleUrl;
2020
+ }
2021
+ /**
2022
+ * True if the branch is read-only. Customer code can use this to skip
2023
+ * write operations during replay when the provider returned a read-only
2024
+ * lease.
2025
+ */
2026
+ get readOnly() {
2027
+ return this.require().readOnly;
2028
+ }
2029
+ /** The historical trace ID that produced the input for this replay item. */
2030
+ get traceId() {
2031
+ return this.require().traceId;
2032
+ }
2033
+ /**
2034
+ * How the resolver pinned this branch.
2035
+ * - "timestamp": snapshot at SDK wall clock; bounded by replication lag.
2036
+ * - "lsn": customer LSN mapped to a replica snapshot (future).
2037
+ */
2038
+ get precision() {
2039
+ return this.require().precision;
2040
+ }
2041
+ /** True when read inside a replay item that has a resolved branch. */
2042
+ get active() {
2043
+ return this.read() !== null;
2044
+ }
2045
+ /** Non-throwing variant for callers that handle the inactive case. */
2046
+ snapshot() {
2047
+ return this.read();
2048
+ }
2049
+ read() {
2050
+ const ctx = getReplayContext();
2051
+ if (!ctx?.dbBranchLease) {
2052
+ return null;
2053
+ }
2054
+ const traceId = ctx.sourceBitfabTraceId ?? ctx.inputSourceTraceId;
2055
+ if (!traceId) {
2056
+ return null;
2057
+ }
2058
+ const lease = ctx.dbBranchLease;
2059
+ return {
2060
+ databaseUrl: lease.databaseUrl,
2061
+ expiresAt: lease.expiresAt,
2062
+ providerConsoleUrl: lease.providerConsoleUrl,
2063
+ readOnly: lease.readOnly,
2064
+ traceId,
2065
+ precision: lease.precision
2066
+ };
2067
+ }
2068
+ require() {
2069
+ const snapshot = this.read();
2070
+ if (!snapshot) {
2071
+ throw new Error(
2072
+ "ReplayEnvironment accessed outside of a replay item. Pass it to bitfab.replay({ environment }) and only read it inside the replayed function."
2073
+ );
2074
+ }
2075
+ return snapshot;
2076
+ }
2077
+ };
2078
+
2079
+ // src/client.ts
1898
2080
  init_serialize();
1899
2081
 
1900
2082
  // src/tracing.ts
@@ -2390,6 +2572,10 @@ var Bitfab = class {
2390
2572
  this.enabled = enabled;
2391
2573
  }
2392
2574
  this.bamlClient = config.bamlClient ?? null;
2575
+ if (config.dbSnapshot) {
2576
+ validateDbSnapshotConfig(config.dbSnapshot);
2577
+ }
2578
+ this.dbSnapshot = config.dbSnapshot;
2393
2579
  this.httpClient = new HttpClient({
2394
2580
  apiKey: this.apiKey,
2395
2581
  serviceUrl: this.serviceUrl,
@@ -2686,7 +2872,8 @@ var Bitfab = class {
2686
2872
  }
2687
2873
  const currentStack = getSpanStack();
2688
2874
  const parentContext = currentStack[currentStack.length - 1];
2689
- const traceId = parentContext?.traceId ?? crypto.randomUUID();
2875
+ const replayCtxForTraceId = parentContext ? null : getReplayContext();
2876
+ const traceId = parentContext?.traceId ?? replayCtxForTraceId?.traceId ?? crypto.randomUUID();
2690
2877
  const spanId = crypto.randomUUID();
2691
2878
  const parentSpanId = parentContext?.spanId ?? null;
2692
2879
  const isRootSpan = parentSpanId === null;
@@ -2696,6 +2883,7 @@ var Bitfab = class {
2696
2883
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
2697
2884
  if (isRootSpan && !activeTraceStates.has(traceId)) {
2698
2885
  const replayCtxAtRoot = getReplayContext();
2886
+ const dbSnapshotRef = self.dbSnapshot ? buildSnapshotRef(self.dbSnapshot, startedAt) : void 0;
2699
2887
  activeTraceStates.set(traceId, {
2700
2888
  traceId,
2701
2889
  startedAt,
@@ -2705,7 +2893,8 @@ var Bitfab = class {
2705
2893
  },
2706
2894
  ...replayCtxAtRoot?.inputSourceTraceId && {
2707
2895
  inputSourceTraceId: replayCtxAtRoot.inputSourceTraceId
2708
- }
2896
+ },
2897
+ ...dbSnapshotRef && { dbSnapshotRef }
2709
2898
  });
2710
2899
  pendingSpanPromises.set(traceId, []);
2711
2900
  }
@@ -2725,6 +2914,7 @@ var Bitfab = class {
2725
2914
  try {
2726
2915
  const endedAt = (/* @__PURE__ */ new Date()).toISOString();
2727
2916
  const replayCtx = getReplayContext();
2917
+ const dbSnapshotRefForSpan = isRootSpan ? activeTraceStates.get(traceId)?.dbSnapshotRef : void 0;
2728
2918
  const spanPromise = self.sendWrapperSpan({
2729
2919
  ...baseSpanParams,
2730
2920
  ...params,
@@ -2734,6 +2924,9 @@ var Bitfab = class {
2734
2924
  ...replayCtx?.testRunId && { testRunId: replayCtx.testRunId },
2735
2925
  ...replayCtx?.inputSourceSpanId && {
2736
2926
  inputSourceSpanId: replayCtx.inputSourceSpanId
2927
+ },
2928
+ ...dbSnapshotRefForSpan && {
2929
+ dbSnapshotRef: dbSnapshotRefForSpan
2737
2930
  }
2738
2931
  });
2739
2932
  if (isRootSpan) {
@@ -2754,7 +2947,8 @@ var Bitfab = class {
2754
2947
  metadata: traceState?.metadata,
2755
2948
  contexts: traceState?.contexts ?? [],
2756
2949
  testRunId: traceState?.testRunId,
2757
- inputSourceTraceId: traceState?.inputSourceTraceId
2950
+ inputSourceTraceId: traceState?.inputSourceTraceId,
2951
+ dbSnapshotRef: traceState?.dbSnapshotRef
2758
2952
  });
2759
2953
  activeTraceStates.delete(traceId);
2760
2954
  } else {
@@ -2915,6 +3109,9 @@ var Bitfab = class {
2915
3109
  if (params.inputSourceTraceId) {
2916
3110
  rawTrace.input_source_trace_id = params.inputSourceTraceId;
2917
3111
  }
3112
+ if (params.dbSnapshotRef) {
3113
+ rawTrace.db_snapshot_ref = params.dbSnapshotRef;
3114
+ }
2918
3115
  this.httpClient.sendExternalTrace({
2919
3116
  type: "sdk-function",
2920
3117
  source: "typescript-sdk-function",
@@ -2957,7 +3154,10 @@ var Bitfab = class {
2957
3154
  ...params.contexts && params.contexts.length > 0 && {
2958
3155
  contexts: params.contexts
2959
3156
  },
2960
- ...params.prompt !== void 0 && { prompt: params.prompt }
3157
+ ...params.prompt !== void 0 && { prompt: params.prompt },
3158
+ ...params.dbSnapshotRef && {
3159
+ db_snapshot_ref: params.dbSnapshotRef
3160
+ }
2961
3161
  }
2962
3162
  };
2963
3163
  if (params.parentSpanId) {
@@ -3000,6 +3200,12 @@ var Bitfab = class {
3000
3200
  );
3001
3201
  }
3002
3202
  };
3203
+ /**
3204
+ * Per-trace environment for `replay({ environment })`. Construct one,
3205
+ * pass it to replay, and read `env.databaseUrl` inside the replayed
3206
+ * function to pick up the per-trace branch URL.
3207
+ */
3208
+ Bitfab.ReplayEnvironment = ReplayEnvironment;
3003
3209
  var BitfabFunction = class {
3004
3210
  constructor(client, traceFunctionKey) {
3005
3211
  this.client = client;
@@ -3061,6 +3267,8 @@ init_http();
3061
3267
  BitfabLangGraphCallbackHandler,
3062
3268
  BitfabOpenAITracingProcessor,
3063
3269
  DEFAULT_SERVICE_URL,
3270
+ ReplayEnvironment,
3271
+ SUPPORTED_PROVIDERS,
3064
3272
  __version__,
3065
3273
  flushTraces,
3066
3274
  getCurrentSpan,