@bitfab/sdk 0.36.6 → 0.36.8

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
@@ -42,7 +42,7 @@ var __version__;
42
42
  var init_version_generated = __esm({
43
43
  "src/version.generated.ts"() {
44
44
  "use strict";
45
- __version__ = "0.36.6";
45
+ __version__ = "0.36.8";
46
46
  }
47
47
  });
48
48
 
@@ -76,33 +76,58 @@ function toArrayBuffer(view) {
76
76
  view.byteOffset + view.byteLength
77
77
  );
78
78
  }
79
+ function compressedRequest(body, rawBytes, compressed) {
80
+ if (compressed.byteLength >= rawBytes) {
81
+ return { body, rawBytes, wireBytes: rawBytes };
82
+ }
83
+ return {
84
+ body: compressed instanceof Uint8Array ? toArrayBuffer(compressed) : compressed,
85
+ contentEncoding: "gzip",
86
+ rawBytes,
87
+ wireBytes: compressed.byteLength
88
+ };
89
+ }
79
90
  async function gzipViaStream(bytes) {
80
91
  const stream = new Blob([bytes]).stream().pipeThrough(new CompressionStream("gzip"));
81
92
  return await new Response(stream).arrayBuffer();
82
93
  }
83
94
  function encodeRequestBody(body) {
84
95
  if (readEnv(DISABLE_COMPRESSION_ENV)) {
85
- return { body };
96
+ const rawBytes = new TextEncoder().encode(body).byteLength;
97
+ return { body, rawBytes, wireBytes: rawBytes };
86
98
  }
87
99
  const bytes = new TextEncoder().encode(body);
88
100
  if (bytes.byteLength < MIN_COMPRESSED_BYTES) {
89
- return { body };
101
+ return {
102
+ body,
103
+ rawBytes: bytes.byteLength,
104
+ wireBytes: bytes.byteLength
105
+ };
90
106
  }
91
107
  if (gzipNode) {
92
108
  return gzipNode(bytes).then(
93
- (compressed) => ({
94
- body: toArrayBuffer(compressed),
95
- contentEncoding: "gzip"
96
- }),
97
- () => ({ body })
109
+ (compressed) => compressedRequest(body, bytes.byteLength, compressed),
110
+ () => ({
111
+ body,
112
+ rawBytes: bytes.byteLength,
113
+ wireBytes: bytes.byteLength
114
+ })
98
115
  );
99
116
  }
100
117
  if (typeof CompressionStream === "undefined") {
101
- return { body };
118
+ return {
119
+ body,
120
+ rawBytes: bytes.byteLength,
121
+ wireBytes: bytes.byteLength
122
+ };
102
123
  }
103
124
  return gzipViaStream(bytes).then(
104
- (compressed) => ({ body: compressed, contentEncoding: "gzip" }),
105
- () => ({ body })
125
+ (compressed) => compressedRequest(body, bytes.byteLength, compressed),
126
+ () => ({
127
+ body,
128
+ rawBytes: bytes.byteLength,
129
+ wireBytes: bytes.byteLength
130
+ })
106
131
  );
107
132
  }
108
133
  var DISABLE_COMPRESSION_ENV, MIN_COMPRESSED_BYTES, gzipNode, _nodeGzipReady;
@@ -143,10 +168,11 @@ var init_errors = __esm({
143
168
  "src/errors.ts"() {
144
169
  "use strict";
145
170
  BitfabError = class extends Error {
146
- constructor(message, url, status) {
171
+ constructor(message, url, status, retryAfterMs) {
147
172
  super(message);
148
173
  this.url = url;
149
174
  this.status = status;
175
+ this.retryAfterMs = retryAfterMs;
150
176
  this.name = "BitfabError";
151
177
  }
152
178
  };
@@ -247,15 +273,15 @@ function carrierBytesOf(encoded, body) {
247
273
  }
248
274
  return encoded.length + extra;
249
275
  }
250
- function fitsCarrierBudget(body) {
276
+ function fitsCarrierBudget(body, maxBytes = MAX_SPAN_CARRIER_BYTES) {
251
277
  const units = body.length;
252
- if (units * MAX_BYTES_PER_UNIT + 2 <= MAX_SPAN_CARRIER_BYTES) {
278
+ if (units * MAX_BYTES_PER_UNIT + 2 <= maxBytes) {
253
279
  return true;
254
280
  }
255
- if (units + 2 > MAX_SPAN_CARRIER_BYTES) {
281
+ if (units + 2 > maxBytes) {
256
282
  return false;
257
283
  }
258
- return carrierByteLength(body) <= MAX_SPAN_CARRIER_BYTES;
284
+ return carrierByteLength(body) <= maxBytes;
259
285
  }
260
286
  function asRecord(value) {
261
287
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
@@ -299,7 +325,7 @@ function collectCandidates(containers) {
299
325
  }
300
326
  return candidates.sort((a, b) => b.size - a.size);
301
327
  }
302
- function trimPayloadToBudget(payload, encode) {
328
+ function trimPayloadToBudget(payload, encode, maxBytes = MAX_SPAN_CARRIER_BYTES) {
303
329
  const { copy, containers } = cloneTrimmable(payload);
304
330
  const candidates = collectCandidates(containers);
305
331
  if (candidates.length === 0) {
@@ -315,30 +341,31 @@ function trimPayloadToBudget(payload, encode) {
315
341
  } catch {
316
342
  return void 0;
317
343
  }
318
- if (fitsCarrierBudget(body)) {
344
+ if (fitsCarrierBudget(body, maxBytes)) {
319
345
  return { value: copy, trimmed };
320
346
  }
321
347
  }
322
348
  return void 0;
323
349
  }
324
- function markPayloadTrimmed(value, trimmed) {
350
+ function markPayloadTrimmed(value, trimmed, maxBytes = MAX_SPAN_CARRIER_BYTES) {
325
351
  const existing = Array.isArray(value.errors) ? value.errors : [];
326
352
  value.errors = [
327
353
  ...existing,
328
354
  {
329
355
  source: "sdk",
330
356
  step: "payload_budget",
331
- error: `trimmed oversized field(s) to fit the ${MAX_SPAN_CARRIER_BYTES}-byte span carrier budget: ${[
357
+ error: `trimmed oversized field(s) to fit the ${maxBytes}-byte span carrier budget: ${[
332
358
  ...new Set(trimmed)
333
359
  ].join(", ")}`
334
360
  }
335
361
  ];
336
362
  }
337
- var MAX_SPAN_CARRIER_BYTES, textEncoder, MAX_BYTES_PER_UNIT, STRUCTURAL_SPAN_KEYS;
363
+ var MAX_SPAN_CARRIER_BYTES, MAX_COMPRESSIBLE_SPAN_CARRIER_BYTES, textEncoder, MAX_BYTES_PER_UNIT, STRUCTURAL_SPAN_KEYS;
338
364
  var init_payloadBudget = __esm({
339
365
  "src/payloadBudget.ts"() {
340
366
  "use strict";
341
367
  MAX_SPAN_CARRIER_BYTES = 28e5;
368
+ MAX_COMPRESSIBLE_SPAN_CARRIER_BYTES = 78e5;
342
369
  textEncoder = typeof TextEncoder !== "undefined" ? new TextEncoder() : null;
343
370
  MAX_BYTES_PER_UNIT = 3;
344
371
  STRUCTURAL_SPAN_KEYS = /* @__PURE__ */ new Set([
@@ -370,30 +397,31 @@ var init_warnOnce = __esm({
370
397
  });
371
398
 
372
399
  // src/serializePayload.ts
373
- function serializePayloadBody(payload) {
400
+ function serializePayloadBody(payload, maxCarrierBytes = MAX_SPAN_CARRIER_BYTES) {
374
401
  const encoded = encodePayloadBody(payload);
375
- if (fitsCarrierBudget(encoded.body)) {
402
+ if (fitsCarrierBudget(encoded.body, maxCarrierBytes)) {
376
403
  return { body: encoded.body, dropped: encoded.dropped };
377
404
  }
378
- return applyPayloadBudget(encoded);
405
+ return applyPayloadBudget(encoded, maxCarrierBytes);
379
406
  }
380
- function applyPayloadBudget(encoded) {
407
+ function applyPayloadBudget(encoded, maxCarrierBytes) {
381
408
  const result = encoded.value ? trimPayloadToBudget(
382
409
  encoded.value,
383
- (value) => encodePayloadBody(value).body
410
+ (value) => encodePayloadBody(value).body,
411
+ maxCarrierBytes
384
412
  ) : void 0;
385
413
  if (!result) {
386
414
  return { body: encoded.body, dropped: encoded.dropped };
387
415
  }
388
416
  warnOnce(
389
417
  "payload:over-budget",
390
- `a span payload exceeded the ${MAX_SPAN_CARRIER_BYTES}-byte carrier budget; its largest field(s) (${[
418
+ `a span payload exceeded the ${maxCarrierBytes}-byte carrier budget; its largest field(s) (${[
391
419
  ...new Set(result.trimmed)
392
420
  ].join(
393
421
  ", "
394
422
  )}) were replaced with placeholders so the span still ships. The span is incomplete and may not be replayable.`
395
423
  );
396
- markPayloadTrimmed(result.value, result.trimmed);
424
+ markPayloadTrimmed(result.value, result.trimmed, maxCarrierBytes);
397
425
  return {
398
426
  body: encodePayloadBody(result.value).body,
399
427
  dropped: encoded.dropped
@@ -499,6 +527,23 @@ var init_serializePayload = __esm({
499
527
  }
500
528
  });
501
529
 
530
+ // src/transportTypes.ts
531
+ var DeliveryError;
532
+ var init_transportTypes = __esm({
533
+ "src/transportTypes.ts"() {
534
+ "use strict";
535
+ DeliveryError = class extends Error {
536
+ constructor(message, options = {}) {
537
+ super(message);
538
+ this.name = "DeliveryError";
539
+ this.retryable = options.retryable ?? false;
540
+ this.oversized = options.oversized ?? false;
541
+ this.retryAfterMs = options.retryAfterMs;
542
+ }
543
+ };
544
+ }
545
+ });
546
+
502
547
  // src/unrefTimer.ts
503
548
  function unrefTimer(timer) {
504
549
  const handle = timer;
@@ -538,59 +583,6 @@ function logError(message, error) {
538
583
  } catch {
539
584
  }
540
585
  }
541
- function recordTraceSubmission(operation, payload) {
542
- const sourceTraceId = resolveSourceTraceId(payload);
543
- if (sourceTraceId === void 0) {
544
- return;
545
- }
546
- if (operation === "external_span") {
547
- const rawSpan = asRecord2(payload.rawSpan);
548
- if (typeof rawSpan?.id !== "string") {
549
- submissionCounter += 1;
550
- }
551
- const sourceSpanId = typeof rawSpan?.id === "string" ? rawSpan.id : `submission-${submissionCounter}`;
552
- const existing = traceSubmissionSpanIds.get(sourceTraceId);
553
- if (existing) {
554
- existing.add(sourceSpanId);
555
- } else {
556
- traceSubmissionSpanIds.set(sourceTraceId, /* @__PURE__ */ new Set([sourceSpanId]));
557
- }
558
- return;
559
- }
560
- if (payload.completed !== true) {
561
- return;
562
- }
563
- if (typeof payload.testRunId === "string") {
564
- replayTraceSubmissions.add(sourceTraceId);
565
- if (!traceSubmissionSpanIds.has(sourceTraceId)) {
566
- traceSubmissionSpanIds.set(sourceTraceId, /* @__PURE__ */ new Set());
567
- }
568
- } else {
569
- traceSubmissionSpanIds.delete(sourceTraceId);
570
- }
571
- }
572
- function takeReplaySpanCounts(traceIds) {
573
- const counts = {};
574
- for (const traceId of traceIds) {
575
- if (!replayTraceSubmissions.has(traceId)) {
576
- continue;
577
- }
578
- counts[traceId] = traceSubmissionSpanIds.get(traceId)?.size ?? 0;
579
- traceSubmissionSpanIds.delete(traceId);
580
- replayTraceSubmissions.delete(traceId);
581
- }
582
- return counts;
583
- }
584
- function asRecord2(value) {
585
- return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
586
- }
587
- function resolveSourceTraceId(payload) {
588
- if (typeof payload.sourceTraceId === "string") {
589
- return payload.sourceTraceId;
590
- }
591
- const rawTrace = asRecord2(payload.externalTrace) ?? asRecord2(payload.rawTrace);
592
- return typeof rawTrace?.id === "string" ? rawTrace.id : void 0;
593
- }
594
586
  function otlpValue(value) {
595
587
  if (typeof value === "boolean") {
596
588
  return { boolValue: value };
@@ -648,7 +640,36 @@ function spanToOtlp(span) {
648
640
  }
649
641
  function encodeSpan(span) {
650
642
  const json = JSON.stringify(spanToOtlp(span));
651
- return { json, size: byteLength(json) };
643
+ return {
644
+ json,
645
+ size: byteLength(json),
646
+ ref: carrierRefs.get(span)
647
+ };
648
+ }
649
+ function trimEncodedSpan(span) {
650
+ try {
651
+ const carrier = JSON.parse(span.json);
652
+ const attribute = carrier.attributes?.find(
653
+ (entry) => entry.key === PAYLOAD_ATTRIBUTE
654
+ );
655
+ const payloadBody = attribute?.value?.stringValue;
656
+ if (!attribute?.value || payloadBody === void 0) {
657
+ return void 0;
658
+ }
659
+ const payload = JSON.parse(payloadBody);
660
+ attribute.value.stringValue = serializePayloadBody(
661
+ payload,
662
+ MAX_SPAN_CARRIER_BYTES
663
+ ).body;
664
+ const json = JSON.stringify(carrier);
665
+ return { json, size: byteLength(json) };
666
+ } catch {
667
+ return void 0;
668
+ }
669
+ }
670
+ async function prepareRequest(body) {
671
+ const prepared = encodeRequestBody(body);
672
+ return prepared instanceof Promise ? await prepared : prepared;
652
673
  }
653
674
  function requestEnvelope(first) {
654
675
  const scope = first.instrumentationScope;
@@ -706,48 +727,27 @@ async function mapWithConcurrency(items, limit, task) {
706
727
  await Promise.all(workers);
707
728
  return results;
708
729
  }
709
- function responseStatus(error) {
710
- return error instanceof BitfabError ? error.status : void 0;
711
- }
712
730
  function isRetryable(error) {
713
- const status = responseStatus(error);
714
- if (status === void 0) {
715
- return true;
716
- }
717
- return RETRYABLE_STATUSES.has(status) || status >= 500;
731
+ return error instanceof DeliveryError && error.retryable;
718
732
  }
719
- function endSpan(span, endTime) {
720
- span.end(endTime);
733
+ function isOversized(error) {
734
+ return error instanceof DeliveryError && error.oversized;
721
735
  }
722
- function spanName(operation, payload) {
723
- if (operation === "external_span") {
724
- const spanData = asRecord2(asRecord2(payload.rawSpan)?.span_data);
725
- if (typeof spanData?.name === "string") {
726
- return spanData.name;
727
- }
736
+ function retryWaitMillis(error, attempt, remainingMillis) {
737
+ const requested = error instanceof DeliveryError ? error.retryAfterMs : void 0;
738
+ const affordable = remainingMillis / 2;
739
+ if (requested !== void 0) {
740
+ return requested < affordable ? requested : null;
728
741
  }
729
- if (typeof payload.traceFunctionKey === "string") {
730
- return payload.traceFunctionKey;
731
- }
732
- return `bitfab.${operation}`;
733
- }
734
- function payloadTimestamp(payload, field) {
735
- const rawSpan = asRecord2(payload.rawSpan);
736
- const rawTrace = asRecord2(payload.externalTrace) ?? asRecord2(payload.rawTrace);
737
- const raw = rawSpan?.[field] ?? rawTrace?.[field];
738
- if (typeof raw !== "string") {
739
- return void 0;
740
- }
741
- const parsed = Date.parse(raw);
742
- return Number.isNaN(parsed) ? void 0 : parsed;
742
+ const backoff = Math.min(
743
+ RETRY_BASE_DELAY_MILLIS * 2 ** attempt,
744
+ RETRY_BACKOFF_CEILING_MILLIS
745
+ );
746
+ const jittered = backoff / 2 + Math.random() * (backoff / 2);
747
+ return jittered < affordable ? jittered : null;
743
748
  }
744
- function hasError(payload) {
745
- const spanData = asRecord2(asRecord2(payload.rawSpan)?.span_data);
746
- if (spanData?.error != null) {
747
- return true;
748
- }
749
- const errors = payload.errors;
750
- return Array.isArray(errors) ? errors.length > 0 : Boolean(errors);
749
+ function endSpan(span, endTime) {
750
+ span.end(endTime);
751
751
  }
752
752
  function createOtelTransport(options) {
753
753
  return new OtelBatchTransport({
@@ -786,7 +786,7 @@ function shutdownOtelTransports(timeoutMs = DEFAULT_LIFECYCLE_TIMEOUT_MS) {
786
786
  (transport, remaining) => transport.shutdown(remaining)
787
787
  );
788
788
  }
789
- var import_api, import_core, import_resources, import_sdk_trace_base, OPERATION_ATTRIBUTE, PAYLOAD_ATTRIBUTE, OTLP_TRACES_ENDPOINT, MAX_EXPORT_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;
789
+ 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;
790
790
  var init_otel = __esm({
791
791
  "src/otel.ts"() {
792
792
  "use strict";
@@ -794,17 +794,19 @@ var init_otel = __esm({
794
794
  import_core = require("@opentelemetry/core");
795
795
  import_resources = require("@opentelemetry/resources");
796
796
  import_sdk_trace_base = require("@opentelemetry/sdk-trace-base");
797
+ init_compress();
797
798
  init_constants();
798
799
  init_errors();
799
800
  init_payloadBudget();
800
801
  init_readEnv();
801
802
  init_serializePayload();
803
+ init_transportTypes();
802
804
  init_unrefTimer();
803
805
  init_warnOnce();
804
806
  OPERATION_ATTRIBUTE = "bitfab.operation";
805
807
  PAYLOAD_ATTRIBUTE = "bitfab.payload";
806
- OTLP_TRACES_ENDPOINT = "/api/sdk/otel/v1/traces";
807
808
  MAX_EXPORT_REQUEST_BYTES = 3e6;
809
+ MAX_DECOMPRESSED_REQUEST_BYTES = 8e6;
808
810
  MAX_REQUEST_BYTES_ENV = "BITFAB_OTEL_MAX_REQUEST_BYTES";
809
811
  EXPORT_CONCURRENCY_ENV = "BITFAB_OTEL_EXPORT_CONCURRENCY";
810
812
  MAX_QUEUE_SIZE = 8192;
@@ -814,25 +816,23 @@ var init_otel = __esm({
814
816
  MAX_EXPORT_CONCURRENCY = 64;
815
817
  SCHEDULE_DELAY_MILLIS = 5e3;
816
818
  EXPORT_TIMEOUT_MILLIS = 3e4;
817
- RETRY_DELAY_MILLIS = 100;
819
+ RETRY_BASE_DELAY_MILLIS = 100;
820
+ RETRY_BACKOFF_CEILING_MILLIS = 5e3;
818
821
  MAX_SEND_ATTEMPTS = 3;
819
822
  DEFAULT_LIFECYCLE_TIMEOUT_MS = 3e4;
820
- RETRYABLE_STATUSES = /* @__PURE__ */ new Set([408, 425, 429]);
821
823
  liveTransports = /* @__PURE__ */ new Set();
822
- traceSubmissionSpanIds = /* @__PURE__ */ new Map();
823
- replayTraceSubmissions = /* @__PURE__ */ new Set();
824
- submissionCounter = 0;
824
+ carrierRefs = /* @__PURE__ */ new WeakMap();
825
825
  SPAN_SEPARATOR_BYTES = 1;
826
- OtlpPayloadTooLargeError = class extends Error {
827
- };
828
- OtlpPartialSuccessError = class extends Error {
829
- };
830
826
  BitfabSpanExporter = class {
831
- constructor(directSender, maxRequestBytes, maxRequestBatchSize, exportConcurrency) {
827
+ constructor(directSender, maxRequestBytes, maxRequestBatchSize, exportConcurrency, onDelivered, exportTimeoutMillis = EXPORT_TIMEOUT_MILLIS) {
832
828
  this.directSender = directSender;
833
829
  this.maxRequestBytes = maxRequestBytes;
834
830
  this.maxRequestBatchSize = maxRequestBatchSize;
835
831
  this.exportConcurrency = exportConcurrency;
832
+ this.onDelivered = onDelivered;
833
+ this.exportTimeoutMillis = exportTimeoutMillis;
834
+ /** Epoch ms until which the server has asked this exporter to stay away. */
835
+ this.throttledUntil = 0;
836
836
  }
837
837
  export(spans, resultCallback) {
838
838
  void this.exportAsync(spans).then(
@@ -887,25 +887,51 @@ var init_otel = __esm({
887
887
  return batches;
888
888
  }
889
889
  async send(envelope, batch) {
890
- if (batch.size > this.maxRequestBytes) {
891
- logError(
892
- "a single OpenTelemetry span exceeded the configured request-size target and could not be exported"
893
- );
894
- return false;
895
- }
896
890
  try {
897
- await this.sendWithRetries(encodeRequest(envelope, batch.spans));
898
- return true;
891
+ let requestSpans = batch.spans;
892
+ let requestRawBytes = batch.size;
893
+ let alreadyTrimmed = false;
894
+ while (true) {
895
+ if (requestRawBytes <= MAX_DECOMPRESSED_REQUEST_BYTES) {
896
+ const prepared = await prepareRequest(
897
+ encodeRequest(envelope, requestSpans)
898
+ );
899
+ if (prepared.wireBytes <= this.maxRequestBytes) {
900
+ await this.sendWithRetries(prepared);
901
+ this.reportDelivered(batch.spans);
902
+ return true;
903
+ }
904
+ }
905
+ if (batch.spans.length !== 1) {
906
+ logError(
907
+ "an OpenTelemetry span batch exceeded the configured request-size target and could not be exported"
908
+ );
909
+ return false;
910
+ }
911
+ if (alreadyTrimmed) {
912
+ logError(
913
+ "a single OpenTelemetry span exceeded the configured request-size target after trimming"
914
+ );
915
+ return false;
916
+ }
917
+ const trimmed = trimEncodedSpan(batch.spans[0]);
918
+ if (!trimmed) {
919
+ logError(
920
+ "a single OpenTelemetry span exceeded the configured request-size target and could not be trimmed"
921
+ );
922
+ return false;
923
+ }
924
+ requestSpans = [trimmed];
925
+ requestRawBytes = envelope.size + trimmed.size;
926
+ alreadyTrimmed = true;
927
+ }
899
928
  } catch (error) {
900
- if (error instanceof OtlpPayloadTooLargeError) {
929
+ if (isOversized(error)) {
901
930
  logError(
902
931
  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"
903
932
  );
904
933
  return false;
905
934
  }
906
- if (error instanceof OtlpPartialSuccessError) {
907
- return false;
908
- }
909
935
  logError("failed to export an OpenTelemetry span batch", error);
910
936
  return false;
911
937
  }
@@ -923,37 +949,79 @@ var init_otel = __esm({
923
949
  * the server does not yet understand. The fix is a client-supplied
924
950
  * idempotency key that ingestion dedupes on.
925
951
  */
926
- async sendWithRetries(body) {
952
+ /**
953
+ * Remember a throttle the server asked for, so the requests fanned out
954
+ * alongside this one respect it too. Delaying only the request that was
955
+ * refused leaves the other seven in the window hitting a server that just
956
+ * asked for room.
957
+ */
958
+ recordThrottle(error) {
959
+ const requested = error instanceof DeliveryError ? error.retryAfterMs : void 0;
960
+ if (requested !== void 0) {
961
+ this.throttledUntil = Math.max(
962
+ this.throttledUntil,
963
+ Date.now() + requested
964
+ );
965
+ }
966
+ }
967
+ /**
968
+ * Waits out an active throttle, or reports the batch undeliverable when the
969
+ * throttle outlasts what we are willing to hold it for. Either way nothing is
970
+ * sent while the server has asked us to stay away.
971
+ */
972
+ async awaitThrottle(deadline) {
973
+ const remaining = this.throttledUntil - Date.now();
974
+ if (remaining <= 0) {
975
+ return;
976
+ }
977
+ if (remaining >= (deadline - Date.now()) / 2) {
978
+ throw new DeliveryError(
979
+ `OTLP ingestion is throttled for another ${remaining}ms, longer than the export budget`
980
+ );
981
+ }
982
+ await delay(remaining);
983
+ }
984
+ async sendWithRetries(request) {
985
+ const deadline = Date.now() + this.exportTimeoutMillis;
927
986
  for (let attempt = 0; attempt < MAX_SEND_ATTEMPTS; attempt += 1) {
928
987
  try {
929
- const response = await this.directSender(
930
- OTLP_TRACES_ENDPOINT,
931
- body,
932
- EXPORT_TIMEOUT_MILLIS
933
- );
934
- const partialSuccess = asRecord2(response?.partialSuccess);
935
- const rejected = partialSuccess?.rejectedSpans;
936
- if (rejected !== void 0 && rejected !== "0" && rejected !== 0) {
937
- logError(
938
- `OTLP ingestion rejected ${rejected} span(s): ${partialSuccess?.errorMessage ?? "no reason provided"}`
939
- );
940
- throw new OtlpPartialSuccessError();
941
- }
988
+ await this.awaitThrottle(deadline);
989
+ await this.directSender(request, Math.max(0, deadline - Date.now()));
942
990
  return;
943
991
  } catch (error) {
944
- if (error instanceof OtlpPartialSuccessError) {
992
+ if (isOversized(error)) {
945
993
  throw error;
946
994
  }
947
- if (responseStatus(error) === 413) {
948
- throw new OtlpPayloadTooLargeError();
949
- }
995
+ this.recordThrottle(error);
950
996
  if (attempt === MAX_SEND_ATTEMPTS - 1 || !isRetryable(error)) {
951
997
  throw error;
952
998
  }
953
- await delay(RETRY_DELAY_MILLIS);
999
+ const wait = retryWaitMillis(error, attempt, deadline - Date.now());
1000
+ if (wait === null) {
1001
+ throw error;
1002
+ }
1003
+ await delay(wait);
954
1004
  }
955
1005
  }
956
1006
  }
1007
+ /**
1008
+ * Announce the carriers a request delivered. Wrapped because a listener that
1009
+ * throws must never turn a delivered batch into a failed export.
1010
+ */
1011
+ reportDelivered(spans) {
1012
+ if (this.onDelivered === void 0) {
1013
+ return;
1014
+ }
1015
+ const refs = spans.map((span) => span.ref).filter((ref) => ref !== void 0);
1016
+ if (refs.length === 0) {
1017
+ return;
1018
+ }
1019
+ try {
1020
+ this.onDelivered(refs);
1021
+ } catch (error) {
1022
+ logError("a delivery listener threw", error);
1023
+ }
1024
+ }
957
1025
  async shutdown() {
958
1026
  }
959
1027
  async forceFlush() {
@@ -1011,7 +1079,9 @@ var init_otel = __esm({
1011
1079
  options.directSender,
1012
1080
  maxRequestBytes,
1013
1081
  maxRequestBatchSize,
1014
- options.exportConcurrency ?? DEFAULT_EXPORT_CONCURRENCY
1082
+ options.exportConcurrency ?? DEFAULT_EXPORT_CONCURRENCY,
1083
+ options.onDelivered,
1084
+ options.exportTimeoutMillis ?? EXPORT_TIMEOUT_MILLIS
1015
1085
  )
1016
1086
  );
1017
1087
  this.processor = new import_sdk_trace_base.BatchSpanProcessor(this.deliveryTracker, {
@@ -1035,8 +1105,7 @@ var init_otel = __esm({
1035
1105
  this.tracer = this.provider.getTracer("bitfab", __version__);
1036
1106
  liveTransports.add(this);
1037
1107
  }
1038
- submit(operation, payload) {
1039
- recordTraceSubmission(operation, payload);
1108
+ submit(operation, payload, meta = {}) {
1040
1109
  if (this.closed) {
1041
1110
  warnOnce(
1042
1111
  "otel-submit-after-shutdown",
@@ -1045,7 +1114,10 @@ var init_otel = __esm({
1045
1114
  return;
1046
1115
  }
1047
1116
  try {
1048
- const { body, dropped } = serializePayloadBody(payload);
1117
+ const { body, dropped } = serializePayloadBody(
1118
+ payload,
1119
+ MAX_COMPRESSIBLE_SPAN_CARRIER_BYTES
1120
+ );
1049
1121
  if (dropped.length > 0) {
1050
1122
  warnOnce(
1051
1123
  "otel-carrier-payload-stubbed",
@@ -1054,17 +1126,20 @@ var init_otel = __esm({
1054
1126
  ].join(", ")}); they were stubbed so the span still ships, but the trace may be incomplete or not replayable.`
1055
1127
  );
1056
1128
  }
1057
- const span = this.tracer.startSpan(spanName(operation, payload), {
1129
+ const span = this.tracer.startSpan(meta.name ?? `bitfab.${operation}`, {
1058
1130
  attributes: {
1059
1131
  [OPERATION_ATTRIBUTE]: operation,
1060
1132
  [PAYLOAD_ATTRIBUTE]: body
1061
1133
  },
1062
- startTime: payloadTimestamp(payload, "started_at")
1134
+ startTime: meta.startTime
1063
1135
  });
1064
- if (hasError(payload)) {
1136
+ if (meta.ref !== void 0) {
1137
+ carrierRefs.set(span, meta.ref);
1138
+ }
1139
+ if (meta.errored === true) {
1065
1140
  span.setStatus({ code: import_api.SpanStatusCode.ERROR });
1066
1141
  }
1067
- endSpan(span, payloadTimestamp(payload, "ended_at"));
1142
+ endSpan(span, meta.endTime);
1068
1143
  } catch (error) {
1069
1144
  logError("failed to queue an OpenTelemetry span", error);
1070
1145
  }
@@ -1114,9 +1189,6 @@ function flushTraceTransports(timeoutMs) {
1114
1189
  function shutdownTraceTransports(timeoutMs) {
1115
1190
  return shutdownOtelTransports(timeoutMs);
1116
1191
  }
1117
- function takeReplaySpanCounts2(traceIds) {
1118
- return takeReplaySpanCounts(traceIds);
1119
- }
1120
1192
  var init_transport = __esm({
1121
1193
  "src/transport.ts"() {
1122
1194
  "use strict";
@@ -1165,7 +1237,96 @@ async function waitForPromises(promises, timeoutMs) {
1165
1237
  }
1166
1238
  }
1167
1239
  }
1168
- var REPLAY_DB_BRANCH_REQUEST_TIMEOUT_MS, EXIT_FLUSH_TIMEOUT_MS, DEFAULT_LIFECYCLE_TIMEOUT_MS2, pendingTracePromises, HttpClient;
1240
+ function readHeader(response, name) {
1241
+ try {
1242
+ return response.headers?.get(name) ?? null;
1243
+ } catch {
1244
+ return null;
1245
+ }
1246
+ }
1247
+ function parseRetryAfterMs(header) {
1248
+ const value = header?.trim();
1249
+ if (!value) {
1250
+ return void 0;
1251
+ }
1252
+ const seconds = Number(value);
1253
+ if (Number.isFinite(seconds)) {
1254
+ return seconds >= 0 ? seconds * 1e3 : void 0;
1255
+ }
1256
+ const at = Date.parse(value);
1257
+ if (Number.isNaN(at)) {
1258
+ return void 0;
1259
+ }
1260
+ return Math.max(0, at - Date.now());
1261
+ }
1262
+ function carrierMeta(operation, payload, ref) {
1263
+ return {
1264
+ ref,
1265
+ name: carrierName(operation, payload),
1266
+ startTime: payloadTimestamp(payload, "started_at"),
1267
+ endTime: payloadTimestamp(payload, "ended_at"),
1268
+ errored: payloadHasError(payload)
1269
+ };
1270
+ }
1271
+ function carrierName(operation, payload) {
1272
+ if (operation === "external_span") {
1273
+ const spanData = asPayloadRecord(
1274
+ asPayloadRecord(payload.rawSpan)?.span_data
1275
+ );
1276
+ if (typeof spanData?.name === "string") {
1277
+ return spanData.name;
1278
+ }
1279
+ }
1280
+ if (typeof payload.traceFunctionKey === "string") {
1281
+ return payload.traceFunctionKey;
1282
+ }
1283
+ return `bitfab.${operation}`;
1284
+ }
1285
+ function payloadTimestamp(payload, field) {
1286
+ const rawSpan = asPayloadRecord(payload.rawSpan);
1287
+ const rawTrace = asPayloadRecord(payload.externalTrace) ?? asPayloadRecord(payload.rawTrace);
1288
+ const raw = rawSpan?.[field] ?? rawTrace?.[field];
1289
+ if (typeof raw !== "string") {
1290
+ return void 0;
1291
+ }
1292
+ const parsed = Date.parse(raw);
1293
+ return Number.isNaN(parsed) ? void 0 : parsed;
1294
+ }
1295
+ function payloadHasError(payload) {
1296
+ const spanData = asPayloadRecord(asPayloadRecord(payload.rawSpan)?.span_data);
1297
+ if (spanData?.error != null) {
1298
+ return true;
1299
+ }
1300
+ const errors = payload.errors;
1301
+ return Array.isArray(errors) ? errors.length > 0 : Boolean(errors);
1302
+ }
1303
+ function asPayloadRecord(value) {
1304
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
1305
+ }
1306
+ function carrierRef(payload) {
1307
+ const traceId = sourceTraceIdOf(payload);
1308
+ if (traceId === void 0) {
1309
+ return void 0;
1310
+ }
1311
+ const rawSpan = payload.rawSpan;
1312
+ if (rawSpan === void 0) {
1313
+ return { traceId };
1314
+ }
1315
+ const spanId = rawSpan?.id;
1316
+ return {
1317
+ traceId,
1318
+ spanId: typeof spanId === "string" ? spanId : `submission-${++carrierSeq}`
1319
+ };
1320
+ }
1321
+ function sourceTraceIdOf(payload) {
1322
+ if (typeof payload.sourceTraceId === "string") {
1323
+ return payload.sourceTraceId;
1324
+ }
1325
+ const rawTrace = payload.externalTrace ?? payload.rawTrace;
1326
+ const id = rawTrace?.id;
1327
+ return typeof id === "string" ? id : void 0;
1328
+ }
1329
+ var REPLAY_DB_BRANCH_REQUEST_TIMEOUT_MS, OTLP_TRACES_ENDPOINT, RETRYABLE_STATUSES, EXIT_FLUSH_TIMEOUT_MS, DEFAULT_LIFECYCLE_TIMEOUT_MS2, pendingTracePromises, carrierSeq, HttpClient;
1169
1330
  var init_http = __esm({
1170
1331
  "src/http.ts"() {
1171
1332
  "use strict";
@@ -1175,9 +1336,12 @@ var init_http = __esm({
1175
1336
  init_replayContext();
1176
1337
  init_serializePayload();
1177
1338
  init_transport();
1339
+ init_transportTypes();
1178
1340
  init_unrefTimer();
1179
1341
  init_warnOnce();
1180
1342
  REPLAY_DB_BRANCH_REQUEST_TIMEOUT_MS = 3e5;
1343
+ OTLP_TRACES_ENDPOINT = "/api/sdk/otel/v1/traces";
1344
+ RETRYABLE_STATUSES = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
1181
1345
  EXIT_FLUSH_TIMEOUT_MS = 5e3;
1182
1346
  DEFAULT_LIFECYCLE_TIMEOUT_MS2 = 3e4;
1183
1347
  pendingTracePromises = /* @__PURE__ */ new Set();
@@ -1197,8 +1361,12 @@ var init_http = __esm({
1197
1361
  });
1198
1362
  });
1199
1363
  }
1364
+ carrierSeq = 0;
1200
1365
  HttpClient = class {
1201
1366
  constructor(config) {
1367
+ // Only traces a caller asked about are tracked, so ordinary tracing stores
1368
+ // nothing here.
1369
+ this.traceDeliveries = /* @__PURE__ */ new Map();
1202
1370
  // Deferred span work owned by THIS client. The module-global set backs the
1203
1371
  // process-wide `flushTraces()` and the exit hook, but per-client lifecycle
1204
1372
  // must not wait on another client's slow finalize: a false `close()` failure
@@ -1234,13 +1402,131 @@ var init_http = __esm({
1234
1402
  }
1235
1403
  if (!this.traceTransport) {
1236
1404
  this.traceTransport = createTraceTransport({
1237
- directSender: (endpoint, body, timeoutMs) => this.sendEncoded(endpoint, body, {
1238
- timeout: timeoutMs
1239
- })
1405
+ directSender: (request, timeoutMs) => this.deliverCarriers(request, timeoutMs),
1406
+ onDelivered: (refs) => this.recordDeliveredCarriers(refs)
1240
1407
  });
1241
1408
  }
1242
1409
  return this.traceTransport;
1243
1410
  }
1411
+ /**
1412
+ * Post one encoded batch and decide what the server's answer means, so the
1413
+ * transport never reads a response. Rejections and permanent statuses come
1414
+ * back as a non-retryable {@link DeliveryError}; anything the server might
1415
+ * still accept on a second try comes back retryable.
1416
+ */
1417
+ async deliverCarriers(request, timeoutMs) {
1418
+ let response;
1419
+ try {
1420
+ response = await this.sendPrepared(
1421
+ OTLP_TRACES_ENDPOINT,
1422
+ request,
1423
+ { timeout: timeoutMs }
1424
+ );
1425
+ } catch (error) {
1426
+ const status = error instanceof BitfabError ? error.status : void 0;
1427
+ if (status === void 0) {
1428
+ throw new DeliveryError(`OTLP ingestion failed: ${String(error)}`, {
1429
+ retryable: true
1430
+ });
1431
+ }
1432
+ throw new DeliveryError(`OTLP ingestion failed with HTTP ${status}`, {
1433
+ retryable: RETRYABLE_STATUSES.has(status),
1434
+ oversized: status === 413,
1435
+ ...error instanceof BitfabError && error.retryAfterMs !== void 0 ? { retryAfterMs: error.retryAfterMs } : {}
1436
+ });
1437
+ }
1438
+ const partialSuccess = asPayloadRecord(response?.partialSuccess);
1439
+ const rejected = partialSuccess?.rejectedSpans;
1440
+ if (rejected !== void 0 && rejected !== "0" && rejected !== 0) {
1441
+ throw new DeliveryError(
1442
+ `OTLP ingestion rejected ${rejected} span(s): ${partialSuccess?.errorMessage ?? "no reason provided"}`
1443
+ );
1444
+ }
1445
+ }
1446
+ /**
1447
+ * Start tracking delivery for `traceIds`. Nothing is recorded for a trace
1448
+ * that was never tracked, so ordinary tracing costs no bookkeeping at all.
1449
+ */
1450
+ trackTraceDeliveries(traceIds) {
1451
+ for (const traceId of traceIds) {
1452
+ if (!this.traceDeliveries.has(traceId)) {
1453
+ this.traceDeliveries.set(traceId, {
1454
+ submittedSpanIds: /* @__PURE__ */ new Set(),
1455
+ ackedSpanIds: /* @__PURE__ */ new Set(),
1456
+ closed: false,
1457
+ closingAcked: false
1458
+ });
1459
+ }
1460
+ }
1461
+ }
1462
+ /** Whether any tracked trace has had its closing carrier submitted. */
1463
+ hasClosedDeliveries(traceIds) {
1464
+ return traceIds.some((traceId) => this.traceDeliveries.get(traceId)?.closed);
1465
+ }
1466
+ /**
1467
+ * Report what each tracked trace submitted and whether the server confirmed
1468
+ * it, and stop tracking them. Every id passed is freed, so a caller cannot
1469
+ * leak a record for a trace that never closed.
1470
+ *
1471
+ * `delivered` is only meaningful once a flush has settled: acks land before
1472
+ * an export resolves, so a flush that reported success has already collected
1473
+ * every ack it is going to collect.
1474
+ */
1475
+ takeTraceDeliveries(traceIds) {
1476
+ const reports = {};
1477
+ for (const traceId of traceIds) {
1478
+ const delivery = this.traceDeliveries.get(traceId);
1479
+ if (delivery === void 0) {
1480
+ continue;
1481
+ }
1482
+ this.traceDeliveries.delete(traceId);
1483
+ reports[traceId] = {
1484
+ spanCount: delivery.submittedSpanIds.size,
1485
+ closed: delivery.closed,
1486
+ delivered: delivery.closingAcked && [...delivery.submittedSpanIds].every(
1487
+ (spanId) => delivery.ackedSpanIds.has(spanId)
1488
+ )
1489
+ };
1490
+ }
1491
+ return reports;
1492
+ }
1493
+ /** Build a carrier's meta and record what it adds to its trace's expected set. */
1494
+ recordedMeta(operation, payload, ref) {
1495
+ this.recordSubmittedCarrier(ref);
1496
+ return carrierMeta(operation, payload, ref);
1497
+ }
1498
+ recordSubmittedCarrier(ref) {
1499
+ if (ref === void 0) {
1500
+ return;
1501
+ }
1502
+ const delivery = this.traceDeliveries.get(ref.traceId);
1503
+ if (delivery === void 0) {
1504
+ return;
1505
+ }
1506
+ if (ref.spanId === void 0) {
1507
+ delivery.closed = true;
1508
+ } else {
1509
+ delivery.submittedSpanIds.add(ref.spanId);
1510
+ }
1511
+ }
1512
+ /**
1513
+ * Ingestion commits every carrier in a request before it answers, so a
1514
+ * delivered ref is proof its row exists: the same fact the replay status
1515
+ * endpoint would report, already in hand.
1516
+ */
1517
+ recordDeliveredCarriers(refs) {
1518
+ for (const ref of refs) {
1519
+ const delivery = this.traceDeliveries.get(ref.traceId);
1520
+ if (delivery === void 0) {
1521
+ continue;
1522
+ }
1523
+ if (ref.spanId === void 0) {
1524
+ delivery.closingAcked = true;
1525
+ } else {
1526
+ delivery.ackedSpanIds.add(ref.spanId);
1527
+ }
1528
+ }
1529
+ }
1244
1530
  /**
1245
1531
  * Track deferred span work so this client's own lifecycle waits for it, and
1246
1532
  * so the process-wide flush and exit hook do too.
@@ -1321,13 +1607,16 @@ var init_http = __esm({
1321
1607
  * same data twice.
1322
1608
  */
1323
1609
  async sendEncoded(endpoint, body, options) {
1610
+ const prepared = encodeRequestBody(body);
1611
+ const encoded = prepared instanceof Promise ? await prepared : prepared;
1612
+ return this.sendPrepared(endpoint, encoded, options);
1613
+ }
1614
+ async sendPrepared(endpoint, encoded, options) {
1324
1615
  const url = `${this.serviceUrl}${endpoint}`;
1325
1616
  const timeout = options?.timeout ?? this.timeout;
1326
1617
  const method = options?.method ?? "POST";
1327
1618
  const controller = new AbortController();
1328
1619
  const timeoutId = setTimeout(() => controller.abort(), timeout);
1329
- const prepared = encodeRequestBody(body);
1330
- const encoded = prepared instanceof Promise ? await prepared : prepared;
1331
1620
  const headers = {
1332
1621
  "Content-Type": "application/json",
1333
1622
  Authorization: `Bearer ${this.resolveApiKey() ?? ""}`
@@ -1347,7 +1636,8 @@ var init_http = __esm({
1347
1636
  throw new BitfabError(
1348
1637
  `HTTP ${response.status}: ${errorText.slice(0, 500)}`,
1349
1638
  void 0,
1350
- response.status
1639
+ response.status,
1640
+ parseRetryAfterMs(readHeader(response, "retry-after"))
1351
1641
  );
1352
1642
  }
1353
1643
  const result = await response.json();
@@ -1433,11 +1723,12 @@ var init_http = __esm({
1433
1723
  * the OTLP carrier has no path to carry it.
1434
1724
  */
1435
1725
  sendInternalTrace(functionId, payload) {
1436
- this.getTraceTransport()?.submit("internal_trace", {
1437
- ...payload,
1438
- functionId,
1439
- sdkVersion: __version__
1440
- });
1726
+ const body = { ...payload, functionId, sdkVersion: __version__ };
1727
+ this.getTraceTransport()?.submit(
1728
+ "internal_trace",
1729
+ body,
1730
+ carrierMeta("internal_trace", body, void 0)
1731
+ );
1441
1732
  }
1442
1733
  /**
1443
1734
  * Queue an external span (from withSpan wrapper or OpenAI tracing) onto this
@@ -1446,10 +1737,11 @@ var init_http = __esm({
1446
1737
  * promise.
1447
1738
  */
1448
1739
  sendExternalSpan(payload) {
1449
- this.getTraceTransport()?.submit("external_span", {
1450
- ...payload,
1451
- sdkVersion: __version__
1452
- });
1740
+ this.getTraceTransport()?.submit(
1741
+ "external_span",
1742
+ { ...payload, sdkVersion: __version__ },
1743
+ this.recordedMeta("external_span", payload, carrierRef(payload))
1744
+ );
1453
1745
  }
1454
1746
  /**
1455
1747
  * Queue an external trace completion (from OpenAI tracing) onto this
@@ -1458,10 +1750,15 @@ var init_http = __esm({
1458
1750
  * server-authoritative barrier in `replay.ts`, not by awaiting this call.
1459
1751
  */
1460
1752
  sendExternalTrace(payload) {
1461
- this.getTraceTransport()?.submit("external_trace", {
1462
- ...payload,
1463
- sdkVersion: __version__
1464
- });
1753
+ this.getTraceTransport()?.submit(
1754
+ "external_trace",
1755
+ { ...payload, sdkVersion: __version__ },
1756
+ this.recordedMeta(
1757
+ "external_trace",
1758
+ payload,
1759
+ payload.completed === true ? carrierRef(payload) : void 0
1760
+ )
1761
+ );
1465
1762
  }
1466
1763
  /**
1467
1764
  * Partial update of an existing trace identified by its Bitfab trace ID.
@@ -1801,8 +2098,8 @@ var init_serialize = __esm({
1801
2098
  import_superjson = __toESM(require("superjson"), 1);
1802
2099
  init_payloadBudget();
1803
2100
  init_warnOnce();
1804
- MAX_SERIALIZED_BYTES = MAX_SPAN_CARRIER_BYTES;
1805
- MAX_FRAMEWORK_SERIALIZED_BYTES = MAX_SPAN_CARRIER_BYTES;
2101
+ MAX_SERIALIZED_BYTES = MAX_COMPRESSIBLE_SPAN_CARRIER_BYTES;
2102
+ MAX_FRAMEWORK_SERIALIZED_BYTES = MAX_COMPRESSIBLE_SPAN_CARRIER_BYTES;
1806
2103
  MAX_SAFE_DEPTH = 6;
1807
2104
  }
1808
2105
  });
@@ -2057,7 +2354,8 @@ __export(replay_exports, {
2057
2354
  ReplayError: () => ReplayError,
2058
2355
  replay: () => replay,
2059
2356
  reportReplayProgress: () => reportReplayProgress,
2060
- serializeReplayResult: () => serializeReplayResult
2357
+ serializeReplayResult: () => serializeReplayResult,
2358
+ waitForReplayPersistence: () => waitForReplayPersistence
2061
2359
  });
2062
2360
  function dbBranchEnabled(dbBranch) {
2063
2361
  return dbBranch !== void 0 && dbBranch !== false;
@@ -2345,15 +2643,29 @@ async function waitForReplayPersistence(httpClient, testRunId, replayedTraceIds)
2345
2643
  REPLAY_PERSISTENCE_TIMEOUT_MS
2346
2644
  );
2347
2645
  if (!deferredSettled) {
2646
+ httpClient.takeTraceDeliveries(replayedTraceIds);
2348
2647
  throw new BitfabError(
2349
2648
  `Replay could not settle deferred span work before the deadline, so the expected span counts are incomplete (testRunId ${testRunId}).`
2350
2649
  );
2351
2650
  }
2352
- const expectedSpanCounts = takeReplaySpanCounts2(replayedTraceIds);
2353
- if (Object.keys(expectedSpanCounts).length === 0) {
2651
+ if (!httpClient.hasClosedDeliveries(replayedTraceIds)) {
2652
+ httpClient.takeTraceDeliveries(replayedTraceIds);
2354
2653
  return;
2355
2654
  }
2356
2655
  const flushed = await flushTraces(REPLAY_PERSISTENCE_TIMEOUT_MS);
2656
+ const deliveries = httpClient.takeTraceDeliveries(replayedTraceIds);
2657
+ const expectedSpanCounts = {};
2658
+ let allDelivered = true;
2659
+ for (const [traceId, delivery] of Object.entries(deliveries)) {
2660
+ if (!delivery.closed) {
2661
+ continue;
2662
+ }
2663
+ expectedSpanCounts[traceId] = delivery.spanCount;
2664
+ allDelivered = allDelivered && delivery.delivered;
2665
+ }
2666
+ if (allDelivered) {
2667
+ return;
2668
+ }
2357
2669
  const deadline = Date.now() + REPLAY_PERSISTENCE_TIMEOUT_MS;
2358
2670
  let missing = Object.keys(expectedSpanCounts).length;
2359
2671
  while (true) {
@@ -2462,6 +2774,7 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
2462
2774
  ...registeredOverrides
2463
2775
  ];
2464
2776
  const replayedTraceIds = serverItems.map(() => randomUuid());
2777
+ httpClient.trackTraceDeliveries(replayedTraceIds);
2465
2778
  const tasks = serverItems.map(
2466
2779
  (serverItem, index) => () => processItem(
2467
2780
  httpClient,
@@ -2659,7 +2972,6 @@ var init_replay = __esm({
2659
2972
  init_randomUuid();
2660
2973
  init_replayContext();
2661
2974
  init_serialize();
2662
- init_transport();
2663
2975
  init_unrefTimer();
2664
2976
  REPLAY_PERSISTENCE_TIMEOUT_MS = 3e4;
2665
2977
  BITFAB_PROGRESS_PREFIX = "@@bitfab:progress ";