@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.d.cts CHANGED
@@ -288,9 +288,7 @@ interface ReplayContext {
288
288
  *
289
289
  * Returns `false` when delivery failed or the deadline expired, so a caller
290
290
  * that depends on persistence (replay does) can react instead of assuming a
291
- * drained queue means the server has the data. When delivery goes through a
292
- * Collector, `true` means the Collector accepted the spans; it is not proof
293
- * that Bitfab committed them.
291
+ * drained queue means the server has the data.
294
292
  *
295
293
  * @param timeoutMs - Maximum total time to wait in milliseconds (default: 5000)
296
294
  */
@@ -399,6 +397,15 @@ declare class HttpClient {
399
397
  timeout?: number;
400
398
  method?: "POST" | "PATCH" | "PUT";
401
399
  }): Promise<T>;
400
+ /**
401
+ * POST an already-encoded body. The span transport encodes its own batches,
402
+ * so routing them back through {@link HttpClient.request} would encode the
403
+ * same data twice.
404
+ */
405
+ sendEncoded<T>(endpoint: string, body: string, options?: {
406
+ timeout?: number;
407
+ method?: "POST" | "PATCH" | "PUT";
408
+ }): Promise<T>;
402
409
  /**
403
410
  * Look up a function by name.
404
411
  * Blocks until complete - needed for function execution.
@@ -2329,7 +2336,7 @@ declare class BitfabFunction {
2329
2336
  /**
2330
2337
  * SDK version from package.json (injected at build time)
2331
2338
  */
2332
- declare const __version__ = "0.34.1";
2339
+ declare const __version__ = "0.34.2";
2333
2340
 
2334
2341
  /**
2335
2342
  * Constants for the Bitfab SDK.
package/dist/index.d.ts CHANGED
@@ -288,9 +288,7 @@ interface ReplayContext {
288
288
  *
289
289
  * Returns `false` when delivery failed or the deadline expired, so a caller
290
290
  * that depends on persistence (replay does) can react instead of assuming a
291
- * drained queue means the server has the data. When delivery goes through a
292
- * Collector, `true` means the Collector accepted the spans; it is not proof
293
- * that Bitfab committed them.
291
+ * drained queue means the server has the data.
294
292
  *
295
293
  * @param timeoutMs - Maximum total time to wait in milliseconds (default: 5000)
296
294
  */
@@ -399,6 +397,15 @@ declare class HttpClient {
399
397
  timeout?: number;
400
398
  method?: "POST" | "PATCH" | "PUT";
401
399
  }): Promise<T>;
400
+ /**
401
+ * POST an already-encoded body. The span transport encodes its own batches,
402
+ * so routing them back through {@link HttpClient.request} would encode the
403
+ * same data twice.
404
+ */
405
+ sendEncoded<T>(endpoint: string, body: string, options?: {
406
+ timeout?: number;
407
+ method?: "POST" | "PATCH" | "PUT";
408
+ }): Promise<T>;
402
409
  /**
403
410
  * Look up a function by name.
404
411
  * Blocks until complete - needed for function execution.
@@ -2329,7 +2336,7 @@ declare class BitfabFunction {
2329
2336
  /**
2330
2337
  * SDK version from package.json (injected at build time)
2331
2338
  */
2332
- declare const __version__ = "0.34.1";
2339
+ declare const __version__ = "0.34.2";
2333
2340
 
2334
2341
  /**
2335
2342
  * Constants for the Bitfab SDK.
package/dist/index.js CHANGED
@@ -11,7 +11,7 @@ import {
11
11
  getCurrentReplayBranch,
12
12
  getCurrentSpan,
13
13
  getCurrentTrace
14
- } from "./chunk-KO373TDH.js";
14
+ } from "./chunk-KQB42J3S.js";
15
15
  import {
16
16
  BITFAB_PROGRESS_PREFIX,
17
17
  BitfabError,
@@ -20,7 +20,7 @@ import {
20
20
  __version__,
21
21
  flushTraces,
22
22
  reportReplayProgress
23
- } from "./chunk-5ZMEY5NX.js";
23
+ } from "./chunk-SBQQOFA5.js";
24
24
  export {
25
25
  BITFAB_PROGRESS_PREFIX,
26
26
  Bitfab,
package/dist/node.cjs CHANGED
@@ -88,7 +88,7 @@ var __version__;
88
88
  var init_version_generated = __esm({
89
89
  "src/version.generated.ts"() {
90
90
  "use strict";
91
- __version__ = "0.34.1";
91
+ __version__ = "0.34.2";
92
92
  }
93
93
  });
94
94
 
@@ -427,32 +427,30 @@ function spanToOtlp(span) {
427
427
  }
428
428
  return result;
429
429
  }
430
- function buildOtlpRequest(first, spans) {
430
+ function byteLength(value) {
431
+ return textEncoder ? textEncoder.encode(value).length : value.length;
432
+ }
433
+ function encodeSpan(span) {
434
+ const json = JSON.stringify(spanToOtlp(span));
435
+ return { json, size: byteLength(json) };
436
+ }
437
+ function requestEnvelope(first) {
431
438
  const scope = first.instrumentationScope;
432
- return {
433
- resourceSpans: [
434
- {
435
- resource: {
436
- attributes: otlpAttributes(
437
- first.resource.attributes
438
- )
439
- },
440
- scopeSpans: [
441
- {
442
- scope: { name: scope.name, version: scope.version ?? "" },
443
- spans
444
- }
445
- ]
446
- }
447
- ]
448
- };
439
+ const resource = JSON.stringify({
440
+ attributes: otlpAttributes(
441
+ first.resource.attributes
442
+ )
443
+ });
444
+ const scopeJson = JSON.stringify({
445
+ name: scope.name,
446
+ version: scope.version ?? ""
447
+ });
448
+ const head = `{"resourceSpans":[{"resource":${resource},"scopeSpans":[{"scope":${scopeJson},"spans":[`;
449
+ const tail = "]}]}]}";
450
+ return { head, tail, size: byteLength(head) + byteLength(tail) };
449
451
  }
450
- function encodedSize(value) {
451
- const json = JSON.stringify(value);
452
- if (typeof TextEncoder !== "undefined") {
453
- return new TextEncoder().encode(json).length;
454
- }
455
- return json.length;
452
+ function encodeRequest(envelope, spans) {
453
+ return envelope.head + spans.map((span) => span.json).join(",") + envelope.tail;
456
454
  }
457
455
  function delay(ms) {
458
456
  return new Promise((resolve) => {
@@ -502,10 +500,6 @@ function isRetryable(error) {
502
500
  }
503
501
  return RETRYABLE_STATUSES.has(status) || status >= 500;
504
502
  }
505
- function normalizeCollectorEndpoint(endpoint) {
506
- const trimmed = endpoint.replace(/\/+$/, "");
507
- return trimmed.endsWith("/v1/traces") ? trimmed : `${trimmed}/v1/traces`;
508
- }
509
503
  function endSpan(span, endTime) {
510
504
  span.end(endTime);
511
505
  }
@@ -542,7 +536,6 @@ function hasError(payload) {
542
536
  function createOtelTransport(options) {
543
537
  return new OtelBatchTransport({
544
538
  ...options,
545
- collectorEndpoint: readEnv(COLLECTOR_ENDPOINT_ENV) || void 0,
546
539
  exportConcurrency: readBoundedIntEnv(
547
540
  EXPORT_CONCURRENCY_ENV,
548
541
  MAX_EXPORT_CONCURRENCY,
@@ -577,7 +570,7 @@ function shutdownOtelTransports(timeoutMs = DEFAULT_LIFECYCLE_TIMEOUT_MS) {
577
570
  (transport, remaining) => transport.shutdown(remaining)
578
571
  );
579
572
  }
580
- 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;
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, 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;
581
574
  var init_otel = __esm({
582
575
  "src/otel.ts"() {
583
576
  "use strict";
@@ -597,10 +590,8 @@ var init_otel = __esm({
597
590
  MAX_EXPORT_REQUEST_BYTES = 3e6;
598
591
  MAX_REQUEST_BYTES_ENV = "BITFAB_OTEL_MAX_REQUEST_BYTES";
599
592
  EXPORT_CONCURRENCY_ENV = "BITFAB_OTEL_EXPORT_CONCURRENCY";
600
- COLLECTOR_ENDPOINT_ENV = "BITFAB_OTEL_EXPORTER_ENDPOINT";
601
593
  MAX_QUEUE_SIZE = 8192;
602
594
  DIRECT_MAX_EXPORT_BATCH_SIZE = 512;
603
- COLLECTOR_MAX_EXPORT_BATCH_SIZE = 32;
604
595
  DIRECT_MAX_REQUEST_BATCH_SIZE = 8;
605
596
  DEFAULT_EXPORT_CONCURRENCY = 32;
606
597
  MAX_EXPORT_CONCURRENCY = 64;
@@ -614,6 +605,8 @@ var init_otel = __esm({
614
605
  traceSubmissionSpanIds = /* @__PURE__ */ new Map();
615
606
  replayTraceSubmissions = /* @__PURE__ */ new Set();
616
607
  submissionCounter = 0;
608
+ textEncoder = typeof TextEncoder === "undefined" ? void 0 : new TextEncoder();
609
+ SPAN_SEPARATOR_BYTES = 1;
617
610
  OtlpPayloadTooLargeError = class extends Error {
618
611
  };
619
612
  OtlpPartialSuccessError = class extends Error {
@@ -642,57 +635,55 @@ var init_otel = __esm({
642
635
  return true;
643
636
  }
644
637
  let encoded;
638
+ let envelope;
645
639
  try {
646
- encoded = spans.map(spanToOtlp);
640
+ encoded = spans.map(encodeSpan);
641
+ envelope = requestEnvelope(spans[0]);
647
642
  } catch (error) {
648
643
  logError("failed to encode an OpenTelemetry span batch", error);
649
644
  return false;
650
645
  }
651
- const first = spans[0];
652
- const batches = this.buildRequestBatches(first, encoded);
646
+ const batches = this.buildRequestBatches(envelope, encoded);
653
647
  const results = await mapWithConcurrency(
654
648
  batches,
655
649
  this.exportConcurrency,
656
- (batch) => this.send(first, batch)
650
+ (batch) => this.send(envelope, batch)
657
651
  );
658
652
  return results.every(Boolean);
659
653
  }
660
- buildRequestBatches(first, spans) {
654
+ buildRequestBatches(envelope, spans) {
661
655
  const batches = [];
662
656
  let current = [];
657
+ let size = envelope.size;
663
658
  for (const span of spans) {
664
- if (current.length >= this.maxRequestBatchSize) {
665
- batches.push(current);
659
+ const addition = span.size + (current.length > 0 ? SPAN_SEPARATOR_BYTES : 0);
660
+ if (current.length > 0 && (current.length >= this.maxRequestBatchSize || size + addition > this.maxRequestBytes)) {
661
+ batches.push({ spans: current, size });
666
662
  current = [];
663
+ size = envelope.size;
667
664
  }
668
- const candidate = [...current, span];
669
- if (current.length > 0 && encodedSize(buildOtlpRequest(first, candidate)) > this.maxRequestBytes) {
670
- batches.push(current);
671
- current = [span];
672
- } else {
673
- current = candidate;
674
- }
665
+ current.push(span);
666
+ size += span.size + (current.length > 1 ? SPAN_SEPARATOR_BYTES : 0);
675
667
  }
676
668
  if (current.length > 0) {
677
- batches.push(current);
669
+ batches.push({ spans: current, size });
678
670
  }
679
671
  return batches;
680
672
  }
681
- async send(first, spans) {
682
- const payload = buildOtlpRequest(first, spans);
683
- if (encodedSize(payload) > this.maxRequestBytes) {
673
+ async send(envelope, batch) {
674
+ if (batch.size > this.maxRequestBytes) {
684
675
  logError(
685
676
  "a single OpenTelemetry span exceeded the configured request-size target and could not be exported"
686
677
  );
687
678
  return false;
688
679
  }
689
680
  try {
690
- await this.sendWithRetries(payload);
681
+ await this.sendWithRetries(encodeRequest(envelope, batch.spans));
691
682
  return true;
692
683
  } catch (error) {
693
684
  if (error instanceof OtlpPayloadTooLargeError) {
694
685
  logError(
695
- 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"
686
+ 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"
696
687
  );
697
688
  return false;
698
689
  }
@@ -716,12 +707,12 @@ var init_otel = __esm({
716
707
  * the server does not yet understand. The fix is a client-supplied
717
708
  * idempotency key that ingestion dedupes on.
718
709
  */
719
- async sendWithRetries(payload) {
710
+ async sendWithRetries(body) {
720
711
  for (let attempt = 0; attempt < MAX_SEND_ATTEMPTS; attempt += 1) {
721
712
  try {
722
713
  const response = await this.directSender(
723
714
  OTLP_TRACES_ENDPOINT,
724
- payload,
715
+ body,
725
716
  EXPORT_TIMEOUT_MILLIS
726
717
  );
727
718
  const partialSuccess = asRecord(response?.partialSuccess);
@@ -752,115 +743,6 @@ var init_otel = __esm({
752
743
  async forceFlush() {
753
744
  }
754
745
  };
755
- CollectorSpanExporter = class {
756
- constructor(endpoint, apiKey, maxRequestBytes) {
757
- this.endpoint = endpoint;
758
- this.apiKey = apiKey;
759
- this.maxRequestBytes = maxRequestBytes;
760
- }
761
- /**
762
- * Loaded through a dynamic import rather than a top-level one so bundlers
763
- * code-split it: Collector delivery is opt-in, and a consumer who never sets
764
- * an endpoint should not pay for the exporter in their initial bundle. It is
765
- * a hard dependency, so this cannot fail for want of the package.
766
- */
767
- loadExporterModule() {
768
- if (!this.pendingModule) {
769
- this.pendingModule = import("@opentelemetry/exporter-trace-otlp-proto");
770
- }
771
- return this.pendingModule;
772
- }
773
- export(spans, resultCallback) {
774
- void this.exportAsync(spans).then(
775
- (succeeded) => {
776
- resultCallback({
777
- code: succeeded ? import_core.ExportResultCode.SUCCESS : import_core.ExportResultCode.FAILED
778
- });
779
- },
780
- (error) => {
781
- resultCallback({ code: import_core.ExportResultCode.FAILED, error });
782
- }
783
- );
784
- }
785
- async exportAsync(spans) {
786
- if (spans.length === 0) {
787
- return true;
788
- }
789
- let delegate;
790
- try {
791
- delegate = await this.resolveDelegate();
792
- } catch (error) {
793
- logError("failed to build the OTLP Collector exporter", error);
794
- return false;
795
- }
796
- const results = await Promise.all(
797
- this.partition(spans).map(
798
- (batch) => new Promise((resolve) => {
799
- try {
800
- delegate.export(batch, (result) => {
801
- resolve(result.code === import_core.ExportResultCode.SUCCESS);
802
- });
803
- } catch (error) {
804
- logError("Collector export threw", error);
805
- resolve(false);
806
- }
807
- })
808
- )
809
- );
810
- return results.every(Boolean);
811
- }
812
- /**
813
- * Partition by the encoded JSON size of each carrier rather than its encoded
814
- * protobuf size. Protobuf is strictly smaller than the equivalent JSON for
815
- * these payloads, so the JSON figure is a conservative bound that keeps every
816
- * request under the target without pulling `@opentelemetry/otlp-transformer`
817
- * into the dependency set purely to measure bytes.
818
- */
819
- partition(spans) {
820
- const batches = [];
821
- let current = [];
822
- let currentSize = 0;
823
- for (const span of spans) {
824
- const size = encodedSize(spanToOtlp(span));
825
- if (current.length > 0 && currentSize + size > this.maxRequestBytes) {
826
- batches.push(current);
827
- current = [];
828
- currentSize = 0;
829
- }
830
- current.push(span);
831
- currentSize += size;
832
- }
833
- if (current.length > 0) {
834
- batches.push(current);
835
- }
836
- return batches;
837
- }
838
- async resolveDelegate() {
839
- const apiKey = this.apiKey() ?? "";
840
- if (this.delegate && this.delegateApiKey === apiKey) {
841
- return this.delegate;
842
- }
843
- const { OTLPTraceExporter } = await this.loadExporterModule();
844
- const previous = this.delegate;
845
- this.delegate = new OTLPTraceExporter({
846
- url: this.endpoint,
847
- headers: { Authorization: `Bearer ${apiKey}` },
848
- timeoutMillis: EXPORT_TIMEOUT_MILLIS
849
- });
850
- this.delegateApiKey = apiKey;
851
- if (previous) {
852
- void previous.shutdown().catch(() => {
853
- });
854
- }
855
- return this.delegate;
856
- }
857
- async shutdown() {
858
- await this.delegate?.shutdown();
859
- }
860
- async forceFlush() {
861
- await this.delegate?.forceFlush?.();
862
- }
863
- };
864
746
  DeliveryTrackingExporter = class {
865
747
  constructor(exporter) {
866
748
  this.exporter = exporter;
@@ -903,27 +785,22 @@ var init_otel = __esm({
903
785
  OtelBatchTransport = class {
904
786
  constructor(options) {
905
787
  this.closed = false;
906
- const collectorEndpoint = options.collectorEndpoint;
907
788
  const maxRequestBytes = options.maxRequestBytes ?? MAX_EXPORT_REQUEST_BYTES;
908
789
  const maxRequestBatchSize = options.maxRequestBatchSize ?? DIRECT_MAX_REQUEST_BATCH_SIZE;
909
790
  if (maxRequestBatchSize <= 0) {
910
791
  throw new BitfabError("maxRequestBatchSize must be a positive integer");
911
792
  }
912
793
  this.deliveryTracker = new DeliveryTrackingExporter(
913
- collectorEndpoint === void 0 ? new BitfabSpanExporter(
794
+ new BitfabSpanExporter(
914
795
  options.directSender,
915
796
  maxRequestBytes,
916
797
  maxRequestBatchSize,
917
798
  options.exportConcurrency ?? DEFAULT_EXPORT_CONCURRENCY
918
- ) : new CollectorSpanExporter(
919
- normalizeCollectorEndpoint(collectorEndpoint),
920
- options.apiKey,
921
- maxRequestBytes
922
799
  )
923
800
  );
924
801
  this.processor = new import_sdk_trace_base.BatchSpanProcessor(this.deliveryTracker, {
925
802
  maxQueueSize: options.maxQueueSize ?? MAX_QUEUE_SIZE,
926
- maxExportBatchSize: options.maxExportBatchSize ?? (collectorEndpoint === void 0 ? DIRECT_MAX_EXPORT_BATCH_SIZE : COLLECTOR_MAX_EXPORT_BATCH_SIZE),
803
+ maxExportBatchSize: options.maxExportBatchSize ?? DIRECT_MAX_EXPORT_BATCH_SIZE,
927
804
  scheduledDelayMillis: SCHEDULE_DELAY_MILLIS,
928
805
  exportTimeoutMillis: options.exportTimeoutMillis ?? EXPORT_TIMEOUT_MILLIS
929
806
  });
@@ -1140,8 +1017,7 @@ var init_http = __esm({
1140
1017
  }
1141
1018
  if (!this.traceTransport) {
1142
1019
  this.traceTransport = createTraceTransport({
1143
- apiKey: () => this.resolveApiKey(),
1144
- directSender: (endpoint, payload, timeoutMs) => this.request(endpoint, payload, {
1020
+ directSender: (endpoint, body, timeoutMs) => this.sendEncoded(endpoint, body, {
1145
1021
  timeout: timeoutMs
1146
1022
  })
1147
1023
  });
@@ -1211,11 +1087,6 @@ var init_http = __esm({
1211
1087
  * @throws {BitfabError} If the request fails
1212
1088
  */
1213
1089
  async request(endpoint, payload, options) {
1214
- const url = `${this.serviceUrl}${endpoint}`;
1215
- const timeout = options?.timeout ?? this.timeout;
1216
- const method = options?.method ?? "POST";
1217
- const controller = new AbortController();
1218
- const timeoutId = setTimeout(() => controller.abort(), timeout);
1219
1090
  const { body, dropped } = serializePayloadBody(payload);
1220
1091
  if (dropped.length > 0) {
1221
1092
  try {
@@ -1225,6 +1096,19 @@ var init_http = __esm({
1225
1096
  } catch {
1226
1097
  }
1227
1098
  }
1099
+ return this.sendEncoded(endpoint, body, options);
1100
+ }
1101
+ /**
1102
+ * POST an already-encoded body. The span transport encodes its own batches,
1103
+ * so routing them back through {@link HttpClient.request} would encode the
1104
+ * same data twice.
1105
+ */
1106
+ async sendEncoded(endpoint, body, options) {
1107
+ const url = `${this.serviceUrl}${endpoint}`;
1108
+ const timeout = options?.timeout ?? this.timeout;
1109
+ const method = options?.method ?? "POST";
1110
+ const controller = new AbortController();
1111
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
1228
1112
  try {
1229
1113
  const response = await fetch(url, {
1230
1114
  method,