@embrace-io/web-sdk
Version:
127 lines (126 loc) • 4.19 kB
JavaScript
import { diag } from "@opentelemetry/api";
//#region src/transport/FetchTransport/FetchTransport.ts
const KEEPALIVE_BYTE_BUDGET = 49152;
const MAX_KEEPALIVE_REQUESTS = 9;
let inflightKeepaliveBytes = 0;
let inflightKeepaliveCount = 0;
/** @internal for testing only */
function _resetKeepaliveTracking() {
inflightKeepaliveBytes = 0;
inflightKeepaliveCount = 0;
}
var FetchTransport = class FetchTransport {
_config;
constructor(_config) {
this._config = _config;
}
static async _compressRequest(data) {
const stream = new CompressionStream("gzip");
const writer = stream.writable.getWriter();
writer.write(data);
writer.close();
const compressedChunks = [];
const reader = stream.readable.getReader();
let done = false;
while (!done) {
const result = await reader.read();
if (result.value) compressedChunks.push(result.value);
done = result.done;
}
const compressedData = new Uint8Array(compressedChunks.reduce((acc, chunk) => acc + chunk.length, 0));
let offset = 0;
for (const chunk of compressedChunks) {
compressedData.set(chunk, offset);
offset += chunk.length;
}
return compressedData;
}
send(data, timeoutMillis) {
return this._asyncSend(data, timeoutMillis);
}
shutdown() {}
async _asyncSend(data, timeoutMillis) {
let request = data;
const headers = {
"Content-Type": "application/json",
...this._config.headers
};
let signal;
let timeoutId;
if ("timeout" in AbortSignal) signal = AbortSignal.timeout(timeoutMillis);
else {
const controller = new AbortController();
signal = controller.signal;
timeoutId = setTimeout(() => {
controller.abort(new DOMException("TimeoutError", "TimeoutError"));
}, timeoutMillis);
}
let keepalive = false;
try {
if (this._config.compression === "gzip") {
try {
request = await FetchTransport._compressRequest(data);
} catch (error) {
const compressError = error instanceof Error ? error : new Error(String(error));
diag.warn(`Fetch transport gzip compression failed: ${compressError.message}`);
return {
status: "failure",
error: compressError
};
}
headers["Content-Encoding"] = "gzip";
headers["Content-Length"] = request.length.toString();
}
const wouldExceedBytes = inflightKeepaliveBytes + request.byteLength > KEEPALIVE_BYTE_BUDGET;
keepalive = !wouldExceedBytes && !(inflightKeepaliveCount >= MAX_KEEPALIVE_REQUESTS);
if (keepalive) {
inflightKeepaliveBytes += request.byteLength;
inflightKeepaliveCount++;
} else {
const reason = wouldExceedBytes ? `inflight bytes (${inflightKeepaliveBytes} + ${request.byteLength}) would exceed ${KEEPALIVE_BYTE_BUDGET}B budget` : `concurrent count (${inflightKeepaliveCount}) at limit of ${MAX_KEEPALIVE_REQUESTS}`;
diag.debug(`Sending without keepalive: ${reason}`);
}
const response = await fetch(this._config.url, {
method: "POST",
keepalive,
headers,
body: request,
signal
});
if (response.ok) return { status: "success" };
const error = /* @__PURE__ */ new Error(`${response.status} Fetch request failed`);
const message = `Fetch transport received HTTP ${response.status} (keepalive=${keepalive})`;
if (response.status >= 500) {
diag.debug(message);
return {
status: "retryable",
error
};
}
diag.warn(message);
return {
status: "failure",
error
};
} catch (error) {
const fetchError = error instanceof Error ? error : new Error(String(error));
const isRetryable = fetchError instanceof TypeError || fetchError instanceof DOMException && fetchError.name === "TimeoutError";
const message = `Fetch transport failed (keepalive=${keepalive}): ${fetchError.message}`;
if (isRetryable) diag.debug(message);
else diag.warn(message);
return {
status: isRetryable ? "retryable" : "failure",
error: fetchError
};
} finally {
if (timeoutId !== void 0) clearTimeout(timeoutId);
if (keepalive) {
inflightKeepaliveBytes -= request.byteLength;
inflightKeepaliveCount--;
}
}
}
};
//#endregion
export { FetchTransport, _resetKeepaliveTracking };
//# sourceMappingURL=FetchTransport.js.map