@bitfab/sdk 0.34.1 → 0.34.2

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.34.1";
45
+ __version__ = "0.34.2";
46
46
  }
47
47
  });
48
48
 
@@ -420,32 +420,30 @@ function spanToOtlp(span) {
420
420
  }
421
421
  return result;
422
422
  }
423
- function buildOtlpRequest(first, spans) {
423
+ function byteLength(value) {
424
+ return textEncoder ? textEncoder.encode(value).length : value.length;
425
+ }
426
+ function encodeSpan(span) {
427
+ const json = JSON.stringify(spanToOtlp(span));
428
+ return { json, size: byteLength(json) };
429
+ }
430
+ function requestEnvelope(first) {
424
431
  const scope = first.instrumentationScope;
425
- return {
426
- resourceSpans: [
427
- {
428
- resource: {
429
- attributes: otlpAttributes(
430
- first.resource.attributes
431
- )
432
- },
433
- scopeSpans: [
434
- {
435
- scope: { name: scope.name, version: scope.version ?? "" },
436
- spans
437
- }
438
- ]
439
- }
440
- ]
441
- };
432
+ const resource = JSON.stringify({
433
+ attributes: otlpAttributes(
434
+ first.resource.attributes
435
+ )
436
+ });
437
+ const scopeJson = JSON.stringify({
438
+ name: scope.name,
439
+ version: scope.version ?? ""
440
+ });
441
+ const head = `{"resourceSpans":[{"resource":${resource},"scopeSpans":[{"scope":${scopeJson},"spans":[`;
442
+ const tail = "]}]}]}";
443
+ return { head, tail, size: byteLength(head) + byteLength(tail) };
442
444
  }
443
- function encodedSize(value) {
444
- const json = JSON.stringify(value);
445
- if (typeof TextEncoder !== "undefined") {
446
- return new TextEncoder().encode(json).length;
447
- }
448
- return json.length;
445
+ function encodeRequest(envelope, spans) {
446
+ return envelope.head + spans.map((span) => span.json).join(",") + envelope.tail;
449
447
  }
450
448
  function delay(ms) {
451
449
  return new Promise((resolve) => {
@@ -495,10 +493,6 @@ function isRetryable(error) {
495
493
  }
496
494
  return RETRYABLE_STATUSES.has(status) || status >= 500;
497
495
  }
498
- function normalizeCollectorEndpoint(endpoint) {
499
- const trimmed = endpoint.replace(/\/+$/, "");
500
- return trimmed.endsWith("/v1/traces") ? trimmed : `${trimmed}/v1/traces`;
501
- }
502
496
  function endSpan(span, endTime) {
503
497
  span.end(endTime);
504
498
  }
@@ -535,7 +529,6 @@ function hasError(payload) {
535
529
  function createOtelTransport(options) {
536
530
  return new OtelBatchTransport({
537
531
  ...options,
538
- collectorEndpoint: readEnv(COLLECTOR_ENDPOINT_ENV) || void 0,
539
532
  exportConcurrency: readBoundedIntEnv(
540
533
  EXPORT_CONCURRENCY_ENV,
541
534
  MAX_EXPORT_CONCURRENCY,
@@ -570,7 +563,7 @@ function shutdownOtelTransports(timeoutMs = DEFAULT_LIFECYCLE_TIMEOUT_MS) {
570
563
  (transport, remaining) => transport.shutdown(remaining)
571
564
  );
572
565
  }
573
- 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, COLLECTOR_ENDPOINT_ENV, MAX_QUEUE_SIZE, DIRECT_MAX_EXPORT_BATCH_SIZE, COLLECTOR_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, OtlpPayloadTooLargeError, OtlpPartialSuccessError, BitfabSpanExporter, CollectorSpanExporter, DeliveryTrackingExporter, OtelBatchTransport;
566
+ 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, textEncoder, SPAN_SEPARATOR_BYTES, OtlpPayloadTooLargeError, OtlpPartialSuccessError, BitfabSpanExporter, DeliveryTrackingExporter, OtelBatchTransport;
574
567
  var init_otel = __esm({
575
568
  "src/otel.ts"() {
576
569
  "use strict";
@@ -590,10 +583,8 @@ var init_otel = __esm({
590
583
  MAX_EXPORT_REQUEST_BYTES = 3e6;
591
584
  MAX_REQUEST_BYTES_ENV = "BITFAB_OTEL_MAX_REQUEST_BYTES";
592
585
  EXPORT_CONCURRENCY_ENV = "BITFAB_OTEL_EXPORT_CONCURRENCY";
593
- COLLECTOR_ENDPOINT_ENV = "BITFAB_OTEL_EXPORTER_ENDPOINT";
594
586
  MAX_QUEUE_SIZE = 8192;
595
587
  DIRECT_MAX_EXPORT_BATCH_SIZE = 512;
596
- COLLECTOR_MAX_EXPORT_BATCH_SIZE = 32;
597
588
  DIRECT_MAX_REQUEST_BATCH_SIZE = 8;
598
589
  DEFAULT_EXPORT_CONCURRENCY = 32;
599
590
  MAX_EXPORT_CONCURRENCY = 64;
@@ -607,6 +598,8 @@ var init_otel = __esm({
607
598
  traceSubmissionSpanIds = /* @__PURE__ */ new Map();
608
599
  replayTraceSubmissions = /* @__PURE__ */ new Set();
609
600
  submissionCounter = 0;
601
+ textEncoder = typeof TextEncoder === "undefined" ? void 0 : new TextEncoder();
602
+ SPAN_SEPARATOR_BYTES = 1;
610
603
  OtlpPayloadTooLargeError = class extends Error {
611
604
  };
612
605
  OtlpPartialSuccessError = class extends Error {
@@ -635,57 +628,55 @@ var init_otel = __esm({
635
628
  return true;
636
629
  }
637
630
  let encoded;
631
+ let envelope;
638
632
  try {
639
- encoded = spans.map(spanToOtlp);
633
+ encoded = spans.map(encodeSpan);
634
+ envelope = requestEnvelope(spans[0]);
640
635
  } catch (error) {
641
636
  logError("failed to encode an OpenTelemetry span batch", error);
642
637
  return false;
643
638
  }
644
- const first = spans[0];
645
- const batches = this.buildRequestBatches(first, encoded);
639
+ const batches = this.buildRequestBatches(envelope, encoded);
646
640
  const results = await mapWithConcurrency(
647
641
  batches,
648
642
  this.exportConcurrency,
649
- (batch) => this.send(first, batch)
643
+ (batch) => this.send(envelope, batch)
650
644
  );
651
645
  return results.every(Boolean);
652
646
  }
653
- buildRequestBatches(first, spans) {
647
+ buildRequestBatches(envelope, spans) {
654
648
  const batches = [];
655
649
  let current = [];
650
+ let size = envelope.size;
656
651
  for (const span of spans) {
657
- if (current.length >= this.maxRequestBatchSize) {
658
- batches.push(current);
652
+ const addition = span.size + (current.length > 0 ? SPAN_SEPARATOR_BYTES : 0);
653
+ if (current.length > 0 && (current.length >= this.maxRequestBatchSize || size + addition > this.maxRequestBytes)) {
654
+ batches.push({ spans: current, size });
659
655
  current = [];
656
+ size = envelope.size;
660
657
  }
661
- const candidate = [...current, span];
662
- if (current.length > 0 && encodedSize(buildOtlpRequest(first, candidate)) > this.maxRequestBytes) {
663
- batches.push(current);
664
- current = [span];
665
- } else {
666
- current = candidate;
667
- }
658
+ current.push(span);
659
+ size += span.size + (current.length > 1 ? SPAN_SEPARATOR_BYTES : 0);
668
660
  }
669
661
  if (current.length > 0) {
670
- batches.push(current);
662
+ batches.push({ spans: current, size });
671
663
  }
672
664
  return batches;
673
665
  }
674
- async send(first, spans) {
675
- const payload = buildOtlpRequest(first, spans);
676
- if (encodedSize(payload) > this.maxRequestBytes) {
666
+ async send(envelope, batch) {
667
+ if (batch.size > this.maxRequestBytes) {
677
668
  logError(
678
669
  "a single OpenTelemetry span exceeded the configured request-size target and could not be exported"
679
670
  );
680
671
  return false;
681
672
  }
682
673
  try {
683
- await this.sendWithRetries(payload);
674
+ await this.sendWithRetries(encodeRequest(envelope, batch.spans));
684
675
  return true;
685
676
  } catch (error) {
686
677
  if (error instanceof OtlpPayloadTooLargeError) {
687
678
  logError(
688
- 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"
679
+ 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"
689
680
  );
690
681
  return false;
691
682
  }
@@ -709,12 +700,12 @@ var init_otel = __esm({
709
700
  * the server does not yet understand. The fix is a client-supplied
710
701
  * idempotency key that ingestion dedupes on.
711
702
  */
712
- async sendWithRetries(payload) {
703
+ async sendWithRetries(body) {
713
704
  for (let attempt = 0; attempt < MAX_SEND_ATTEMPTS; attempt += 1) {
714
705
  try {
715
706
  const response = await this.directSender(
716
707
  OTLP_TRACES_ENDPOINT,
717
- payload,
708
+ body,
718
709
  EXPORT_TIMEOUT_MILLIS
719
710
  );
720
711
  const partialSuccess = asRecord(response?.partialSuccess);
@@ -745,115 +736,6 @@ var init_otel = __esm({
745
736
  async forceFlush() {
746
737
  }
747
738
  };
748
- CollectorSpanExporter = class {
749
- constructor(endpoint, apiKey, maxRequestBytes) {
750
- this.endpoint = endpoint;
751
- this.apiKey = apiKey;
752
- this.maxRequestBytes = maxRequestBytes;
753
- }
754
- /**
755
- * Loaded through a dynamic import rather than a top-level one so bundlers
756
- * code-split it: Collector delivery is opt-in, and a consumer who never sets
757
- * an endpoint should not pay for the exporter in their initial bundle. It is
758
- * a hard dependency, so this cannot fail for want of the package.
759
- */
760
- loadExporterModule() {
761
- if (!this.pendingModule) {
762
- this.pendingModule = import("@opentelemetry/exporter-trace-otlp-proto");
763
- }
764
- return this.pendingModule;
765
- }
766
- export(spans, resultCallback) {
767
- void this.exportAsync(spans).then(
768
- (succeeded) => {
769
- resultCallback({
770
- code: succeeded ? import_core.ExportResultCode.SUCCESS : import_core.ExportResultCode.FAILED
771
- });
772
- },
773
- (error) => {
774
- resultCallback({ code: import_core.ExportResultCode.FAILED, error });
775
- }
776
- );
777
- }
778
- async exportAsync(spans) {
779
- if (spans.length === 0) {
780
- return true;
781
- }
782
- let delegate;
783
- try {
784
- delegate = await this.resolveDelegate();
785
- } catch (error) {
786
- logError("failed to build the OTLP Collector exporter", error);
787
- return false;
788
- }
789
- const results = await Promise.all(
790
- this.partition(spans).map(
791
- (batch) => new Promise((resolve) => {
792
- try {
793
- delegate.export(batch, (result) => {
794
- resolve(result.code === import_core.ExportResultCode.SUCCESS);
795
- });
796
- } catch (error) {
797
- logError("Collector export threw", error);
798
- resolve(false);
799
- }
800
- })
801
- )
802
- );
803
- return results.every(Boolean);
804
- }
805
- /**
806
- * Partition by the encoded JSON size of each carrier rather than its encoded
807
- * protobuf size. Protobuf is strictly smaller than the equivalent JSON for
808
- * these payloads, so the JSON figure is a conservative bound that keeps every
809
- * request under the target without pulling `@opentelemetry/otlp-transformer`
810
- * into the dependency set purely to measure bytes.
811
- */
812
- partition(spans) {
813
- const batches = [];
814
- let current = [];
815
- let currentSize = 0;
816
- for (const span of spans) {
817
- const size = encodedSize(spanToOtlp(span));
818
- if (current.length > 0 && currentSize + size > this.maxRequestBytes) {
819
- batches.push(current);
820
- current = [];
821
- currentSize = 0;
822
- }
823
- current.push(span);
824
- currentSize += size;
825
- }
826
- if (current.length > 0) {
827
- batches.push(current);
828
- }
829
- return batches;
830
- }
831
- async resolveDelegate() {
832
- const apiKey = this.apiKey() ?? "";
833
- if (this.delegate && this.delegateApiKey === apiKey) {
834
- return this.delegate;
835
- }
836
- const { OTLPTraceExporter } = await this.loadExporterModule();
837
- const previous = this.delegate;
838
- this.delegate = new OTLPTraceExporter({
839
- url: this.endpoint,
840
- headers: { Authorization: `Bearer ${apiKey}` },
841
- timeoutMillis: EXPORT_TIMEOUT_MILLIS
842
- });
843
- this.delegateApiKey = apiKey;
844
- if (previous) {
845
- void previous.shutdown().catch(() => {
846
- });
847
- }
848
- return this.delegate;
849
- }
850
- async shutdown() {
851
- await this.delegate?.shutdown();
852
- }
853
- async forceFlush() {
854
- await this.delegate?.forceFlush?.();
855
- }
856
- };
857
739
  DeliveryTrackingExporter = class {
858
740
  constructor(exporter) {
859
741
  this.exporter = exporter;
@@ -896,27 +778,22 @@ var init_otel = __esm({
896
778
  OtelBatchTransport = class {
897
779
  constructor(options) {
898
780
  this.closed = false;
899
- const collectorEndpoint = options.collectorEndpoint;
900
781
  const maxRequestBytes = options.maxRequestBytes ?? MAX_EXPORT_REQUEST_BYTES;
901
782
  const maxRequestBatchSize = options.maxRequestBatchSize ?? DIRECT_MAX_REQUEST_BATCH_SIZE;
902
783
  if (maxRequestBatchSize <= 0) {
903
784
  throw new BitfabError("maxRequestBatchSize must be a positive integer");
904
785
  }
905
786
  this.deliveryTracker = new DeliveryTrackingExporter(
906
- collectorEndpoint === void 0 ? new BitfabSpanExporter(
787
+ new BitfabSpanExporter(
907
788
  options.directSender,
908
789
  maxRequestBytes,
909
790
  maxRequestBatchSize,
910
791
  options.exportConcurrency ?? DEFAULT_EXPORT_CONCURRENCY
911
- ) : new CollectorSpanExporter(
912
- normalizeCollectorEndpoint(collectorEndpoint),
913
- options.apiKey,
914
- maxRequestBytes
915
792
  )
916
793
  );
917
794
  this.processor = new import_sdk_trace_base.BatchSpanProcessor(this.deliveryTracker, {
918
795
  maxQueueSize: options.maxQueueSize ?? MAX_QUEUE_SIZE,
919
- maxExportBatchSize: options.maxExportBatchSize ?? (collectorEndpoint === void 0 ? DIRECT_MAX_EXPORT_BATCH_SIZE : COLLECTOR_MAX_EXPORT_BATCH_SIZE),
796
+ maxExportBatchSize: options.maxExportBatchSize ?? DIRECT_MAX_EXPORT_BATCH_SIZE,
920
797
  scheduledDelayMillis: SCHEDULE_DELAY_MILLIS,
921
798
  exportTimeoutMillis: options.exportTimeoutMillis ?? EXPORT_TIMEOUT_MILLIS
922
799
  });
@@ -1133,8 +1010,7 @@ var init_http = __esm({
1133
1010
  }
1134
1011
  if (!this.traceTransport) {
1135
1012
  this.traceTransport = createTraceTransport({
1136
- apiKey: () => this.resolveApiKey(),
1137
- directSender: (endpoint, payload, timeoutMs) => this.request(endpoint, payload, {
1013
+ directSender: (endpoint, body, timeoutMs) => this.sendEncoded(endpoint, body, {
1138
1014
  timeout: timeoutMs
1139
1015
  })
1140
1016
  });
@@ -1204,11 +1080,6 @@ var init_http = __esm({
1204
1080
  * @throws {BitfabError} If the request fails
1205
1081
  */
1206
1082
  async request(endpoint, payload, options) {
1207
- const url = `${this.serviceUrl}${endpoint}`;
1208
- const timeout = options?.timeout ?? this.timeout;
1209
- const method = options?.method ?? "POST";
1210
- const controller = new AbortController();
1211
- const timeoutId = setTimeout(() => controller.abort(), timeout);
1212
1083
  const { body, dropped } = serializePayloadBody(payload);
1213
1084
  if (dropped.length > 0) {
1214
1085
  try {
@@ -1218,6 +1089,19 @@ var init_http = __esm({
1218
1089
  } catch {
1219
1090
  }
1220
1091
  }
1092
+ return this.sendEncoded(endpoint, body, options);
1093
+ }
1094
+ /**
1095
+ * POST an already-encoded body. The span transport encodes its own batches,
1096
+ * so routing them back through {@link HttpClient.request} would encode the
1097
+ * same data twice.
1098
+ */
1099
+ async sendEncoded(endpoint, body, options) {
1100
+ const url = `${this.serviceUrl}${endpoint}`;
1101
+ const timeout = options?.timeout ?? this.timeout;
1102
+ const method = options?.method ?? "POST";
1103
+ const controller = new AbortController();
1104
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
1221
1105
  try {
1222
1106
  const response = await fetch(url, {
1223
1107
  method,