bitfab 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
@@ -90,7 +90,7 @@ var __version__;
90
90
  var init_version_generated = __esm({
91
91
  "src/version.generated.ts"() {
92
92
  "use strict";
93
- __version__ = "0.13.3";
93
+ __version__ = "0.13.5";
94
94
  }
95
95
  });
96
96
 
@@ -104,6 +104,21 @@ var init_constants = __esm({
104
104
  }
105
105
  });
106
106
 
107
+ // src/errors.ts
108
+ var BitfabError;
109
+ var init_errors = __esm({
110
+ "src/errors.ts"() {
111
+ "use strict";
112
+ BitfabError = class extends Error {
113
+ constructor(message, url) {
114
+ super(message);
115
+ this.url = url;
116
+ this.name = "BitfabError";
117
+ }
118
+ };
119
+ }
120
+ });
121
+
107
122
  // src/http.ts
108
123
  function awaitOnExit(promise) {
109
124
  pendingTracePromises.add(promise);
@@ -122,18 +137,12 @@ async function flushTraces(timeoutMs = 5e3) {
122
137
  new Promise((resolve) => setTimeout(resolve, timeoutMs))
123
138
  ]);
124
139
  }
125
- var BitfabError, pendingTracePromises, HttpClient;
140
+ var pendingTracePromises, HttpClient;
126
141
  var init_http = __esm({
127
142
  "src/http.ts"() {
128
143
  "use strict";
129
144
  init_constants();
130
- BitfabError = class extends Error {
131
- constructor(message, url) {
132
- super(message);
133
- this.url = url;
134
- this.name = "BitfabError";
135
- }
136
- };
145
+ init_errors();
137
146
  pendingTracePromises = /* @__PURE__ */ new Set();
138
147
  if (typeof process !== "undefined" && process.versions != null && process.versions.node != null) {
139
148
  let isFlushing = false;
@@ -312,7 +321,7 @@ var init_http = __esm({
312
321
  * Start a replay session by fetching historical traces.
313
322
  * Blocking call — creates a test run and returns lightweight item references.
314
323
  */
315
- async startReplay(traceFunctionKey, limit, traceIds, codeChangeDescription, codeChangeFiles) {
324
+ async startReplay(traceFunctionKey, limit, traceIds, codeChangeDescription, codeChangeFiles, includeDbBranchLease) {
316
325
  const payload = { traceFunctionKey, limit };
317
326
  if (traceIds) {
318
327
  payload.traceIds = traceIds;
@@ -323,8 +332,12 @@ var init_http = __esm({
323
332
  if (codeChangeFiles !== void 0) {
324
333
  payload.codeChangeFiles = codeChangeFiles;
325
334
  }
335
+ if (includeDbBranchLease) {
336
+ payload.includeDbBranchLease = true;
337
+ }
338
+ const timeout = includeDbBranchLease ? 18e4 : 3e4;
326
339
  return this.request("/api/sdk/replay/start", payload, {
327
- timeout: 3e4
340
+ timeout
328
341
  });
329
342
  }
330
343
  /**
@@ -410,6 +423,27 @@ var init_http = __esm({
410
423
  { timeout: 3e4 }
411
424
  );
412
425
  }
426
+ /**
427
+ * Ask the server to materialize a per-trace DB branch lease from a
428
+ * captured `dbSnapshotRef`. Blocking — the resolver creates a Neon
429
+ * snapshot + preview branch and polls operations to readiness, which
430
+ * can take seconds.
431
+ */
432
+ async resolveDbBranchLease(testRunId, traceId, dbSnapshotRef) {
433
+ return this.request(
434
+ "/api/sdk/replay/resolveDbBranchLease",
435
+ { testRunId, traceId, dbSnapshotRef },
436
+ { timeout: 9e4 }
437
+ );
438
+ }
439
+ /** Release a previously-resolved DB branch lease. Idempotent server-side. */
440
+ async releaseDbBranchLease(leaseId) {
441
+ await this.request(
442
+ "/api/sdk/replay/releaseDbBranchLease",
443
+ { leaseId },
444
+ { timeout: 3e4 }
445
+ );
446
+ }
413
447
  };
414
448
  }
415
449
  });
@@ -548,42 +582,66 @@ function buildMockTree(rootNode) {
548
582
  }
549
583
  return { spans };
550
584
  }
551
- async function processItem(httpClient, serverItem, fn, testRunId, mockStrategy) {
552
- const span = await httpClient.getExternalSpan(serverItem.externalSpanId);
553
- const spanData = span.rawData?.span_data ?? {};
554
- const inputs = deserializeInputs(spanData);
555
- const originalOutput = deserializeOutput(spanData);
556
- let mockTree;
557
- if (mockStrategy === "all" || mockStrategy === "marked") {
558
- const treeResponse = await httpClient.getSpanTree(serverItem.externalSpanId);
559
- mockTree = buildMockTree(treeResponse.root);
560
- }
585
+ async function processItem(httpClient, serverItem, fn, testRunId, mockStrategy, environment) {
586
+ const lease = environment ? serverItem.dbBranchLease : void 0;
587
+ let inputs = [];
588
+ let originalOutput;
561
589
  let result;
562
590
  let error = null;
591
+ const replayedTraceId = crypto.randomUUID();
563
592
  try {
593
+ const span = await httpClient.getExternalSpan(serverItem.externalSpanId);
594
+ const spanData = span.rawData?.span_data ?? {};
595
+ inputs = deserializeInputs(spanData);
596
+ originalOutput = deserializeOutput(spanData);
597
+ let mockTree;
598
+ if (mockStrategy === "all" || mockStrategy === "marked") {
599
+ const treeResponse = await httpClient.getSpanTree(
600
+ serverItem.externalSpanId
601
+ );
602
+ mockTree = buildMockTree(treeResponse.root);
603
+ }
564
604
  const maybePromise = runWithReplayContext(
565
605
  {
566
606
  testRunId,
607
+ traceId: replayedTraceId,
567
608
  inputSourceSpanId: span.id,
568
609
  inputSourceTraceId: span.externalTraceId,
610
+ sourceBitfabTraceId: serverItem.traceId,
569
611
  mockTree,
570
612
  callCounters: mockTree ? /* @__PURE__ */ new Map() : void 0,
571
- mockStrategy
613
+ mockStrategy,
614
+ dbBranchLease: lease
572
615
  },
573
616
  () => fn(...inputs)
574
617
  );
575
618
  result = maybePromise instanceof Promise ? await maybePromise : maybePromise;
576
619
  } catch (e) {
577
620
  error = e instanceof Error ? e.message : String(e);
621
+ } finally {
622
+ if (lease) {
623
+ try {
624
+ await httpClient.releaseDbBranchLease(lease.leaseId);
625
+ } catch (e) {
626
+ try {
627
+ console.warn(
628
+ `Bitfab: failed to release DB branch lease ${lease.leaseId} (TTL janitor will catch it): ${e instanceof Error ? e.message : String(e)}`
629
+ );
630
+ } catch {
631
+ }
632
+ }
633
+ }
578
634
  }
579
635
  return {
636
+ traceId: replayedTraceId,
580
637
  input: inputs,
581
638
  result,
582
639
  originalOutput,
583
640
  error,
584
641
  durationMs: serverItem.durationMs ?? null,
585
642
  tokens: serverItem.tokens ?? null,
586
- model: serverItem.model ?? null
643
+ model: serverItem.model ?? null,
644
+ dbSnapshotRef: serverItem.dbSnapshotRef ?? null
587
645
  };
588
646
  }
589
647
  async function mapWithConcurrency(tasks, maxConcurrency) {
@@ -613,23 +671,39 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options) {
613
671
  options?.limit ?? 5,
614
672
  options?.traceIds,
615
673
  options?.codeChangeDescription,
616
- options?.codeChangeFiles
674
+ options?.codeChangeFiles,
675
+ options?.environment !== void 0
676
+ // includeDbBranchLease
617
677
  );
618
678
  const mockStrategy = options?.mock ?? "none";
619
679
  const maxConcurrency = options?.maxConcurrency ?? 10;
620
680
  const tasks = serverItems.map(
621
- (serverItem) => () => processItem(httpClient, serverItem, fn, testRunId, mockStrategy)
681
+ (serverItem) => () => processItem(
682
+ httpClient,
683
+ serverItem,
684
+ fn,
685
+ testRunId,
686
+ mockStrategy,
687
+ options?.environment
688
+ )
622
689
  );
623
690
  const resultItems = await mapWithConcurrency(tasks, maxConcurrency);
624
691
  await flushTraces();
692
+ let serverTraceIds = {};
625
693
  try {
626
- await httpClient.completeReplay(testRunId);
694
+ const completeResult = await httpClient.completeReplay(testRunId);
695
+ serverTraceIds = completeResult.traceIds ?? {};
627
696
  } catch (e) {
628
697
  try {
629
698
  console.error("Bitfab: Failed to complete replay:", e);
630
699
  } catch {
631
700
  }
632
701
  }
702
+ for (const item of resultItems) {
703
+ if (item.traceId) {
704
+ item.traceId = serverTraceIds[item.traceId] ?? null;
705
+ }
706
+ }
633
707
  return {
634
708
  items: resultItems,
635
709
  testRunId,
@@ -655,6 +729,8 @@ __export(node_exports, {
655
729
  BitfabLangGraphCallbackHandler: () => BitfabLangGraphCallbackHandler,
656
730
  BitfabOpenAITracingProcessor: () => BitfabOpenAITracingProcessor,
657
731
  DEFAULT_SERVICE_URL: () => DEFAULT_SERVICE_URL,
732
+ ReplayEnvironment: () => ReplayEnvironment,
733
+ SUPPORTED_PROVIDERS: () => SUPPORTED_PROVIDERS,
658
734
  __version__: () => __version__,
659
735
  flushTraces: () => flushTraces,
660
736
  getCurrentSpan: () => getCurrentSpan,
@@ -1438,6 +1514,34 @@ async function runFunctionWithBaml(bamlSource, inputs, providers, envVars) {
1438
1514
 
1439
1515
  // src/client.ts
1440
1516
  init_constants();
1517
+
1518
+ // src/dbSnapshot.ts
1519
+ init_errors();
1520
+ var SUPPORTED_PROVIDERS = [
1521
+ "neon",
1522
+ "ardent",
1523
+ "dolt",
1524
+ "gfs",
1525
+ "mongodb-atlas",
1526
+ "dynamodb",
1527
+ "snowflake",
1528
+ "bigquery"
1529
+ ];
1530
+ function validateDbSnapshotConfig(config) {
1531
+ if (!SUPPORTED_PROVIDERS.includes(config.provider)) {
1532
+ throw new BitfabError(
1533
+ `dbSnapshot.provider "${config.provider}" is not supported. Supported providers: ${SUPPORTED_PROVIDERS.join(", ")}.`
1534
+ );
1535
+ }
1536
+ }
1537
+ function buildSnapshotRef(config, sdkWallClockBeforeFn) {
1538
+ return {
1539
+ provider: config.provider,
1540
+ sdkWallClockBeforeFn
1541
+ };
1542
+ }
1543
+
1544
+ // src/client.ts
1441
1545
  init_http();
1442
1546
 
1443
1547
  // src/langgraph.ts
@@ -1918,6 +2022,84 @@ var BitfabLangGraphCallbackHandler = class {
1918
2022
 
1919
2023
  // src/client.ts
1920
2024
  init_replayContext();
2025
+
2026
+ // src/replayEnvironment.ts
2027
+ init_replayContext();
2028
+ var ReplayEnvironment = class {
2029
+ /**
2030
+ * The per-trace branch URL for the item currently being replayed.
2031
+ * Throws if read outside a replay item.
2032
+ */
2033
+ get databaseUrl() {
2034
+ return this.require().databaseUrl;
2035
+ }
2036
+ /** When the per-trace branch URL stops being valid. ISO-8601. */
2037
+ get expiresAt() {
2038
+ return this.require().expiresAt;
2039
+ }
2040
+ /** Deep link to the branch in the provider console, if available. */
2041
+ get providerConsoleUrl() {
2042
+ return this.require().providerConsoleUrl;
2043
+ }
2044
+ /**
2045
+ * True if the branch is read-only. Customer code can use this to skip
2046
+ * write operations during replay when the provider returned a read-only
2047
+ * lease.
2048
+ */
2049
+ get readOnly() {
2050
+ return this.require().readOnly;
2051
+ }
2052
+ /** The historical trace ID that produced the input for this replay item. */
2053
+ get traceId() {
2054
+ return this.require().traceId;
2055
+ }
2056
+ /**
2057
+ * How the resolver pinned this branch.
2058
+ * - "timestamp": snapshot at SDK wall clock; bounded by replication lag.
2059
+ * - "lsn": customer LSN mapped to a replica snapshot (future).
2060
+ */
2061
+ get precision() {
2062
+ return this.require().precision;
2063
+ }
2064
+ /** True when read inside a replay item that has a resolved branch. */
2065
+ get active() {
2066
+ return this.read() !== null;
2067
+ }
2068
+ /** Non-throwing variant for callers that handle the inactive case. */
2069
+ snapshot() {
2070
+ return this.read();
2071
+ }
2072
+ read() {
2073
+ const ctx = getReplayContext();
2074
+ if (!ctx?.dbBranchLease) {
2075
+ return null;
2076
+ }
2077
+ const traceId = ctx.sourceBitfabTraceId ?? ctx.inputSourceTraceId;
2078
+ if (!traceId) {
2079
+ return null;
2080
+ }
2081
+ const lease = ctx.dbBranchLease;
2082
+ return {
2083
+ databaseUrl: lease.databaseUrl,
2084
+ expiresAt: lease.expiresAt,
2085
+ providerConsoleUrl: lease.providerConsoleUrl,
2086
+ readOnly: lease.readOnly,
2087
+ traceId,
2088
+ precision: lease.precision
2089
+ };
2090
+ }
2091
+ require() {
2092
+ const snapshot = this.read();
2093
+ if (!snapshot) {
2094
+ throw new Error(
2095
+ "ReplayEnvironment accessed outside of a replay item. Pass it to bitfab.replay({ environment }) and only read it inside the replayed function."
2096
+ );
2097
+ }
2098
+ return snapshot;
2099
+ }
2100
+ };
2101
+
2102
+ // src/client.ts
1921
2103
  init_serialize();
1922
2104
 
1923
2105
  // src/tracing.ts
@@ -2413,6 +2595,10 @@ var Bitfab = class {
2413
2595
  this.enabled = enabled;
2414
2596
  }
2415
2597
  this.bamlClient = config.bamlClient ?? null;
2598
+ if (config.dbSnapshot) {
2599
+ validateDbSnapshotConfig(config.dbSnapshot);
2600
+ }
2601
+ this.dbSnapshot = config.dbSnapshot;
2416
2602
  this.httpClient = new HttpClient({
2417
2603
  apiKey: this.apiKey,
2418
2604
  serviceUrl: this.serviceUrl,
@@ -2709,7 +2895,8 @@ var Bitfab = class {
2709
2895
  }
2710
2896
  const currentStack = getSpanStack();
2711
2897
  const parentContext = currentStack[currentStack.length - 1];
2712
- const traceId = parentContext?.traceId ?? crypto.randomUUID();
2898
+ const replayCtxForTraceId = parentContext ? null : getReplayContext();
2899
+ const traceId = parentContext?.traceId ?? replayCtxForTraceId?.traceId ?? crypto.randomUUID();
2713
2900
  const spanId = crypto.randomUUID();
2714
2901
  const parentSpanId = parentContext?.spanId ?? null;
2715
2902
  const isRootSpan = parentSpanId === null;
@@ -2719,6 +2906,7 @@ var Bitfab = class {
2719
2906
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
2720
2907
  if (isRootSpan && !activeTraceStates.has(traceId)) {
2721
2908
  const replayCtxAtRoot = getReplayContext();
2909
+ const dbSnapshotRef = self.dbSnapshot ? buildSnapshotRef(self.dbSnapshot, startedAt) : void 0;
2722
2910
  activeTraceStates.set(traceId, {
2723
2911
  traceId,
2724
2912
  startedAt,
@@ -2728,7 +2916,8 @@ var Bitfab = class {
2728
2916
  },
2729
2917
  ...replayCtxAtRoot?.inputSourceTraceId && {
2730
2918
  inputSourceTraceId: replayCtxAtRoot.inputSourceTraceId
2731
- }
2919
+ },
2920
+ ...dbSnapshotRef && { dbSnapshotRef }
2732
2921
  });
2733
2922
  pendingSpanPromises.set(traceId, []);
2734
2923
  }
@@ -2748,6 +2937,7 @@ var Bitfab = class {
2748
2937
  try {
2749
2938
  const endedAt = (/* @__PURE__ */ new Date()).toISOString();
2750
2939
  const replayCtx = getReplayContext();
2940
+ const dbSnapshotRefForSpan = isRootSpan ? activeTraceStates.get(traceId)?.dbSnapshotRef : void 0;
2751
2941
  const spanPromise = self.sendWrapperSpan({
2752
2942
  ...baseSpanParams,
2753
2943
  ...params,
@@ -2757,6 +2947,9 @@ var Bitfab = class {
2757
2947
  ...replayCtx?.testRunId && { testRunId: replayCtx.testRunId },
2758
2948
  ...replayCtx?.inputSourceSpanId && {
2759
2949
  inputSourceSpanId: replayCtx.inputSourceSpanId
2950
+ },
2951
+ ...dbSnapshotRefForSpan && {
2952
+ dbSnapshotRef: dbSnapshotRefForSpan
2760
2953
  }
2761
2954
  });
2762
2955
  if (isRootSpan) {
@@ -2777,7 +2970,8 @@ var Bitfab = class {
2777
2970
  metadata: traceState?.metadata,
2778
2971
  contexts: traceState?.contexts ?? [],
2779
2972
  testRunId: traceState?.testRunId,
2780
- inputSourceTraceId: traceState?.inputSourceTraceId
2973
+ inputSourceTraceId: traceState?.inputSourceTraceId,
2974
+ dbSnapshotRef: traceState?.dbSnapshotRef
2781
2975
  });
2782
2976
  activeTraceStates.delete(traceId);
2783
2977
  } else {
@@ -2938,6 +3132,9 @@ var Bitfab = class {
2938
3132
  if (params.inputSourceTraceId) {
2939
3133
  rawTrace.input_source_trace_id = params.inputSourceTraceId;
2940
3134
  }
3135
+ if (params.dbSnapshotRef) {
3136
+ rawTrace.db_snapshot_ref = params.dbSnapshotRef;
3137
+ }
2941
3138
  this.httpClient.sendExternalTrace({
2942
3139
  type: "sdk-function",
2943
3140
  source: "typescript-sdk-function",
@@ -2980,7 +3177,10 @@ var Bitfab = class {
2980
3177
  ...params.contexts && params.contexts.length > 0 && {
2981
3178
  contexts: params.contexts
2982
3179
  },
2983
- ...params.prompt !== void 0 && { prompt: params.prompt }
3180
+ ...params.prompt !== void 0 && { prompt: params.prompt },
3181
+ ...params.dbSnapshotRef && {
3182
+ db_snapshot_ref: params.dbSnapshotRef
3183
+ }
2984
3184
  }
2985
3185
  };
2986
3186
  if (params.parentSpanId) {
@@ -3023,6 +3223,12 @@ var Bitfab = class {
3023
3223
  );
3024
3224
  }
3025
3225
  };
3226
+ /**
3227
+ * Per-trace environment for `replay({ environment })`. Construct one,
3228
+ * pass it to replay, and read `env.databaseUrl` inside the replayed
3229
+ * function to pick up the per-trace branch URL.
3230
+ */
3231
+ Bitfab.ReplayEnvironment = ReplayEnvironment;
3026
3232
  var BitfabFunction = class {
3027
3233
  constructor(client, traceFunctionKey) {
3028
3234
  this.client = client;
@@ -3088,6 +3294,8 @@ assertAsyncStorageRegistered();
3088
3294
  BitfabLangGraphCallbackHandler,
3089
3295
  BitfabOpenAITracingProcessor,
3090
3296
  DEFAULT_SERVICE_URL,
3297
+ ReplayEnvironment,
3298
+ SUPPORTED_PROVIDERS,
3091
3299
  __version__,
3092
3300
  flushTraces,
3093
3301
  getCurrentSpan,