@bitfab/sdk 0.36.5 → 0.36.7

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.5";
45
+ __version__ = "0.36.7";
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;
@@ -247,15 +272,15 @@ function carrierBytesOf(encoded, body) {
247
272
  }
248
273
  return encoded.length + extra;
249
274
  }
250
- function fitsCarrierBudget(body) {
275
+ function fitsCarrierBudget(body, maxBytes = MAX_SPAN_CARRIER_BYTES) {
251
276
  const units = body.length;
252
- if (units * MAX_BYTES_PER_UNIT + 2 <= MAX_SPAN_CARRIER_BYTES) {
277
+ if (units * MAX_BYTES_PER_UNIT + 2 <= maxBytes) {
253
278
  return true;
254
279
  }
255
- if (units + 2 > MAX_SPAN_CARRIER_BYTES) {
280
+ if (units + 2 > maxBytes) {
256
281
  return false;
257
282
  }
258
- return carrierByteLength(body) <= MAX_SPAN_CARRIER_BYTES;
283
+ return carrierByteLength(body) <= maxBytes;
259
284
  }
260
285
  function asRecord(value) {
261
286
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
@@ -299,7 +324,7 @@ function collectCandidates(containers) {
299
324
  }
300
325
  return candidates.sort((a, b) => b.size - a.size);
301
326
  }
302
- function trimPayloadToBudget(payload, encode) {
327
+ function trimPayloadToBudget(payload, encode, maxBytes = MAX_SPAN_CARRIER_BYTES) {
303
328
  const { copy, containers } = cloneTrimmable(payload);
304
329
  const candidates = collectCandidates(containers);
305
330
  if (candidates.length === 0) {
@@ -315,30 +340,31 @@ function trimPayloadToBudget(payload, encode) {
315
340
  } catch {
316
341
  return void 0;
317
342
  }
318
- if (fitsCarrierBudget(body)) {
343
+ if (fitsCarrierBudget(body, maxBytes)) {
319
344
  return { value: copy, trimmed };
320
345
  }
321
346
  }
322
347
  return void 0;
323
348
  }
324
- function markPayloadTrimmed(value, trimmed) {
349
+ function markPayloadTrimmed(value, trimmed, maxBytes = MAX_SPAN_CARRIER_BYTES) {
325
350
  const existing = Array.isArray(value.errors) ? value.errors : [];
326
351
  value.errors = [
327
352
  ...existing,
328
353
  {
329
354
  source: "sdk",
330
355
  step: "payload_budget",
331
- error: `trimmed oversized field(s) to fit the ${MAX_SPAN_CARRIER_BYTES}-byte span carrier budget: ${[
356
+ error: `trimmed oversized field(s) to fit the ${maxBytes}-byte span carrier budget: ${[
332
357
  ...new Set(trimmed)
333
358
  ].join(", ")}`
334
359
  }
335
360
  ];
336
361
  }
337
- var MAX_SPAN_CARRIER_BYTES, textEncoder, MAX_BYTES_PER_UNIT, STRUCTURAL_SPAN_KEYS;
362
+ var MAX_SPAN_CARRIER_BYTES, MAX_COMPRESSIBLE_SPAN_CARRIER_BYTES, textEncoder, MAX_BYTES_PER_UNIT, STRUCTURAL_SPAN_KEYS;
338
363
  var init_payloadBudget = __esm({
339
364
  "src/payloadBudget.ts"() {
340
365
  "use strict";
341
366
  MAX_SPAN_CARRIER_BYTES = 28e5;
367
+ MAX_COMPRESSIBLE_SPAN_CARRIER_BYTES = 78e5;
342
368
  textEncoder = typeof TextEncoder !== "undefined" ? new TextEncoder() : null;
343
369
  MAX_BYTES_PER_UNIT = 3;
344
370
  STRUCTURAL_SPAN_KEYS = /* @__PURE__ */ new Set([
@@ -370,30 +396,31 @@ var init_warnOnce = __esm({
370
396
  });
371
397
 
372
398
  // src/serializePayload.ts
373
- function serializePayloadBody(payload) {
399
+ function serializePayloadBody(payload, maxCarrierBytes = MAX_SPAN_CARRIER_BYTES) {
374
400
  const encoded = encodePayloadBody(payload);
375
- if (fitsCarrierBudget(encoded.body)) {
401
+ if (fitsCarrierBudget(encoded.body, maxCarrierBytes)) {
376
402
  return { body: encoded.body, dropped: encoded.dropped };
377
403
  }
378
- return applyPayloadBudget(encoded);
404
+ return applyPayloadBudget(encoded, maxCarrierBytes);
379
405
  }
380
- function applyPayloadBudget(encoded) {
406
+ function applyPayloadBudget(encoded, maxCarrierBytes) {
381
407
  const result = encoded.value ? trimPayloadToBudget(
382
408
  encoded.value,
383
- (value) => encodePayloadBody(value).body
409
+ (value) => encodePayloadBody(value).body,
410
+ maxCarrierBytes
384
411
  ) : void 0;
385
412
  if (!result) {
386
413
  return { body: encoded.body, dropped: encoded.dropped };
387
414
  }
388
415
  warnOnce(
389
416
  "payload:over-budget",
390
- `a span payload exceeded the ${MAX_SPAN_CARRIER_BYTES}-byte carrier budget; its largest field(s) (${[
417
+ `a span payload exceeded the ${maxCarrierBytes}-byte carrier budget; its largest field(s) (${[
391
418
  ...new Set(result.trimmed)
392
419
  ].join(
393
420
  ", "
394
421
  )}) were replaced with placeholders so the span still ships. The span is incomplete and may not be replayable.`
395
422
  );
396
- markPayloadTrimmed(result.value, result.trimmed);
423
+ markPayloadTrimmed(result.value, result.trimmed, maxCarrierBytes);
397
424
  return {
398
425
  body: encodePayloadBody(result.value).body,
399
426
  dropped: encoded.dropped
@@ -650,6 +677,31 @@ function encodeSpan(span) {
650
677
  const json = JSON.stringify(spanToOtlp(span));
651
678
  return { json, size: byteLength(json) };
652
679
  }
680
+ function trimEncodedSpan(span) {
681
+ try {
682
+ const carrier = JSON.parse(span.json);
683
+ const attribute = carrier.attributes?.find(
684
+ (entry) => entry.key === PAYLOAD_ATTRIBUTE
685
+ );
686
+ const payloadBody = attribute?.value?.stringValue;
687
+ if (!attribute?.value || payloadBody === void 0) {
688
+ return void 0;
689
+ }
690
+ const payload = JSON.parse(payloadBody);
691
+ attribute.value.stringValue = serializePayloadBody(
692
+ payload,
693
+ MAX_SPAN_CARRIER_BYTES
694
+ ).body;
695
+ const json = JSON.stringify(carrier);
696
+ return { json, size: byteLength(json) };
697
+ } catch {
698
+ return void 0;
699
+ }
700
+ }
701
+ async function prepareRequest(body) {
702
+ const prepared = encodeRequestBody(body);
703
+ return prepared instanceof Promise ? await prepared : prepared;
704
+ }
653
705
  function requestEnvelope(first) {
654
706
  const scope = first.instrumentationScope;
655
707
  const resource = JSON.stringify({
@@ -786,7 +838,7 @@ function shutdownOtelTransports(timeoutMs = DEFAULT_LIFECYCLE_TIMEOUT_MS) {
786
838
  (transport, remaining) => transport.shutdown(remaining)
787
839
  );
788
840
  }
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;
841
+ var import_api, import_core, import_resources, import_sdk_trace_base, OPERATION_ATTRIBUTE, PAYLOAD_ATTRIBUTE, OTLP_TRACES_ENDPOINT, MAX_EXPORT_REQUEST_BYTES, MAX_DECOMPRESSED_REQUEST_BYTES, MAX_REQUEST_BYTES_ENV, EXPORT_CONCURRENCY_ENV, MAX_QUEUE_SIZE, DIRECT_MAX_EXPORT_BATCH_SIZE, DIRECT_MAX_REQUEST_BATCH_SIZE, DEFAULT_EXPORT_CONCURRENCY, MAX_EXPORT_CONCURRENCY, SCHEDULE_DELAY_MILLIS, EXPORT_TIMEOUT_MILLIS, RETRY_DELAY_MILLIS, MAX_SEND_ATTEMPTS, DEFAULT_LIFECYCLE_TIMEOUT_MS, RETRYABLE_STATUSES, liveTransports, traceSubmissionSpanIds, replayTraceSubmissions, submissionCounter, SPAN_SEPARATOR_BYTES, OtlpPayloadTooLargeError, OtlpPartialSuccessError, BitfabSpanExporter, DeliveryTrackingExporter, OtelBatchTransport;
790
842
  var init_otel = __esm({
791
843
  "src/otel.ts"() {
792
844
  "use strict";
@@ -794,6 +846,7 @@ var init_otel = __esm({
794
846
  import_core = require("@opentelemetry/core");
795
847
  import_resources = require("@opentelemetry/resources");
796
848
  import_sdk_trace_base = require("@opentelemetry/sdk-trace-base");
849
+ init_compress();
797
850
  init_constants();
798
851
  init_errors();
799
852
  init_payloadBudget();
@@ -805,6 +858,7 @@ var init_otel = __esm({
805
858
  PAYLOAD_ATTRIBUTE = "bitfab.payload";
806
859
  OTLP_TRACES_ENDPOINT = "/api/sdk/otel/v1/traces";
807
860
  MAX_EXPORT_REQUEST_BYTES = 3e6;
861
+ MAX_DECOMPRESSED_REQUEST_BYTES = 8e6;
808
862
  MAX_REQUEST_BYTES_ENV = "BITFAB_OTEL_MAX_REQUEST_BYTES";
809
863
  EXPORT_CONCURRENCY_ENV = "BITFAB_OTEL_EXPORT_CONCURRENCY";
810
864
  MAX_QUEUE_SIZE = 8192;
@@ -887,15 +941,43 @@ var init_otel = __esm({
887
941
  return batches;
888
942
  }
889
943
  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
944
  try {
897
- await this.sendWithRetries(encodeRequest(envelope, batch.spans));
898
- return true;
945
+ let requestSpans = batch.spans;
946
+ let requestRawBytes = batch.size;
947
+ let alreadyTrimmed = false;
948
+ while (true) {
949
+ if (requestRawBytes <= MAX_DECOMPRESSED_REQUEST_BYTES) {
950
+ const prepared = await prepareRequest(
951
+ encodeRequest(envelope, requestSpans)
952
+ );
953
+ if (prepared.wireBytes <= this.maxRequestBytes) {
954
+ await this.sendWithRetries(prepared);
955
+ return true;
956
+ }
957
+ }
958
+ if (batch.spans.length !== 1) {
959
+ logError(
960
+ "an OpenTelemetry span batch exceeded the configured request-size target and could not be exported"
961
+ );
962
+ return false;
963
+ }
964
+ if (alreadyTrimmed) {
965
+ logError(
966
+ "a single OpenTelemetry span exceeded the configured request-size target after trimming"
967
+ );
968
+ return false;
969
+ }
970
+ const trimmed = trimEncodedSpan(batch.spans[0]);
971
+ if (!trimmed) {
972
+ logError(
973
+ "a single OpenTelemetry span exceeded the configured request-size target and could not be trimmed"
974
+ );
975
+ return false;
976
+ }
977
+ requestSpans = [trimmed];
978
+ requestRawBytes = envelope.size + trimmed.size;
979
+ alreadyTrimmed = true;
980
+ }
899
981
  } catch (error) {
900
982
  if (error instanceof OtlpPayloadTooLargeError) {
901
983
  logError(
@@ -923,12 +1005,12 @@ var init_otel = __esm({
923
1005
  * the server does not yet understand. The fix is a client-supplied
924
1006
  * idempotency key that ingestion dedupes on.
925
1007
  */
926
- async sendWithRetries(body) {
1008
+ async sendWithRetries(request) {
927
1009
  for (let attempt = 0; attempt < MAX_SEND_ATTEMPTS; attempt += 1) {
928
1010
  try {
929
1011
  const response = await this.directSender(
930
1012
  OTLP_TRACES_ENDPOINT,
931
- body,
1013
+ request,
932
1014
  EXPORT_TIMEOUT_MILLIS
933
1015
  );
934
1016
  const partialSuccess = asRecord2(response?.partialSuccess);
@@ -1045,7 +1127,10 @@ var init_otel = __esm({
1045
1127
  return;
1046
1128
  }
1047
1129
  try {
1048
- const { body, dropped } = serializePayloadBody(payload);
1130
+ const { body, dropped } = serializePayloadBody(
1131
+ payload,
1132
+ MAX_COMPRESSIBLE_SPAN_CARRIER_BYTES
1133
+ );
1049
1134
  if (dropped.length > 0) {
1050
1135
  warnOnce(
1051
1136
  "otel-carrier-payload-stubbed",
@@ -1234,7 +1319,7 @@ var init_http = __esm({
1234
1319
  }
1235
1320
  if (!this.traceTransport) {
1236
1321
  this.traceTransport = createTraceTransport({
1237
- directSender: (endpoint, body, timeoutMs) => this.sendEncoded(endpoint, body, {
1322
+ directSender: (endpoint, request, timeoutMs) => this.sendPrepared(endpoint, request, {
1238
1323
  timeout: timeoutMs
1239
1324
  })
1240
1325
  });
@@ -1321,13 +1406,16 @@ var init_http = __esm({
1321
1406
  * same data twice.
1322
1407
  */
1323
1408
  async sendEncoded(endpoint, body, options) {
1409
+ const prepared = encodeRequestBody(body);
1410
+ const encoded = prepared instanceof Promise ? await prepared : prepared;
1411
+ return this.sendPrepared(endpoint, encoded, options);
1412
+ }
1413
+ async sendPrepared(endpoint, encoded, options) {
1324
1414
  const url = `${this.serviceUrl}${endpoint}`;
1325
1415
  const timeout = options?.timeout ?? this.timeout;
1326
1416
  const method = options?.method ?? "POST";
1327
1417
  const controller = new AbortController();
1328
1418
  const timeoutId = setTimeout(() => controller.abort(), timeout);
1329
- const prepared = encodeRequestBody(body);
1330
- const encoded = prepared instanceof Promise ? await prepared : prepared;
1331
1419
  const headers = {
1332
1420
  "Content-Type": "application/json",
1333
1421
  Authorization: `Bearer ${this.resolveApiKey() ?? ""}`
@@ -1801,8 +1889,8 @@ var init_serialize = __esm({
1801
1889
  import_superjson = __toESM(require("superjson"), 1);
1802
1890
  init_payloadBudget();
1803
1891
  init_warnOnce();
1804
- MAX_SERIALIZED_BYTES = MAX_SPAN_CARRIER_BYTES;
1805
- MAX_FRAMEWORK_SERIALIZED_BYTES = MAX_SPAN_CARRIER_BYTES;
1892
+ MAX_SERIALIZED_BYTES = MAX_COMPRESSIBLE_SPAN_CARRIER_BYTES;
1893
+ MAX_FRAMEWORK_SERIALIZED_BYTES = MAX_COMPRESSIBLE_SPAN_CARRIER_BYTES;
1806
1894
  MAX_SAFE_DEPTH = 6;
1807
1895
  }
1808
1896
  });
@@ -2384,12 +2472,13 @@ function sleep(ms) {
2384
2472
  unrefTimer(timer);
2385
2473
  });
2386
2474
  }
2387
- async function mapWithConcurrency2(tasks, maxConcurrency, onSettled) {
2475
+ async function mapWithConcurrency2(tasks, maxConcurrency, onSettled, onStarted) {
2388
2476
  const results = new Array(tasks.length);
2389
2477
  let nextIndex = 0;
2390
2478
  async function worker() {
2391
2479
  while (nextIndex < tasks.length) {
2392
2480
  const index = nextIndex++;
2481
+ onStarted?.(index);
2393
2482
  const result = await tasks[index]();
2394
2483
  results[index] = result;
2395
2484
  onSettled?.(result, index);
@@ -2476,13 +2565,15 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
2476
2565
  )
2477
2566
  );
2478
2567
  const total = tasks.length;
2568
+ const onItemFinish = options?.onItemFinish ?? options?.onProgress;
2479
2569
  let completed = 0;
2570
+ let started = 0;
2480
2571
  let succeeded = 0;
2481
2572
  let errored = 0;
2482
2573
  const resultItems = await mapWithConcurrency2(
2483
2574
  tasks,
2484
2575
  maxConcurrency,
2485
- options?.onProgress ? (item) => {
2576
+ (item) => {
2486
2577
  completed += 1;
2487
2578
  if (item.error === null) {
2488
2579
  succeeded += 1;
@@ -2490,18 +2581,17 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
2490
2581
  errored += 1;
2491
2582
  }
2492
2583
  try {
2493
- options?.onProgress?.({
2584
+ onItemFinish?.({
2494
2585
  testRunId,
2495
2586
  completed,
2496
2587
  total,
2497
2588
  succeeded,
2498
2589
  errored,
2499
2590
  item: {
2500
- // The server replay trace id isn't known until completeReplay
2501
- // runs (below), so it can't be reported mid-run and we never
2502
- // emit the client-side placeholder. originalTraceId (the
2503
- // historical trace) is known now and is what a UI keys on to
2504
- // identify what just settled.
2591
+ // The server replay trace id isn't known until completeReplay runs
2592
+ // (below), so it can't be reported mid-run and we never emit the
2593
+ // client-side placeholder. originalTraceId (the historical trace)
2594
+ // is known now and is what a UI keys on to identify what settled.
2505
2595
  traceId: null,
2506
2596
  originalTraceId: item.originalTraceId ?? null,
2507
2597
  originalSpanId: item.originalSpanId ?? null,
@@ -2522,6 +2612,30 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
2522
2612
  });
2523
2613
  } catch {
2524
2614
  }
2615
+ },
2616
+ options?.onItemStart ? (index) => {
2617
+ started += 1;
2618
+ const serverItem = serverItems[index];
2619
+ const originalTraceId = serverItem.originalTraceId ?? serverItem.sourceTraceId;
2620
+ const originalSpanId = serverItem.originalSpanId ?? serverItem.sourceSpanId;
2621
+ try {
2622
+ options.onItemStart?.({
2623
+ type: "started",
2624
+ testRunId,
2625
+ started,
2626
+ completed,
2627
+ total,
2628
+ succeeded,
2629
+ errored,
2630
+ item: {
2631
+ originalTraceId,
2632
+ originalSpanId,
2633
+ sourceTraceId: originalTraceId,
2634
+ sourceSpanId: originalSpanId
2635
+ }
2636
+ });
2637
+ } catch {
2638
+ }
2525
2639
  } : void 0
2526
2640
  );
2527
2641
  await preserveReplayFailure(
@@ -2584,17 +2698,19 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
2584
2698
  testRunUrl: fullTestRunUrl
2585
2699
  };
2586
2700
  await writeReplayResultFile(result);
2587
- try {
2588
- options?.onProgress?.({
2589
- type: "complete",
2590
- testRunId,
2591
- completed: total,
2592
- total,
2593
- succeeded,
2594
- errored,
2595
- result
2596
- });
2597
- } catch {
2701
+ if (!options?.onItemFinish) {
2702
+ try {
2703
+ options?.onProgress?.({
2704
+ type: "complete",
2705
+ testRunId,
2706
+ completed: total,
2707
+ total,
2708
+ succeeded,
2709
+ errored,
2710
+ result
2711
+ });
2712
+ } catch {
2713
+ }
2598
2714
  }
2599
2715
  return result;
2600
2716
  }