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.mjs CHANGED
@@ -55,7 +55,8 @@ import {
55
55
  promptDataSchema,
56
56
  promptSchema,
57
57
  toolsSchema,
58
- gitMetadataSettingsSchema
58
+ gitMetadataSettingsSchema,
59
+ BRAINTRUST_ATTACHMENT
59
60
  } from "@braintrust/core/typespecs";
60
61
 
61
62
  // src/util.ts
@@ -106,7 +107,7 @@ var LazyValue = class {
106
107
 
107
108
  // src/logger.ts
108
109
  import Mustache from "mustache";
109
- import { z as z2 } from "zod";
110
+ import { z as z2, ZodError } from "zod";
110
111
 
111
112
  // src/functions/stream.ts
112
113
  import {
@@ -396,6 +397,9 @@ var NoopSpan = class {
396
397
  async export() {
397
398
  return "";
398
399
  }
400
+ async permalink() {
401
+ return "";
402
+ }
399
403
  async flush() {
400
404
  }
401
405
  close(args) {
@@ -537,11 +541,11 @@ var BraintrustState = class _BraintrustState {
537
541
  state.loginReplaceApiConn(state.apiConn());
538
542
  return state;
539
543
  }
540
- setFetch(fetch) {
541
- this.loginParams.fetch = fetch;
542
- this.fetch = fetch;
543
- this._apiConn?.setFetch(fetch);
544
- this._appConn?.setFetch(fetch);
544
+ setFetch(fetch2) {
545
+ this.loginParams.fetch = fetch2;
546
+ this.fetch = fetch2;
547
+ this._apiConn?.setFetch(fetch2);
548
+ this._appConn?.setFetch(fetch2);
545
549
  }
546
550
  async login(loginParams) {
547
551
  if (this.apiUrl && !loginParams.forceLogin) {
@@ -630,15 +634,15 @@ var HTTPConnection = class _HTTPConnection {
630
634
  token;
631
635
  headers;
632
636
  fetch;
633
- constructor(base_url, fetch) {
637
+ constructor(base_url, fetch2) {
634
638
  this.base_url = base_url;
635
639
  this.token = null;
636
640
  this.headers = {};
637
641
  this._reset();
638
- this.fetch = fetch;
642
+ this.fetch = fetch2;
639
643
  }
640
- setFetch(fetch) {
641
- this.fetch = fetch;
644
+ setFetch(fetch2) {
645
+ this.fetch = fetch2;
642
646
  }
643
647
  async ping() {
644
648
  try {
@@ -674,12 +678,14 @@ var HTTPConnection = class _HTTPConnection {
674
678
  ([k, v]) => v !== void 0 ? typeof v === "string" ? [[k, v]] : v.map((x) => [k, x]) : []
675
679
  ) : []
676
680
  ).toString();
681
+ const this_fetch = this.fetch;
682
+ const this_headers = this.headers;
677
683
  return await checkResponse(
678
684
  // Using toString() here makes it work with isomorphic fetch
679
- await this.fetch(url.toString(), {
685
+ await this_fetch(url.toString(), {
680
686
  headers: {
681
687
  Accept: "application/json",
682
- ...this.headers,
688
+ ...this_headers,
683
689
  ...headers
684
690
  },
685
691
  keepalive: true,
@@ -689,13 +695,16 @@ var HTTPConnection = class _HTTPConnection {
689
695
  }
690
696
  async post(path, params, config) {
691
697
  const { headers, ...rest } = config || {};
698
+ const this_fetch = this.fetch;
699
+ const this_base_url = this.base_url;
700
+ const this_headers = this.headers;
692
701
  return await checkResponse(
693
- await this.fetch(_urljoin(this.base_url, path), {
702
+ await this_fetch(_urljoin(this_base_url, path), {
694
703
  method: "POST",
695
704
  headers: {
696
705
  Accept: "application/json",
697
706
  "Content-Type": "application/json",
698
- ...this.headers,
707
+ ...this_headers,
699
708
  ...headers
700
709
  },
701
710
  body: typeof params === "string" ? params : params ? JSON.stringify(params) : void 0,
@@ -728,6 +737,167 @@ var HTTPConnection = class _HTTPConnection {
728
737
  return await resp.json();
729
738
  }
730
739
  };
740
+ var Attachment = class {
741
+ /**
742
+ * The object that replaces this `Attachment` at upload time.
743
+ */
744
+ reference;
745
+ uploader;
746
+ data;
747
+ state;
748
+ // For debug logging only.
749
+ dataDebugString;
750
+ /**
751
+ * Construct an attachment.
752
+ *
753
+ * @param data A string representing the path of the file on disk, or a
754
+ * `Blob`/`ArrayBuffer` with the file's contents. The caller is responsible
755
+ * for ensuring the file/blob/buffer is not modified until upload is complete.
756
+ *
757
+ * @param filename The desired name of the file in Braintrust after uploading.
758
+ * This parameter is for visualization purposes only and has no effect on
759
+ * attachment storage.
760
+ *
761
+ * @param contentType The MIME type of the file.
762
+ *
763
+ * @param state (Optional) For internal use.
764
+ */
765
+ constructor({ data, filename, contentType, state }) {
766
+ this.reference = {
767
+ type: BRAINTRUST_ATTACHMENT,
768
+ filename,
769
+ content_type: contentType,
770
+ key: newId()
771
+ };
772
+ this.state = state;
773
+ this.dataDebugString = typeof data === "string" ? data : "<in-memory data>";
774
+ this.data = this.initData(data);
775
+ this.uploader = this.initUploader();
776
+ }
777
+ /**
778
+ * On first access, (1) reads the attachment from disk if needed, (2)
779
+ * authenticates with the data plane to request a signed URL, (3) uploads to
780
+ * object store, and (4) updates the attachment.
781
+ *
782
+ * @returns The attachment status.
783
+ */
784
+ async upload() {
785
+ return await this.uploader.get();
786
+ }
787
+ /**
788
+ * A human-readable description for logging and debugging.
789
+ *
790
+ * @returns The debug object. The return type is not stable and may change in
791
+ * a future release.
792
+ */
793
+ debugInfo() {
794
+ return {
795
+ inputData: this.dataDebugString,
796
+ reference: this.reference,
797
+ state: this.state
798
+ };
799
+ }
800
+ initUploader() {
801
+ const doUpload = async (conn, orgId) => {
802
+ const requestParams = {
803
+ key: this.reference.key,
804
+ filename: this.reference.filename,
805
+ content_type: this.reference.content_type,
806
+ org_id: orgId
807
+ };
808
+ const [metadataPromiseResult, dataPromiseResult] = await Promise.allSettled([
809
+ conn.post("/attachment", requestParams),
810
+ this.data.get()
811
+ ]);
812
+ if (metadataPromiseResult.status === "rejected") {
813
+ const errorStr = JSON.stringify(metadataPromiseResult.reason);
814
+ throw new Error(
815
+ `Failed to request signed URL from API server: ${errorStr}`
816
+ );
817
+ }
818
+ if (dataPromiseResult.status === "rejected") {
819
+ const errorStr = JSON.stringify(dataPromiseResult.reason);
820
+ throw new Error(`Failed to read file: ${errorStr}`);
821
+ }
822
+ const metadataResponse = metadataPromiseResult.value;
823
+ const data = dataPromiseResult.value;
824
+ let signedUrl;
825
+ let headers;
826
+ try {
827
+ ({ signedUrl, headers } = z2.object({
828
+ signedUrl: z2.string().url(),
829
+ headers: z2.record(z2.string())
830
+ }).parse(await metadataResponse.json()));
831
+ } catch (error) {
832
+ if (error instanceof ZodError) {
833
+ const errorStr = JSON.stringify(error.flatten());
834
+ throw new Error(`Invalid response from API server: ${errorStr}`);
835
+ }
836
+ throw error;
837
+ }
838
+ let objectStoreResponse;
839
+ try {
840
+ objectStoreResponse = await checkResponse(
841
+ await fetch(signedUrl, {
842
+ method: "PUT",
843
+ headers,
844
+ body: data
845
+ })
846
+ );
847
+ } catch (error) {
848
+ if (error instanceof FailedHTTPResponse) {
849
+ throw new Error(
850
+ `Failed to upload attachment to object store: ${error.status} ${error.text} ${error.data}`
851
+ );
852
+ }
853
+ throw error;
854
+ }
855
+ return { signedUrl, metadataResponse, objectStoreResponse };
856
+ };
857
+ const errorWrapper = async () => {
858
+ const status = { upload_status: "done" };
859
+ const state = this.state ?? _globalState;
860
+ await state.login({});
861
+ const conn = state.apiConn();
862
+ const orgId = state.orgId ?? "";
863
+ try {
864
+ await doUpload(conn, orgId);
865
+ } catch (error) {
866
+ status.upload_status = "error";
867
+ status.error_message = error instanceof Error ? error.message : JSON.stringify(error);
868
+ }
869
+ const requestParams = {
870
+ key: this.reference.key,
871
+ org_id: orgId,
872
+ status
873
+ };
874
+ const statusResponse = await conn.post(
875
+ "/attachment/status",
876
+ requestParams
877
+ );
878
+ if (!statusResponse.ok) {
879
+ const errorStr = JSON.stringify(statusResponse);
880
+ throw new Error(`Couldn't log attachment status: ${errorStr}`);
881
+ }
882
+ return status;
883
+ };
884
+ return new LazyValue(errorWrapper);
885
+ }
886
+ initData(data) {
887
+ if (typeof data === "string") {
888
+ const readFile = isomorph_default.readFile;
889
+ if (!readFile) {
890
+ throw new Error(
891
+ `This platform does not support reading the filesystem. Construct the Attachment
892
+ with a Blob/ArrayBuffer, or run the program on Node.js.`
893
+ );
894
+ }
895
+ return new LazyValue(async () => new Blob([await readFile(data)]));
896
+ } else {
897
+ return new LazyValue(async () => new Blob([data]));
898
+ }
899
+ }
900
+ };
731
901
  function logFeedbackImpl(state, parentObjectType, parentObjectId, {
732
902
  id,
733
903
  expected,
@@ -752,7 +922,7 @@ function logFeedbackImpl(state, parentObjectType, parentObjectId, {
752
922
  expected,
753
923
  tags
754
924
  });
755
- let { metadata, ...updateEvent } = validatedEvent;
925
+ let { metadata, ...updateEvent } = deepCopyEvent(validatedEvent);
756
926
  updateEvent = Object.fromEntries(
757
927
  Object.entries(updateEvent).filter(([_, v]) => !isEmpty(v))
758
928
  );
@@ -801,10 +971,12 @@ function updateSpanImpl({
801
971
  id,
802
972
  event
803
973
  }) {
804
- const updateEvent = validateAndSanitizeExperimentLogPartialArgs({
805
- id,
806
- ...event
807
- });
974
+ const updateEvent = deepCopyEvent(
975
+ validateAndSanitizeExperimentLogPartialArgs({
976
+ id,
977
+ ...event
978
+ })
979
+ );
808
980
  const parentIds = async () => new SpanComponentsV3({
809
981
  object_type: parentObjectType,
810
982
  object_id: await parentObjectId.get()
@@ -1005,7 +1177,7 @@ var Logger = class {
1005
1177
  * @param event.id: (Optional) a unique identifier for the event. If you don't provide one, BrainTrust will generate one for you.
1006
1178
  * @param options Additional logging options
1007
1179
  * @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.
1008
- * :returns: The `id` of the logged event.
1180
+ * @returns The `id` of the logged event.
1009
1181
  */
1010
1182
  log(event, options) {
1011
1183
  if (this.calledStartSpan && !options?.allowConcurrentWithSpans) {
@@ -1028,7 +1200,7 @@ var Logger = class {
1028
1200
  /**
1029
1201
  * Create a new toplevel span underneath the logger. The name defaults to "root".
1030
1202
  *
1031
- * See `Span.traced` for full details.
1203
+ * See {@link Span.traced} for full details.
1032
1204
  */
1033
1205
  traced(callback, args) {
1034
1206
  const { setCurrent, ...argsRest } = args ?? {};
@@ -1062,7 +1234,7 @@ var Logger = class {
1062
1234
  * where you cannot use callbacks. However, spans started with `startSpan` will not be marked as the "current span",
1063
1235
  * so `currentSpan()` and `traced()` will be no-ops. If you want to mark a span as current, use `traced` instead.
1064
1236
  *
1065
- * See `traced` for full details.
1237
+ * See {@link traced} for full details.
1066
1238
  */
1067
1239
  startSpan(args) {
1068
1240
  this.calledStartSpan = true;
@@ -1102,7 +1274,7 @@ var Logger = class {
1102
1274
  * 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,
1103
1275
  * since otherwise updates to the span may conflict with the original span.
1104
1276
  *
1105
- * @param event The event data to update the span with. Must include `id`. See `Experiment.log` for a full list of valid fields.
1277
+ * @param event The event data to update the span with. Must include `id`. See {@link Experiment.log} for a full list of valid fields.
1106
1278
  */
1107
1279
  updateSpan(event) {
1108
1280
  const { id, ...eventRest } = event;
@@ -1118,7 +1290,9 @@ var Logger = class {
1118
1290
  });
1119
1291
  }
1120
1292
  /**
1121
- * Return a serialized representation of the logger that can be used to start subspans in other places. See `Span.start_span` for more details.
1293
+ * Return a serialized representation of the logger that can be used to start subspans in other places.
1294
+ *
1295
+ * See {@link Span.startSpan} for more details.
1122
1296
  */
1123
1297
  async export() {
1124
1298
  return new SpanComponentsV3({
@@ -1158,6 +1332,7 @@ var BackgroundLogger = class _BackgroundLogger {
1158
1332
  activeFlush = Promise.resolve();
1159
1333
  activeFlushResolved = true;
1160
1334
  activeFlushError = void 0;
1335
+ onFlushError;
1161
1336
  syncFlush = false;
1162
1337
  // 6 MB for the AWS lambda gateway (from our own testing).
1163
1338
  maxRequestSize = 6 * 1024 * 1024;
@@ -1221,6 +1396,7 @@ var BackgroundLogger = class _BackgroundLogger {
1221
1396
  await this.flush();
1222
1397
  });
1223
1398
  }
1399
+ this.onFlushError = opts.onFlushError;
1224
1400
  }
1225
1401
  log(items) {
1226
1402
  const [addedItems, droppedItems] = (() => {
@@ -1252,14 +1428,16 @@ var BackgroundLogger = class _BackgroundLogger {
1252
1428
  if (this.activeFlushError) {
1253
1429
  const err = this.activeFlushError;
1254
1430
  this.activeFlushError = void 0;
1255
- throw err;
1431
+ if (this.syncFlush) {
1432
+ throw err;
1433
+ }
1256
1434
  }
1257
1435
  }
1258
1436
  async flushOnce(args) {
1259
1437
  const batchSize = args?.batchSize ?? this.defaultBatchSize;
1260
1438
  const wrappedItems = this.items;
1261
1439
  this.items = [];
1262
- const allItems = await this.unwrapLazyValues(wrappedItems);
1440
+ const [allItems, attachments] = await this.unwrapLazyValues(wrappedItems);
1263
1441
  if (allItems.length === 0) {
1264
1442
  return;
1265
1443
  }
@@ -1291,6 +1469,23 @@ var BackgroundLogger = class _BackgroundLogger {
1291
1469
  );
1292
1470
  }
1293
1471
  }
1472
+ const attachmentErrors = [];
1473
+ for (const attachment of attachments) {
1474
+ try {
1475
+ const result = await attachment.upload();
1476
+ if (result.upload_status === "error" && result.error_message) {
1477
+ attachmentErrors.push(new Error(result.error_message));
1478
+ }
1479
+ } catch (error) {
1480
+ attachmentErrors.push(error);
1481
+ }
1482
+ }
1483
+ if (attachmentErrors.length > 0) {
1484
+ throw new AggregateError(
1485
+ attachmentErrors,
1486
+ `Encountered the following errors while uploading attachments:`
1487
+ );
1488
+ }
1294
1489
  if (this.items.length > 0) {
1295
1490
  await this.flushOnce(args);
1296
1491
  }
@@ -1298,8 +1493,10 @@ var BackgroundLogger = class _BackgroundLogger {
1298
1493
  async unwrapLazyValues(wrappedItems) {
1299
1494
  for (let i = 0; i < this.numTries; ++i) {
1300
1495
  try {
1301
- const itemPromises = wrappedItems.map((x) => x.get());
1302
- return mergeRowBatch(await Promise.all(itemPromises));
1496
+ const items = await Promise.all(wrappedItems.map((x) => x.get()));
1497
+ const attachments = [];
1498
+ items.forEach((item) => extractAttachments(item, attachments));
1499
+ return [mergeRowBatch(items), attachments];
1303
1500
  } catch (e) {
1304
1501
  let errmsg = "Encountered error when constructing records to flush";
1305
1502
  const isRetrying = i + 1 < this.numTries;
@@ -1307,7 +1504,10 @@ var BackgroundLogger = class _BackgroundLogger {
1307
1504
  errmsg += ". Retrying";
1308
1505
  }
1309
1506
  console.warn(errmsg);
1310
- if (!isRetrying && this.syncFlush) {
1507
+ if (!isRetrying) {
1508
+ console.warn(
1509
+ `Failed to construct log records to flush after ${this.numTries} attempts. Dropping batch`
1510
+ );
1311
1511
  throw e;
1312
1512
  } else {
1313
1513
  console.warn(e);
@@ -1315,10 +1515,7 @@ var BackgroundLogger = class _BackgroundLogger {
1315
1515
  }
1316
1516
  }
1317
1517
  }
1318
- console.warn(
1319
- `Failed to construct log records to flush after ${this.numTries} attempts. Dropping batch`
1320
- );
1321
- return [];
1518
+ throw new Error("Impossible");
1322
1519
  }
1323
1520
  async submitLogsRequest(items) {
1324
1521
  const conn = await this.apiConn.get();
@@ -1334,16 +1531,14 @@ var BackgroundLogger = class _BackgroundLogger {
1334
1531
  let error = void 0;
1335
1532
  try {
1336
1533
  await conn.post_json("logs3", dataStr);
1337
- } catch (e) {
1534
+ } catch {
1338
1535
  try {
1339
1536
  const legacyDataS = constructJsonArray(
1340
- items.map(
1341
- (r) => JSON.stringify(makeLegacyEvent(JSON.parse(r)))
1342
- )
1537
+ items.map((r) => JSON.stringify(makeLegacyEvent(JSON.parse(r))))
1343
1538
  );
1344
1539
  await conn.post_json("logs", legacyDataS);
1345
- } catch (e2) {
1346
- error = e2;
1540
+ } catch (e) {
1541
+ error = e;
1347
1542
  }
1348
1543
  }
1349
1544
  if (error === void 0) {
@@ -1367,7 +1562,10 @@ Error: ${errorText}`;
1367
1562
  });
1368
1563
  this.logFailedPayloadsDir();
1369
1564
  }
1370
- if (!isRetrying && this.syncFlush) {
1565
+ if (!isRetrying) {
1566
+ console.warn(
1567
+ `log request failed after ${this.numTries} retries. Dropping batch`
1568
+ );
1371
1569
  throw new Error(errMsg);
1372
1570
  } else {
1373
1571
  console.warn(errMsg);
@@ -1376,10 +1574,6 @@ Error: ${errorText}`;
1376
1574
  }
1377
1575
  }
1378
1576
  }
1379
- console.warn(
1380
- `log request failed after ${this.numTries} retries. Dropping batch`
1381
- );
1382
- return;
1383
1577
  }
1384
1578
  registerDroppedItemCount(numItems) {
1385
1579
  if (numItems <= 0) {
@@ -1407,15 +1601,17 @@ Error: ${errorText}`;
1407
1601
  return;
1408
1602
  }
1409
1603
  try {
1410
- const allItems = await this.unwrapLazyValues(wrappedItems);
1604
+ const [allItems, allAttachments] = await this.unwrapLazyValues(wrappedItems);
1411
1605
  const dataStr = constructLogs3Data(
1412
1606
  allItems.map((x) => JSON.stringify(x))
1413
1607
  );
1608
+ const attachmentStr = JSON.stringify(
1609
+ allAttachments.map((a) => a.debugInfo())
1610
+ );
1611
+ const payload = `{"data": ${dataStr}, "attachments": ${attachmentStr}}
1612
+ `;
1414
1613
  for (const payloadDir of publishPayloadsDir) {
1415
- await _BackgroundLogger.writePayloadToDir({
1416
- payloadDir,
1417
- payload: dataStr
1418
- });
1614
+ await _BackgroundLogger.writePayloadToDir({ payloadDir, payload });
1419
1615
  }
1420
1616
  } catch (e) {
1421
1617
  console.error(e);
@@ -1454,6 +1650,13 @@ Error: ${errorText}`;
1454
1650
  try {
1455
1651
  await this.flushOnce();
1456
1652
  } catch (err) {
1653
+ if (err instanceof AggregateError) {
1654
+ for (const e of err.errors) {
1655
+ this.onFlushError?.(e);
1656
+ }
1657
+ } else {
1658
+ this.onFlushError?.(err);
1659
+ }
1457
1660
  this.activeFlushError = err;
1458
1661
  } finally {
1459
1662
  this.activeFlushResolved = true;
@@ -1496,7 +1699,7 @@ function init(projectOrOptions, optionalOptions) {
1496
1699
  apiKey,
1497
1700
  orgName,
1498
1701
  forceLogin,
1499
- fetch,
1702
+ fetch: fetch2,
1500
1703
  metadata,
1501
1704
  gitMetadataSettings,
1502
1705
  projectId,
@@ -1514,7 +1717,7 @@ function init(projectOrOptions, optionalOptions) {
1514
1717
  }
1515
1718
  const lazyMetadata2 = new LazyValue(
1516
1719
  async () => {
1517
- await state.login({ apiKey, appUrl, orgName, fetch, forceLogin });
1720
+ await state.login({ apiKey, appUrl, orgName, fetch: fetch2, forceLogin });
1518
1721
  const args = {
1519
1722
  project_name: project,
1520
1723
  project_id: projectId,
@@ -1685,7 +1888,7 @@ function initDataset(projectOrOptions, optionalOptions) {
1685
1888
  appUrl,
1686
1889
  apiKey,
1687
1890
  orgName,
1688
- fetch,
1891
+ fetch: fetch2,
1689
1892
  forceLogin,
1690
1893
  projectId,
1691
1894
  metadata,
@@ -1699,7 +1902,7 @@ function initDataset(projectOrOptions, optionalOptions) {
1699
1902
  orgName,
1700
1903
  apiKey,
1701
1904
  appUrl,
1702
- fetch,
1905
+ fetch: fetch2,
1703
1906
  forceLogin
1704
1907
  });
1705
1908
  const args = {
@@ -1781,7 +1984,7 @@ function initLogger(options = {}) {
1781
1984
  apiKey,
1782
1985
  orgName,
1783
1986
  forceLogin,
1784
- fetch,
1987
+ fetch: fetch2,
1785
1988
  state: stateArg
1786
1989
  } = options || {};
1787
1990
  const computeMetadataArgs = {
@@ -1796,7 +1999,7 @@ function initLogger(options = {}) {
1796
1999
  apiKey,
1797
2000
  appUrl,
1798
2001
  forceLogin,
1799
- fetch
2002
+ fetch: fetch2
1800
2003
  });
1801
2004
  return computeLoggerMetadata(state, computeMetadataArgs);
1802
2005
  }
@@ -1820,7 +2023,7 @@ async function loadPrompt({
1820
2023
  appUrl,
1821
2024
  apiKey,
1822
2025
  orgName,
1823
- fetch,
2026
+ fetch: fetch2,
1824
2027
  forceLogin,
1825
2028
  state: stateArg
1826
2029
  }) {
@@ -1835,7 +2038,7 @@ async function loadPrompt({
1835
2038
  orgName,
1836
2039
  apiKey,
1837
2040
  appUrl,
1838
- fetch,
2041
+ fetch: fetch2,
1839
2042
  forceLogin
1840
2043
  });
1841
2044
  const args = {
@@ -1886,7 +2089,7 @@ async function loginToState(options = {}) {
1886
2089
  appUrl = isomorph_default.getEnv("BRAINTRUST_APP_URL") || "https://www.braintrust.dev",
1887
2090
  apiKey = isomorph_default.getEnv("BRAINTRUST_API_KEY"),
1888
2091
  orgName = isomorph_default.getEnv("BRAINTRUST_ORG_NAME"),
1889
- fetch = globalThis.fetch
2092
+ fetch: fetch2 = globalThis.fetch
1890
2093
  } = options || {};
1891
2094
  const appPublicUrl = isomorph_default.getEnv("BRAINTRUST_APP_PUBLIC_URL") || appUrl;
1892
2095
  const state = new BraintrustState(options);
@@ -1896,7 +2099,7 @@ async function loginToState(options = {}) {
1896
2099
  let conn = null;
1897
2100
  if (apiKey !== void 0) {
1898
2101
  const resp = await checkResponse(
1899
- await fetch(_urljoin(state.appUrl, `/api/apikey/login`), {
2102
+ await fetch2(_urljoin(state.appUrl, `/api/apikey/login`), {
1900
2103
  method: "POST",
1901
2104
  headers: {
1902
2105
  "Content-Type": "application/json",
@@ -2064,8 +2267,8 @@ async function flush(options) {
2064
2267
  const state = options?.state ?? _globalState;
2065
2268
  return await state.bgLogger().flush();
2066
2269
  }
2067
- function setFetch(fetch) {
2068
- _globalState.setFetch(fetch);
2270
+ function setFetch(fetch2) {
2271
+ _globalState.setFetch(fetch2);
2069
2272
  }
2070
2273
  function startSpanAndIsLogger(args) {
2071
2274
  const state = args?.state ?? _globalState;
@@ -2193,6 +2396,47 @@ function validateAndSanitizeExperimentLogPartialArgs(event) {
2193
2396
  return { ...event };
2194
2397
  }
2195
2398
  }
2399
+ function deepCopyEvent(event) {
2400
+ const attachments = [];
2401
+ const IDENTIFIER = "_bt_internal_saved_attachment";
2402
+ const savedAttachmentSchema = z2.strictObject({ [IDENTIFIER]: z2.number() });
2403
+ const serialized = JSON.stringify(event, (_k, v) => {
2404
+ if (v instanceof SpanImpl || v instanceof NoopSpan) {
2405
+ return `<span>`;
2406
+ } else if (v instanceof Experiment) {
2407
+ return `<experiment>`;
2408
+ } else if (v instanceof Dataset) {
2409
+ return `<dataset>`;
2410
+ } else if (v instanceof Logger) {
2411
+ return `<logger>`;
2412
+ } else if (v instanceof Attachment) {
2413
+ const idx = attachments.push(v);
2414
+ return { [IDENTIFIER]: idx - 1 };
2415
+ }
2416
+ return v;
2417
+ });
2418
+ const x = JSON.parse(serialized, (_k, v) => {
2419
+ const parsedAttachment = savedAttachmentSchema.safeParse(v);
2420
+ if (parsedAttachment.success) {
2421
+ return attachments[parsedAttachment.data[IDENTIFIER]];
2422
+ }
2423
+ return v;
2424
+ });
2425
+ return x;
2426
+ }
2427
+ function extractAttachments(event, attachments) {
2428
+ for (const [key, value] of Object.entries(event)) {
2429
+ if (value instanceof Attachment) {
2430
+ attachments.push(value);
2431
+ event[key] = value.reference;
2432
+ continue;
2433
+ }
2434
+ if (!(value instanceof Object)) {
2435
+ continue;
2436
+ }
2437
+ extractAttachments(value, attachments);
2438
+ }
2439
+ }
2196
2440
  function validateAndSanitizeExperimentLogFullArgs(event, hasDataset) {
2197
2441
  if ("input" in event && !isEmpty(event.input) && "inputs" in event && !isEmpty(event.inputs) || !("input" in event) && !("inputs" in event)) {
2198
2442
  throw new Error(
@@ -2323,10 +2567,9 @@ var Experiment = class extends ObjectFetcher {
2323
2567
  * @param event.metrics: (Optional) a dictionary of metrics to log. The following keys are populated automatically: "start", "end".
2324
2568
  * @param event.id: (Optional) a unique identifier for the event. If you don't provide one, BrainTrust will generate one for you.
2325
2569
  * @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.
2326
- * @param event.inputs: (Deprecated) the same as `input` (will be removed in a future version).
2327
2570
  * @param options Additional logging options
2328
2571
  * @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.
2329
- * :returns: The `id` of the logged event.
2572
+ * @returns The `id` of the logged event.
2330
2573
  */
2331
2574
  log(event, options) {
2332
2575
  if (this.calledStartSpan && !options?.allowConcurrentWithSpans) {
@@ -2342,7 +2585,7 @@ var Experiment = class extends ObjectFetcher {
2342
2585
  /**
2343
2586
  * Create a new toplevel span underneath the experiment. The name defaults to "root".
2344
2587
  *
2345
- * See `Span.traced` for full details.
2588
+ * See {@link Span.traced} for full details.
2346
2589
  */
2347
2590
  traced(callback, args) {
2348
2591
  const { setCurrent, ...argsRest } = args ?? {};
@@ -2368,7 +2611,7 @@ var Experiment = class extends ObjectFetcher {
2368
2611
  * where you cannot use callbacks. However, spans started with `startSpan` will not be marked as the "current span",
2369
2612
  * so `currentSpan()` and `traced()` will be no-ops. If you want to mark a span as current, use `traced` instead.
2370
2613
  *
2371
- * See `traced` for full details.
2614
+ * See {@link traced} for full details.
2372
2615
  */
2373
2616
  startSpan(args) {
2374
2617
  this.calledStartSpan = true;
@@ -2480,7 +2723,7 @@ var Experiment = class extends ObjectFetcher {
2480
2723
  * 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,
2481
2724
  * since otherwise updates to the span may conflict with the original span.
2482
2725
  *
2483
- * @param event The event data to update the span with. Must include `id`. See `Experiment.log` for a full list of valid fields.
2726
+ * @param event The event data to update the span with. Must include `id`. See {@link Experiment.log} for a full list of valid fields.
2484
2727
  */
2485
2728
  updateSpan(event) {
2486
2729
  const { id, ...eventRest } = event;
@@ -2496,7 +2739,9 @@ var Experiment = class extends ObjectFetcher {
2496
2739
  });
2497
2740
  }
2498
2741
  /**
2499
- * Return a serialized representation of the experiment that can be used to start subspans in other places. See `Span.start_span` for more details.
2742
+ * Return a serialized representation of the experiment that can be used to start subspans in other places.
2743
+ *
2744
+ * See {@link Span.startSpan} for more details.
2500
2745
  */
2501
2746
  async export() {
2502
2747
  return new SpanComponentsV3({
@@ -2511,7 +2756,7 @@ var Experiment = class extends ObjectFetcher {
2511
2756
  return await this.state.bgLogger().flush();
2512
2757
  }
2513
2758
  /**
2514
- * This function is deprecated. You can simply remove it from your code.
2759
+ * @deprecated This function is deprecated. You can simply remove it from your code.
2515
2760
  */
2516
2761
  async close() {
2517
2762
  console.warn(
@@ -2653,27 +2898,14 @@ var SpanImpl = class _SpanImpl {
2653
2898
  event,
2654
2899
  internalData
2655
2900
  });
2656
- let partialRecord = {
2901
+ const partialRecord = deepCopyEvent({
2657
2902
  id: this.id,
2658
2903
  span_id: this.spanId,
2659
2904
  root_span_id: this.rootSpanId,
2660
2905
  span_parents: this.spanParents,
2661
2906
  ...serializableInternalData,
2662
2907
  [IS_MERGE_FIELD]: this.isMerge
2663
- };
2664
- const serializedPartialRecord = JSON.stringify(partialRecord, (_k, v) => {
2665
- if (v instanceof _SpanImpl) {
2666
- return `<span>`;
2667
- } else if (v instanceof Experiment) {
2668
- return `<experiment>`;
2669
- } else if (v instanceof Dataset) {
2670
- return `<dataset>`;
2671
- } else if (v instanceof Logger) {
2672
- return `<logger>`;
2673
- }
2674
- return v;
2675
2908
  });
2676
- partialRecord = JSON.parse(serializedPartialRecord);
2677
2909
  if (partialRecord.metrics?.end) {
2678
2910
  this.loggedEndTime = partialRecord.metrics?.end;
2679
2911
  }
@@ -2759,6 +2991,11 @@ var SpanImpl = class _SpanImpl {
2759
2991
  propagated_event: this.propagatedEvent
2760
2992
  }).toStr();
2761
2993
  }
2994
+ async permalink() {
2995
+ return await permalink(await this.export(), {
2996
+ state: this.state
2997
+ });
2998
+ }
2762
2999
  async flush() {
2763
3000
  return await this.state.bgLogger().flush();
2764
3001
  }
@@ -2908,15 +3145,17 @@ var Dataset = class extends ObjectFetcher {
2908
3145
  }) {
2909
3146
  this.validateEvent({ metadata, expected, output, tags });
2910
3147
  const rowId = id || uuidv4();
2911
- const args = this.createArgs({
2912
- id: rowId,
2913
- input,
2914
- expected,
2915
- metadata,
2916
- tags,
2917
- output,
2918
- isMerge: false
2919
- });
3148
+ const args = this.createArgs(
3149
+ deepCopyEvent({
3150
+ id: rowId,
3151
+ input,
3152
+ expected,
3153
+ metadata,
3154
+ tags,
3155
+ output,
3156
+ isMerge: false
3157
+ })
3158
+ );
2920
3159
  this.state.bgLogger().log([args]);
2921
3160
  return rowId;
2922
3161
  }
@@ -2941,14 +3180,16 @@ var Dataset = class extends ObjectFetcher {
2941
3180
  id
2942
3181
  }) {
2943
3182
  this.validateEvent({ metadata, expected, tags });
2944
- const args = this.createArgs({
2945
- id,
2946
- input,
2947
- expected,
2948
- metadata,
2949
- tags,
2950
- isMerge: true
2951
- });
3183
+ const args = this.createArgs(
3184
+ deepCopyEvent({
3185
+ id,
3186
+ input,
3187
+ expected,
3188
+ metadata,
3189
+ tags,
3190
+ isMerge: true
3191
+ })
3192
+ );
2952
3193
  this.state.bgLogger().log([args]);
2953
3194
  return id;
2954
3195
  }
@@ -3003,7 +3244,7 @@ var Dataset = class extends ObjectFetcher {
3003
3244
  return await this.state.bgLogger().flush();
3004
3245
  }
3005
3246
  /**
3006
- * This function is deprecated. You can simply remove it from your code.
3247
+ * @deprecated This function is deprecated. You can simply remove it from your code.
3007
3248
  */
3008
3249
  async close() {
3009
3250
  console.warn(
@@ -3167,6 +3408,7 @@ var Prompt = class {
3167
3408
  return this.parsedPromptData;
3168
3409
  }
3169
3410
  };
3411
+ var _exportsForTestingOnly = { extractAttachments, deepCopyEvent };
3170
3412
 
3171
3413
  // src/browser-config.ts
3172
3414
  var browserConfigured = false;
@@ -3193,6 +3435,7 @@ function configureBrowser() {
3193
3435
  // src/exports-browser.ts
3194
3436
  var exports_browser_exports = {};
3195
3437
  __export(exports_browser_exports, {
3438
+ Attachment: () => Attachment,
3196
3439
  BraintrustState: () => BraintrustState,
3197
3440
  BraintrustStream: () => BraintrustStream,
3198
3441
  Dataset: () => Dataset,
@@ -3206,6 +3449,7 @@ __export(exports_browser_exports, {
3206
3449
  ReadonlyExperiment: () => ReadonlyExperiment,
3207
3450
  SpanImpl: () => SpanImpl,
3208
3451
  X_CACHED_HEADER: () => X_CACHED_HEADER,
3452
+ _exportsForTestingOnly: () => _exportsForTestingOnly,
3209
3453
  _internalGetGlobalState: () => _internalGetGlobalState,
3210
3454
  _internalSetInitialState: () => _internalSetInitialState,
3211
3455
  braintrustStreamChunkSchema: () => braintrustStreamChunkSchema,
@@ -3256,7 +3500,7 @@ async function invoke(args) {
3256
3500
  apiKey,
3257
3501
  appUrl,
3258
3502
  forceLogin,
3259
- fetch,
3503
+ fetch: fetch2,
3260
3504
  input,
3261
3505
  messages,
3262
3506
  parent: parentArg,
@@ -3272,7 +3516,7 @@ async function invoke(args) {
3272
3516
  apiKey,
3273
3517
  appUrl,
3274
3518
  forceLogin,
3275
- fetch
3519
+ fetch: fetch2
3276
3520
  });
3277
3521
  const parent = parentArg ? typeof parentArg === "string" ? parentArg : await parentArg.export() : await getSpanParentObject().export();
3278
3522
  const functionId = functionIdSchema.safeParse({
@@ -3699,6 +3943,7 @@ var WrapperStream = class {
3699
3943
  configureBrowser();
3700
3944
  var browser_default = exports_browser_exports;
3701
3945
  export {
3946
+ Attachment,
3702
3947
  BraintrustState,
3703
3948
  BraintrustStream,
3704
3949
  Dataset,
@@ -3712,6 +3957,7 @@ export {
3712
3957
  ReadonlyExperiment,
3713
3958
  SpanImpl,
3714
3959
  X_CACHED_HEADER,
3960
+ _exportsForTestingOnly,
3715
3961
  _internalGetGlobalState,
3716
3962
  _internalSetInitialState,
3717
3963
  braintrustStreamChunkSchema,