bitfab 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/node.cjs CHANGED
@@ -97,7 +97,7 @@ var __version__;
97
97
  var init_version_generated = __esm({
98
98
  "src/version.generated.ts"() {
99
99
  "use strict";
100
- __version__ = "0.36.6";
100
+ __version__ = "0.36.8";
101
101
  }
102
102
  });
103
103
 
@@ -131,33 +131,58 @@ function toArrayBuffer(view) {
131
131
  view.byteOffset + view.byteLength
132
132
  );
133
133
  }
134
+ function compressedRequest(body, rawBytes, compressed) {
135
+ if (compressed.byteLength >= rawBytes) {
136
+ return { body, rawBytes, wireBytes: rawBytes };
137
+ }
138
+ return {
139
+ body: compressed instanceof Uint8Array ? toArrayBuffer(compressed) : compressed,
140
+ contentEncoding: "gzip",
141
+ rawBytes,
142
+ wireBytes: compressed.byteLength
143
+ };
144
+ }
134
145
  async function gzipViaStream(bytes) {
135
146
  const stream = new Blob([bytes]).stream().pipeThrough(new CompressionStream("gzip"));
136
147
  return await new Response(stream).arrayBuffer();
137
148
  }
138
149
  function encodeRequestBody(body) {
139
150
  if (readEnv(DISABLE_COMPRESSION_ENV)) {
140
- return { body };
151
+ const rawBytes = new TextEncoder().encode(body).byteLength;
152
+ return { body, rawBytes, wireBytes: rawBytes };
141
153
  }
142
154
  const bytes = new TextEncoder().encode(body);
143
155
  if (bytes.byteLength < MIN_COMPRESSED_BYTES) {
144
- return { body };
156
+ return {
157
+ body,
158
+ rawBytes: bytes.byteLength,
159
+ wireBytes: bytes.byteLength
160
+ };
145
161
  }
146
162
  if (gzipNode) {
147
163
  return gzipNode(bytes).then(
148
- (compressed) => ({
149
- body: toArrayBuffer(compressed),
150
- contentEncoding: "gzip"
151
- }),
152
- () => ({ body })
164
+ (compressed) => compressedRequest(body, bytes.byteLength, compressed),
165
+ () => ({
166
+ body,
167
+ rawBytes: bytes.byteLength,
168
+ wireBytes: bytes.byteLength
169
+ })
153
170
  );
154
171
  }
155
172
  if (typeof CompressionStream === "undefined") {
156
- return { body };
173
+ return {
174
+ body,
175
+ rawBytes: bytes.byteLength,
176
+ wireBytes: bytes.byteLength
177
+ };
157
178
  }
158
179
  return gzipViaStream(bytes).then(
159
- (compressed) => ({ body: compressed, contentEncoding: "gzip" }),
160
- () => ({ body })
180
+ (compressed) => compressedRequest(body, bytes.byteLength, compressed),
181
+ () => ({
182
+ body,
183
+ rawBytes: bytes.byteLength,
184
+ wireBytes: bytes.byteLength
185
+ })
161
186
  );
162
187
  }
163
188
  var DISABLE_COMPRESSION_ENV, MIN_COMPRESSED_BYTES, gzipNode, _nodeGzipReady;
@@ -198,10 +223,11 @@ var init_errors = __esm({
198
223
  "src/errors.ts"() {
199
224
  "use strict";
200
225
  BitfabError = class extends Error {
201
- constructor(message, url, status) {
226
+ constructor(message, url, status, retryAfterMs) {
202
227
  super(message);
203
228
  this.url = url;
204
229
  this.status = status;
230
+ this.retryAfterMs = retryAfterMs;
205
231
  this.name = "BitfabError";
206
232
  }
207
233
  };
@@ -263,15 +289,15 @@ function carrierBytesOf(encoded, body) {
263
289
  }
264
290
  return encoded.length + extra;
265
291
  }
266
- function fitsCarrierBudget(body) {
292
+ function fitsCarrierBudget(body, maxBytes = MAX_SPAN_CARRIER_BYTES) {
267
293
  const units = body.length;
268
- if (units * MAX_BYTES_PER_UNIT + 2 <= MAX_SPAN_CARRIER_BYTES) {
294
+ if (units * MAX_BYTES_PER_UNIT + 2 <= maxBytes) {
269
295
  return true;
270
296
  }
271
- if (units + 2 > MAX_SPAN_CARRIER_BYTES) {
297
+ if (units + 2 > maxBytes) {
272
298
  return false;
273
299
  }
274
- return carrierByteLength(body) <= MAX_SPAN_CARRIER_BYTES;
300
+ return carrierByteLength(body) <= maxBytes;
275
301
  }
276
302
  function asRecord(value) {
277
303
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
@@ -315,7 +341,7 @@ function collectCandidates(containers) {
315
341
  }
316
342
  return candidates.sort((a, b) => b.size - a.size);
317
343
  }
318
- function trimPayloadToBudget(payload, encode) {
344
+ function trimPayloadToBudget(payload, encode, maxBytes = MAX_SPAN_CARRIER_BYTES) {
319
345
  const { copy, containers } = cloneTrimmable(payload);
320
346
  const candidates = collectCandidates(containers);
321
347
  if (candidates.length === 0) {
@@ -331,30 +357,31 @@ function trimPayloadToBudget(payload, encode) {
331
357
  } catch {
332
358
  return void 0;
333
359
  }
334
- if (fitsCarrierBudget(body)) {
360
+ if (fitsCarrierBudget(body, maxBytes)) {
335
361
  return { value: copy, trimmed };
336
362
  }
337
363
  }
338
364
  return void 0;
339
365
  }
340
- function markPayloadTrimmed(value, trimmed) {
366
+ function markPayloadTrimmed(value, trimmed, maxBytes = MAX_SPAN_CARRIER_BYTES) {
341
367
  const existing = Array.isArray(value.errors) ? value.errors : [];
342
368
  value.errors = [
343
369
  ...existing,
344
370
  {
345
371
  source: "sdk",
346
372
  step: "payload_budget",
347
- error: `trimmed oversized field(s) to fit the ${MAX_SPAN_CARRIER_BYTES}-byte span carrier budget: ${[
373
+ error: `trimmed oversized field(s) to fit the ${maxBytes}-byte span carrier budget: ${[
348
374
  ...new Set(trimmed)
349
375
  ].join(", ")}`
350
376
  }
351
377
  ];
352
378
  }
353
- var MAX_SPAN_CARRIER_BYTES, textEncoder, MAX_BYTES_PER_UNIT, STRUCTURAL_SPAN_KEYS;
379
+ var MAX_SPAN_CARRIER_BYTES, MAX_COMPRESSIBLE_SPAN_CARRIER_BYTES, textEncoder, MAX_BYTES_PER_UNIT, STRUCTURAL_SPAN_KEYS;
354
380
  var init_payloadBudget = __esm({
355
381
  "src/payloadBudget.ts"() {
356
382
  "use strict";
357
383
  MAX_SPAN_CARRIER_BYTES = 28e5;
384
+ MAX_COMPRESSIBLE_SPAN_CARRIER_BYTES = 78e5;
358
385
  textEncoder = typeof TextEncoder !== "undefined" ? new TextEncoder() : null;
359
386
  MAX_BYTES_PER_UNIT = 3;
360
387
  STRUCTURAL_SPAN_KEYS = /* @__PURE__ */ new Set([
@@ -386,30 +413,31 @@ var init_warnOnce = __esm({
386
413
  });
387
414
 
388
415
  // src/serializePayload.ts
389
- function serializePayloadBody(payload) {
416
+ function serializePayloadBody(payload, maxCarrierBytes = MAX_SPAN_CARRIER_BYTES) {
390
417
  const encoded = encodePayloadBody(payload);
391
- if (fitsCarrierBudget(encoded.body)) {
418
+ if (fitsCarrierBudget(encoded.body, maxCarrierBytes)) {
392
419
  return { body: encoded.body, dropped: encoded.dropped };
393
420
  }
394
- return applyPayloadBudget(encoded);
421
+ return applyPayloadBudget(encoded, maxCarrierBytes);
395
422
  }
396
- function applyPayloadBudget(encoded) {
423
+ function applyPayloadBudget(encoded, maxCarrierBytes) {
397
424
  const result = encoded.value ? trimPayloadToBudget(
398
425
  encoded.value,
399
- (value) => encodePayloadBody(value).body
426
+ (value) => encodePayloadBody(value).body,
427
+ maxCarrierBytes
400
428
  ) : void 0;
401
429
  if (!result) {
402
430
  return { body: encoded.body, dropped: encoded.dropped };
403
431
  }
404
432
  warnOnce(
405
433
  "payload:over-budget",
406
- `a span payload exceeded the ${MAX_SPAN_CARRIER_BYTES}-byte carrier budget; its largest field(s) (${[
434
+ `a span payload exceeded the ${maxCarrierBytes}-byte carrier budget; its largest field(s) (${[
407
435
  ...new Set(result.trimmed)
408
436
  ].join(
409
437
  ", "
410
438
  )}) were replaced with placeholders so the span still ships. The span is incomplete and may not be replayable.`
411
439
  );
412
- markPayloadTrimmed(result.value, result.trimmed);
440
+ markPayloadTrimmed(result.value, result.trimmed, maxCarrierBytes);
413
441
  return {
414
442
  body: encodePayloadBody(result.value).body,
415
443
  dropped: encoded.dropped
@@ -515,6 +543,23 @@ var init_serializePayload = __esm({
515
543
  }
516
544
  });
517
545
 
546
+ // src/transportTypes.ts
547
+ var DeliveryError;
548
+ var init_transportTypes = __esm({
549
+ "src/transportTypes.ts"() {
550
+ "use strict";
551
+ DeliveryError = class extends Error {
552
+ constructor(message, options = {}) {
553
+ super(message);
554
+ this.name = "DeliveryError";
555
+ this.retryable = options.retryable ?? false;
556
+ this.oversized = options.oversized ?? false;
557
+ this.retryAfterMs = options.retryAfterMs;
558
+ }
559
+ };
560
+ }
561
+ });
562
+
518
563
  // src/unrefTimer.ts
519
564
  function unrefTimer(timer) {
520
565
  const handle = timer;
@@ -554,59 +599,6 @@ function logError(message, error) {
554
599
  } catch {
555
600
  }
556
601
  }
557
- function recordTraceSubmission(operation, payload) {
558
- const sourceTraceId = resolveSourceTraceId(payload);
559
- if (sourceTraceId === void 0) {
560
- return;
561
- }
562
- if (operation === "external_span") {
563
- const rawSpan = asRecord2(payload.rawSpan);
564
- if (typeof rawSpan?.id !== "string") {
565
- submissionCounter += 1;
566
- }
567
- const sourceSpanId = typeof rawSpan?.id === "string" ? rawSpan.id : `submission-${submissionCounter}`;
568
- const existing = traceSubmissionSpanIds.get(sourceTraceId);
569
- if (existing) {
570
- existing.add(sourceSpanId);
571
- } else {
572
- traceSubmissionSpanIds.set(sourceTraceId, /* @__PURE__ */ new Set([sourceSpanId]));
573
- }
574
- return;
575
- }
576
- if (payload.completed !== true) {
577
- return;
578
- }
579
- if (typeof payload.testRunId === "string") {
580
- replayTraceSubmissions.add(sourceTraceId);
581
- if (!traceSubmissionSpanIds.has(sourceTraceId)) {
582
- traceSubmissionSpanIds.set(sourceTraceId, /* @__PURE__ */ new Set());
583
- }
584
- } else {
585
- traceSubmissionSpanIds.delete(sourceTraceId);
586
- }
587
- }
588
- function takeReplaySpanCounts(traceIds) {
589
- const counts = {};
590
- for (const traceId of traceIds) {
591
- if (!replayTraceSubmissions.has(traceId)) {
592
- continue;
593
- }
594
- counts[traceId] = traceSubmissionSpanIds.get(traceId)?.size ?? 0;
595
- traceSubmissionSpanIds.delete(traceId);
596
- replayTraceSubmissions.delete(traceId);
597
- }
598
- return counts;
599
- }
600
- function asRecord2(value) {
601
- return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
602
- }
603
- function resolveSourceTraceId(payload) {
604
- if (typeof payload.sourceTraceId === "string") {
605
- return payload.sourceTraceId;
606
- }
607
- const rawTrace = asRecord2(payload.externalTrace) ?? asRecord2(payload.rawTrace);
608
- return typeof rawTrace?.id === "string" ? rawTrace.id : void 0;
609
- }
610
602
  function otlpValue(value) {
611
603
  if (typeof value === "boolean") {
612
604
  return { boolValue: value };
@@ -664,7 +656,36 @@ function spanToOtlp(span) {
664
656
  }
665
657
  function encodeSpan(span) {
666
658
  const json = JSON.stringify(spanToOtlp(span));
667
- return { json, size: byteLength(json) };
659
+ return {
660
+ json,
661
+ size: byteLength(json),
662
+ ref: carrierRefs.get(span)
663
+ };
664
+ }
665
+ function trimEncodedSpan(span) {
666
+ try {
667
+ const carrier = JSON.parse(span.json);
668
+ const attribute = carrier.attributes?.find(
669
+ (entry) => entry.key === PAYLOAD_ATTRIBUTE
670
+ );
671
+ const payloadBody = attribute?.value?.stringValue;
672
+ if (!attribute?.value || payloadBody === void 0) {
673
+ return void 0;
674
+ }
675
+ const payload = JSON.parse(payloadBody);
676
+ attribute.value.stringValue = serializePayloadBody(
677
+ payload,
678
+ MAX_SPAN_CARRIER_BYTES
679
+ ).body;
680
+ const json = JSON.stringify(carrier);
681
+ return { json, size: byteLength(json) };
682
+ } catch {
683
+ return void 0;
684
+ }
685
+ }
686
+ async function prepareRequest(body) {
687
+ const prepared = encodeRequestBody(body);
688
+ return prepared instanceof Promise ? await prepared : prepared;
668
689
  }
669
690
  function requestEnvelope(first) {
670
691
  const scope = first.instrumentationScope;
@@ -722,48 +743,27 @@ async function mapWithConcurrency(items, limit, task) {
722
743
  await Promise.all(workers);
723
744
  return results;
724
745
  }
725
- function responseStatus(error) {
726
- return error instanceof BitfabError ? error.status : void 0;
727
- }
728
746
  function isRetryable(error) {
729
- const status = responseStatus(error);
730
- if (status === void 0) {
731
- return true;
732
- }
733
- return RETRYABLE_STATUSES.has(status) || status >= 500;
747
+ return error instanceof DeliveryError && error.retryable;
734
748
  }
735
- function endSpan(span, endTime) {
736
- span.end(endTime);
749
+ function isOversized(error) {
750
+ return error instanceof DeliveryError && error.oversized;
737
751
  }
738
- function spanName(operation, payload) {
739
- if (operation === "external_span") {
740
- const spanData = asRecord2(asRecord2(payload.rawSpan)?.span_data);
741
- if (typeof spanData?.name === "string") {
742
- return spanData.name;
743
- }
752
+ function retryWaitMillis(error, attempt, remainingMillis) {
753
+ const requested = error instanceof DeliveryError ? error.retryAfterMs : void 0;
754
+ const affordable = remainingMillis / 2;
755
+ if (requested !== void 0) {
756
+ return requested < affordable ? requested : null;
744
757
  }
745
- if (typeof payload.traceFunctionKey === "string") {
746
- return payload.traceFunctionKey;
747
- }
748
- return `bitfab.${operation}`;
749
- }
750
- function payloadTimestamp(payload, field) {
751
- const rawSpan = asRecord2(payload.rawSpan);
752
- const rawTrace = asRecord2(payload.externalTrace) ?? asRecord2(payload.rawTrace);
753
- const raw = rawSpan?.[field] ?? rawTrace?.[field];
754
- if (typeof raw !== "string") {
755
- return void 0;
756
- }
757
- const parsed = Date.parse(raw);
758
- return Number.isNaN(parsed) ? void 0 : parsed;
758
+ const backoff = Math.min(
759
+ RETRY_BASE_DELAY_MILLIS * 2 ** attempt,
760
+ RETRY_BACKOFF_CEILING_MILLIS
761
+ );
762
+ const jittered = backoff / 2 + Math.random() * (backoff / 2);
763
+ return jittered < affordable ? jittered : null;
759
764
  }
760
- function hasError(payload) {
761
- const spanData = asRecord2(asRecord2(payload.rawSpan)?.span_data);
762
- if (spanData?.error != null) {
763
- return true;
764
- }
765
- const errors = payload.errors;
766
- return Array.isArray(errors) ? errors.length > 0 : Boolean(errors);
765
+ function endSpan(span, endTime) {
766
+ span.end(endTime);
767
767
  }
768
768
  function createOtelTransport(options) {
769
769
  return new OtelBatchTransport({
@@ -802,7 +802,7 @@ function shutdownOtelTransports(timeoutMs = DEFAULT_LIFECYCLE_TIMEOUT_MS) {
802
802
  (transport, remaining) => transport.shutdown(remaining)
803
803
  );
804
804
  }
805
- 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;
805
+ 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;
806
806
  var init_otel = __esm({
807
807
  "src/otel.ts"() {
808
808
  "use strict";
@@ -810,17 +810,19 @@ var init_otel = __esm({
810
810
  import_core = require("@opentelemetry/core");
811
811
  import_resources = require("@opentelemetry/resources");
812
812
  import_sdk_trace_base = require("@opentelemetry/sdk-trace-base");
813
+ init_compress();
813
814
  init_constants();
814
815
  init_errors();
815
816
  init_payloadBudget();
816
817
  init_readEnv();
817
818
  init_serializePayload();
819
+ init_transportTypes();
818
820
  init_unrefTimer();
819
821
  init_warnOnce();
820
822
  OPERATION_ATTRIBUTE = "bitfab.operation";
821
823
  PAYLOAD_ATTRIBUTE = "bitfab.payload";
822
- OTLP_TRACES_ENDPOINT = "/api/sdk/otel/v1/traces";
823
824
  MAX_EXPORT_REQUEST_BYTES = 3e6;
825
+ MAX_DECOMPRESSED_REQUEST_BYTES = 8e6;
824
826
  MAX_REQUEST_BYTES_ENV = "BITFAB_OTEL_MAX_REQUEST_BYTES";
825
827
  EXPORT_CONCURRENCY_ENV = "BITFAB_OTEL_EXPORT_CONCURRENCY";
826
828
  MAX_QUEUE_SIZE = 8192;
@@ -830,25 +832,23 @@ var init_otel = __esm({
830
832
  MAX_EXPORT_CONCURRENCY = 64;
831
833
  SCHEDULE_DELAY_MILLIS = 5e3;
832
834
  EXPORT_TIMEOUT_MILLIS = 3e4;
833
- RETRY_DELAY_MILLIS = 100;
835
+ RETRY_BASE_DELAY_MILLIS = 100;
836
+ RETRY_BACKOFF_CEILING_MILLIS = 5e3;
834
837
  MAX_SEND_ATTEMPTS = 3;
835
838
  DEFAULT_LIFECYCLE_TIMEOUT_MS = 3e4;
836
- RETRYABLE_STATUSES = /* @__PURE__ */ new Set([408, 425, 429]);
837
839
  liveTransports = /* @__PURE__ */ new Set();
838
- traceSubmissionSpanIds = /* @__PURE__ */ new Map();
839
- replayTraceSubmissions = /* @__PURE__ */ new Set();
840
- submissionCounter = 0;
840
+ carrierRefs = /* @__PURE__ */ new WeakMap();
841
841
  SPAN_SEPARATOR_BYTES = 1;
842
- OtlpPayloadTooLargeError = class extends Error {
843
- };
844
- OtlpPartialSuccessError = class extends Error {
845
- };
846
842
  BitfabSpanExporter = class {
847
- constructor(directSender, maxRequestBytes, maxRequestBatchSize, exportConcurrency) {
843
+ constructor(directSender, maxRequestBytes, maxRequestBatchSize, exportConcurrency, onDelivered, exportTimeoutMillis = EXPORT_TIMEOUT_MILLIS) {
848
844
  this.directSender = directSender;
849
845
  this.maxRequestBytes = maxRequestBytes;
850
846
  this.maxRequestBatchSize = maxRequestBatchSize;
851
847
  this.exportConcurrency = exportConcurrency;
848
+ this.onDelivered = onDelivered;
849
+ this.exportTimeoutMillis = exportTimeoutMillis;
850
+ /** Epoch ms until which the server has asked this exporter to stay away. */
851
+ this.throttledUntil = 0;
852
852
  }
853
853
  export(spans, resultCallback) {
854
854
  void this.exportAsync(spans).then(
@@ -903,25 +903,51 @@ var init_otel = __esm({
903
903
  return batches;
904
904
  }
905
905
  async send(envelope, batch) {
906
- if (batch.size > this.maxRequestBytes) {
907
- logError(
908
- "a single OpenTelemetry span exceeded the configured request-size target and could not be exported"
909
- );
910
- return false;
911
- }
912
906
  try {
913
- await this.sendWithRetries(encodeRequest(envelope, batch.spans));
914
- return true;
907
+ let requestSpans = batch.spans;
908
+ let requestRawBytes = batch.size;
909
+ let alreadyTrimmed = false;
910
+ while (true) {
911
+ if (requestRawBytes <= MAX_DECOMPRESSED_REQUEST_BYTES) {
912
+ const prepared = await prepareRequest(
913
+ encodeRequest(envelope, requestSpans)
914
+ );
915
+ if (prepared.wireBytes <= this.maxRequestBytes) {
916
+ await this.sendWithRetries(prepared);
917
+ this.reportDelivered(batch.spans);
918
+ return true;
919
+ }
920
+ }
921
+ if (batch.spans.length !== 1) {
922
+ logError(
923
+ "an OpenTelemetry span batch exceeded the configured request-size target and could not be exported"
924
+ );
925
+ return false;
926
+ }
927
+ if (alreadyTrimmed) {
928
+ logError(
929
+ "a single OpenTelemetry span exceeded the configured request-size target after trimming"
930
+ );
931
+ return false;
932
+ }
933
+ const trimmed = trimEncodedSpan(batch.spans[0]);
934
+ if (!trimmed) {
935
+ logError(
936
+ "a single OpenTelemetry span exceeded the configured request-size target and could not be trimmed"
937
+ );
938
+ return false;
939
+ }
940
+ requestSpans = [trimmed];
941
+ requestRawBytes = envelope.size + trimmed.size;
942
+ alreadyTrimmed = true;
943
+ }
915
944
  } catch (error) {
916
- if (error instanceof OtlpPayloadTooLargeError) {
945
+ if (isOversized(error)) {
917
946
  logError(
918
947
  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"
919
948
  );
920
949
  return false;
921
950
  }
922
- if (error instanceof OtlpPartialSuccessError) {
923
- return false;
924
- }
925
951
  logError("failed to export an OpenTelemetry span batch", error);
926
952
  return false;
927
953
  }
@@ -939,37 +965,79 @@ var init_otel = __esm({
939
965
  * the server does not yet understand. The fix is a client-supplied
940
966
  * idempotency key that ingestion dedupes on.
941
967
  */
942
- async sendWithRetries(body) {
968
+ /**
969
+ * Remember a throttle the server asked for, so the requests fanned out
970
+ * alongside this one respect it too. Delaying only the request that was
971
+ * refused leaves the other seven in the window hitting a server that just
972
+ * asked for room.
973
+ */
974
+ recordThrottle(error) {
975
+ const requested = error instanceof DeliveryError ? error.retryAfterMs : void 0;
976
+ if (requested !== void 0) {
977
+ this.throttledUntil = Math.max(
978
+ this.throttledUntil,
979
+ Date.now() + requested
980
+ );
981
+ }
982
+ }
983
+ /**
984
+ * Waits out an active throttle, or reports the batch undeliverable when the
985
+ * throttle outlasts what we are willing to hold it for. Either way nothing is
986
+ * sent while the server has asked us to stay away.
987
+ */
988
+ async awaitThrottle(deadline) {
989
+ const remaining = this.throttledUntil - Date.now();
990
+ if (remaining <= 0) {
991
+ return;
992
+ }
993
+ if (remaining >= (deadline - Date.now()) / 2) {
994
+ throw new DeliveryError(
995
+ `OTLP ingestion is throttled for another ${remaining}ms, longer than the export budget`
996
+ );
997
+ }
998
+ await delay(remaining);
999
+ }
1000
+ async sendWithRetries(request) {
1001
+ const deadline = Date.now() + this.exportTimeoutMillis;
943
1002
  for (let attempt = 0; attempt < MAX_SEND_ATTEMPTS; attempt += 1) {
944
1003
  try {
945
- const response = await this.directSender(
946
- OTLP_TRACES_ENDPOINT,
947
- body,
948
- EXPORT_TIMEOUT_MILLIS
949
- );
950
- const partialSuccess = asRecord2(response?.partialSuccess);
951
- const rejected = partialSuccess?.rejectedSpans;
952
- if (rejected !== void 0 && rejected !== "0" && rejected !== 0) {
953
- logError(
954
- `OTLP ingestion rejected ${rejected} span(s): ${partialSuccess?.errorMessage ?? "no reason provided"}`
955
- );
956
- throw new OtlpPartialSuccessError();
957
- }
1004
+ await this.awaitThrottle(deadline);
1005
+ await this.directSender(request, Math.max(0, deadline - Date.now()));
958
1006
  return;
959
1007
  } catch (error) {
960
- if (error instanceof OtlpPartialSuccessError) {
1008
+ if (isOversized(error)) {
961
1009
  throw error;
962
1010
  }
963
- if (responseStatus(error) === 413) {
964
- throw new OtlpPayloadTooLargeError();
965
- }
1011
+ this.recordThrottle(error);
966
1012
  if (attempt === MAX_SEND_ATTEMPTS - 1 || !isRetryable(error)) {
967
1013
  throw error;
968
1014
  }
969
- await delay(RETRY_DELAY_MILLIS);
1015
+ const wait = retryWaitMillis(error, attempt, deadline - Date.now());
1016
+ if (wait === null) {
1017
+ throw error;
1018
+ }
1019
+ await delay(wait);
970
1020
  }
971
1021
  }
972
1022
  }
1023
+ /**
1024
+ * Announce the carriers a request delivered. Wrapped because a listener that
1025
+ * throws must never turn a delivered batch into a failed export.
1026
+ */
1027
+ reportDelivered(spans) {
1028
+ if (this.onDelivered === void 0) {
1029
+ return;
1030
+ }
1031
+ const refs = spans.map((span) => span.ref).filter((ref) => ref !== void 0);
1032
+ if (refs.length === 0) {
1033
+ return;
1034
+ }
1035
+ try {
1036
+ this.onDelivered(refs);
1037
+ } catch (error) {
1038
+ logError("a delivery listener threw", error);
1039
+ }
1040
+ }
973
1041
  async shutdown() {
974
1042
  }
975
1043
  async forceFlush() {
@@ -1027,7 +1095,9 @@ var init_otel = __esm({
1027
1095
  options.directSender,
1028
1096
  maxRequestBytes,
1029
1097
  maxRequestBatchSize,
1030
- options.exportConcurrency ?? DEFAULT_EXPORT_CONCURRENCY
1098
+ options.exportConcurrency ?? DEFAULT_EXPORT_CONCURRENCY,
1099
+ options.onDelivered,
1100
+ options.exportTimeoutMillis ?? EXPORT_TIMEOUT_MILLIS
1031
1101
  )
1032
1102
  );
1033
1103
  this.processor = new import_sdk_trace_base.BatchSpanProcessor(this.deliveryTracker, {
@@ -1051,8 +1121,7 @@ var init_otel = __esm({
1051
1121
  this.tracer = this.provider.getTracer("bitfab", __version__);
1052
1122
  liveTransports.add(this);
1053
1123
  }
1054
- submit(operation, payload) {
1055
- recordTraceSubmission(operation, payload);
1124
+ submit(operation, payload, meta = {}) {
1056
1125
  if (this.closed) {
1057
1126
  warnOnce(
1058
1127
  "otel-submit-after-shutdown",
@@ -1061,7 +1130,10 @@ var init_otel = __esm({
1061
1130
  return;
1062
1131
  }
1063
1132
  try {
1064
- const { body, dropped } = serializePayloadBody(payload);
1133
+ const { body, dropped } = serializePayloadBody(
1134
+ payload,
1135
+ MAX_COMPRESSIBLE_SPAN_CARRIER_BYTES
1136
+ );
1065
1137
  if (dropped.length > 0) {
1066
1138
  warnOnce(
1067
1139
  "otel-carrier-payload-stubbed",
@@ -1070,17 +1142,20 @@ var init_otel = __esm({
1070
1142
  ].join(", ")}); they were stubbed so the span still ships, but the trace may be incomplete or not replayable.`
1071
1143
  );
1072
1144
  }
1073
- const span = this.tracer.startSpan(spanName(operation, payload), {
1145
+ const span = this.tracer.startSpan(meta.name ?? `bitfab.${operation}`, {
1074
1146
  attributes: {
1075
1147
  [OPERATION_ATTRIBUTE]: operation,
1076
1148
  [PAYLOAD_ATTRIBUTE]: body
1077
1149
  },
1078
- startTime: payloadTimestamp(payload, "started_at")
1150
+ startTime: meta.startTime
1079
1151
  });
1080
- if (hasError(payload)) {
1152
+ if (meta.ref !== void 0) {
1153
+ carrierRefs.set(span, meta.ref);
1154
+ }
1155
+ if (meta.errored === true) {
1081
1156
  span.setStatus({ code: import_api.SpanStatusCode.ERROR });
1082
1157
  }
1083
- endSpan(span, payloadTimestamp(payload, "ended_at"));
1158
+ endSpan(span, meta.endTime);
1084
1159
  } catch (error) {
1085
1160
  logError("failed to queue an OpenTelemetry span", error);
1086
1161
  }
@@ -1130,9 +1205,6 @@ function flushTraceTransports(timeoutMs) {
1130
1205
  function shutdownTraceTransports(timeoutMs) {
1131
1206
  return shutdownOtelTransports(timeoutMs);
1132
1207
  }
1133
- function takeReplaySpanCounts2(traceIds) {
1134
- return takeReplaySpanCounts(traceIds);
1135
- }
1136
1208
  var init_transport = __esm({
1137
1209
  "src/transport.ts"() {
1138
1210
  "use strict";
@@ -1181,7 +1253,96 @@ async function waitForPromises(promises, timeoutMs) {
1181
1253
  }
1182
1254
  }
1183
1255
  }
1184
- var REPLAY_DB_BRANCH_REQUEST_TIMEOUT_MS, EXIT_FLUSH_TIMEOUT_MS, DEFAULT_LIFECYCLE_TIMEOUT_MS2, pendingTracePromises, HttpClient;
1256
+ function readHeader(response, name) {
1257
+ try {
1258
+ return response.headers?.get(name) ?? null;
1259
+ } catch {
1260
+ return null;
1261
+ }
1262
+ }
1263
+ function parseRetryAfterMs(header) {
1264
+ const value = header?.trim();
1265
+ if (!value) {
1266
+ return void 0;
1267
+ }
1268
+ const seconds = Number(value);
1269
+ if (Number.isFinite(seconds)) {
1270
+ return seconds >= 0 ? seconds * 1e3 : void 0;
1271
+ }
1272
+ const at = Date.parse(value);
1273
+ if (Number.isNaN(at)) {
1274
+ return void 0;
1275
+ }
1276
+ return Math.max(0, at - Date.now());
1277
+ }
1278
+ function carrierMeta(operation, payload, ref) {
1279
+ return {
1280
+ ref,
1281
+ name: carrierName(operation, payload),
1282
+ startTime: payloadTimestamp(payload, "started_at"),
1283
+ endTime: payloadTimestamp(payload, "ended_at"),
1284
+ errored: payloadHasError(payload)
1285
+ };
1286
+ }
1287
+ function carrierName(operation, payload) {
1288
+ if (operation === "external_span") {
1289
+ const spanData = asPayloadRecord(
1290
+ asPayloadRecord(payload.rawSpan)?.span_data
1291
+ );
1292
+ if (typeof spanData?.name === "string") {
1293
+ return spanData.name;
1294
+ }
1295
+ }
1296
+ if (typeof payload.traceFunctionKey === "string") {
1297
+ return payload.traceFunctionKey;
1298
+ }
1299
+ return `bitfab.${operation}`;
1300
+ }
1301
+ function payloadTimestamp(payload, field) {
1302
+ const rawSpan = asPayloadRecord(payload.rawSpan);
1303
+ const rawTrace = asPayloadRecord(payload.externalTrace) ?? asPayloadRecord(payload.rawTrace);
1304
+ const raw = rawSpan?.[field] ?? rawTrace?.[field];
1305
+ if (typeof raw !== "string") {
1306
+ return void 0;
1307
+ }
1308
+ const parsed = Date.parse(raw);
1309
+ return Number.isNaN(parsed) ? void 0 : parsed;
1310
+ }
1311
+ function payloadHasError(payload) {
1312
+ const spanData = asPayloadRecord(asPayloadRecord(payload.rawSpan)?.span_data);
1313
+ if (spanData?.error != null) {
1314
+ return true;
1315
+ }
1316
+ const errors = payload.errors;
1317
+ return Array.isArray(errors) ? errors.length > 0 : Boolean(errors);
1318
+ }
1319
+ function asPayloadRecord(value) {
1320
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
1321
+ }
1322
+ function carrierRef(payload) {
1323
+ const traceId = sourceTraceIdOf(payload);
1324
+ if (traceId === void 0) {
1325
+ return void 0;
1326
+ }
1327
+ const rawSpan = payload.rawSpan;
1328
+ if (rawSpan === void 0) {
1329
+ return { traceId };
1330
+ }
1331
+ const spanId = rawSpan?.id;
1332
+ return {
1333
+ traceId,
1334
+ spanId: typeof spanId === "string" ? spanId : `submission-${++carrierSeq}`
1335
+ };
1336
+ }
1337
+ function sourceTraceIdOf(payload) {
1338
+ if (typeof payload.sourceTraceId === "string") {
1339
+ return payload.sourceTraceId;
1340
+ }
1341
+ const rawTrace = payload.externalTrace ?? payload.rawTrace;
1342
+ const id = rawTrace?.id;
1343
+ return typeof id === "string" ? id : void 0;
1344
+ }
1345
+ var REPLAY_DB_BRANCH_REQUEST_TIMEOUT_MS, OTLP_TRACES_ENDPOINT, RETRYABLE_STATUSES, EXIT_FLUSH_TIMEOUT_MS, DEFAULT_LIFECYCLE_TIMEOUT_MS2, pendingTracePromises, carrierSeq, HttpClient;
1185
1346
  var init_http = __esm({
1186
1347
  "src/http.ts"() {
1187
1348
  "use strict";
@@ -1191,9 +1352,12 @@ var init_http = __esm({
1191
1352
  init_replayContext();
1192
1353
  init_serializePayload();
1193
1354
  init_transport();
1355
+ init_transportTypes();
1194
1356
  init_unrefTimer();
1195
1357
  init_warnOnce();
1196
1358
  REPLAY_DB_BRANCH_REQUEST_TIMEOUT_MS = 3e5;
1359
+ OTLP_TRACES_ENDPOINT = "/api/sdk/otel/v1/traces";
1360
+ RETRYABLE_STATUSES = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
1197
1361
  EXIT_FLUSH_TIMEOUT_MS = 5e3;
1198
1362
  DEFAULT_LIFECYCLE_TIMEOUT_MS2 = 3e4;
1199
1363
  pendingTracePromises = /* @__PURE__ */ new Set();
@@ -1213,8 +1377,12 @@ var init_http = __esm({
1213
1377
  });
1214
1378
  });
1215
1379
  }
1380
+ carrierSeq = 0;
1216
1381
  HttpClient = class {
1217
1382
  constructor(config) {
1383
+ // Only traces a caller asked about are tracked, so ordinary tracing stores
1384
+ // nothing here.
1385
+ this.traceDeliveries = /* @__PURE__ */ new Map();
1218
1386
  // Deferred span work owned by THIS client. The module-global set backs the
1219
1387
  // process-wide `flushTraces()` and the exit hook, but per-client lifecycle
1220
1388
  // must not wait on another client's slow finalize: a false `close()` failure
@@ -1250,13 +1418,131 @@ var init_http = __esm({
1250
1418
  }
1251
1419
  if (!this.traceTransport) {
1252
1420
  this.traceTransport = createTraceTransport({
1253
- directSender: (endpoint, body, timeoutMs) => this.sendEncoded(endpoint, body, {
1254
- timeout: timeoutMs
1255
- })
1421
+ directSender: (request, timeoutMs) => this.deliverCarriers(request, timeoutMs),
1422
+ onDelivered: (refs) => this.recordDeliveredCarriers(refs)
1256
1423
  });
1257
1424
  }
1258
1425
  return this.traceTransport;
1259
1426
  }
1427
+ /**
1428
+ * Post one encoded batch and decide what the server's answer means, so the
1429
+ * transport never reads a response. Rejections and permanent statuses come
1430
+ * back as a non-retryable {@link DeliveryError}; anything the server might
1431
+ * still accept on a second try comes back retryable.
1432
+ */
1433
+ async deliverCarriers(request, timeoutMs) {
1434
+ let response;
1435
+ try {
1436
+ response = await this.sendPrepared(
1437
+ OTLP_TRACES_ENDPOINT,
1438
+ request,
1439
+ { timeout: timeoutMs }
1440
+ );
1441
+ } catch (error) {
1442
+ const status = error instanceof BitfabError ? error.status : void 0;
1443
+ if (status === void 0) {
1444
+ throw new DeliveryError(`OTLP ingestion failed: ${String(error)}`, {
1445
+ retryable: true
1446
+ });
1447
+ }
1448
+ throw new DeliveryError(`OTLP ingestion failed with HTTP ${status}`, {
1449
+ retryable: RETRYABLE_STATUSES.has(status),
1450
+ oversized: status === 413,
1451
+ ...error instanceof BitfabError && error.retryAfterMs !== void 0 ? { retryAfterMs: error.retryAfterMs } : {}
1452
+ });
1453
+ }
1454
+ const partialSuccess = asPayloadRecord(response?.partialSuccess);
1455
+ const rejected = partialSuccess?.rejectedSpans;
1456
+ if (rejected !== void 0 && rejected !== "0" && rejected !== 0) {
1457
+ throw new DeliveryError(
1458
+ `OTLP ingestion rejected ${rejected} span(s): ${partialSuccess?.errorMessage ?? "no reason provided"}`
1459
+ );
1460
+ }
1461
+ }
1462
+ /**
1463
+ * Start tracking delivery for `traceIds`. Nothing is recorded for a trace
1464
+ * that was never tracked, so ordinary tracing costs no bookkeeping at all.
1465
+ */
1466
+ trackTraceDeliveries(traceIds) {
1467
+ for (const traceId of traceIds) {
1468
+ if (!this.traceDeliveries.has(traceId)) {
1469
+ this.traceDeliveries.set(traceId, {
1470
+ submittedSpanIds: /* @__PURE__ */ new Set(),
1471
+ ackedSpanIds: /* @__PURE__ */ new Set(),
1472
+ closed: false,
1473
+ closingAcked: false
1474
+ });
1475
+ }
1476
+ }
1477
+ }
1478
+ /** Whether any tracked trace has had its closing carrier submitted. */
1479
+ hasClosedDeliveries(traceIds) {
1480
+ return traceIds.some((traceId) => this.traceDeliveries.get(traceId)?.closed);
1481
+ }
1482
+ /**
1483
+ * Report what each tracked trace submitted and whether the server confirmed
1484
+ * it, and stop tracking them. Every id passed is freed, so a caller cannot
1485
+ * leak a record for a trace that never closed.
1486
+ *
1487
+ * `delivered` is only meaningful once a flush has settled: acks land before
1488
+ * an export resolves, so a flush that reported success has already collected
1489
+ * every ack it is going to collect.
1490
+ */
1491
+ takeTraceDeliveries(traceIds) {
1492
+ const reports = {};
1493
+ for (const traceId of traceIds) {
1494
+ const delivery = this.traceDeliveries.get(traceId);
1495
+ if (delivery === void 0) {
1496
+ continue;
1497
+ }
1498
+ this.traceDeliveries.delete(traceId);
1499
+ reports[traceId] = {
1500
+ spanCount: delivery.submittedSpanIds.size,
1501
+ closed: delivery.closed,
1502
+ delivered: delivery.closingAcked && [...delivery.submittedSpanIds].every(
1503
+ (spanId) => delivery.ackedSpanIds.has(spanId)
1504
+ )
1505
+ };
1506
+ }
1507
+ return reports;
1508
+ }
1509
+ /** Build a carrier's meta and record what it adds to its trace's expected set. */
1510
+ recordedMeta(operation, payload, ref) {
1511
+ this.recordSubmittedCarrier(ref);
1512
+ return carrierMeta(operation, payload, ref);
1513
+ }
1514
+ recordSubmittedCarrier(ref) {
1515
+ if (ref === void 0) {
1516
+ return;
1517
+ }
1518
+ const delivery = this.traceDeliveries.get(ref.traceId);
1519
+ if (delivery === void 0) {
1520
+ return;
1521
+ }
1522
+ if (ref.spanId === void 0) {
1523
+ delivery.closed = true;
1524
+ } else {
1525
+ delivery.submittedSpanIds.add(ref.spanId);
1526
+ }
1527
+ }
1528
+ /**
1529
+ * Ingestion commits every carrier in a request before it answers, so a
1530
+ * delivered ref is proof its row exists: the same fact the replay status
1531
+ * endpoint would report, already in hand.
1532
+ */
1533
+ recordDeliveredCarriers(refs) {
1534
+ for (const ref of refs) {
1535
+ const delivery = this.traceDeliveries.get(ref.traceId);
1536
+ if (delivery === void 0) {
1537
+ continue;
1538
+ }
1539
+ if (ref.spanId === void 0) {
1540
+ delivery.closingAcked = true;
1541
+ } else {
1542
+ delivery.ackedSpanIds.add(ref.spanId);
1543
+ }
1544
+ }
1545
+ }
1260
1546
  /**
1261
1547
  * Track deferred span work so this client's own lifecycle waits for it, and
1262
1548
  * so the process-wide flush and exit hook do too.
@@ -1337,13 +1623,16 @@ var init_http = __esm({
1337
1623
  * same data twice.
1338
1624
  */
1339
1625
  async sendEncoded(endpoint, body, options) {
1626
+ const prepared = encodeRequestBody(body);
1627
+ const encoded = prepared instanceof Promise ? await prepared : prepared;
1628
+ return this.sendPrepared(endpoint, encoded, options);
1629
+ }
1630
+ async sendPrepared(endpoint, encoded, options) {
1340
1631
  const url = `${this.serviceUrl}${endpoint}`;
1341
1632
  const timeout = options?.timeout ?? this.timeout;
1342
1633
  const method = options?.method ?? "POST";
1343
1634
  const controller = new AbortController();
1344
1635
  const timeoutId = setTimeout(() => controller.abort(), timeout);
1345
- const prepared = encodeRequestBody(body);
1346
- const encoded = prepared instanceof Promise ? await prepared : prepared;
1347
1636
  const headers = {
1348
1637
  "Content-Type": "application/json",
1349
1638
  Authorization: `Bearer ${this.resolveApiKey() ?? ""}`
@@ -1363,7 +1652,8 @@ var init_http = __esm({
1363
1652
  throw new BitfabError(
1364
1653
  `HTTP ${response.status}: ${errorText.slice(0, 500)}`,
1365
1654
  void 0,
1366
- response.status
1655
+ response.status,
1656
+ parseRetryAfterMs(readHeader(response, "retry-after"))
1367
1657
  );
1368
1658
  }
1369
1659
  const result = await response.json();
@@ -1449,11 +1739,12 @@ var init_http = __esm({
1449
1739
  * the OTLP carrier has no path to carry it.
1450
1740
  */
1451
1741
  sendInternalTrace(functionId, payload) {
1452
- this.getTraceTransport()?.submit("internal_trace", {
1453
- ...payload,
1454
- functionId,
1455
- sdkVersion: __version__
1456
- });
1742
+ const body = { ...payload, functionId, sdkVersion: __version__ };
1743
+ this.getTraceTransport()?.submit(
1744
+ "internal_trace",
1745
+ body,
1746
+ carrierMeta("internal_trace", body, void 0)
1747
+ );
1457
1748
  }
1458
1749
  /**
1459
1750
  * Queue an external span (from withSpan wrapper or OpenAI tracing) onto this
@@ -1462,10 +1753,11 @@ var init_http = __esm({
1462
1753
  * promise.
1463
1754
  */
1464
1755
  sendExternalSpan(payload) {
1465
- this.getTraceTransport()?.submit("external_span", {
1466
- ...payload,
1467
- sdkVersion: __version__
1468
- });
1756
+ this.getTraceTransport()?.submit(
1757
+ "external_span",
1758
+ { ...payload, sdkVersion: __version__ },
1759
+ this.recordedMeta("external_span", payload, carrierRef(payload))
1760
+ );
1469
1761
  }
1470
1762
  /**
1471
1763
  * Queue an external trace completion (from OpenAI tracing) onto this
@@ -1474,10 +1766,15 @@ var init_http = __esm({
1474
1766
  * server-authoritative barrier in `replay.ts`, not by awaiting this call.
1475
1767
  */
1476
1768
  sendExternalTrace(payload) {
1477
- this.getTraceTransport()?.submit("external_trace", {
1478
- ...payload,
1479
- sdkVersion: __version__
1480
- });
1769
+ this.getTraceTransport()?.submit(
1770
+ "external_trace",
1771
+ { ...payload, sdkVersion: __version__ },
1772
+ this.recordedMeta(
1773
+ "external_trace",
1774
+ payload,
1775
+ payload.completed === true ? carrierRef(payload) : void 0
1776
+ )
1777
+ );
1481
1778
  }
1482
1779
  /**
1483
1780
  * Partial update of an existing trace identified by its Bitfab trace ID.
@@ -1817,8 +2114,8 @@ var init_serialize = __esm({
1817
2114
  import_superjson = __toESM(require("superjson"), 1);
1818
2115
  init_payloadBudget();
1819
2116
  init_warnOnce();
1820
- MAX_SERIALIZED_BYTES = MAX_SPAN_CARRIER_BYTES;
1821
- MAX_FRAMEWORK_SERIALIZED_BYTES = MAX_SPAN_CARRIER_BYTES;
2117
+ MAX_SERIALIZED_BYTES = MAX_COMPRESSIBLE_SPAN_CARRIER_BYTES;
2118
+ MAX_FRAMEWORK_SERIALIZED_BYTES = MAX_COMPRESSIBLE_SPAN_CARRIER_BYTES;
1822
2119
  MAX_SAFE_DEPTH = 6;
1823
2120
  }
1824
2121
  });
@@ -2073,7 +2370,8 @@ __export(replay_exports, {
2073
2370
  ReplayError: () => ReplayError,
2074
2371
  replay: () => replay,
2075
2372
  reportReplayProgress: () => reportReplayProgress,
2076
- serializeReplayResult: () => serializeReplayResult
2373
+ serializeReplayResult: () => serializeReplayResult,
2374
+ waitForReplayPersistence: () => waitForReplayPersistence
2077
2375
  });
2078
2376
  function dbBranchEnabled(dbBranch) {
2079
2377
  return dbBranch !== void 0 && dbBranch !== false;
@@ -2361,15 +2659,29 @@ async function waitForReplayPersistence(httpClient, testRunId, replayedTraceIds)
2361
2659
  REPLAY_PERSISTENCE_TIMEOUT_MS
2362
2660
  );
2363
2661
  if (!deferredSettled) {
2662
+ httpClient.takeTraceDeliveries(replayedTraceIds);
2364
2663
  throw new BitfabError(
2365
2664
  `Replay could not settle deferred span work before the deadline, so the expected span counts are incomplete (testRunId ${testRunId}).`
2366
2665
  );
2367
2666
  }
2368
- const expectedSpanCounts = takeReplaySpanCounts2(replayedTraceIds);
2369
- if (Object.keys(expectedSpanCounts).length === 0) {
2667
+ if (!httpClient.hasClosedDeliveries(replayedTraceIds)) {
2668
+ httpClient.takeTraceDeliveries(replayedTraceIds);
2370
2669
  return;
2371
2670
  }
2372
2671
  const flushed = await flushTraces(REPLAY_PERSISTENCE_TIMEOUT_MS);
2672
+ const deliveries = httpClient.takeTraceDeliveries(replayedTraceIds);
2673
+ const expectedSpanCounts = {};
2674
+ let allDelivered = true;
2675
+ for (const [traceId, delivery] of Object.entries(deliveries)) {
2676
+ if (!delivery.closed) {
2677
+ continue;
2678
+ }
2679
+ expectedSpanCounts[traceId] = delivery.spanCount;
2680
+ allDelivered = allDelivered && delivery.delivered;
2681
+ }
2682
+ if (allDelivered) {
2683
+ return;
2684
+ }
2373
2685
  const deadline = Date.now() + REPLAY_PERSISTENCE_TIMEOUT_MS;
2374
2686
  let missing = Object.keys(expectedSpanCounts).length;
2375
2687
  while (true) {
@@ -2478,6 +2790,7 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
2478
2790
  ...registeredOverrides
2479
2791
  ];
2480
2792
  const replayedTraceIds = serverItems.map(() => randomUuid());
2793
+ httpClient.trackTraceDeliveries(replayedTraceIds);
2481
2794
  const tasks = serverItems.map(
2482
2795
  (serverItem, index) => () => processItem(
2483
2796
  httpClient,
@@ -2675,7 +2988,6 @@ var init_replay = __esm({
2675
2988
  init_randomUuid();
2676
2989
  init_replayContext();
2677
2990
  init_serialize();
2678
- init_transport();
2679
2991
  init_unrefTimer();
2680
2992
  REPLAY_PERSISTENCE_TIMEOUT_MS = 3e4;
2681
2993
  BITFAB_PROGRESS_PREFIX = "@@bitfab:progress ";