bitfab 0.36.7 → 0.36.9

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
@@ -51,7 +51,7 @@ var __version__;
51
51
  var init_version_generated = __esm({
52
52
  "src/version.generated.ts"() {
53
53
  "use strict";
54
- __version__ = "0.36.7";
54
+ __version__ = "0.36.9";
55
55
  }
56
56
  });
57
57
 
@@ -177,10 +177,11 @@ var init_errors = __esm({
177
177
  "src/errors.ts"() {
178
178
  "use strict";
179
179
  BitfabError = class extends Error {
180
- constructor(message, url, status) {
180
+ constructor(message, url, status, retryAfterMs) {
181
181
  super(message);
182
182
  this.url = url;
183
183
  this.status = status;
184
+ this.retryAfterMs = retryAfterMs;
184
185
  this.name = "BitfabError";
185
186
  }
186
187
  };
@@ -535,6 +536,23 @@ var init_serializePayload = __esm({
535
536
  }
536
537
  });
537
538
 
539
+ // src/transportTypes.ts
540
+ var DeliveryError;
541
+ var init_transportTypes = __esm({
542
+ "src/transportTypes.ts"() {
543
+ "use strict";
544
+ DeliveryError = class extends Error {
545
+ constructor(message, options = {}) {
546
+ super(message);
547
+ this.name = "DeliveryError";
548
+ this.retryable = options.retryable ?? false;
549
+ this.oversized = options.oversized ?? false;
550
+ this.retryAfterMs = options.retryAfterMs;
551
+ }
552
+ };
553
+ }
554
+ });
555
+
538
556
  // src/unrefTimer.ts
539
557
  function unrefTimer(timer) {
540
558
  const handle = timer;
@@ -574,59 +592,6 @@ function logError(message, error) {
574
592
  } catch {
575
593
  }
576
594
  }
577
- function recordTraceSubmission(operation, payload) {
578
- const sourceTraceId = resolveSourceTraceId(payload);
579
- if (sourceTraceId === void 0) {
580
- return;
581
- }
582
- if (operation === "external_span") {
583
- const rawSpan = asRecord2(payload.rawSpan);
584
- if (typeof rawSpan?.id !== "string") {
585
- submissionCounter += 1;
586
- }
587
- const sourceSpanId = typeof rawSpan?.id === "string" ? rawSpan.id : `submission-${submissionCounter}`;
588
- const existing = traceSubmissionSpanIds.get(sourceTraceId);
589
- if (existing) {
590
- existing.add(sourceSpanId);
591
- } else {
592
- traceSubmissionSpanIds.set(sourceTraceId, /* @__PURE__ */ new Set([sourceSpanId]));
593
- }
594
- return;
595
- }
596
- if (payload.completed !== true) {
597
- return;
598
- }
599
- if (typeof payload.testRunId === "string") {
600
- replayTraceSubmissions.add(sourceTraceId);
601
- if (!traceSubmissionSpanIds.has(sourceTraceId)) {
602
- traceSubmissionSpanIds.set(sourceTraceId, /* @__PURE__ */ new Set());
603
- }
604
- } else {
605
- traceSubmissionSpanIds.delete(sourceTraceId);
606
- }
607
- }
608
- function takeReplaySpanCounts(traceIds) {
609
- const counts = {};
610
- for (const traceId of traceIds) {
611
- if (!replayTraceSubmissions.has(traceId)) {
612
- continue;
613
- }
614
- counts[traceId] = traceSubmissionSpanIds.get(traceId)?.size ?? 0;
615
- traceSubmissionSpanIds.delete(traceId);
616
- replayTraceSubmissions.delete(traceId);
617
- }
618
- return counts;
619
- }
620
- function asRecord2(value) {
621
- return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
622
- }
623
- function resolveSourceTraceId(payload) {
624
- if (typeof payload.sourceTraceId === "string") {
625
- return payload.sourceTraceId;
626
- }
627
- const rawTrace = asRecord2(payload.externalTrace) ?? asRecord2(payload.rawTrace);
628
- return typeof rawTrace?.id === "string" ? rawTrace.id : void 0;
629
- }
630
595
  function otlpValue(value) {
631
596
  if (typeof value === "boolean") {
632
597
  return { boolValue: value };
@@ -684,7 +649,11 @@ function spanToOtlp(span) {
684
649
  }
685
650
  function encodeSpan(span) {
686
651
  const json = JSON.stringify(spanToOtlp(span));
687
- return { json, size: byteLength(json) };
652
+ return {
653
+ json,
654
+ size: byteLength(json),
655
+ ref: carrierRefs.get(span)
656
+ };
688
657
  }
689
658
  function trimEncodedSpan(span) {
690
659
  try {
@@ -767,48 +736,27 @@ async function mapWithConcurrency(items, limit, task) {
767
736
  await Promise.all(workers);
768
737
  return results;
769
738
  }
770
- function responseStatus(error) {
771
- return error instanceof BitfabError ? error.status : void 0;
772
- }
773
739
  function isRetryable(error) {
774
- const status = responseStatus(error);
775
- if (status === void 0) {
776
- return true;
777
- }
778
- return RETRYABLE_STATUSES.has(status) || status >= 500;
740
+ return error instanceof DeliveryError && error.retryable;
779
741
  }
780
- function endSpan(span, endTime) {
781
- span.end(endTime);
742
+ function isOversized(error) {
743
+ return error instanceof DeliveryError && error.oversized;
782
744
  }
783
- function spanName(operation, payload) {
784
- if (operation === "external_span") {
785
- const spanData = asRecord2(asRecord2(payload.rawSpan)?.span_data);
786
- if (typeof spanData?.name === "string") {
787
- return spanData.name;
788
- }
745
+ function retryWaitMillis(error, attempt, remainingMillis) {
746
+ const requested = error instanceof DeliveryError ? error.retryAfterMs : void 0;
747
+ const affordable = remainingMillis / 2;
748
+ if (requested !== void 0) {
749
+ return requested < affordable ? requested : null;
789
750
  }
790
- if (typeof payload.traceFunctionKey === "string") {
791
- return payload.traceFunctionKey;
792
- }
793
- return `bitfab.${operation}`;
794
- }
795
- function payloadTimestamp(payload, field) {
796
- const rawSpan = asRecord2(payload.rawSpan);
797
- const rawTrace = asRecord2(payload.externalTrace) ?? asRecord2(payload.rawTrace);
798
- const raw = rawSpan?.[field] ?? rawTrace?.[field];
799
- if (typeof raw !== "string") {
800
- return void 0;
801
- }
802
- const parsed = Date.parse(raw);
803
- return Number.isNaN(parsed) ? void 0 : parsed;
751
+ const backoff = Math.min(
752
+ RETRY_BASE_DELAY_MILLIS * 2 ** attempt,
753
+ RETRY_BACKOFF_CEILING_MILLIS
754
+ );
755
+ const jittered = backoff / 2 + Math.random() * (backoff / 2);
756
+ return jittered < affordable ? jittered : null;
804
757
  }
805
- function hasError(payload) {
806
- const spanData = asRecord2(asRecord2(payload.rawSpan)?.span_data);
807
- if (spanData?.error != null) {
808
- return true;
809
- }
810
- const errors = payload.errors;
811
- return Array.isArray(errors) ? errors.length > 0 : Boolean(errors);
758
+ function endSpan(span, endTime) {
759
+ span.end(endTime);
812
760
  }
813
761
  function createOtelTransport(options) {
814
762
  return new OtelBatchTransport({
@@ -847,7 +795,7 @@ function shutdownOtelTransports(timeoutMs = DEFAULT_LIFECYCLE_TIMEOUT_MS) {
847
795
  (transport, remaining) => transport.shutdown(remaining)
848
796
  );
849
797
  }
850
- var import_api, import_core, import_resources, import_sdk_trace_base, OPERATION_ATTRIBUTE, PAYLOAD_ATTRIBUTE, OTLP_TRACES_ENDPOINT, MAX_EXPORT_REQUEST_BYTES, MAX_DECOMPRESSED_REQUEST_BYTES, MAX_REQUEST_BYTES_ENV, EXPORT_CONCURRENCY_ENV, MAX_QUEUE_SIZE, DIRECT_MAX_EXPORT_BATCH_SIZE, DIRECT_MAX_REQUEST_BATCH_SIZE, DEFAULT_EXPORT_CONCURRENCY, MAX_EXPORT_CONCURRENCY, SCHEDULE_DELAY_MILLIS, EXPORT_TIMEOUT_MILLIS, RETRY_DELAY_MILLIS, MAX_SEND_ATTEMPTS, DEFAULT_LIFECYCLE_TIMEOUT_MS, RETRYABLE_STATUSES, liveTransports, traceSubmissionSpanIds, replayTraceSubmissions, submissionCounter, SPAN_SEPARATOR_BYTES, OtlpPayloadTooLargeError, OtlpPartialSuccessError, BitfabSpanExporter, DeliveryTrackingExporter, OtelBatchTransport;
798
+ var import_api, import_core, import_resources, import_sdk_trace_base, OPERATION_ATTRIBUTE, PAYLOAD_ATTRIBUTE, MAX_EXPORT_REQUEST_BYTES, MAX_DECOMPRESSED_REQUEST_BYTES, MAX_REQUEST_BYTES_ENV, EXPORT_CONCURRENCY_ENV, MAX_QUEUE_SIZE, DIRECT_MAX_EXPORT_BATCH_SIZE, DIRECT_MAX_REQUEST_BATCH_SIZE, DEFAULT_EXPORT_CONCURRENCY, MAX_EXPORT_CONCURRENCY, SCHEDULE_DELAY_MILLIS, EXPORT_TIMEOUT_MILLIS, RETRY_BASE_DELAY_MILLIS, RETRY_BACKOFF_CEILING_MILLIS, MAX_SEND_ATTEMPTS, DEFAULT_LIFECYCLE_TIMEOUT_MS, liveTransports, carrierRefs, SPAN_SEPARATOR_BYTES, BitfabSpanExporter, DeliveryTrackingExporter, OtelBatchTransport;
851
799
  var init_otel = __esm({
852
800
  "src/otel.ts"() {
853
801
  "use strict";
@@ -861,11 +809,11 @@ var init_otel = __esm({
861
809
  init_payloadBudget();
862
810
  init_readEnv();
863
811
  init_serializePayload();
812
+ init_transportTypes();
864
813
  init_unrefTimer();
865
814
  init_warnOnce();
866
815
  OPERATION_ATTRIBUTE = "bitfab.operation";
867
816
  PAYLOAD_ATTRIBUTE = "bitfab.payload";
868
- OTLP_TRACES_ENDPOINT = "/api/sdk/otel/v1/traces";
869
817
  MAX_EXPORT_REQUEST_BYTES = 3e6;
870
818
  MAX_DECOMPRESSED_REQUEST_BYTES = 8e6;
871
819
  MAX_REQUEST_BYTES_ENV = "BITFAB_OTEL_MAX_REQUEST_BYTES";
@@ -877,25 +825,23 @@ var init_otel = __esm({
877
825
  MAX_EXPORT_CONCURRENCY = 64;
878
826
  SCHEDULE_DELAY_MILLIS = 5e3;
879
827
  EXPORT_TIMEOUT_MILLIS = 3e4;
880
- RETRY_DELAY_MILLIS = 100;
828
+ RETRY_BASE_DELAY_MILLIS = 100;
829
+ RETRY_BACKOFF_CEILING_MILLIS = 5e3;
881
830
  MAX_SEND_ATTEMPTS = 3;
882
831
  DEFAULT_LIFECYCLE_TIMEOUT_MS = 3e4;
883
- RETRYABLE_STATUSES = /* @__PURE__ */ new Set([408, 425, 429]);
884
832
  liveTransports = /* @__PURE__ */ new Set();
885
- traceSubmissionSpanIds = /* @__PURE__ */ new Map();
886
- replayTraceSubmissions = /* @__PURE__ */ new Set();
887
- submissionCounter = 0;
833
+ carrierRefs = /* @__PURE__ */ new WeakMap();
888
834
  SPAN_SEPARATOR_BYTES = 1;
889
- OtlpPayloadTooLargeError = class extends Error {
890
- };
891
- OtlpPartialSuccessError = class extends Error {
892
- };
893
835
  BitfabSpanExporter = class {
894
- constructor(directSender, maxRequestBytes, maxRequestBatchSize, exportConcurrency) {
836
+ constructor(directSender, maxRequestBytes, maxRequestBatchSize, exportConcurrency, onDelivered, exportTimeoutMillis = EXPORT_TIMEOUT_MILLIS) {
895
837
  this.directSender = directSender;
896
838
  this.maxRequestBytes = maxRequestBytes;
897
839
  this.maxRequestBatchSize = maxRequestBatchSize;
898
840
  this.exportConcurrency = exportConcurrency;
841
+ this.onDelivered = onDelivered;
842
+ this.exportTimeoutMillis = exportTimeoutMillis;
843
+ /** Epoch ms until which the server has asked this exporter to stay away. */
844
+ this.throttledUntil = 0;
899
845
  }
900
846
  export(spans, resultCallback) {
901
847
  void this.exportAsync(spans).then(
@@ -961,6 +907,7 @@ var init_otel = __esm({
961
907
  );
962
908
  if (prepared.wireBytes <= this.maxRequestBytes) {
963
909
  await this.sendWithRetries(prepared);
910
+ this.reportDelivered(batch.spans);
964
911
  return true;
965
912
  }
966
913
  }
@@ -988,15 +935,12 @@ var init_otel = __esm({
988
935
  alreadyTrimmed = true;
989
936
  }
990
937
  } catch (error) {
991
- if (error instanceof OtlpPayloadTooLargeError) {
938
+ if (isOversized(error)) {
992
939
  logError(
993
940
  batch.spans.length === 1 ? "a single OpenTelemetry span exceeded the ingestion request limit and could not be exported" : "an OpenTelemetry span batch exceeded the ingestion request limit and could not be exported"
994
941
  );
995
942
  return false;
996
943
  }
997
- if (error instanceof OtlpPartialSuccessError) {
998
- return false;
999
- }
1000
944
  logError("failed to export an OpenTelemetry span batch", error);
1001
945
  return false;
1002
946
  }
@@ -1014,37 +958,79 @@ var init_otel = __esm({
1014
958
  * the server does not yet understand. The fix is a client-supplied
1015
959
  * idempotency key that ingestion dedupes on.
1016
960
  */
961
+ /**
962
+ * Remember a throttle the server asked for, so the requests fanned out
963
+ * alongside this one respect it too. Delaying only the request that was
964
+ * refused leaves the other seven in the window hitting a server that just
965
+ * asked for room.
966
+ */
967
+ recordThrottle(error) {
968
+ const requested = error instanceof DeliveryError ? error.retryAfterMs : void 0;
969
+ if (requested !== void 0) {
970
+ this.throttledUntil = Math.max(
971
+ this.throttledUntil,
972
+ Date.now() + requested
973
+ );
974
+ }
975
+ }
976
+ /**
977
+ * Waits out an active throttle, or reports the batch undeliverable when the
978
+ * throttle outlasts what we are willing to hold it for. Either way nothing is
979
+ * sent while the server has asked us to stay away.
980
+ */
981
+ async awaitThrottle(deadline) {
982
+ const remaining = this.throttledUntil - Date.now();
983
+ if (remaining <= 0) {
984
+ return;
985
+ }
986
+ if (remaining >= (deadline - Date.now()) / 2) {
987
+ throw new DeliveryError(
988
+ `OTLP ingestion is throttled for another ${remaining}ms, longer than the export budget`
989
+ );
990
+ }
991
+ await delay(remaining);
992
+ }
1017
993
  async sendWithRetries(request) {
994
+ const deadline = Date.now() + this.exportTimeoutMillis;
1018
995
  for (let attempt = 0; attempt < MAX_SEND_ATTEMPTS; attempt += 1) {
1019
996
  try {
1020
- const response = await this.directSender(
1021
- OTLP_TRACES_ENDPOINT,
1022
- request,
1023
- EXPORT_TIMEOUT_MILLIS
1024
- );
1025
- const partialSuccess = asRecord2(response?.partialSuccess);
1026
- const rejected = partialSuccess?.rejectedSpans;
1027
- if (rejected !== void 0 && rejected !== "0" && rejected !== 0) {
1028
- logError(
1029
- `OTLP ingestion rejected ${rejected} span(s): ${partialSuccess?.errorMessage ?? "no reason provided"}`
1030
- );
1031
- throw new OtlpPartialSuccessError();
1032
- }
997
+ await this.awaitThrottle(deadline);
998
+ await this.directSender(request, Math.max(0, deadline - Date.now()));
1033
999
  return;
1034
1000
  } catch (error) {
1035
- if (error instanceof OtlpPartialSuccessError) {
1001
+ if (isOversized(error)) {
1036
1002
  throw error;
1037
1003
  }
1038
- if (responseStatus(error) === 413) {
1039
- throw new OtlpPayloadTooLargeError();
1040
- }
1004
+ this.recordThrottle(error);
1041
1005
  if (attempt === MAX_SEND_ATTEMPTS - 1 || !isRetryable(error)) {
1042
1006
  throw error;
1043
1007
  }
1044
- await delay(RETRY_DELAY_MILLIS);
1008
+ const wait = retryWaitMillis(error, attempt, deadline - Date.now());
1009
+ if (wait === null) {
1010
+ throw error;
1011
+ }
1012
+ await delay(wait);
1045
1013
  }
1046
1014
  }
1047
1015
  }
1016
+ /**
1017
+ * Announce the carriers a request delivered. Wrapped because a listener that
1018
+ * throws must never turn a delivered batch into a failed export.
1019
+ */
1020
+ reportDelivered(spans) {
1021
+ if (this.onDelivered === void 0) {
1022
+ return;
1023
+ }
1024
+ const refs = spans.map((span) => span.ref).filter((ref) => ref !== void 0);
1025
+ if (refs.length === 0) {
1026
+ return;
1027
+ }
1028
+ try {
1029
+ this.onDelivered(refs);
1030
+ } catch (error) {
1031
+ logError("a delivery listener threw", error);
1032
+ }
1033
+ }
1048
1034
  async shutdown() {
1049
1035
  }
1050
1036
  async forceFlush() {
@@ -1102,7 +1088,9 @@ var init_otel = __esm({
1102
1088
  options.directSender,
1103
1089
  maxRequestBytes,
1104
1090
  maxRequestBatchSize,
1105
- options.exportConcurrency ?? DEFAULT_EXPORT_CONCURRENCY
1091
+ options.exportConcurrency ?? DEFAULT_EXPORT_CONCURRENCY,
1092
+ options.onDelivered,
1093
+ options.exportTimeoutMillis ?? EXPORT_TIMEOUT_MILLIS
1106
1094
  )
1107
1095
  );
1108
1096
  this.processor = new import_sdk_trace_base.BatchSpanProcessor(this.deliveryTracker, {
@@ -1126,8 +1114,7 @@ var init_otel = __esm({
1126
1114
  this.tracer = this.provider.getTracer("bitfab", __version__);
1127
1115
  liveTransports.add(this);
1128
1116
  }
1129
- submit(operation, payload) {
1130
- recordTraceSubmission(operation, payload);
1117
+ submit(operation, payload, meta = {}) {
1131
1118
  if (this.closed) {
1132
1119
  warnOnce(
1133
1120
  "otel-submit-after-shutdown",
@@ -1148,17 +1135,20 @@ var init_otel = __esm({
1148
1135
  ].join(", ")}); they were stubbed so the span still ships, but the trace may be incomplete or not replayable.`
1149
1136
  );
1150
1137
  }
1151
- const span = this.tracer.startSpan(spanName(operation, payload), {
1138
+ const span = this.tracer.startSpan(meta.name ?? `bitfab.${operation}`, {
1152
1139
  attributes: {
1153
1140
  [OPERATION_ATTRIBUTE]: operation,
1154
1141
  [PAYLOAD_ATTRIBUTE]: body
1155
1142
  },
1156
- startTime: payloadTimestamp(payload, "started_at")
1143
+ startTime: meta.startTime
1157
1144
  });
1158
- if (hasError(payload)) {
1145
+ if (meta.ref !== void 0) {
1146
+ carrierRefs.set(span, meta.ref);
1147
+ }
1148
+ if (meta.errored === true) {
1159
1149
  span.setStatus({ code: import_api.SpanStatusCode.ERROR });
1160
1150
  }
1161
- endSpan(span, payloadTimestamp(payload, "ended_at"));
1151
+ endSpan(span, meta.endTime);
1162
1152
  } catch (error) {
1163
1153
  logError("failed to queue an OpenTelemetry span", error);
1164
1154
  }
@@ -1208,9 +1198,6 @@ function flushTraceTransports(timeoutMs) {
1208
1198
  function shutdownTraceTransports(timeoutMs) {
1209
1199
  return shutdownOtelTransports(timeoutMs);
1210
1200
  }
1211
- function takeReplaySpanCounts2(traceIds) {
1212
- return takeReplaySpanCounts(traceIds);
1213
- }
1214
1201
  var init_transport = __esm({
1215
1202
  "src/transport.ts"() {
1216
1203
  "use strict";
@@ -1259,7 +1246,96 @@ async function waitForPromises(promises, timeoutMs) {
1259
1246
  }
1260
1247
  }
1261
1248
  }
1262
- var REPLAY_DB_BRANCH_REQUEST_TIMEOUT_MS, EXIT_FLUSH_TIMEOUT_MS, DEFAULT_LIFECYCLE_TIMEOUT_MS2, pendingTracePromises, HttpClient;
1249
+ function readHeader(response, name) {
1250
+ try {
1251
+ return response.headers?.get(name) ?? null;
1252
+ } catch {
1253
+ return null;
1254
+ }
1255
+ }
1256
+ function parseRetryAfterMs(header) {
1257
+ const value = header?.trim();
1258
+ if (!value) {
1259
+ return void 0;
1260
+ }
1261
+ const seconds = Number(value);
1262
+ if (Number.isFinite(seconds)) {
1263
+ return seconds >= 0 ? seconds * 1e3 : void 0;
1264
+ }
1265
+ const at = Date.parse(value);
1266
+ if (Number.isNaN(at)) {
1267
+ return void 0;
1268
+ }
1269
+ return Math.max(0, at - Date.now());
1270
+ }
1271
+ function carrierMeta(operation, payload, ref) {
1272
+ return {
1273
+ ref,
1274
+ name: carrierName(operation, payload),
1275
+ startTime: payloadTimestamp(payload, "started_at"),
1276
+ endTime: payloadTimestamp(payload, "ended_at"),
1277
+ errored: payloadHasError(payload)
1278
+ };
1279
+ }
1280
+ function carrierName(operation, payload) {
1281
+ if (operation === "external_span") {
1282
+ const spanData = asPayloadRecord(
1283
+ asPayloadRecord(payload.rawSpan)?.span_data
1284
+ );
1285
+ if (typeof spanData?.name === "string") {
1286
+ return spanData.name;
1287
+ }
1288
+ }
1289
+ if (typeof payload.traceFunctionKey === "string") {
1290
+ return payload.traceFunctionKey;
1291
+ }
1292
+ return `bitfab.${operation}`;
1293
+ }
1294
+ function payloadTimestamp(payload, field) {
1295
+ const rawSpan = asPayloadRecord(payload.rawSpan);
1296
+ const rawTrace = asPayloadRecord(payload.externalTrace) ?? asPayloadRecord(payload.rawTrace);
1297
+ const raw = rawSpan?.[field] ?? rawTrace?.[field];
1298
+ if (typeof raw !== "string") {
1299
+ return void 0;
1300
+ }
1301
+ const parsed = Date.parse(raw);
1302
+ return Number.isNaN(parsed) ? void 0 : parsed;
1303
+ }
1304
+ function payloadHasError(payload) {
1305
+ const spanData = asPayloadRecord(asPayloadRecord(payload.rawSpan)?.span_data);
1306
+ if (spanData?.error != null) {
1307
+ return true;
1308
+ }
1309
+ const errors = payload.errors;
1310
+ return Array.isArray(errors) ? errors.length > 0 : Boolean(errors);
1311
+ }
1312
+ function asPayloadRecord(value) {
1313
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
1314
+ }
1315
+ function carrierRef(payload) {
1316
+ const traceId = sourceTraceIdOf(payload);
1317
+ if (traceId === void 0) {
1318
+ return void 0;
1319
+ }
1320
+ const rawSpan = payload.rawSpan;
1321
+ if (rawSpan === void 0) {
1322
+ return { traceId };
1323
+ }
1324
+ const spanId = rawSpan?.id;
1325
+ return {
1326
+ traceId,
1327
+ spanId: typeof spanId === "string" ? spanId : `submission-${++carrierSeq}`
1328
+ };
1329
+ }
1330
+ function sourceTraceIdOf(payload) {
1331
+ if (typeof payload.sourceTraceId === "string") {
1332
+ return payload.sourceTraceId;
1333
+ }
1334
+ const rawTrace = payload.externalTrace ?? payload.rawTrace;
1335
+ const id = rawTrace?.id;
1336
+ return typeof id === "string" ? id : void 0;
1337
+ }
1338
+ var REPLAY_DB_BRANCH_REQUEST_TIMEOUT_MS, OTLP_TRACES_ENDPOINT, RETRYABLE_STATUSES, EXIT_FLUSH_TIMEOUT_MS, DEFAULT_LIFECYCLE_TIMEOUT_MS2, pendingTracePromises, carrierSeq, HttpClient;
1263
1339
  var init_http = __esm({
1264
1340
  "src/http.ts"() {
1265
1341
  "use strict";
@@ -1269,9 +1345,12 @@ var init_http = __esm({
1269
1345
  init_replayContext();
1270
1346
  init_serializePayload();
1271
1347
  init_transport();
1348
+ init_transportTypes();
1272
1349
  init_unrefTimer();
1273
1350
  init_warnOnce();
1274
1351
  REPLAY_DB_BRANCH_REQUEST_TIMEOUT_MS = 3e5;
1352
+ OTLP_TRACES_ENDPOINT = "/api/sdk/otel/v1/traces";
1353
+ RETRYABLE_STATUSES = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
1275
1354
  EXIT_FLUSH_TIMEOUT_MS = 5e3;
1276
1355
  DEFAULT_LIFECYCLE_TIMEOUT_MS2 = 3e4;
1277
1356
  pendingTracePromises = /* @__PURE__ */ new Set();
@@ -1291,8 +1370,12 @@ var init_http = __esm({
1291
1370
  });
1292
1371
  });
1293
1372
  }
1373
+ carrierSeq = 0;
1294
1374
  HttpClient = class {
1295
1375
  constructor(config) {
1376
+ // Only traces a caller asked about are tracked, so ordinary tracing stores
1377
+ // nothing here.
1378
+ this.traceDeliveries = /* @__PURE__ */ new Map();
1296
1379
  // Deferred span work owned by THIS client. The module-global set backs the
1297
1380
  // process-wide `flushTraces()` and the exit hook, but per-client lifecycle
1298
1381
  // must not wait on another client's slow finalize: a false `close()` failure
@@ -1328,13 +1411,131 @@ var init_http = __esm({
1328
1411
  }
1329
1412
  if (!this.traceTransport) {
1330
1413
  this.traceTransport = createTraceTransport({
1331
- directSender: (endpoint, request, timeoutMs) => this.sendPrepared(endpoint, request, {
1332
- timeout: timeoutMs
1333
- })
1414
+ directSender: (request, timeoutMs) => this.deliverCarriers(request, timeoutMs),
1415
+ onDelivered: (refs) => this.recordDeliveredCarriers(refs)
1334
1416
  });
1335
1417
  }
1336
1418
  return this.traceTransport;
1337
1419
  }
1420
+ /**
1421
+ * Post one encoded batch and decide what the server's answer means, so the
1422
+ * transport never reads a response. Rejections and permanent statuses come
1423
+ * back as a non-retryable {@link DeliveryError}; anything the server might
1424
+ * still accept on a second try comes back retryable.
1425
+ */
1426
+ async deliverCarriers(request, timeoutMs) {
1427
+ let response;
1428
+ try {
1429
+ response = await this.sendPrepared(
1430
+ OTLP_TRACES_ENDPOINT,
1431
+ request,
1432
+ { timeout: timeoutMs }
1433
+ );
1434
+ } catch (error) {
1435
+ const status = error instanceof BitfabError ? error.status : void 0;
1436
+ if (status === void 0) {
1437
+ throw new DeliveryError(`OTLP ingestion failed: ${String(error)}`, {
1438
+ retryable: true
1439
+ });
1440
+ }
1441
+ throw new DeliveryError(`OTLP ingestion failed with HTTP ${status}`, {
1442
+ retryable: RETRYABLE_STATUSES.has(status),
1443
+ oversized: status === 413,
1444
+ ...error instanceof BitfabError && error.retryAfterMs !== void 0 ? { retryAfterMs: error.retryAfterMs } : {}
1445
+ });
1446
+ }
1447
+ const partialSuccess = asPayloadRecord(response?.partialSuccess);
1448
+ const rejected = partialSuccess?.rejectedSpans;
1449
+ if (rejected !== void 0 && rejected !== "0" && rejected !== 0) {
1450
+ throw new DeliveryError(
1451
+ `OTLP ingestion rejected ${rejected} span(s): ${partialSuccess?.errorMessage ?? "no reason provided"}`
1452
+ );
1453
+ }
1454
+ }
1455
+ /**
1456
+ * Start tracking delivery for `traceIds`. Nothing is recorded for a trace
1457
+ * that was never tracked, so ordinary tracing costs no bookkeeping at all.
1458
+ */
1459
+ trackTraceDeliveries(traceIds) {
1460
+ for (const traceId of traceIds) {
1461
+ if (!this.traceDeliveries.has(traceId)) {
1462
+ this.traceDeliveries.set(traceId, {
1463
+ submittedSpanIds: /* @__PURE__ */ new Set(),
1464
+ ackedSpanIds: /* @__PURE__ */ new Set(),
1465
+ closed: false,
1466
+ closingAcked: false
1467
+ });
1468
+ }
1469
+ }
1470
+ }
1471
+ /** Whether any tracked trace has had its closing carrier submitted. */
1472
+ hasClosedDeliveries(traceIds) {
1473
+ return traceIds.some((traceId) => this.traceDeliveries.get(traceId)?.closed);
1474
+ }
1475
+ /**
1476
+ * Report what each tracked trace submitted and whether the server confirmed
1477
+ * it, and stop tracking them. Every id passed is freed, so a caller cannot
1478
+ * leak a record for a trace that never closed.
1479
+ *
1480
+ * `delivered` is only meaningful once a flush has settled: acks land before
1481
+ * an export resolves, so a flush that reported success has already collected
1482
+ * every ack it is going to collect.
1483
+ */
1484
+ takeTraceDeliveries(traceIds) {
1485
+ const reports = {};
1486
+ for (const traceId of traceIds) {
1487
+ const delivery = this.traceDeliveries.get(traceId);
1488
+ if (delivery === void 0) {
1489
+ continue;
1490
+ }
1491
+ this.traceDeliveries.delete(traceId);
1492
+ reports[traceId] = {
1493
+ spanCount: delivery.submittedSpanIds.size,
1494
+ closed: delivery.closed,
1495
+ delivered: delivery.closingAcked && [...delivery.submittedSpanIds].every(
1496
+ (spanId) => delivery.ackedSpanIds.has(spanId)
1497
+ )
1498
+ };
1499
+ }
1500
+ return reports;
1501
+ }
1502
+ /** Build a carrier's meta and record what it adds to its trace's expected set. */
1503
+ recordedMeta(operation, payload, ref) {
1504
+ this.recordSubmittedCarrier(ref);
1505
+ return carrierMeta(operation, payload, ref);
1506
+ }
1507
+ recordSubmittedCarrier(ref) {
1508
+ if (ref === void 0) {
1509
+ return;
1510
+ }
1511
+ const delivery = this.traceDeliveries.get(ref.traceId);
1512
+ if (delivery === void 0) {
1513
+ return;
1514
+ }
1515
+ if (ref.spanId === void 0) {
1516
+ delivery.closed = true;
1517
+ } else {
1518
+ delivery.submittedSpanIds.add(ref.spanId);
1519
+ }
1520
+ }
1521
+ /**
1522
+ * Ingestion commits every carrier in a request before it answers, so a
1523
+ * delivered ref is proof its row exists: the same fact the replay status
1524
+ * endpoint would report, already in hand.
1525
+ */
1526
+ recordDeliveredCarriers(refs) {
1527
+ for (const ref of refs) {
1528
+ const delivery = this.traceDeliveries.get(ref.traceId);
1529
+ if (delivery === void 0) {
1530
+ continue;
1531
+ }
1532
+ if (ref.spanId === void 0) {
1533
+ delivery.closingAcked = true;
1534
+ } else {
1535
+ delivery.ackedSpanIds.add(ref.spanId);
1536
+ }
1537
+ }
1538
+ }
1338
1539
  /**
1339
1540
  * Track deferred span work so this client's own lifecycle waits for it, and
1340
1541
  * so the process-wide flush and exit hook do too.
@@ -1444,7 +1645,8 @@ var init_http = __esm({
1444
1645
  throw new BitfabError(
1445
1646
  `HTTP ${response.status}: ${errorText.slice(0, 500)}`,
1446
1647
  void 0,
1447
- response.status
1648
+ response.status,
1649
+ parseRetryAfterMs(readHeader(response, "retry-after"))
1448
1650
  );
1449
1651
  }
1450
1652
  const result = await response.json();
@@ -1530,11 +1732,12 @@ var init_http = __esm({
1530
1732
  * the OTLP carrier has no path to carry it.
1531
1733
  */
1532
1734
  sendInternalTrace(functionId, payload) {
1533
- this.getTraceTransport()?.submit("internal_trace", {
1534
- ...payload,
1535
- functionId,
1536
- sdkVersion: __version__
1537
- });
1735
+ const body = { ...payload, functionId, sdkVersion: __version__ };
1736
+ this.getTraceTransport()?.submit(
1737
+ "internal_trace",
1738
+ body,
1739
+ carrierMeta("internal_trace", body, void 0)
1740
+ );
1538
1741
  }
1539
1742
  /**
1540
1743
  * Queue an external span (from withSpan wrapper or OpenAI tracing) onto this
@@ -1543,10 +1746,11 @@ var init_http = __esm({
1543
1746
  * promise.
1544
1747
  */
1545
1748
  sendExternalSpan(payload) {
1546
- this.getTraceTransport()?.submit("external_span", {
1547
- ...payload,
1548
- sdkVersion: __version__
1549
- });
1749
+ this.getTraceTransport()?.submit(
1750
+ "external_span",
1751
+ { ...payload, sdkVersion: __version__ },
1752
+ this.recordedMeta("external_span", payload, carrierRef(payload))
1753
+ );
1550
1754
  }
1551
1755
  /**
1552
1756
  * Queue an external trace completion (from OpenAI tracing) onto this
@@ -1555,10 +1759,15 @@ var init_http = __esm({
1555
1759
  * server-authoritative barrier in `replay.ts`, not by awaiting this call.
1556
1760
  */
1557
1761
  sendExternalTrace(payload) {
1558
- this.getTraceTransport()?.submit("external_trace", {
1559
- ...payload,
1560
- sdkVersion: __version__
1561
- });
1762
+ this.getTraceTransport()?.submit(
1763
+ "external_trace",
1764
+ { ...payload, sdkVersion: __version__ },
1765
+ this.recordedMeta(
1766
+ "external_trace",
1767
+ payload,
1768
+ payload.completed === true ? carrierRef(payload) : void 0
1769
+ )
1770
+ );
1562
1771
  }
1563
1772
  /**
1564
1773
  * Partial update of an existing trace identified by its Bitfab trace ID.
@@ -2154,7 +2363,9 @@ __export(replay_exports, {
2154
2363
  ReplayError: () => ReplayError,
2155
2364
  replay: () => replay,
2156
2365
  reportReplayProgress: () => reportReplayProgress,
2157
- serializeReplayResult: () => serializeReplayResult
2366
+ serializeReplayResult: () => serializeReplayResult,
2367
+ sleepForReplayPersistence: () => sleepForReplayPersistence,
2368
+ waitForReplayPersistence: () => waitForReplayPersistence
2158
2369
  });
2159
2370
  function dbBranchEnabled(dbBranch) {
2160
2371
  return dbBranch !== void 0 && dbBranch !== false;
@@ -2442,15 +2653,29 @@ async function waitForReplayPersistence(httpClient, testRunId, replayedTraceIds)
2442
2653
  REPLAY_PERSISTENCE_TIMEOUT_MS
2443
2654
  );
2444
2655
  if (!deferredSettled) {
2656
+ httpClient.takeTraceDeliveries(replayedTraceIds);
2445
2657
  throw new BitfabError(
2446
2658
  `Replay could not settle deferred span work before the deadline, so the expected span counts are incomplete (testRunId ${testRunId}).`
2447
2659
  );
2448
2660
  }
2449
- const expectedSpanCounts = takeReplaySpanCounts2(replayedTraceIds);
2450
- if (Object.keys(expectedSpanCounts).length === 0) {
2661
+ if (!httpClient.hasClosedDeliveries(replayedTraceIds)) {
2662
+ httpClient.takeTraceDeliveries(replayedTraceIds);
2451
2663
  return;
2452
2664
  }
2453
2665
  const flushed = await flushTraces(REPLAY_PERSISTENCE_TIMEOUT_MS);
2666
+ const deliveries = httpClient.takeTraceDeliveries(replayedTraceIds);
2667
+ const expectedSpanCounts = {};
2668
+ let allDelivered = true;
2669
+ for (const [traceId, delivery] of Object.entries(deliveries)) {
2670
+ if (!delivery.closed) {
2671
+ continue;
2672
+ }
2673
+ expectedSpanCounts[traceId] = delivery.spanCount;
2674
+ allDelivered = allDelivered && delivery.delivered;
2675
+ }
2676
+ if (allDelivered) {
2677
+ return;
2678
+ }
2454
2679
  const deadline = Date.now() + REPLAY_PERSISTENCE_TIMEOUT_MS;
2455
2680
  let missing = Object.keys(expectedSpanCounts).length;
2456
2681
  while (true) {
@@ -2468,17 +2693,18 @@ async function waitForReplayPersistence(httpClient, testRunId, replayedTraceIds)
2468
2693
  if (Date.now() >= deadline) {
2469
2694
  break;
2470
2695
  }
2471
- await sleep(Math.min(100, Math.max(0, deadline - Date.now())));
2696
+ await sleepForReplayPersistence(
2697
+ Math.min(100, Math.max(0, deadline - Date.now()))
2698
+ );
2472
2699
  }
2473
2700
  const cause = flushed ? "" : " Delivery was also not confirmed before the flush deadline, so the spans likely never reached the server.";
2474
2701
  throw new BitfabError(
2475
2702
  `Replay traces were not fully persisted before the delivery deadline (testRunId ${testRunId}, missing ${missing} of ${Object.keys(expectedSpanCounts).length} trace(s)).${cause}`
2476
2703
  );
2477
2704
  }
2478
- function sleep(ms) {
2705
+ function sleepForReplayPersistence(ms) {
2479
2706
  return new Promise((resolve) => {
2480
- const timer = setTimeout(resolve, ms);
2481
- unrefTimer(timer);
2707
+ setTimeout(resolve, ms);
2482
2708
  });
2483
2709
  }
2484
2710
  async function mapWithConcurrency2(tasks, maxConcurrency, onSettled, onStarted) {
@@ -2559,6 +2785,7 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
2559
2785
  ...registeredOverrides
2560
2786
  ];
2561
2787
  const replayedTraceIds = serverItems.map(() => randomUuid());
2788
+ httpClient.trackTraceDeliveries(replayedTraceIds);
2562
2789
  const tasks = serverItems.map(
2563
2790
  (serverItem, index) => () => processItem(
2564
2791
  httpClient,
@@ -2756,8 +2983,6 @@ var init_replay = __esm({
2756
2983
  init_randomUuid();
2757
2984
  init_replayContext();
2758
2985
  init_serialize();
2759
- init_transport();
2760
- init_unrefTimer();
2761
2986
  REPLAY_PERSISTENCE_TIMEOUT_MS = 3e4;
2762
2987
  BITFAB_PROGRESS_PREFIX = "@@bitfab:progress ";
2763
2988
  ReplayError = class extends BitfabError {