braintrust 0.0.166 → 0.0.168

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/browser.js CHANGED
@@ -30,6 +30,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/browser.ts
31
31
  var browser_exports = {};
32
32
  __export(browser_exports, {
33
+ Attachment: () => Attachment,
33
34
  BraintrustState: () => BraintrustState,
34
35
  BraintrustStream: () => BraintrustStream,
35
36
  Dataset: () => Dataset,
@@ -43,6 +44,7 @@ __export(browser_exports, {
43
44
  ReadonlyExperiment: () => ReadonlyExperiment,
44
45
  SpanImpl: () => SpanImpl,
45
46
  X_CACHED_HEADER: () => X_CACHED_HEADER,
47
+ _exportsForTestingOnly: () => _exportsForTestingOnly,
46
48
  _internalGetGlobalState: () => _internalGetGlobalState,
47
49
  _internalSetInitialState: () => _internalSetInitialState,
48
50
  braintrustStreamChunkSchema: () => braintrustStreamChunkSchema,
@@ -446,6 +448,9 @@ var NoopSpan = class {
446
448
  async export() {
447
449
  return "";
448
450
  }
451
+ async permalink() {
452
+ return "";
453
+ }
449
454
  async flush() {
450
455
  }
451
456
  close(args) {
@@ -587,11 +592,11 @@ var BraintrustState = class _BraintrustState {
587
592
  state.loginReplaceApiConn(state.apiConn());
588
593
  return state;
589
594
  }
590
- setFetch(fetch) {
591
- this.loginParams.fetch = fetch;
592
- this.fetch = fetch;
593
- this._apiConn?.setFetch(fetch);
594
- this._appConn?.setFetch(fetch);
595
+ setFetch(fetch2) {
596
+ this.loginParams.fetch = fetch2;
597
+ this.fetch = fetch2;
598
+ this._apiConn?.setFetch(fetch2);
599
+ this._appConn?.setFetch(fetch2);
595
600
  }
596
601
  async login(loginParams) {
597
602
  if (this.apiUrl && !loginParams.forceLogin) {
@@ -680,15 +685,15 @@ var HTTPConnection = class _HTTPConnection {
680
685
  token;
681
686
  headers;
682
687
  fetch;
683
- constructor(base_url, fetch) {
688
+ constructor(base_url, fetch2) {
684
689
  this.base_url = base_url;
685
690
  this.token = null;
686
691
  this.headers = {};
687
692
  this._reset();
688
- this.fetch = fetch;
693
+ this.fetch = fetch2;
689
694
  }
690
- setFetch(fetch) {
691
- this.fetch = fetch;
695
+ setFetch(fetch2) {
696
+ this.fetch = fetch2;
692
697
  }
693
698
  async ping() {
694
699
  try {
@@ -724,12 +729,14 @@ var HTTPConnection = class _HTTPConnection {
724
729
  ([k, v]) => v !== void 0 ? typeof v === "string" ? [[k, v]] : v.map((x) => [k, x]) : []
725
730
  ) : []
726
731
  ).toString();
732
+ const this_fetch = this.fetch;
733
+ const this_headers = this.headers;
727
734
  return await checkResponse(
728
735
  // Using toString() here makes it work with isomorphic fetch
729
- await this.fetch(url.toString(), {
736
+ await this_fetch(url.toString(), {
730
737
  headers: {
731
738
  Accept: "application/json",
732
- ...this.headers,
739
+ ...this_headers,
733
740
  ...headers
734
741
  },
735
742
  keepalive: true,
@@ -739,13 +746,16 @@ var HTTPConnection = class _HTTPConnection {
739
746
  }
740
747
  async post(path, params, config) {
741
748
  const { headers, ...rest } = config || {};
749
+ const this_fetch = this.fetch;
750
+ const this_base_url = this.base_url;
751
+ const this_headers = this.headers;
742
752
  return await checkResponse(
743
- await this.fetch((0, import_core._urljoin)(this.base_url, path), {
753
+ await this_fetch((0, import_core._urljoin)(this_base_url, path), {
744
754
  method: "POST",
745
755
  headers: {
746
756
  Accept: "application/json",
747
757
  "Content-Type": "application/json",
748
- ...this.headers,
758
+ ...this_headers,
749
759
  ...headers
750
760
  },
751
761
  body: typeof params === "string" ? params : params ? JSON.stringify(params) : void 0,
@@ -778,6 +788,167 @@ var HTTPConnection = class _HTTPConnection {
778
788
  return await resp.json();
779
789
  }
780
790
  };
791
+ var Attachment = class {
792
+ /**
793
+ * The object that replaces this `Attachment` at upload time.
794
+ */
795
+ reference;
796
+ uploader;
797
+ data;
798
+ state;
799
+ // For debug logging only.
800
+ dataDebugString;
801
+ /**
802
+ * Construct an attachment.
803
+ *
804
+ * @param data A string representing the path of the file on disk, or a
805
+ * `Blob`/`ArrayBuffer` with the file's contents. The caller is responsible
806
+ * for ensuring the file/blob/buffer is not modified until upload is complete.
807
+ *
808
+ * @param filename The desired name of the file in Braintrust after uploading.
809
+ * This parameter is for visualization purposes only and has no effect on
810
+ * attachment storage.
811
+ *
812
+ * @param contentType The MIME type of the file.
813
+ *
814
+ * @param state (Optional) For internal use.
815
+ */
816
+ constructor({ data, filename, contentType, state }) {
817
+ this.reference = {
818
+ type: import_typespecs2.BRAINTRUST_ATTACHMENT,
819
+ filename,
820
+ content_type: contentType,
821
+ key: newId()
822
+ };
823
+ this.state = state;
824
+ this.dataDebugString = typeof data === "string" ? data : "<in-memory data>";
825
+ this.data = this.initData(data);
826
+ this.uploader = this.initUploader();
827
+ }
828
+ /**
829
+ * On first access, (1) reads the attachment from disk if needed, (2)
830
+ * authenticates with the data plane to request a signed URL, (3) uploads to
831
+ * object store, and (4) updates the attachment.
832
+ *
833
+ * @returns The attachment status.
834
+ */
835
+ async upload() {
836
+ return await this.uploader.get();
837
+ }
838
+ /**
839
+ * A human-readable description for logging and debugging.
840
+ *
841
+ * @returns The debug object. The return type is not stable and may change in
842
+ * a future release.
843
+ */
844
+ debugInfo() {
845
+ return {
846
+ inputData: this.dataDebugString,
847
+ reference: this.reference,
848
+ state: this.state
849
+ };
850
+ }
851
+ initUploader() {
852
+ const doUpload = async (conn, orgId) => {
853
+ const requestParams = {
854
+ key: this.reference.key,
855
+ filename: this.reference.filename,
856
+ content_type: this.reference.content_type,
857
+ org_id: orgId
858
+ };
859
+ const [metadataPromiseResult, dataPromiseResult] = await Promise.allSettled([
860
+ conn.post("/attachment", requestParams),
861
+ this.data.get()
862
+ ]);
863
+ if (metadataPromiseResult.status === "rejected") {
864
+ const errorStr = JSON.stringify(metadataPromiseResult.reason);
865
+ throw new Error(
866
+ `Failed to request signed URL from API server: ${errorStr}`
867
+ );
868
+ }
869
+ if (dataPromiseResult.status === "rejected") {
870
+ const errorStr = JSON.stringify(dataPromiseResult.reason);
871
+ throw new Error(`Failed to read file: ${errorStr}`);
872
+ }
873
+ const metadataResponse = metadataPromiseResult.value;
874
+ const data = dataPromiseResult.value;
875
+ let signedUrl;
876
+ let headers;
877
+ try {
878
+ ({ signedUrl, headers } = import_zod2.z.object({
879
+ signedUrl: import_zod2.z.string().url(),
880
+ headers: import_zod2.z.record(import_zod2.z.string())
881
+ }).parse(await metadataResponse.json()));
882
+ } catch (error) {
883
+ if (error instanceof import_zod2.ZodError) {
884
+ const errorStr = JSON.stringify(error.flatten());
885
+ throw new Error(`Invalid response from API server: ${errorStr}`);
886
+ }
887
+ throw error;
888
+ }
889
+ let objectStoreResponse;
890
+ try {
891
+ objectStoreResponse = await checkResponse(
892
+ await fetch(signedUrl, {
893
+ method: "PUT",
894
+ headers,
895
+ body: data
896
+ })
897
+ );
898
+ } catch (error) {
899
+ if (error instanceof FailedHTTPResponse) {
900
+ throw new Error(
901
+ `Failed to upload attachment to object store: ${error.status} ${error.text} ${error.data}`
902
+ );
903
+ }
904
+ throw error;
905
+ }
906
+ return { signedUrl, metadataResponse, objectStoreResponse };
907
+ };
908
+ const errorWrapper = async () => {
909
+ const status = { upload_status: "done" };
910
+ const state = this.state ?? _globalState;
911
+ await state.login({});
912
+ const conn = state.apiConn();
913
+ const orgId = state.orgId ?? "";
914
+ try {
915
+ await doUpload(conn, orgId);
916
+ } catch (error) {
917
+ status.upload_status = "error";
918
+ status.error_message = error instanceof Error ? error.message : JSON.stringify(error);
919
+ }
920
+ const requestParams = {
921
+ key: this.reference.key,
922
+ org_id: orgId,
923
+ status
924
+ };
925
+ const statusResponse = await conn.post(
926
+ "/attachment/status",
927
+ requestParams
928
+ );
929
+ if (!statusResponse.ok) {
930
+ const errorStr = JSON.stringify(statusResponse);
931
+ throw new Error(`Couldn't log attachment status: ${errorStr}`);
932
+ }
933
+ return status;
934
+ };
935
+ return new LazyValue(errorWrapper);
936
+ }
937
+ initData(data) {
938
+ if (typeof data === "string") {
939
+ const readFile = isomorph_default.readFile;
940
+ if (!readFile) {
941
+ throw new Error(
942
+ `This platform does not support reading the filesystem. Construct the Attachment
943
+ with a Blob/ArrayBuffer, or run the program on Node.js.`
944
+ );
945
+ }
946
+ return new LazyValue(async () => new Blob([await readFile(data)]));
947
+ } else {
948
+ return new LazyValue(async () => new Blob([data]));
949
+ }
950
+ }
951
+ };
781
952
  function logFeedbackImpl(state, parentObjectType, parentObjectId, {
782
953
  id,
783
954
  expected,
@@ -802,7 +973,7 @@ function logFeedbackImpl(state, parentObjectType, parentObjectId, {
802
973
  expected,
803
974
  tags
804
975
  });
805
- let { metadata, ...updateEvent } = validatedEvent;
976
+ let { metadata, ...updateEvent } = deepCopyEvent(validatedEvent);
806
977
  updateEvent = Object.fromEntries(
807
978
  Object.entries(updateEvent).filter(([_, v]) => !isEmpty(v))
808
979
  );
@@ -851,10 +1022,12 @@ function updateSpanImpl({
851
1022
  id,
852
1023
  event
853
1024
  }) {
854
- const updateEvent = validateAndSanitizeExperimentLogPartialArgs({
855
- id,
856
- ...event
857
- });
1025
+ const updateEvent = deepCopyEvent(
1026
+ validateAndSanitizeExperimentLogPartialArgs({
1027
+ id,
1028
+ ...event
1029
+ })
1030
+ );
858
1031
  const parentIds = async () => new import_core.SpanComponentsV3({
859
1032
  object_type: parentObjectType,
860
1033
  object_id: await parentObjectId.get()
@@ -1055,7 +1228,7 @@ var Logger = class {
1055
1228
  * @param event.id: (Optional) a unique identifier for the event. If you don't provide one, BrainTrust will generate one for you.
1056
1229
  * @param options Additional logging options
1057
1230
  * @param options.allowConcurrentWithSpans in rare cases where you need to log at the top level separately from spans on the logger elsewhere, set this to true.
1058
- * :returns: The `id` of the logged event.
1231
+ * @returns The `id` of the logged event.
1059
1232
  */
1060
1233
  log(event, options) {
1061
1234
  if (this.calledStartSpan && !options?.allowConcurrentWithSpans) {
@@ -1078,7 +1251,7 @@ var Logger = class {
1078
1251
  /**
1079
1252
  * Create a new toplevel span underneath the logger. The name defaults to "root".
1080
1253
  *
1081
- * See `Span.traced` for full details.
1254
+ * See {@link Span.traced} for full details.
1082
1255
  */
1083
1256
  traced(callback, args) {
1084
1257
  const { setCurrent, ...argsRest } = args ?? {};
@@ -1112,7 +1285,7 @@ var Logger = class {
1112
1285
  * where you cannot use callbacks. However, spans started with `startSpan` will not be marked as the "current span",
1113
1286
  * so `currentSpan()` and `traced()` will be no-ops. If you want to mark a span as current, use `traced` instead.
1114
1287
  *
1115
- * See `traced` for full details.
1288
+ * See {@link traced} for full details.
1116
1289
  */
1117
1290
  startSpan(args) {
1118
1291
  this.calledStartSpan = true;
@@ -1152,7 +1325,7 @@ var Logger = class {
1152
1325
  * Update a span in the experiment using its id. It is important that you only update a span once the original span has been fully written and flushed,
1153
1326
  * since otherwise updates to the span may conflict with the original span.
1154
1327
  *
1155
- * @param event The event data to update the span with. Must include `id`. See `Experiment.log` for a full list of valid fields.
1328
+ * @param event The event data to update the span with. Must include `id`. See {@link Experiment.log} for a full list of valid fields.
1156
1329
  */
1157
1330
  updateSpan(event) {
1158
1331
  const { id, ...eventRest } = event;
@@ -1168,7 +1341,9 @@ var Logger = class {
1168
1341
  });
1169
1342
  }
1170
1343
  /**
1171
- * Return a serialized representation of the logger that can be used to start subspans in other places. See `Span.start_span` for more details.
1344
+ * Return a serialized representation of the logger that can be used to start subspans in other places.
1345
+ *
1346
+ * See {@link Span.startSpan} for more details.
1172
1347
  */
1173
1348
  async export() {
1174
1349
  return new import_core.SpanComponentsV3({
@@ -1208,6 +1383,7 @@ var BackgroundLogger = class _BackgroundLogger {
1208
1383
  activeFlush = Promise.resolve();
1209
1384
  activeFlushResolved = true;
1210
1385
  activeFlushError = void 0;
1386
+ onFlushError;
1211
1387
  syncFlush = false;
1212
1388
  // 6 MB for the AWS lambda gateway (from our own testing).
1213
1389
  maxRequestSize = 6 * 1024 * 1024;
@@ -1271,6 +1447,7 @@ var BackgroundLogger = class _BackgroundLogger {
1271
1447
  await this.flush();
1272
1448
  });
1273
1449
  }
1450
+ this.onFlushError = opts.onFlushError;
1274
1451
  }
1275
1452
  log(items) {
1276
1453
  const [addedItems, droppedItems] = (() => {
@@ -1302,14 +1479,16 @@ var BackgroundLogger = class _BackgroundLogger {
1302
1479
  if (this.activeFlushError) {
1303
1480
  const err = this.activeFlushError;
1304
1481
  this.activeFlushError = void 0;
1305
- throw err;
1482
+ if (this.syncFlush) {
1483
+ throw err;
1484
+ }
1306
1485
  }
1307
1486
  }
1308
1487
  async flushOnce(args) {
1309
1488
  const batchSize = args?.batchSize ?? this.defaultBatchSize;
1310
1489
  const wrappedItems = this.items;
1311
1490
  this.items = [];
1312
- const allItems = await this.unwrapLazyValues(wrappedItems);
1491
+ const [allItems, attachments] = await this.unwrapLazyValues(wrappedItems);
1313
1492
  if (allItems.length === 0) {
1314
1493
  return;
1315
1494
  }
@@ -1341,6 +1520,23 @@ var BackgroundLogger = class _BackgroundLogger {
1341
1520
  );
1342
1521
  }
1343
1522
  }
1523
+ const attachmentErrors = [];
1524
+ for (const attachment of attachments) {
1525
+ try {
1526
+ const result = await attachment.upload();
1527
+ if (result.upload_status === "error" && result.error_message) {
1528
+ attachmentErrors.push(new Error(result.error_message));
1529
+ }
1530
+ } catch (error) {
1531
+ attachmentErrors.push(error);
1532
+ }
1533
+ }
1534
+ if (attachmentErrors.length > 0) {
1535
+ throw new AggregateError(
1536
+ attachmentErrors,
1537
+ `Encountered the following errors while uploading attachments:`
1538
+ );
1539
+ }
1344
1540
  if (this.items.length > 0) {
1345
1541
  await this.flushOnce(args);
1346
1542
  }
@@ -1348,8 +1544,10 @@ var BackgroundLogger = class _BackgroundLogger {
1348
1544
  async unwrapLazyValues(wrappedItems) {
1349
1545
  for (let i = 0; i < this.numTries; ++i) {
1350
1546
  try {
1351
- const itemPromises = wrappedItems.map((x) => x.get());
1352
- return (0, import_core.mergeRowBatch)(await Promise.all(itemPromises));
1547
+ const items = await Promise.all(wrappedItems.map((x) => x.get()));
1548
+ const attachments = [];
1549
+ items.forEach((item) => extractAttachments(item, attachments));
1550
+ return [(0, import_core.mergeRowBatch)(items), attachments];
1353
1551
  } catch (e) {
1354
1552
  let errmsg = "Encountered error when constructing records to flush";
1355
1553
  const isRetrying = i + 1 < this.numTries;
@@ -1357,7 +1555,10 @@ var BackgroundLogger = class _BackgroundLogger {
1357
1555
  errmsg += ". Retrying";
1358
1556
  }
1359
1557
  console.warn(errmsg);
1360
- if (!isRetrying && this.syncFlush) {
1558
+ if (!isRetrying) {
1559
+ console.warn(
1560
+ `Failed to construct log records to flush after ${this.numTries} attempts. Dropping batch`
1561
+ );
1361
1562
  throw e;
1362
1563
  } else {
1363
1564
  console.warn(e);
@@ -1365,10 +1566,7 @@ var BackgroundLogger = class _BackgroundLogger {
1365
1566
  }
1366
1567
  }
1367
1568
  }
1368
- console.warn(
1369
- `Failed to construct log records to flush after ${this.numTries} attempts. Dropping batch`
1370
- );
1371
- return [];
1569
+ throw new Error("Impossible");
1372
1570
  }
1373
1571
  async submitLogsRequest(items) {
1374
1572
  const conn = await this.apiConn.get();
@@ -1384,16 +1582,14 @@ var BackgroundLogger = class _BackgroundLogger {
1384
1582
  let error = void 0;
1385
1583
  try {
1386
1584
  await conn.post_json("logs3", dataStr);
1387
- } catch (e) {
1585
+ } catch {
1388
1586
  try {
1389
1587
  const legacyDataS = (0, import_core.constructJsonArray)(
1390
- items.map(
1391
- (r) => JSON.stringify((0, import_core.makeLegacyEvent)(JSON.parse(r)))
1392
- )
1588
+ items.map((r) => JSON.stringify((0, import_core.makeLegacyEvent)(JSON.parse(r))))
1393
1589
  );
1394
1590
  await conn.post_json("logs", legacyDataS);
1395
- } catch (e2) {
1396
- error = e2;
1591
+ } catch (e) {
1592
+ error = e;
1397
1593
  }
1398
1594
  }
1399
1595
  if (error === void 0) {
@@ -1417,7 +1613,10 @@ Error: ${errorText}`;
1417
1613
  });
1418
1614
  this.logFailedPayloadsDir();
1419
1615
  }
1420
- if (!isRetrying && this.syncFlush) {
1616
+ if (!isRetrying) {
1617
+ console.warn(
1618
+ `log request failed after ${this.numTries} retries. Dropping batch`
1619
+ );
1421
1620
  throw new Error(errMsg);
1422
1621
  } else {
1423
1622
  console.warn(errMsg);
@@ -1426,10 +1625,6 @@ Error: ${errorText}`;
1426
1625
  }
1427
1626
  }
1428
1627
  }
1429
- console.warn(
1430
- `log request failed after ${this.numTries} retries. Dropping batch`
1431
- );
1432
- return;
1433
1628
  }
1434
1629
  registerDroppedItemCount(numItems) {
1435
1630
  if (numItems <= 0) {
@@ -1457,15 +1652,17 @@ Error: ${errorText}`;
1457
1652
  return;
1458
1653
  }
1459
1654
  try {
1460
- const allItems = await this.unwrapLazyValues(wrappedItems);
1655
+ const [allItems, allAttachments] = await this.unwrapLazyValues(wrappedItems);
1461
1656
  const dataStr = constructLogs3Data(
1462
1657
  allItems.map((x) => JSON.stringify(x))
1463
1658
  );
1659
+ const attachmentStr = JSON.stringify(
1660
+ allAttachments.map((a) => a.debugInfo())
1661
+ );
1662
+ const payload = `{"data": ${dataStr}, "attachments": ${attachmentStr}}
1663
+ `;
1464
1664
  for (const payloadDir of publishPayloadsDir) {
1465
- await _BackgroundLogger.writePayloadToDir({
1466
- payloadDir,
1467
- payload: dataStr
1468
- });
1665
+ await _BackgroundLogger.writePayloadToDir({ payloadDir, payload });
1469
1666
  }
1470
1667
  } catch (e) {
1471
1668
  console.error(e);
@@ -1504,6 +1701,13 @@ Error: ${errorText}`;
1504
1701
  try {
1505
1702
  await this.flushOnce();
1506
1703
  } catch (err) {
1704
+ if (err instanceof AggregateError) {
1705
+ for (const e of err.errors) {
1706
+ this.onFlushError?.(e);
1707
+ }
1708
+ } else {
1709
+ this.onFlushError?.(err);
1710
+ }
1507
1711
  this.activeFlushError = err;
1508
1712
  } finally {
1509
1713
  this.activeFlushResolved = true;
@@ -1546,7 +1750,7 @@ function init(projectOrOptions, optionalOptions) {
1546
1750
  apiKey,
1547
1751
  orgName,
1548
1752
  forceLogin,
1549
- fetch,
1753
+ fetch: fetch2,
1550
1754
  metadata,
1551
1755
  gitMetadataSettings,
1552
1756
  projectId,
@@ -1564,7 +1768,7 @@ function init(projectOrOptions, optionalOptions) {
1564
1768
  }
1565
1769
  const lazyMetadata2 = new LazyValue(
1566
1770
  async () => {
1567
- await state.login({ apiKey, appUrl, orgName, fetch, forceLogin });
1771
+ await state.login({ apiKey, appUrl, orgName, fetch: fetch2, forceLogin });
1568
1772
  const args = {
1569
1773
  project_name: project,
1570
1774
  project_id: projectId,
@@ -1735,7 +1939,7 @@ function initDataset(projectOrOptions, optionalOptions) {
1735
1939
  appUrl,
1736
1940
  apiKey,
1737
1941
  orgName,
1738
- fetch,
1942
+ fetch: fetch2,
1739
1943
  forceLogin,
1740
1944
  projectId,
1741
1945
  metadata,
@@ -1749,7 +1953,7 @@ function initDataset(projectOrOptions, optionalOptions) {
1749
1953
  orgName,
1750
1954
  apiKey,
1751
1955
  appUrl,
1752
- fetch,
1956
+ fetch: fetch2,
1753
1957
  forceLogin
1754
1958
  });
1755
1959
  const args = {
@@ -1831,7 +2035,7 @@ function initLogger(options = {}) {
1831
2035
  apiKey,
1832
2036
  orgName,
1833
2037
  forceLogin,
1834
- fetch,
2038
+ fetch: fetch2,
1835
2039
  state: stateArg
1836
2040
  } = options || {};
1837
2041
  const computeMetadataArgs = {
@@ -1846,7 +2050,7 @@ function initLogger(options = {}) {
1846
2050
  apiKey,
1847
2051
  appUrl,
1848
2052
  forceLogin,
1849
- fetch
2053
+ fetch: fetch2
1850
2054
  });
1851
2055
  return computeLoggerMetadata(state, computeMetadataArgs);
1852
2056
  }
@@ -1870,7 +2074,7 @@ async function loadPrompt({
1870
2074
  appUrl,
1871
2075
  apiKey,
1872
2076
  orgName,
1873
- fetch,
2077
+ fetch: fetch2,
1874
2078
  forceLogin,
1875
2079
  state: stateArg
1876
2080
  }) {
@@ -1885,7 +2089,7 @@ async function loadPrompt({
1885
2089
  orgName,
1886
2090
  apiKey,
1887
2091
  appUrl,
1888
- fetch,
2092
+ fetch: fetch2,
1889
2093
  forceLogin
1890
2094
  });
1891
2095
  const args = {
@@ -1936,7 +2140,7 @@ async function loginToState(options = {}) {
1936
2140
  appUrl = isomorph_default.getEnv("BRAINTRUST_APP_URL") || "https://www.braintrust.dev",
1937
2141
  apiKey = isomorph_default.getEnv("BRAINTRUST_API_KEY"),
1938
2142
  orgName = isomorph_default.getEnv("BRAINTRUST_ORG_NAME"),
1939
- fetch = globalThis.fetch
2143
+ fetch: fetch2 = globalThis.fetch
1940
2144
  } = options || {};
1941
2145
  const appPublicUrl = isomorph_default.getEnv("BRAINTRUST_APP_PUBLIC_URL") || appUrl;
1942
2146
  const state = new BraintrustState(options);
@@ -1946,7 +2150,7 @@ async function loginToState(options = {}) {
1946
2150
  let conn = null;
1947
2151
  if (apiKey !== void 0) {
1948
2152
  const resp = await checkResponse(
1949
- await fetch((0, import_core._urljoin)(state.appUrl, `/api/apikey/login`), {
2153
+ await fetch2((0, import_core._urljoin)(state.appUrl, `/api/apikey/login`), {
1950
2154
  method: "POST",
1951
2155
  headers: {
1952
2156
  "Content-Type": "application/json",
@@ -2114,8 +2318,8 @@ async function flush(options) {
2114
2318
  const state = options?.state ?? _globalState;
2115
2319
  return await state.bgLogger().flush();
2116
2320
  }
2117
- function setFetch(fetch) {
2118
- _globalState.setFetch(fetch);
2321
+ function setFetch(fetch2) {
2322
+ _globalState.setFetch(fetch2);
2119
2323
  }
2120
2324
  function startSpanAndIsLogger(args) {
2121
2325
  const state = args?.state ?? _globalState;
@@ -2243,6 +2447,47 @@ function validateAndSanitizeExperimentLogPartialArgs(event) {
2243
2447
  return { ...event };
2244
2448
  }
2245
2449
  }
2450
+ function deepCopyEvent(event) {
2451
+ const attachments = [];
2452
+ const IDENTIFIER = "_bt_internal_saved_attachment";
2453
+ const savedAttachmentSchema = import_zod2.z.strictObject({ [IDENTIFIER]: import_zod2.z.number() });
2454
+ const serialized = JSON.stringify(event, (_k, v) => {
2455
+ if (v instanceof SpanImpl || v instanceof NoopSpan) {
2456
+ return `<span>`;
2457
+ } else if (v instanceof Experiment) {
2458
+ return `<experiment>`;
2459
+ } else if (v instanceof Dataset) {
2460
+ return `<dataset>`;
2461
+ } else if (v instanceof Logger) {
2462
+ return `<logger>`;
2463
+ } else if (v instanceof Attachment) {
2464
+ const idx = attachments.push(v);
2465
+ return { [IDENTIFIER]: idx - 1 };
2466
+ }
2467
+ return v;
2468
+ });
2469
+ const x = JSON.parse(serialized, (_k, v) => {
2470
+ const parsedAttachment = savedAttachmentSchema.safeParse(v);
2471
+ if (parsedAttachment.success) {
2472
+ return attachments[parsedAttachment.data[IDENTIFIER]];
2473
+ }
2474
+ return v;
2475
+ });
2476
+ return x;
2477
+ }
2478
+ function extractAttachments(event, attachments) {
2479
+ for (const [key, value] of Object.entries(event)) {
2480
+ if (value instanceof Attachment) {
2481
+ attachments.push(value);
2482
+ event[key] = value.reference;
2483
+ continue;
2484
+ }
2485
+ if (!(value instanceof Object)) {
2486
+ continue;
2487
+ }
2488
+ extractAttachments(value, attachments);
2489
+ }
2490
+ }
2246
2491
  function validateAndSanitizeExperimentLogFullArgs(event, hasDataset) {
2247
2492
  if ("input" in event && !isEmpty(event.input) && "inputs" in event && !isEmpty(event.inputs) || !("input" in event) && !("inputs" in event)) {
2248
2493
  throw new Error(
@@ -2373,10 +2618,9 @@ var Experiment = class extends ObjectFetcher {
2373
2618
  * @param event.metrics: (Optional) a dictionary of metrics to log. The following keys are populated automatically: "start", "end".
2374
2619
  * @param event.id: (Optional) a unique identifier for the event. If you don't provide one, BrainTrust will generate one for you.
2375
2620
  * @param event.dataset_record_id: (Optional) the id of the dataset record that this event is associated with. This field is required if and only if the experiment is associated with a dataset.
2376
- * @param event.inputs: (Deprecated) the same as `input` (will be removed in a future version).
2377
2621
  * @param options Additional logging options
2378
2622
  * @param options.allowConcurrentWithSpans in rare cases where you need to log at the top level separately from spans on the experiment elsewhere, set this to true.
2379
- * :returns: The `id` of the logged event.
2623
+ * @returns The `id` of the logged event.
2380
2624
  */
2381
2625
  log(event, options) {
2382
2626
  if (this.calledStartSpan && !options?.allowConcurrentWithSpans) {
@@ -2392,7 +2636,7 @@ var Experiment = class extends ObjectFetcher {
2392
2636
  /**
2393
2637
  * Create a new toplevel span underneath the experiment. The name defaults to "root".
2394
2638
  *
2395
- * See `Span.traced` for full details.
2639
+ * See {@link Span.traced} for full details.
2396
2640
  */
2397
2641
  traced(callback, args) {
2398
2642
  const { setCurrent, ...argsRest } = args ?? {};
@@ -2418,7 +2662,7 @@ var Experiment = class extends ObjectFetcher {
2418
2662
  * where you cannot use callbacks. However, spans started with `startSpan` will not be marked as the "current span",
2419
2663
  * so `currentSpan()` and `traced()` will be no-ops. If you want to mark a span as current, use `traced` instead.
2420
2664
  *
2421
- * See `traced` for full details.
2665
+ * See {@link traced} for full details.
2422
2666
  */
2423
2667
  startSpan(args) {
2424
2668
  this.calledStartSpan = true;
@@ -2530,7 +2774,7 @@ var Experiment = class extends ObjectFetcher {
2530
2774
  * Update a span in the experiment using its id. It is important that you only update a span once the original span has been fully written and flushed,
2531
2775
  * since otherwise updates to the span may conflict with the original span.
2532
2776
  *
2533
- * @param event The event data to update the span with. Must include `id`. See `Experiment.log` for a full list of valid fields.
2777
+ * @param event The event data to update the span with. Must include `id`. See {@link Experiment.log} for a full list of valid fields.
2534
2778
  */
2535
2779
  updateSpan(event) {
2536
2780
  const { id, ...eventRest } = event;
@@ -2546,7 +2790,9 @@ var Experiment = class extends ObjectFetcher {
2546
2790
  });
2547
2791
  }
2548
2792
  /**
2549
- * Return a serialized representation of the experiment that can be used to start subspans in other places. See `Span.start_span` for more details.
2793
+ * Return a serialized representation of the experiment that can be used to start subspans in other places.
2794
+ *
2795
+ * See {@link Span.startSpan} for more details.
2550
2796
  */
2551
2797
  async export() {
2552
2798
  return new import_core.SpanComponentsV3({
@@ -2561,7 +2807,7 @@ var Experiment = class extends ObjectFetcher {
2561
2807
  return await this.state.bgLogger().flush();
2562
2808
  }
2563
2809
  /**
2564
- * This function is deprecated. You can simply remove it from your code.
2810
+ * @deprecated This function is deprecated. You can simply remove it from your code.
2565
2811
  */
2566
2812
  async close() {
2567
2813
  console.warn(
@@ -2703,27 +2949,14 @@ var SpanImpl = class _SpanImpl {
2703
2949
  event,
2704
2950
  internalData
2705
2951
  });
2706
- let partialRecord = {
2952
+ const partialRecord = deepCopyEvent({
2707
2953
  id: this.id,
2708
2954
  span_id: this.spanId,
2709
2955
  root_span_id: this.rootSpanId,
2710
2956
  span_parents: this.spanParents,
2711
2957
  ...serializableInternalData,
2712
2958
  [import_core.IS_MERGE_FIELD]: this.isMerge
2713
- };
2714
- const serializedPartialRecord = JSON.stringify(partialRecord, (_k, v) => {
2715
- if (v instanceof _SpanImpl) {
2716
- return `<span>`;
2717
- } else if (v instanceof Experiment) {
2718
- return `<experiment>`;
2719
- } else if (v instanceof Dataset) {
2720
- return `<dataset>`;
2721
- } else if (v instanceof Logger) {
2722
- return `<logger>`;
2723
- }
2724
- return v;
2725
2959
  });
2726
- partialRecord = JSON.parse(serializedPartialRecord);
2727
2960
  if (partialRecord.metrics?.end) {
2728
2961
  this.loggedEndTime = partialRecord.metrics?.end;
2729
2962
  }
@@ -2809,6 +3042,11 @@ var SpanImpl = class _SpanImpl {
2809
3042
  propagated_event: this.propagatedEvent
2810
3043
  }).toStr();
2811
3044
  }
3045
+ async permalink() {
3046
+ return await permalink(await this.export(), {
3047
+ state: this.state
3048
+ });
3049
+ }
2812
3050
  async flush() {
2813
3051
  return await this.state.bgLogger().flush();
2814
3052
  }
@@ -2958,15 +3196,17 @@ var Dataset = class extends ObjectFetcher {
2958
3196
  }) {
2959
3197
  this.validateEvent({ metadata, expected, output, tags });
2960
3198
  const rowId = id || (0, import_uuid.v4)();
2961
- const args = this.createArgs({
2962
- id: rowId,
2963
- input,
2964
- expected,
2965
- metadata,
2966
- tags,
2967
- output,
2968
- isMerge: false
2969
- });
3199
+ const args = this.createArgs(
3200
+ deepCopyEvent({
3201
+ id: rowId,
3202
+ input,
3203
+ expected,
3204
+ metadata,
3205
+ tags,
3206
+ output,
3207
+ isMerge: false
3208
+ })
3209
+ );
2970
3210
  this.state.bgLogger().log([args]);
2971
3211
  return rowId;
2972
3212
  }
@@ -2991,14 +3231,16 @@ var Dataset = class extends ObjectFetcher {
2991
3231
  id
2992
3232
  }) {
2993
3233
  this.validateEvent({ metadata, expected, tags });
2994
- const args = this.createArgs({
2995
- id,
2996
- input,
2997
- expected,
2998
- metadata,
2999
- tags,
3000
- isMerge: true
3001
- });
3234
+ const args = this.createArgs(
3235
+ deepCopyEvent({
3236
+ id,
3237
+ input,
3238
+ expected,
3239
+ metadata,
3240
+ tags,
3241
+ isMerge: true
3242
+ })
3243
+ );
3002
3244
  this.state.bgLogger().log([args]);
3003
3245
  return id;
3004
3246
  }
@@ -3053,7 +3295,7 @@ var Dataset = class extends ObjectFetcher {
3053
3295
  return await this.state.bgLogger().flush();
3054
3296
  }
3055
3297
  /**
3056
- * This function is deprecated. You can simply remove it from your code.
3298
+ * @deprecated This function is deprecated. You can simply remove it from your code.
3057
3299
  */
3058
3300
  async close() {
3059
3301
  console.warn(
@@ -3217,6 +3459,7 @@ var Prompt = class {
3217
3459
  return this.parsedPromptData;
3218
3460
  }
3219
3461
  };
3462
+ var _exportsForTestingOnly = { extractAttachments, deepCopyEvent };
3220
3463
 
3221
3464
  // src/browser-config.ts
3222
3465
  var browserConfigured = false;
@@ -3243,6 +3486,7 @@ function configureBrowser() {
3243
3486
  // src/exports-browser.ts
3244
3487
  var exports_browser_exports = {};
3245
3488
  __export(exports_browser_exports, {
3489
+ Attachment: () => Attachment,
3246
3490
  BraintrustState: () => BraintrustState,
3247
3491
  BraintrustStream: () => BraintrustStream,
3248
3492
  Dataset: () => Dataset,
@@ -3256,6 +3500,7 @@ __export(exports_browser_exports, {
3256
3500
  ReadonlyExperiment: () => ReadonlyExperiment,
3257
3501
  SpanImpl: () => SpanImpl,
3258
3502
  X_CACHED_HEADER: () => X_CACHED_HEADER,
3503
+ _exportsForTestingOnly: () => _exportsForTestingOnly,
3259
3504
  _internalGetGlobalState: () => _internalGetGlobalState,
3260
3505
  _internalSetInitialState: () => _internalSetInitialState,
3261
3506
  braintrustStreamChunkSchema: () => braintrustStreamChunkSchema,
@@ -3304,7 +3549,7 @@ async function invoke(args) {
3304
3549
  apiKey,
3305
3550
  appUrl,
3306
3551
  forceLogin,
3307
- fetch,
3552
+ fetch: fetch2,
3308
3553
  input,
3309
3554
  messages,
3310
3555
  parent: parentArg,
@@ -3320,7 +3565,7 @@ async function invoke(args) {
3320
3565
  apiKey,
3321
3566
  appUrl,
3322
3567
  forceLogin,
3323
- fetch
3568
+ fetch: fetch2
3324
3569
  });
3325
3570
  const parent = parentArg ? typeof parentArg === "string" ? parentArg : await parentArg.export() : await getSpanParentObject().export();
3326
3571
  const functionId = import_typespecs3.functionIdSchema.safeParse({
@@ -3748,6 +3993,7 @@ configureBrowser();
3748
3993
  var browser_default = exports_browser_exports;
3749
3994
  // Annotate the CommonJS export names for ESM import in node:
3750
3995
  0 && (module.exports = {
3996
+ Attachment,
3751
3997
  BraintrustState,
3752
3998
  BraintrustStream,
3753
3999
  Dataset,
@@ -3761,6 +4007,7 @@ var browser_default = exports_browser_exports;
3761
4007
  ReadonlyExperiment,
3762
4008
  SpanImpl,
3763
4009
  X_CACHED_HEADER,
4010
+ _exportsForTestingOnly,
3764
4011
  _internalGetGlobalState,
3765
4012
  _internalSetInitialState,
3766
4013
  braintrustStreamChunkSchema,