100 lines
2.5 KiB
JavaScript
100 lines
2.5 KiB
JavaScript
import { projectTelemetryEvent } from "../../contracts/telemetry.js";
|
|
|
|
export const noOpTelemetry = Object.freeze({
|
|
emit: () => {},
|
|
});
|
|
|
|
/**
|
|
* @param {{
|
|
* enabled: boolean,
|
|
* endpoint?: string,
|
|
* fetcher?: typeof fetch,
|
|
* maxQueue?: number,
|
|
* schedule?: (callback: () => void) => void
|
|
* }} options
|
|
*/
|
|
export function createTelemetryAdapter(options) {
|
|
if (!options.enabled || !options.endpoint) {
|
|
return Object.freeze({
|
|
...noOpTelemetry,
|
|
flush: async () => {},
|
|
pendingCount: () => 0,
|
|
droppedCount: () => 0,
|
|
});
|
|
}
|
|
|
|
const endpoint = /** @type {string} */ (options.endpoint);
|
|
const fetcher = options.fetcher ?? fetch;
|
|
const maxQueue = options.maxQueue ?? 100;
|
|
const schedule = options.schedule ?? queueMicrotask;
|
|
const queue =
|
|
/** @type {Array<{eventName: string, attributes: Readonly<Record<string, unknown>>}>} */ (
|
|
[]
|
|
);
|
|
let scheduled = false;
|
|
let flushing = false;
|
|
let dropped = 0;
|
|
|
|
/** @param {string} eventName @param {Record<string, unknown>} attributes */
|
|
function emit(eventName, attributes) {
|
|
const projected = projectTelemetryEvent(eventName, attributes);
|
|
if (!projected.success) {
|
|
dropped += 1;
|
|
return;
|
|
}
|
|
|
|
if (queue.length >= maxQueue) {
|
|
queue.shift();
|
|
dropped += 1;
|
|
}
|
|
queue.push(projected.event);
|
|
|
|
if (!scheduled) {
|
|
scheduled = true;
|
|
schedule(() => {
|
|
scheduled = false;
|
|
void flush();
|
|
});
|
|
}
|
|
}
|
|
|
|
async function flush() {
|
|
if (flushing || queue.length === 0) return;
|
|
flushing = true;
|
|
const batch = queue.splice(0, queue.length);
|
|
try {
|
|
const response = await fetcher(endpoint, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ events: batch }),
|
|
keepalive: true,
|
|
});
|
|
if (!response.ok) dropped += batch.length;
|
|
} catch {
|
|
dropped += batch.length;
|
|
} finally {
|
|
flushing = false;
|
|
}
|
|
}
|
|
|
|
return Object.freeze({
|
|
emit,
|
|
flush,
|
|
pendingCount: () => queue.length,
|
|
droppedCount: () => dropped,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Propagates only a structurally valid W3C traceparent. Invalid/raw headers are
|
|
* discarded rather than logged or surfaced.
|
|
*
|
|
* @param {string | null | undefined} traceparent
|
|
*/
|
|
export function safeTraceparent(traceparent) {
|
|
return typeof traceparent === "string" &&
|
|
/^00-[0-9a-f]{32}-[0-9a-f]{16}-0[01]$/i.test(traceparent)
|
|
? traceparent.toLowerCase()
|
|
: null;
|
|
}
|