@bitfab/sdk 0.36.7 → 0.36.9
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/{chunk-CFECTUDU.js → chunk-MBKY42QC.js} +379 -160
- package/dist/chunk-MBKY42QC.js.map +1 -0
- package/dist/{chunk-EDOZ3DP3.js → chunk-TJAFKZ4X.js} +3 -3
- package/dist/index.cjs +389 -164
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +56 -8
- package/dist/index.d.ts +56 -8
- package/dist/index.js +2 -2
- package/dist/node.cjs +389 -164
- package/dist/node.cjs.map +1 -1
- package/dist/node.js +2 -2
- package/dist/replay-MH5QFXET.js +21 -0
- package/package.json +1 -1
- package/dist/chunk-CFECTUDU.js.map +0 -1
- package/dist/replay-RAKYO5UI.js +0 -17
- /package/dist/{chunk-EDOZ3DP3.js.map → chunk-TJAFKZ4X.js.map} +0 -0
- /package/dist/{replay-RAKYO5UI.js.map → replay-MH5QFXET.js.map} +0 -0
|
@@ -199,10 +199,11 @@ function looksBinary(s) {
|
|
|
199
199
|
|
|
200
200
|
// src/errors.ts
|
|
201
201
|
var BitfabError = class extends Error {
|
|
202
|
-
constructor(message, url, status) {
|
|
202
|
+
constructor(message, url, status, retryAfterMs) {
|
|
203
203
|
super(message);
|
|
204
204
|
this.url = url;
|
|
205
205
|
this.status = status;
|
|
206
|
+
this.retryAfterMs = retryAfterMs;
|
|
206
207
|
this.name = "BitfabError";
|
|
207
208
|
}
|
|
208
209
|
};
|
|
@@ -303,7 +304,7 @@ function encodeRequestBody(body) {
|
|
|
303
304
|
}
|
|
304
305
|
|
|
305
306
|
// src/version.generated.ts
|
|
306
|
-
var __version__ = "0.36.
|
|
307
|
+
var __version__ = "0.36.9";
|
|
307
308
|
|
|
308
309
|
// src/constants.ts
|
|
309
310
|
var DEFAULT_SERVICE_URL = "https://bitfab.ai";
|
|
@@ -643,6 +644,17 @@ import {
|
|
|
643
644
|
BatchSpanProcessor
|
|
644
645
|
} from "@opentelemetry/sdk-trace-base";
|
|
645
646
|
|
|
647
|
+
// src/transportTypes.ts
|
|
648
|
+
var DeliveryError = class extends Error {
|
|
649
|
+
constructor(message, options = {}) {
|
|
650
|
+
super(message);
|
|
651
|
+
this.name = "DeliveryError";
|
|
652
|
+
this.retryable = options.retryable ?? false;
|
|
653
|
+
this.oversized = options.oversized ?? false;
|
|
654
|
+
this.retryAfterMs = options.retryAfterMs;
|
|
655
|
+
}
|
|
656
|
+
};
|
|
657
|
+
|
|
646
658
|
// src/unrefTimer.ts
|
|
647
659
|
function unrefTimer(timer) {
|
|
648
660
|
const handle = timer;
|
|
@@ -654,7 +666,6 @@ function unrefTimer(timer) {
|
|
|
654
666
|
// src/otel.ts
|
|
655
667
|
var OPERATION_ATTRIBUTE = "bitfab.operation";
|
|
656
668
|
var PAYLOAD_ATTRIBUTE = "bitfab.payload";
|
|
657
|
-
var OTLP_TRACES_ENDPOINT = "/api/sdk/otel/v1/traces";
|
|
658
669
|
var MAX_EXPORT_REQUEST_BYTES = 3e6;
|
|
659
670
|
var MAX_DECOMPRESSED_REQUEST_BYTES = 8e6;
|
|
660
671
|
var MAX_REQUEST_BYTES_ENV = "BITFAB_OTEL_MAX_REQUEST_BYTES";
|
|
@@ -666,14 +677,12 @@ var DEFAULT_EXPORT_CONCURRENCY = 32;
|
|
|
666
677
|
var MAX_EXPORT_CONCURRENCY = 64;
|
|
667
678
|
var SCHEDULE_DELAY_MILLIS = 5e3;
|
|
668
679
|
var EXPORT_TIMEOUT_MILLIS = 3e4;
|
|
669
|
-
var
|
|
680
|
+
var RETRY_BASE_DELAY_MILLIS = 100;
|
|
681
|
+
var RETRY_BACKOFF_CEILING_MILLIS = 5e3;
|
|
670
682
|
var MAX_SEND_ATTEMPTS = 3;
|
|
671
683
|
var DEFAULT_LIFECYCLE_TIMEOUT_MS = 3e4;
|
|
672
|
-
var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([408, 425, 429]);
|
|
673
684
|
var liveTransports = /* @__PURE__ */ new Set();
|
|
674
|
-
var
|
|
675
|
-
var replayTraceSubmissions = /* @__PURE__ */ new Set();
|
|
676
|
-
var submissionCounter = 0;
|
|
685
|
+
var carrierRefs = /* @__PURE__ */ new WeakMap();
|
|
677
686
|
function readBoundedIntEnv(name, max, fallback, warnKey) {
|
|
678
687
|
const raw = readEnv(name);
|
|
679
688
|
if (raw === void 0) {
|
|
@@ -699,59 +708,6 @@ function logError(message, error) {
|
|
|
699
708
|
} catch {
|
|
700
709
|
}
|
|
701
710
|
}
|
|
702
|
-
function recordTraceSubmission(operation, payload) {
|
|
703
|
-
const sourceTraceId = resolveSourceTraceId(payload);
|
|
704
|
-
if (sourceTraceId === void 0) {
|
|
705
|
-
return;
|
|
706
|
-
}
|
|
707
|
-
if (operation === "external_span") {
|
|
708
|
-
const rawSpan = asRecord2(payload.rawSpan);
|
|
709
|
-
if (typeof rawSpan?.id !== "string") {
|
|
710
|
-
submissionCounter += 1;
|
|
711
|
-
}
|
|
712
|
-
const sourceSpanId = typeof rawSpan?.id === "string" ? rawSpan.id : `submission-${submissionCounter}`;
|
|
713
|
-
const existing = traceSubmissionSpanIds.get(sourceTraceId);
|
|
714
|
-
if (existing) {
|
|
715
|
-
existing.add(sourceSpanId);
|
|
716
|
-
} else {
|
|
717
|
-
traceSubmissionSpanIds.set(sourceTraceId, /* @__PURE__ */ new Set([sourceSpanId]));
|
|
718
|
-
}
|
|
719
|
-
return;
|
|
720
|
-
}
|
|
721
|
-
if (payload.completed !== true) {
|
|
722
|
-
return;
|
|
723
|
-
}
|
|
724
|
-
if (typeof payload.testRunId === "string") {
|
|
725
|
-
replayTraceSubmissions.add(sourceTraceId);
|
|
726
|
-
if (!traceSubmissionSpanIds.has(sourceTraceId)) {
|
|
727
|
-
traceSubmissionSpanIds.set(sourceTraceId, /* @__PURE__ */ new Set());
|
|
728
|
-
}
|
|
729
|
-
} else {
|
|
730
|
-
traceSubmissionSpanIds.delete(sourceTraceId);
|
|
731
|
-
}
|
|
732
|
-
}
|
|
733
|
-
function takeReplaySpanCounts(traceIds) {
|
|
734
|
-
const counts = {};
|
|
735
|
-
for (const traceId of traceIds) {
|
|
736
|
-
if (!replayTraceSubmissions.has(traceId)) {
|
|
737
|
-
continue;
|
|
738
|
-
}
|
|
739
|
-
counts[traceId] = traceSubmissionSpanIds.get(traceId)?.size ?? 0;
|
|
740
|
-
traceSubmissionSpanIds.delete(traceId);
|
|
741
|
-
replayTraceSubmissions.delete(traceId);
|
|
742
|
-
}
|
|
743
|
-
return counts;
|
|
744
|
-
}
|
|
745
|
-
function asRecord2(value) {
|
|
746
|
-
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
747
|
-
}
|
|
748
|
-
function resolveSourceTraceId(payload) {
|
|
749
|
-
if (typeof payload.sourceTraceId === "string") {
|
|
750
|
-
return payload.sourceTraceId;
|
|
751
|
-
}
|
|
752
|
-
const rawTrace = asRecord2(payload.externalTrace) ?? asRecord2(payload.rawTrace);
|
|
753
|
-
return typeof rawTrace?.id === "string" ? rawTrace.id : void 0;
|
|
754
|
-
}
|
|
755
711
|
function otlpValue(value) {
|
|
756
712
|
if (typeof value === "boolean") {
|
|
757
713
|
return { boolValue: value };
|
|
@@ -810,7 +766,11 @@ function spanToOtlp(span) {
|
|
|
810
766
|
var SPAN_SEPARATOR_BYTES = 1;
|
|
811
767
|
function encodeSpan(span) {
|
|
812
768
|
const json = JSON.stringify(spanToOtlp(span));
|
|
813
|
-
return {
|
|
769
|
+
return {
|
|
770
|
+
json,
|
|
771
|
+
size: byteLength(json),
|
|
772
|
+
ref: carrierRefs.get(span)
|
|
773
|
+
};
|
|
814
774
|
}
|
|
815
775
|
function trimEncodedSpan(span) {
|
|
816
776
|
try {
|
|
@@ -893,26 +853,35 @@ async function mapWithConcurrency(items, limit, task) {
|
|
|
893
853
|
await Promise.all(workers);
|
|
894
854
|
return results;
|
|
895
855
|
}
|
|
896
|
-
var OtlpPayloadTooLargeError = class extends Error {
|
|
897
|
-
};
|
|
898
|
-
var OtlpPartialSuccessError = class extends Error {
|
|
899
|
-
};
|
|
900
|
-
function responseStatus(error) {
|
|
901
|
-
return error instanceof BitfabError ? error.status : void 0;
|
|
902
|
-
}
|
|
903
856
|
function isRetryable(error) {
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
857
|
+
return error instanceof DeliveryError && error.retryable;
|
|
858
|
+
}
|
|
859
|
+
function isOversized(error) {
|
|
860
|
+
return error instanceof DeliveryError && error.oversized;
|
|
861
|
+
}
|
|
862
|
+
function retryWaitMillis(error, attempt, remainingMillis) {
|
|
863
|
+
const requested = error instanceof DeliveryError ? error.retryAfterMs : void 0;
|
|
864
|
+
const affordable = remainingMillis / 2;
|
|
865
|
+
if (requested !== void 0) {
|
|
866
|
+
return requested < affordable ? requested : null;
|
|
907
867
|
}
|
|
908
|
-
|
|
868
|
+
const backoff = Math.min(
|
|
869
|
+
RETRY_BASE_DELAY_MILLIS * 2 ** attempt,
|
|
870
|
+
RETRY_BACKOFF_CEILING_MILLIS
|
|
871
|
+
);
|
|
872
|
+
const jittered = backoff / 2 + Math.random() * (backoff / 2);
|
|
873
|
+
return jittered < affordable ? jittered : null;
|
|
909
874
|
}
|
|
910
875
|
var BitfabSpanExporter = class {
|
|
911
|
-
constructor(directSender, maxRequestBytes, maxRequestBatchSize, exportConcurrency) {
|
|
876
|
+
constructor(directSender, maxRequestBytes, maxRequestBatchSize, exportConcurrency, onDelivered, exportTimeoutMillis = EXPORT_TIMEOUT_MILLIS) {
|
|
912
877
|
this.directSender = directSender;
|
|
913
878
|
this.maxRequestBytes = maxRequestBytes;
|
|
914
879
|
this.maxRequestBatchSize = maxRequestBatchSize;
|
|
915
880
|
this.exportConcurrency = exportConcurrency;
|
|
881
|
+
this.onDelivered = onDelivered;
|
|
882
|
+
this.exportTimeoutMillis = exportTimeoutMillis;
|
|
883
|
+
/** Epoch ms until which the server has asked this exporter to stay away. */
|
|
884
|
+
this.throttledUntil = 0;
|
|
916
885
|
}
|
|
917
886
|
export(spans, resultCallback) {
|
|
918
887
|
void this.exportAsync(spans).then(
|
|
@@ -978,6 +947,7 @@ var BitfabSpanExporter = class {
|
|
|
978
947
|
);
|
|
979
948
|
if (prepared.wireBytes <= this.maxRequestBytes) {
|
|
980
949
|
await this.sendWithRetries(prepared);
|
|
950
|
+
this.reportDelivered(batch.spans);
|
|
981
951
|
return true;
|
|
982
952
|
}
|
|
983
953
|
}
|
|
@@ -1005,15 +975,12 @@ var BitfabSpanExporter = class {
|
|
|
1005
975
|
alreadyTrimmed = true;
|
|
1006
976
|
}
|
|
1007
977
|
} catch (error) {
|
|
1008
|
-
if (error
|
|
978
|
+
if (isOversized(error)) {
|
|
1009
979
|
logError(
|
|
1010
980
|
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"
|
|
1011
981
|
);
|
|
1012
982
|
return false;
|
|
1013
983
|
}
|
|
1014
|
-
if (error instanceof OtlpPartialSuccessError) {
|
|
1015
|
-
return false;
|
|
1016
|
-
}
|
|
1017
984
|
logError("failed to export an OpenTelemetry span batch", error);
|
|
1018
985
|
return false;
|
|
1019
986
|
}
|
|
@@ -1031,37 +998,79 @@ var BitfabSpanExporter = class {
|
|
|
1031
998
|
* the server does not yet understand. The fix is a client-supplied
|
|
1032
999
|
* idempotency key that ingestion dedupes on.
|
|
1033
1000
|
*/
|
|
1001
|
+
/**
|
|
1002
|
+
* Remember a throttle the server asked for, so the requests fanned out
|
|
1003
|
+
* alongside this one respect it too. Delaying only the request that was
|
|
1004
|
+
* refused leaves the other seven in the window hitting a server that just
|
|
1005
|
+
* asked for room.
|
|
1006
|
+
*/
|
|
1007
|
+
recordThrottle(error) {
|
|
1008
|
+
const requested = error instanceof DeliveryError ? error.retryAfterMs : void 0;
|
|
1009
|
+
if (requested !== void 0) {
|
|
1010
|
+
this.throttledUntil = Math.max(
|
|
1011
|
+
this.throttledUntil,
|
|
1012
|
+
Date.now() + requested
|
|
1013
|
+
);
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
/**
|
|
1017
|
+
* Waits out an active throttle, or reports the batch undeliverable when the
|
|
1018
|
+
* throttle outlasts what we are willing to hold it for. Either way nothing is
|
|
1019
|
+
* sent while the server has asked us to stay away.
|
|
1020
|
+
*/
|
|
1021
|
+
async awaitThrottle(deadline) {
|
|
1022
|
+
const remaining = this.throttledUntil - Date.now();
|
|
1023
|
+
if (remaining <= 0) {
|
|
1024
|
+
return;
|
|
1025
|
+
}
|
|
1026
|
+
if (remaining >= (deadline - Date.now()) / 2) {
|
|
1027
|
+
throw new DeliveryError(
|
|
1028
|
+
`OTLP ingestion is throttled for another ${remaining}ms, longer than the export budget`
|
|
1029
|
+
);
|
|
1030
|
+
}
|
|
1031
|
+
await delay(remaining);
|
|
1032
|
+
}
|
|
1034
1033
|
async sendWithRetries(request) {
|
|
1034
|
+
const deadline = Date.now() + this.exportTimeoutMillis;
|
|
1035
1035
|
for (let attempt = 0; attempt < MAX_SEND_ATTEMPTS; attempt += 1) {
|
|
1036
1036
|
try {
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
request,
|
|
1040
|
-
EXPORT_TIMEOUT_MILLIS
|
|
1041
|
-
);
|
|
1042
|
-
const partialSuccess = asRecord2(response?.partialSuccess);
|
|
1043
|
-
const rejected = partialSuccess?.rejectedSpans;
|
|
1044
|
-
if (rejected !== void 0 && rejected !== "0" && rejected !== 0) {
|
|
1045
|
-
logError(
|
|
1046
|
-
`OTLP ingestion rejected ${rejected} span(s): ${partialSuccess?.errorMessage ?? "no reason provided"}`
|
|
1047
|
-
);
|
|
1048
|
-
throw new OtlpPartialSuccessError();
|
|
1049
|
-
}
|
|
1037
|
+
await this.awaitThrottle(deadline);
|
|
1038
|
+
await this.directSender(request, Math.max(0, deadline - Date.now()));
|
|
1050
1039
|
return;
|
|
1051
1040
|
} catch (error) {
|
|
1052
|
-
if (error
|
|
1041
|
+
if (isOversized(error)) {
|
|
1053
1042
|
throw error;
|
|
1054
1043
|
}
|
|
1055
|
-
|
|
1056
|
-
throw new OtlpPayloadTooLargeError();
|
|
1057
|
-
}
|
|
1044
|
+
this.recordThrottle(error);
|
|
1058
1045
|
if (attempt === MAX_SEND_ATTEMPTS - 1 || !isRetryable(error)) {
|
|
1059
1046
|
throw error;
|
|
1060
1047
|
}
|
|
1061
|
-
|
|
1048
|
+
const wait = retryWaitMillis(error, attempt, deadline - Date.now());
|
|
1049
|
+
if (wait === null) {
|
|
1050
|
+
throw error;
|
|
1051
|
+
}
|
|
1052
|
+
await delay(wait);
|
|
1062
1053
|
}
|
|
1063
1054
|
}
|
|
1064
1055
|
}
|
|
1056
|
+
/**
|
|
1057
|
+
* Announce the carriers a request delivered. Wrapped because a listener that
|
|
1058
|
+
* throws must never turn a delivered batch into a failed export.
|
|
1059
|
+
*/
|
|
1060
|
+
reportDelivered(spans) {
|
|
1061
|
+
if (this.onDelivered === void 0) {
|
|
1062
|
+
return;
|
|
1063
|
+
}
|
|
1064
|
+
const refs = spans.map((span) => span.ref).filter((ref) => ref !== void 0);
|
|
1065
|
+
if (refs.length === 0) {
|
|
1066
|
+
return;
|
|
1067
|
+
}
|
|
1068
|
+
try {
|
|
1069
|
+
this.onDelivered(refs);
|
|
1070
|
+
} catch (error) {
|
|
1071
|
+
logError("a delivery listener threw", error);
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1065
1074
|
async shutdown() {
|
|
1066
1075
|
}
|
|
1067
1076
|
async forceFlush() {
|
|
@@ -1119,7 +1128,9 @@ var OtelBatchTransport = class {
|
|
|
1119
1128
|
options.directSender,
|
|
1120
1129
|
maxRequestBytes,
|
|
1121
1130
|
maxRequestBatchSize,
|
|
1122
|
-
options.exportConcurrency ?? DEFAULT_EXPORT_CONCURRENCY
|
|
1131
|
+
options.exportConcurrency ?? DEFAULT_EXPORT_CONCURRENCY,
|
|
1132
|
+
options.onDelivered,
|
|
1133
|
+
options.exportTimeoutMillis ?? EXPORT_TIMEOUT_MILLIS
|
|
1123
1134
|
)
|
|
1124
1135
|
);
|
|
1125
1136
|
this.processor = new BatchSpanProcessor(this.deliveryTracker, {
|
|
@@ -1143,8 +1154,7 @@ var OtelBatchTransport = class {
|
|
|
1143
1154
|
this.tracer = this.provider.getTracer("bitfab", __version__);
|
|
1144
1155
|
liveTransports.add(this);
|
|
1145
1156
|
}
|
|
1146
|
-
submit(operation, payload) {
|
|
1147
|
-
recordTraceSubmission(operation, payload);
|
|
1157
|
+
submit(operation, payload, meta = {}) {
|
|
1148
1158
|
if (this.closed) {
|
|
1149
1159
|
warnOnce(
|
|
1150
1160
|
"otel-submit-after-shutdown",
|
|
@@ -1165,17 +1175,20 @@ var OtelBatchTransport = class {
|
|
|
1165
1175
|
].join(", ")}); they were stubbed so the span still ships, but the trace may be incomplete or not replayable.`
|
|
1166
1176
|
);
|
|
1167
1177
|
}
|
|
1168
|
-
const span = this.tracer.startSpan(
|
|
1178
|
+
const span = this.tracer.startSpan(meta.name ?? `bitfab.${operation}`, {
|
|
1169
1179
|
attributes: {
|
|
1170
1180
|
[OPERATION_ATTRIBUTE]: operation,
|
|
1171
1181
|
[PAYLOAD_ATTRIBUTE]: body
|
|
1172
1182
|
},
|
|
1173
|
-
startTime:
|
|
1183
|
+
startTime: meta.startTime
|
|
1174
1184
|
});
|
|
1175
|
-
if (
|
|
1185
|
+
if (meta.ref !== void 0) {
|
|
1186
|
+
carrierRefs.set(span, meta.ref);
|
|
1187
|
+
}
|
|
1188
|
+
if (meta.errored === true) {
|
|
1176
1189
|
span.setStatus({ code: SpanStatusCode.ERROR });
|
|
1177
1190
|
}
|
|
1178
|
-
endSpan(span,
|
|
1191
|
+
endSpan(span, meta.endTime);
|
|
1179
1192
|
} catch (error) {
|
|
1180
1193
|
logError("failed to queue an OpenTelemetry span", error);
|
|
1181
1194
|
}
|
|
@@ -1215,36 +1228,6 @@ var OtelBatchTransport = class {
|
|
|
1215
1228
|
function endSpan(span, endTime) {
|
|
1216
1229
|
span.end(endTime);
|
|
1217
1230
|
}
|
|
1218
|
-
function spanName(operation, payload) {
|
|
1219
|
-
if (operation === "external_span") {
|
|
1220
|
-
const spanData = asRecord2(asRecord2(payload.rawSpan)?.span_data);
|
|
1221
|
-
if (typeof spanData?.name === "string") {
|
|
1222
|
-
return spanData.name;
|
|
1223
|
-
}
|
|
1224
|
-
}
|
|
1225
|
-
if (typeof payload.traceFunctionKey === "string") {
|
|
1226
|
-
return payload.traceFunctionKey;
|
|
1227
|
-
}
|
|
1228
|
-
return `bitfab.${operation}`;
|
|
1229
|
-
}
|
|
1230
|
-
function payloadTimestamp(payload, field) {
|
|
1231
|
-
const rawSpan = asRecord2(payload.rawSpan);
|
|
1232
|
-
const rawTrace = asRecord2(payload.externalTrace) ?? asRecord2(payload.rawTrace);
|
|
1233
|
-
const raw = rawSpan?.[field] ?? rawTrace?.[field];
|
|
1234
|
-
if (typeof raw !== "string") {
|
|
1235
|
-
return void 0;
|
|
1236
|
-
}
|
|
1237
|
-
const parsed = Date.parse(raw);
|
|
1238
|
-
return Number.isNaN(parsed) ? void 0 : parsed;
|
|
1239
|
-
}
|
|
1240
|
-
function hasError(payload) {
|
|
1241
|
-
const spanData = asRecord2(asRecord2(payload.rawSpan)?.span_data);
|
|
1242
|
-
if (spanData?.error != null) {
|
|
1243
|
-
return true;
|
|
1244
|
-
}
|
|
1245
|
-
const errors = payload.errors;
|
|
1246
|
-
return Array.isArray(errors) ? errors.length > 0 : Boolean(errors);
|
|
1247
|
-
}
|
|
1248
1231
|
function createOtelTransport(options) {
|
|
1249
1232
|
return new OtelBatchTransport({
|
|
1250
1233
|
...options,
|
|
@@ -1293,12 +1276,11 @@ function flushTraceTransports(timeoutMs) {
|
|
|
1293
1276
|
function shutdownTraceTransports(timeoutMs) {
|
|
1294
1277
|
return shutdownOtelTransports(timeoutMs);
|
|
1295
1278
|
}
|
|
1296
|
-
function takeReplaySpanCounts2(traceIds) {
|
|
1297
|
-
return takeReplaySpanCounts(traceIds);
|
|
1298
|
-
}
|
|
1299
1279
|
|
|
1300
1280
|
// src/http.ts
|
|
1301
1281
|
var REPLAY_DB_BRANCH_REQUEST_TIMEOUT_MS = 3e5;
|
|
1282
|
+
var OTLP_TRACES_ENDPOINT = "/api/sdk/otel/v1/traces";
|
|
1283
|
+
var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
|
|
1302
1284
|
var EXIT_FLUSH_TIMEOUT_MS = 5e3;
|
|
1303
1285
|
var DEFAULT_LIFECYCLE_TIMEOUT_MS2 = 3e4;
|
|
1304
1286
|
var pendingTracePromises = /* @__PURE__ */ new Set();
|
|
@@ -1358,8 +1340,101 @@ if (typeof process !== "undefined" && process.versions != null && process.versio
|
|
|
1358
1340
|
});
|
|
1359
1341
|
});
|
|
1360
1342
|
}
|
|
1343
|
+
function readHeader(response, name) {
|
|
1344
|
+
try {
|
|
1345
|
+
return response.headers?.get(name) ?? null;
|
|
1346
|
+
} catch {
|
|
1347
|
+
return null;
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
function parseRetryAfterMs(header) {
|
|
1351
|
+
const value = header?.trim();
|
|
1352
|
+
if (!value) {
|
|
1353
|
+
return void 0;
|
|
1354
|
+
}
|
|
1355
|
+
const seconds = Number(value);
|
|
1356
|
+
if (Number.isFinite(seconds)) {
|
|
1357
|
+
return seconds >= 0 ? seconds * 1e3 : void 0;
|
|
1358
|
+
}
|
|
1359
|
+
const at = Date.parse(value);
|
|
1360
|
+
if (Number.isNaN(at)) {
|
|
1361
|
+
return void 0;
|
|
1362
|
+
}
|
|
1363
|
+
return Math.max(0, at - Date.now());
|
|
1364
|
+
}
|
|
1365
|
+
function carrierMeta(operation, payload, ref) {
|
|
1366
|
+
return {
|
|
1367
|
+
ref,
|
|
1368
|
+
name: carrierName(operation, payload),
|
|
1369
|
+
startTime: payloadTimestamp(payload, "started_at"),
|
|
1370
|
+
endTime: payloadTimestamp(payload, "ended_at"),
|
|
1371
|
+
errored: payloadHasError(payload)
|
|
1372
|
+
};
|
|
1373
|
+
}
|
|
1374
|
+
function carrierName(operation, payload) {
|
|
1375
|
+
if (operation === "external_span") {
|
|
1376
|
+
const spanData = asPayloadRecord(
|
|
1377
|
+
asPayloadRecord(payload.rawSpan)?.span_data
|
|
1378
|
+
);
|
|
1379
|
+
if (typeof spanData?.name === "string") {
|
|
1380
|
+
return spanData.name;
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
if (typeof payload.traceFunctionKey === "string") {
|
|
1384
|
+
return payload.traceFunctionKey;
|
|
1385
|
+
}
|
|
1386
|
+
return `bitfab.${operation}`;
|
|
1387
|
+
}
|
|
1388
|
+
function payloadTimestamp(payload, field) {
|
|
1389
|
+
const rawSpan = asPayloadRecord(payload.rawSpan);
|
|
1390
|
+
const rawTrace = asPayloadRecord(payload.externalTrace) ?? asPayloadRecord(payload.rawTrace);
|
|
1391
|
+
const raw = rawSpan?.[field] ?? rawTrace?.[field];
|
|
1392
|
+
if (typeof raw !== "string") {
|
|
1393
|
+
return void 0;
|
|
1394
|
+
}
|
|
1395
|
+
const parsed = Date.parse(raw);
|
|
1396
|
+
return Number.isNaN(parsed) ? void 0 : parsed;
|
|
1397
|
+
}
|
|
1398
|
+
function payloadHasError(payload) {
|
|
1399
|
+
const spanData = asPayloadRecord(asPayloadRecord(payload.rawSpan)?.span_data);
|
|
1400
|
+
if (spanData?.error != null) {
|
|
1401
|
+
return true;
|
|
1402
|
+
}
|
|
1403
|
+
const errors = payload.errors;
|
|
1404
|
+
return Array.isArray(errors) ? errors.length > 0 : Boolean(errors);
|
|
1405
|
+
}
|
|
1406
|
+
function asPayloadRecord(value) {
|
|
1407
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
1408
|
+
}
|
|
1409
|
+
function carrierRef(payload) {
|
|
1410
|
+
const traceId = sourceTraceIdOf(payload);
|
|
1411
|
+
if (traceId === void 0) {
|
|
1412
|
+
return void 0;
|
|
1413
|
+
}
|
|
1414
|
+
const rawSpan = payload.rawSpan;
|
|
1415
|
+
if (rawSpan === void 0) {
|
|
1416
|
+
return { traceId };
|
|
1417
|
+
}
|
|
1418
|
+
const spanId = rawSpan?.id;
|
|
1419
|
+
return {
|
|
1420
|
+
traceId,
|
|
1421
|
+
spanId: typeof spanId === "string" ? spanId : `submission-${++carrierSeq}`
|
|
1422
|
+
};
|
|
1423
|
+
}
|
|
1424
|
+
function sourceTraceIdOf(payload) {
|
|
1425
|
+
if (typeof payload.sourceTraceId === "string") {
|
|
1426
|
+
return payload.sourceTraceId;
|
|
1427
|
+
}
|
|
1428
|
+
const rawTrace = payload.externalTrace ?? payload.rawTrace;
|
|
1429
|
+
const id = rawTrace?.id;
|
|
1430
|
+
return typeof id === "string" ? id : void 0;
|
|
1431
|
+
}
|
|
1432
|
+
var carrierSeq = 0;
|
|
1361
1433
|
var HttpClient = class {
|
|
1362
1434
|
constructor(config) {
|
|
1435
|
+
// Only traces a caller asked about are tracked, so ordinary tracing stores
|
|
1436
|
+
// nothing here.
|
|
1437
|
+
this.traceDeliveries = /* @__PURE__ */ new Map();
|
|
1363
1438
|
// Deferred span work owned by THIS client. The module-global set backs the
|
|
1364
1439
|
// process-wide `flushTraces()` and the exit hook, but per-client lifecycle
|
|
1365
1440
|
// must not wait on another client's slow finalize: a false `close()` failure
|
|
@@ -1395,13 +1470,131 @@ var HttpClient = class {
|
|
|
1395
1470
|
}
|
|
1396
1471
|
if (!this.traceTransport) {
|
|
1397
1472
|
this.traceTransport = createTraceTransport({
|
|
1398
|
-
directSender: (
|
|
1399
|
-
|
|
1400
|
-
})
|
|
1473
|
+
directSender: (request, timeoutMs) => this.deliverCarriers(request, timeoutMs),
|
|
1474
|
+
onDelivered: (refs) => this.recordDeliveredCarriers(refs)
|
|
1401
1475
|
});
|
|
1402
1476
|
}
|
|
1403
1477
|
return this.traceTransport;
|
|
1404
1478
|
}
|
|
1479
|
+
/**
|
|
1480
|
+
* Post one encoded batch and decide what the server's answer means, so the
|
|
1481
|
+
* transport never reads a response. Rejections and permanent statuses come
|
|
1482
|
+
* back as a non-retryable {@link DeliveryError}; anything the server might
|
|
1483
|
+
* still accept on a second try comes back retryable.
|
|
1484
|
+
*/
|
|
1485
|
+
async deliverCarriers(request, timeoutMs) {
|
|
1486
|
+
let response;
|
|
1487
|
+
try {
|
|
1488
|
+
response = await this.sendPrepared(
|
|
1489
|
+
OTLP_TRACES_ENDPOINT,
|
|
1490
|
+
request,
|
|
1491
|
+
{ timeout: timeoutMs }
|
|
1492
|
+
);
|
|
1493
|
+
} catch (error) {
|
|
1494
|
+
const status = error instanceof BitfabError ? error.status : void 0;
|
|
1495
|
+
if (status === void 0) {
|
|
1496
|
+
throw new DeliveryError(`OTLP ingestion failed: ${String(error)}`, {
|
|
1497
|
+
retryable: true
|
|
1498
|
+
});
|
|
1499
|
+
}
|
|
1500
|
+
throw new DeliveryError(`OTLP ingestion failed with HTTP ${status}`, {
|
|
1501
|
+
retryable: RETRYABLE_STATUSES.has(status),
|
|
1502
|
+
oversized: status === 413,
|
|
1503
|
+
...error instanceof BitfabError && error.retryAfterMs !== void 0 ? { retryAfterMs: error.retryAfterMs } : {}
|
|
1504
|
+
});
|
|
1505
|
+
}
|
|
1506
|
+
const partialSuccess = asPayloadRecord(response?.partialSuccess);
|
|
1507
|
+
const rejected = partialSuccess?.rejectedSpans;
|
|
1508
|
+
if (rejected !== void 0 && rejected !== "0" && rejected !== 0) {
|
|
1509
|
+
throw new DeliveryError(
|
|
1510
|
+
`OTLP ingestion rejected ${rejected} span(s): ${partialSuccess?.errorMessage ?? "no reason provided"}`
|
|
1511
|
+
);
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
/**
|
|
1515
|
+
* Start tracking delivery for `traceIds`. Nothing is recorded for a trace
|
|
1516
|
+
* that was never tracked, so ordinary tracing costs no bookkeeping at all.
|
|
1517
|
+
*/
|
|
1518
|
+
trackTraceDeliveries(traceIds) {
|
|
1519
|
+
for (const traceId of traceIds) {
|
|
1520
|
+
if (!this.traceDeliveries.has(traceId)) {
|
|
1521
|
+
this.traceDeliveries.set(traceId, {
|
|
1522
|
+
submittedSpanIds: /* @__PURE__ */ new Set(),
|
|
1523
|
+
ackedSpanIds: /* @__PURE__ */ new Set(),
|
|
1524
|
+
closed: false,
|
|
1525
|
+
closingAcked: false
|
|
1526
|
+
});
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
/** Whether any tracked trace has had its closing carrier submitted. */
|
|
1531
|
+
hasClosedDeliveries(traceIds) {
|
|
1532
|
+
return traceIds.some((traceId) => this.traceDeliveries.get(traceId)?.closed);
|
|
1533
|
+
}
|
|
1534
|
+
/**
|
|
1535
|
+
* Report what each tracked trace submitted and whether the server confirmed
|
|
1536
|
+
* it, and stop tracking them. Every id passed is freed, so a caller cannot
|
|
1537
|
+
* leak a record for a trace that never closed.
|
|
1538
|
+
*
|
|
1539
|
+
* `delivered` is only meaningful once a flush has settled: acks land before
|
|
1540
|
+
* an export resolves, so a flush that reported success has already collected
|
|
1541
|
+
* every ack it is going to collect.
|
|
1542
|
+
*/
|
|
1543
|
+
takeTraceDeliveries(traceIds) {
|
|
1544
|
+
const reports = {};
|
|
1545
|
+
for (const traceId of traceIds) {
|
|
1546
|
+
const delivery = this.traceDeliveries.get(traceId);
|
|
1547
|
+
if (delivery === void 0) {
|
|
1548
|
+
continue;
|
|
1549
|
+
}
|
|
1550
|
+
this.traceDeliveries.delete(traceId);
|
|
1551
|
+
reports[traceId] = {
|
|
1552
|
+
spanCount: delivery.submittedSpanIds.size,
|
|
1553
|
+
closed: delivery.closed,
|
|
1554
|
+
delivered: delivery.closingAcked && [...delivery.submittedSpanIds].every(
|
|
1555
|
+
(spanId) => delivery.ackedSpanIds.has(spanId)
|
|
1556
|
+
)
|
|
1557
|
+
};
|
|
1558
|
+
}
|
|
1559
|
+
return reports;
|
|
1560
|
+
}
|
|
1561
|
+
/** Build a carrier's meta and record what it adds to its trace's expected set. */
|
|
1562
|
+
recordedMeta(operation, payload, ref) {
|
|
1563
|
+
this.recordSubmittedCarrier(ref);
|
|
1564
|
+
return carrierMeta(operation, payload, ref);
|
|
1565
|
+
}
|
|
1566
|
+
recordSubmittedCarrier(ref) {
|
|
1567
|
+
if (ref === void 0) {
|
|
1568
|
+
return;
|
|
1569
|
+
}
|
|
1570
|
+
const delivery = this.traceDeliveries.get(ref.traceId);
|
|
1571
|
+
if (delivery === void 0) {
|
|
1572
|
+
return;
|
|
1573
|
+
}
|
|
1574
|
+
if (ref.spanId === void 0) {
|
|
1575
|
+
delivery.closed = true;
|
|
1576
|
+
} else {
|
|
1577
|
+
delivery.submittedSpanIds.add(ref.spanId);
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
/**
|
|
1581
|
+
* Ingestion commits every carrier in a request before it answers, so a
|
|
1582
|
+
* delivered ref is proof its row exists: the same fact the replay status
|
|
1583
|
+
* endpoint would report, already in hand.
|
|
1584
|
+
*/
|
|
1585
|
+
recordDeliveredCarriers(refs) {
|
|
1586
|
+
for (const ref of refs) {
|
|
1587
|
+
const delivery = this.traceDeliveries.get(ref.traceId);
|
|
1588
|
+
if (delivery === void 0) {
|
|
1589
|
+
continue;
|
|
1590
|
+
}
|
|
1591
|
+
if (ref.spanId === void 0) {
|
|
1592
|
+
delivery.closingAcked = true;
|
|
1593
|
+
} else {
|
|
1594
|
+
delivery.ackedSpanIds.add(ref.spanId);
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1405
1598
|
/**
|
|
1406
1599
|
* Track deferred span work so this client's own lifecycle waits for it, and
|
|
1407
1600
|
* so the process-wide flush and exit hook do too.
|
|
@@ -1511,7 +1704,8 @@ var HttpClient = class {
|
|
|
1511
1704
|
throw new BitfabError(
|
|
1512
1705
|
`HTTP ${response.status}: ${errorText.slice(0, 500)}`,
|
|
1513
1706
|
void 0,
|
|
1514
|
-
response.status
|
|
1707
|
+
response.status,
|
|
1708
|
+
parseRetryAfterMs(readHeader(response, "retry-after"))
|
|
1515
1709
|
);
|
|
1516
1710
|
}
|
|
1517
1711
|
const result = await response.json();
|
|
@@ -1597,11 +1791,12 @@ var HttpClient = class {
|
|
|
1597
1791
|
* the OTLP carrier has no path to carry it.
|
|
1598
1792
|
*/
|
|
1599
1793
|
sendInternalTrace(functionId, payload) {
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1794
|
+
const body = { ...payload, functionId, sdkVersion: __version__ };
|
|
1795
|
+
this.getTraceTransport()?.submit(
|
|
1796
|
+
"internal_trace",
|
|
1797
|
+
body,
|
|
1798
|
+
carrierMeta("internal_trace", body, void 0)
|
|
1799
|
+
);
|
|
1605
1800
|
}
|
|
1606
1801
|
/**
|
|
1607
1802
|
* Queue an external span (from withSpan wrapper or OpenAI tracing) onto this
|
|
@@ -1610,10 +1805,11 @@ var HttpClient = class {
|
|
|
1610
1805
|
* promise.
|
|
1611
1806
|
*/
|
|
1612
1807
|
sendExternalSpan(payload) {
|
|
1613
|
-
this.getTraceTransport()?.submit(
|
|
1614
|
-
|
|
1615
|
-
sdkVersion: __version__
|
|
1616
|
-
|
|
1808
|
+
this.getTraceTransport()?.submit(
|
|
1809
|
+
"external_span",
|
|
1810
|
+
{ ...payload, sdkVersion: __version__ },
|
|
1811
|
+
this.recordedMeta("external_span", payload, carrierRef(payload))
|
|
1812
|
+
);
|
|
1617
1813
|
}
|
|
1618
1814
|
/**
|
|
1619
1815
|
* Queue an external trace completion (from OpenAI tracing) onto this
|
|
@@ -1622,10 +1818,15 @@ var HttpClient = class {
|
|
|
1622
1818
|
* server-authoritative barrier in `replay.ts`, not by awaiting this call.
|
|
1623
1819
|
*/
|
|
1624
1820
|
sendExternalTrace(payload) {
|
|
1625
|
-
this.getTraceTransport()?.submit(
|
|
1626
|
-
|
|
1627
|
-
sdkVersion: __version__
|
|
1628
|
-
|
|
1821
|
+
this.getTraceTransport()?.submit(
|
|
1822
|
+
"external_trace",
|
|
1823
|
+
{ ...payload, sdkVersion: __version__ },
|
|
1824
|
+
this.recordedMeta(
|
|
1825
|
+
"external_trace",
|
|
1826
|
+
payload,
|
|
1827
|
+
payload.completed === true ? carrierRef(payload) : void 0
|
|
1828
|
+
)
|
|
1829
|
+
);
|
|
1629
1830
|
}
|
|
1630
1831
|
/**
|
|
1631
1832
|
* Partial update of an existing trace identified by its Bitfab trace ID.
|
|
@@ -2303,15 +2504,29 @@ async function waitForReplayPersistence(httpClient, testRunId, replayedTraceIds)
|
|
|
2303
2504
|
REPLAY_PERSISTENCE_TIMEOUT_MS
|
|
2304
2505
|
);
|
|
2305
2506
|
if (!deferredSettled) {
|
|
2507
|
+
httpClient.takeTraceDeliveries(replayedTraceIds);
|
|
2306
2508
|
throw new BitfabError(
|
|
2307
2509
|
`Replay could not settle deferred span work before the deadline, so the expected span counts are incomplete (testRunId ${testRunId}).`
|
|
2308
2510
|
);
|
|
2309
2511
|
}
|
|
2310
|
-
|
|
2311
|
-
|
|
2512
|
+
if (!httpClient.hasClosedDeliveries(replayedTraceIds)) {
|
|
2513
|
+
httpClient.takeTraceDeliveries(replayedTraceIds);
|
|
2312
2514
|
return;
|
|
2313
2515
|
}
|
|
2314
2516
|
const flushed = await flushTraces(REPLAY_PERSISTENCE_TIMEOUT_MS);
|
|
2517
|
+
const deliveries = httpClient.takeTraceDeliveries(replayedTraceIds);
|
|
2518
|
+
const expectedSpanCounts = {};
|
|
2519
|
+
let allDelivered = true;
|
|
2520
|
+
for (const [traceId, delivery] of Object.entries(deliveries)) {
|
|
2521
|
+
if (!delivery.closed) {
|
|
2522
|
+
continue;
|
|
2523
|
+
}
|
|
2524
|
+
expectedSpanCounts[traceId] = delivery.spanCount;
|
|
2525
|
+
allDelivered = allDelivered && delivery.delivered;
|
|
2526
|
+
}
|
|
2527
|
+
if (allDelivered) {
|
|
2528
|
+
return;
|
|
2529
|
+
}
|
|
2315
2530
|
const deadline = Date.now() + REPLAY_PERSISTENCE_TIMEOUT_MS;
|
|
2316
2531
|
let missing = Object.keys(expectedSpanCounts).length;
|
|
2317
2532
|
while (true) {
|
|
@@ -2329,17 +2544,18 @@ async function waitForReplayPersistence(httpClient, testRunId, replayedTraceIds)
|
|
|
2329
2544
|
if (Date.now() >= deadline) {
|
|
2330
2545
|
break;
|
|
2331
2546
|
}
|
|
2332
|
-
await
|
|
2547
|
+
await sleepForReplayPersistence(
|
|
2548
|
+
Math.min(100, Math.max(0, deadline - Date.now()))
|
|
2549
|
+
);
|
|
2333
2550
|
}
|
|
2334
2551
|
const cause = flushed ? "" : " Delivery was also not confirmed before the flush deadline, so the spans likely never reached the server.";
|
|
2335
2552
|
throw new BitfabError(
|
|
2336
2553
|
`Replay traces were not fully persisted before the delivery deadline (testRunId ${testRunId}, missing ${missing} of ${Object.keys(expectedSpanCounts).length} trace(s)).${cause}`
|
|
2337
2554
|
);
|
|
2338
2555
|
}
|
|
2339
|
-
function
|
|
2556
|
+
function sleepForReplayPersistence(ms) {
|
|
2340
2557
|
return new Promise((resolve) => {
|
|
2341
|
-
|
|
2342
|
-
unrefTimer(timer);
|
|
2558
|
+
setTimeout(resolve, ms);
|
|
2343
2559
|
});
|
|
2344
2560
|
}
|
|
2345
2561
|
async function mapWithConcurrency2(tasks, maxConcurrency, onSettled, onStarted) {
|
|
@@ -2420,6 +2636,7 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
|
|
|
2420
2636
|
...registeredOverrides
|
|
2421
2637
|
];
|
|
2422
2638
|
const replayedTraceIds = serverItems.map(() => randomUuid());
|
|
2639
|
+
httpClient.trackTraceDeliveries(replayedTraceIds);
|
|
2423
2640
|
const tasks = serverItems.map(
|
|
2424
2641
|
(serverItem, index) => () => processItem(
|
|
2425
2642
|
httpClient,
|
|
@@ -2634,6 +2851,8 @@ export {
|
|
|
2634
2851
|
ReplayError,
|
|
2635
2852
|
DbBranchReplayError,
|
|
2636
2853
|
serializeReplayResult,
|
|
2854
|
+
waitForReplayPersistence,
|
|
2855
|
+
sleepForReplayPersistence,
|
|
2637
2856
|
replay
|
|
2638
2857
|
};
|
|
2639
|
-
//# sourceMappingURL=chunk-
|
|
2858
|
+
//# sourceMappingURL=chunk-MBKY42QC.js.map
|