autotel
Version:
Write Once, Observe Anywhere
1,183 lines (1,174 loc) • 37.6 kB
JavaScript
import { getValidationConfig, isInitialized, warnIfNotInitialized, getConfig, getEventsConfig, getLogger } from './chunk-ZDPIWKWD.js';
import { getConfig as getConfig$1 } from './chunk-J5QENANM.js';
import { SpanStatusCode, trace, context, propagation } from '@opentelemetry/api';
import { AsyncLocalStorage } from 'async_hooks';
// src/flatten-attributes.ts
function toAttributeValue(value) {
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
return value;
}
if (Array.isArray(value)) {
if (value.every((v) => typeof v === "string") || value.every((v) => typeof v === "number") || value.every((v) => typeof v === "boolean")) {
return value;
}
try {
return JSON.stringify(value);
} catch {
return "<serialization-failed>";
}
}
if (value instanceof Date) {
return value.toISOString();
}
if (value instanceof Error) {
return value.message;
}
return void 0;
}
function flattenToAttributes(fields, prefix = "") {
const out = {};
const seen = /* @__PURE__ */ new WeakSet();
function flatten(obj, currentPrefix) {
for (const [key, value] of Object.entries(obj)) {
if (value == null) continue;
const nextKey = currentPrefix ? `${currentPrefix}.${key}` : key;
const attr = toAttributeValue(value);
if (attr !== void 0) {
out[nextKey] = attr;
continue;
}
if (typeof value === "object" && value.constructor === Object) {
if (seen.has(value)) {
out[nextKey] = "<circular-reference>";
continue;
}
seen.add(value);
flatten(value, nextKey);
continue;
}
try {
out[nextKey] = JSON.stringify(value);
} catch {
out[nextKey] = "<serialization-failed>";
}
}
}
flatten(fields, prefix);
return out;
}
// src/structured-error.ts
var internalKey = /* @__PURE__ */ Symbol.for("autotel.error.internal");
function createStructuredError(input) {
const error = new Error(input.message, {
cause: input.cause
});
error.name = input.name ?? "StructuredError";
if (input.why !== void 0) error.why = input.why;
if (input.fix !== void 0) error.fix = input.fix;
if (input.link !== void 0) error.link = input.link;
if (input.code !== void 0) error.code = input.code;
if (input.status !== void 0) error.status = input.status;
if (input.details !== void 0) error.details = input.details;
if (input.internal !== void 0) {
Object.defineProperty(error, internalKey, {
value: input.internal,
enumerable: false,
writable: false,
configurable: true
});
}
Object.defineProperty(error, "internal", {
get() {
return this[internalKey];
},
enumerable: false,
configurable: true
});
error.toString = () => {
const lines = [`${error.name}: ${error.message}`];
if (error.why) lines.push(` Why: ${error.why}`);
if (error.fix) lines.push(` Fix: ${error.fix}`);
if (error.link) lines.push(` Link: ${error.link}`);
if (error.code !== void 0) lines.push(` Code: ${error.code}`);
if (error.status !== void 0) lines.push(` Status: ${error.status}`);
if (error.cause) {
const cause = error.cause;
lines.push(` Caused by: ${cause.name}: ${cause.message}`);
}
return lines.join("\n");
};
return error;
}
function structuredErrorToJSON(error) {
const result = {
name: error.name,
message: error.message
};
if (error.status !== void 0) result.status = error.status;
if (error.why || error.fix || error.link) {
result.data = {
...error.why && { why: error.why },
...error.fix && { fix: error.fix },
...error.link && { link: error.link }
};
}
if (error.code !== void 0) result.code = error.code;
if (error.details) result.details = error.details;
if (error.cause instanceof Error) {
result.cause = { name: error.cause.name, message: error.cause.message };
}
return result;
}
function getStructuredErrorAttributes(error) {
const structured = error;
const attributes = {
"error.type": error.name || "Error",
"error.message": error.message
};
if (error.stack) attributes["error.stack"] = error.stack;
if (structured.why) attributes["error.why"] = structured.why;
if (structured.fix) attributes["error.fix"] = structured.fix;
if (structured.link) attributes["error.link"] = structured.link;
if (structured.code !== void 0) {
attributes["error.code"] = typeof structured.code === "string" ? structured.code : String(structured.code);
}
if (structured.status !== void 0) {
attributes["error.status"] = structured.status;
}
if (structured.details) {
Object.assign(
attributes,
flattenToAttributes(structured.details, "error.details")
);
}
return attributes;
}
function recordStructuredError(ctx, error) {
const maybeRecordException = ctx.recordException;
if (typeof maybeRecordException === "function") {
maybeRecordException(error);
}
ctx.setStatus({
code: SpanStatusCode.ERROR,
message: error.message
});
ctx.setAttributes(getStructuredErrorAttributes(error));
}
// src/rate-limiter.ts
var TokenBucketRateLimiter = class {
tokens;
maxTokens;
refillRate;
// tokens per millisecond
lastRefill;
constructor(config) {
this.maxTokens = config.burstCapacity || config.maxEventsPerSecond * 2;
this.tokens = this.maxTokens;
this.refillRate = config.maxEventsPerSecond / 1e3;
this.lastRefill = Date.now();
}
/**
* Try to consume a token (allow an event)
* Returns true if allowed, false if rate limit exceeded
*/
tryConsume(count = 1) {
this.refill();
if (this.tokens >= count) {
this.tokens -= count;
return true;
}
return false;
}
/**
* Wait until a token is available (async rate limiting)
* Returns a promise that resolves when the event can be processed
*/
async waitForToken(count = 1) {
this.refill();
if (this.tokens >= count) {
this.tokens -= count;
return;
}
const tokensNeeded = count - this.tokens;
const waitMs = Math.ceil(tokensNeeded / this.refillRate);
await new Promise((resolve) => setTimeout(resolve, waitMs));
return this.waitForToken(count);
}
/**
* Refill tokens based on elapsed time
*/
refill() {
const now = Date.now();
const elapsed = now - this.lastRefill;
const tokensToAdd = elapsed * this.refillRate;
this.tokens = Math.min(this.maxTokens, this.tokens + tokensToAdd);
this.lastRefill = now;
}
/**
* Get current available tokens (for testing/debugging)
*/
getAvailableTokens() {
this.refill();
return Math.floor(this.tokens);
}
/**
* Reset the rate limiter (for testing)
*/
reset() {
this.tokens = this.maxTokens;
this.lastRefill = Date.now();
}
};
// src/event-queue.ts
var DEFAULT_CONFIG = {
maxSize: 5e4,
batchSize: 100,
flushInterval: 1e4,
maxRetries: 3,
rateLimit: {
maxEventsPerSecond: 100,
burstCapacity: 200
}
};
function getSubscriberName(subscriber) {
if (subscriber.name) {
return subscriber.name.toLowerCase();
}
const className = subscriber.constructor?.name || "unknown";
return className.replace(/Subscriber$/i, "").toLowerCase();
}
var EventQueue = class {
queue = [];
flushTimer = null;
config;
subscribers;
rateLimiter;
flushPromise = null;
isShuttingDown = false;
// Metrics
metrics = null;
// Observable callback cleanup functions
observableCleanups = [];
// Subscriber health tracking (for observable gauges)
subscriberHealthy = /* @__PURE__ */ new Map();
constructor(subscribers, config) {
this.subscribers = subscribers;
this.config = { ...DEFAULT_CONFIG, ...config };
this.rateLimiter = this.config.rateLimit ? new TokenBucketRateLimiter(this.config.rateLimit) : null;
for (const subscriber of subscribers) {
const name = getSubscriberName(subscriber);
this.subscriberHealthy.set(name, true);
}
this.initMetrics();
}
/**
* Initialize OTel metrics for queue observability
*/
initMetrics() {
const runtimeConfig = getConfig$1();
const meter = runtimeConfig.meter;
const queueSize = meter.createObservableGauge(
"autotel.event_delivery.queue.size",
{
description: "Current number of events in the delivery queue",
unit: "count"
}
);
const queueSizeCallback = (observableResult) => {
observableResult.observe(this.queue.length);
};
queueSize.addCallback(queueSizeCallback);
this.observableCleanups.push(
() => queueSize.removeCallback(queueSizeCallback)
);
const oldestAge = meter.createObservableGauge(
"autotel.event_delivery.queue.oldest_age_ms",
{
description: "Age of the oldest event in the queue in milliseconds",
unit: "ms"
}
);
const oldestAgeCallback = (observableResult) => {
if (this.queue.length > 0) {
const oldest = this.queue[0];
const ageMs = Date.now() - oldest.timestamp;
observableResult.observe(ageMs);
} else {
observableResult.observe(0);
}
};
oldestAge.addCallback(oldestAgeCallback);
this.observableCleanups.push(
() => oldestAge.removeCallback(oldestAgeCallback)
);
const delivered = meter.createCounter(
"autotel.event_delivery.queue.delivered",
{
description: "Number of events successfully delivered to subscribers",
unit: "count"
}
);
const failed = meter.createCounter("autotel.event_delivery.queue.failed", {
description: "Number of events that failed delivery after all retry attempts",
unit: "count"
});
const dropped = meter.createCounter(
"autotel.event_delivery.queue.dropped",
{
description: "Number of events dropped from the queue",
unit: "count"
}
);
const latency = meter.createHistogram(
"autotel.event_delivery.queue.latency_ms",
{
description: "Event delivery latency from enqueue to successful send",
unit: "ms"
}
);
const subscriberHealth = meter.createObservableGauge(
"autotel.event_delivery.subscriber.health",
{
description: "Subscriber health status (1=healthy, 0=unhealthy)",
unit: "1"
}
);
const subscriberHealthCallback = (observableResult) => {
for (const [subscriberName, isHealthy] of this.subscriberHealthy) {
observableResult.observe(isHealthy ? 1 : 0, {
subscriber: subscriberName
});
}
};
subscriberHealth.addCallback(subscriberHealthCallback);
this.observableCleanups.push(
() => subscriberHealth.removeCallback(subscriberHealthCallback)
);
this.metrics = {
queueSize,
oldestAge,
delivered,
failed,
dropped,
latency,
subscriberHealth
};
}
/**
* Record a dropped event with reason and emit debug breadcrumb
*/
recordDropped(reason, event, subscriberName) {
const attrs = { reason };
if (subscriberName) {
attrs.subscriber = subscriberName;
}
this.metrics?.dropped.add(1, attrs);
const logLevel = reason === "payload_invalid" ? "error" : "warn";
const logger = getLogger();
if (logLevel === "error") {
logger.error(
{
eventName: event?.name,
subscriber: subscriberName,
reason,
correlationId: event?._correlationId,
traceId: event?._traceId
},
`[autotel] Event dropped: ${reason}`
);
} else {
logger.warn(
{
eventName: event?.name,
subscriber: subscriberName,
reason,
correlationId: event?._correlationId,
traceId: event?._traceId
},
`[autotel] Event dropped: ${reason}`
);
}
}
/**
* Record permanent delivery failure (after all retries exhausted)
* Increments failed counter and logs error
*/
recordFailed(event, subscriberName, error) {
this.metrics?.failed.add(1, { subscriber: subscriberName });
this.subscriberHealthy.set(subscriberName, false);
getLogger().error(
{
eventName: event.name,
subscriber: subscriberName,
correlationId: event._correlationId,
traceId: event._traceId,
err: error
},
`[autotel] Event delivery failed after all retries`
);
}
/**
* Mark subscriber as unhealthy on transient failure (without incrementing failed counter)
* Used during retry attempts - only recordFailed should increment the counter
*/
markSubscriberUnhealthy(subscriberName) {
this.subscriberHealthy.set(subscriberName, false);
}
/**
* Record successful delivery
*/
recordDelivered(event, subscriberName, startTime) {
const latencyMs = Date.now() - startTime;
this.metrics?.delivered.add(1, { subscriber: subscriberName });
this.metrics?.latency.record(latencyMs, { subscriber: subscriberName });
this.subscriberHealthy.set(subscriberName, true);
}
/**
* Enqueue an event for sending
*
* Backpressure policy:
* - Drops oldest event and logs warning if queue is full (same behavior in all environments)
*/
enqueue(event) {
if (this.isShuttingDown) {
this.recordDropped("shutdown", event);
return;
}
if (this.queue.length >= this.config.maxSize) {
const droppedEvent = this.queue.shift();
this.recordDropped("rate_limit", droppedEvent);
getLogger().warn(
{
droppedEvent: droppedEvent?.name
},
`[autotel] Events queue full (${this.config.maxSize} events). Dropping oldest event. Events are being produced faster than they can be sent. Check your subscribers or reduce tracking frequency.`
);
}
const enrichedEvent = {
...event,
_correlationId: event._correlationId || getOrCreateCorrelationId()
};
this.queue.push(enrichedEvent);
this.scheduleBatchFlush();
}
/**
* Schedule a batch flush if not already scheduled
*/
scheduleBatchFlush() {
if (this.flushTimer || this.flushPromise) return;
this.flushTimer = setTimeout(() => {
this.flushTimer = null;
void this.flushBatch();
}, this.config.flushInterval);
}
/**
* Flush a batch of events
* Uses promise-based concurrency control to prevent race conditions
*/
async flushBatch() {
if (this.queue.length === 0) return;
if (this.flushPromise) {
await this.flushPromise;
return;
}
this.flushPromise = this.doFlushBatch();
try {
await this.flushPromise;
} finally {
this.flushPromise = null;
if (this.queue.length > 0) {
this.scheduleBatchFlush();
}
}
}
/**
* Internal flush implementation
*/
async doFlushBatch() {
const batch = this.queue.splice(0, this.config.batchSize);
await this.sendWithRetry(batch, this.config.maxRetries);
}
/**
* Send events with exponential backoff retry
* Tracks per-event, per-subscriber failures so failed counter reflects actual failed deliveries.
* On retry, only failed (event, subscriber) pairs are re-sent to avoid double-counting delivered.
*/
async sendWithRetry(events, retriesLeft, subscribersByEventIndex) {
const failedDeliveries = await this.sendToSubscribers(
events,
subscribersByEventIndex
);
if (failedDeliveries.length > 0) {
if (retriesLeft > 0) {
const failedEventIndices = new Set(
failedDeliveries.map((f) => f.eventIndex)
);
const failedEventIndicesOrdered = [...failedEventIndices].sort(
(a, b) => a - b
);
const eventsToRetry = failedEventIndicesOrdered.map(
(i) => events[i]
);
const failedSubscribersByRetryIndex = /* @__PURE__ */ new Map();
for (let j = 0; j < failedEventIndicesOrdered.length; j++) {
const origIndex = failedEventIndicesOrdered[j];
const set = /* @__PURE__ */ new Set();
for (const { eventIndex, subscriberName } of failedDeliveries) {
if (eventIndex === origIndex) set.add(subscriberName);
}
failedSubscribersByRetryIndex.set(j, set);
}
const delay = Math.pow(2, this.config.maxRetries - retriesLeft) * 1e3;
await new Promise((resolve) => setTimeout(resolve, delay));
return this.sendWithRetry(
eventsToRetry,
retriesLeft - 1,
failedSubscribersByRetryIndex
);
} else {
for (const { eventIndex, subscriberName, error } of failedDeliveries) {
const event = events[eventIndex];
if (event) this.recordFailed(event, subscriberName, error);
}
const failedSubscriberNames = [
...new Set(failedDeliveries.map((f) => f.subscriberName))
];
getLogger().error(
{
failedSubscribers: failedSubscriberNames,
retriesAttempted: this.config.maxRetries
},
"[autotel] Failed to send events after retries"
);
}
}
}
/**
* Send events to configured subscribers with rate limiting and metrics.
* When subscribersByEventIndex is provided (retry path), only those subscribers are tried per event.
* Returns per-event, per-subscriber failures (empty if all succeeded).
*/
async sendToSubscribers(events, subscribersByEventIndex) {
const failedDeliveries = [];
const sendOne = async (event, eventIndex) => {
const subscriberNames = subscribersByEventIndex?.get(eventIndex);
const failures = await this.sendEventToSubscribers(
event,
subscriberNames ?? void 0
);
for (const failure of failures) {
failedDeliveries.push({
eventIndex,
subscriberName: failure.subscriberName,
error: failure.error
});
}
};
if (!this.rateLimiter) {
for (let i = 0; i < events.length; i++) {
const event = events[i];
if (event) await sendOne(event, i);
}
return failedDeliveries;
}
for (let i = 0; i < events.length; i++) {
await this.rateLimiter.waitForToken();
const event = events[i];
if (event) await sendOne(event, i);
}
return failedDeliveries;
}
/**
* Send a single event to subscribers.
* - When subscriberNames is undefined (initial attempt): send to all subscribers.
* - When subscriberNames is provided (retry): send only to those subscribers (never re-send to healthy ones).
* Returns list of subscribers that failed (empty if all succeeded).
*/
async sendEventToSubscribers(event, subscriberNames) {
const startTime = event.timestamp;
const failures = [];
const subscribersToTry = subscriberNames === void 0 ? this.subscribers : this.subscribers.filter(
(s) => subscriberNames.has(getSubscriberName(s))
);
const results = await Promise.allSettled(
subscribersToTry.map(async (subscriber) => {
const subscriberName = getSubscriberName(subscriber);
try {
await subscriber.trackEvent(event.name, event.attributes, {
autotel: event.autotel
});
this.recordDelivered(event, subscriberName, startTime);
return { subscriberName, success: true };
} catch (error) {
this.markSubscriberUnhealthy(subscriberName);
return {
subscriberName,
success: false,
error: error instanceof Error ? error : void 0
};
}
})
);
for (const result of results) {
if (result.status === "fulfilled" && !result.value.success) {
failures.push({
subscriberName: result.value.subscriberName,
error: result.value.error
});
}
}
return failures;
}
/**
* Flush all remaining events. Queue remains usable after flush (e.g. for
* auto-flush at root span end). Use shutdown() when tearing down the queue.
*/
async flush() {
if (this.flushTimer) {
clearTimeout(this.flushTimer);
this.flushTimer = null;
}
if (this.flushPromise) {
await this.flushPromise;
}
while (this.queue.length > 0) {
await this.doFlushBatch();
}
}
/**
* Flush remaining events and permanently disable the queue (reject new events).
* Use for process/SDK shutdown; use flush() for periodic or span-end drain.
*/
async shutdown() {
this.isShuttingDown = true;
await this.flush();
}
/**
* Cleanup observable metric callbacks to prevent memory leaks
* Call this when destroying the EventQueue instance
*/
cleanup() {
for (const cleanupFn of this.observableCleanups) {
try {
cleanupFn();
} catch {
}
}
this.observableCleanups = [];
}
/**
* Get queue size (for testing/debugging)
*/
size() {
return this.queue.length;
}
/**
* Get subscriber health status (for testing/debugging)
*/
getSubscriberHealth() {
return new Map(this.subscriberHealthy);
}
/**
* Check if a specific subscriber is healthy
*/
isSubscriberHealthy(subscriberName) {
return this.subscriberHealthy.get(subscriberName.toLowerCase()) ?? true;
}
/**
* Manually mark a subscriber as healthy or unhealthy
* (used for circuit breaker integration)
*/
setSubscriberHealth(subscriberName, healthy) {
this.subscriberHealthy.set(subscriberName.toLowerCase(), healthy);
}
};
// src/validation.ts
var DEFAULT_CONFIG2 = {
maxEventNameLength: 100,
maxAttributeKeyLength: 100,
maxAttributeValueLength: 1e3,
maxAttributeCount: 50,
maxNestingDepth: 3,
sensitivePatterns: [
/password/i,
/secret/i,
/token/i,
/api[_-]?key/i,
/access[_-]?key/i,
/private[_-]?key/i,
/auth/i,
/credential/i,
/ssn/i,
/credit[_-]?card/i
]
};
var ValidationError = class extends Error {
constructor(message) {
super(message);
this.name = "ValidationError";
}
};
function validateEventName(eventName, config = DEFAULT_CONFIG2) {
if (typeof eventName !== "string") {
throw new ValidationError(
`Event name must be a string, got ${typeof eventName}`
);
}
const trimmed = eventName.trim();
if (trimmed.length === 0) {
throw new ValidationError("Event name cannot be empty");
}
if (trimmed.length > config.maxEventNameLength) {
throw new ValidationError(
`Event name too long (${trimmed.length} chars). Max: ${config.maxEventNameLength}`
);
}
if (!/^[a-zA-Z0-9._-]+$/.test(trimmed)) {
throw new ValidationError(
`Event name contains invalid characters: "${trimmed}". Use only letters, numbers, dots, underscores, and hyphens.`
);
}
return trimmed;
}
function validateAttributes(attributes, config = DEFAULT_CONFIG2) {
if (attributes === void 0 || attributes === null) {
return void 0;
}
if (typeof attributes !== "object" || Array.isArray(attributes)) {
throw new ValidationError("Attributes must be an object");
}
const keys = Object.keys(attributes);
if (keys.length > config.maxAttributeCount) {
throw new ValidationError(
`Too many attributes (${keys.length}). Max: ${config.maxAttributeCount}`
);
}
const sanitized = {};
for (const key of keys) {
if (key.length > config.maxAttributeKeyLength) {
throw new ValidationError(
`Attribute key too long: "${key.slice(0, 20)}..." (${key.length} chars). Max: ${config.maxAttributeKeyLength}`
);
}
const isSensitive = config.sensitivePatterns.some(
(pattern) => pattern.test(key)
);
if (isSensitive) {
sanitized[key] = "[REDACTED]";
continue;
}
const value = attributes[key];
sanitized[key] = sanitizeValue(value, config, 1);
}
return sanitized;
}
function sanitizeValue(value, config, depth) {
if (depth > config.maxNestingDepth) {
return "[MAX_DEPTH_EXCEEDED]";
}
if (value === null || value === void 0) {
return value;
}
if (typeof value === "string") {
if (value.length > config.maxAttributeValueLength) {
return value.slice(0, config.maxAttributeValueLength) + "...";
}
return value;
}
if (typeof value === "number" || typeof value === "boolean") {
return value;
}
if (Array.isArray(value)) {
return value.map((item) => sanitizeValue(item, config, depth + 1));
}
if (typeof value === "object") {
try {
JSON.stringify(value);
const sanitized = {};
for (const key in value) {
if (Object.prototype.hasOwnProperty.call(value, key)) {
const isSensitive = config.sensitivePatterns.some(
(pattern) => pattern.test(key)
);
if (isSensitive) {
sanitized[key] = "[REDACTED]";
} else {
sanitized[key] = sanitizeValue(
value[key],
config,
depth + 1
);
}
}
}
return sanitized;
} catch {
return "[CIRCULAR]";
}
}
return `[${typeof value}]`;
}
function validateEvent(eventName, attributes, config) {
const fullConfig = { ...DEFAULT_CONFIG2, ...config };
return {
eventName: validateEventName(eventName, fullConfig),
attributes: validateAttributes(attributes, fullConfig)
};
}
// src/track.ts
var eventsQueue = null;
function buildAutotelContext(span) {
const eventsConfig = getEventsConfig();
const config = getConfig();
const correlationId = getOrCreateCorrelationId();
if (!eventsConfig?.includeTraceContext) {
return {
correlation_id: correlationId
};
}
const autotelContext = {
correlation_id: correlationId
};
const spanContext = span?.spanContext();
if (spanContext) {
autotelContext.trace_id = spanContext.traceId;
autotelContext.span_id = spanContext.spanId;
autotelContext.trace_flags = spanContext.traceFlags.toString(16).padStart(2, "0");
const traceState = spanContext.traceState;
if (traceState) {
try {
if (typeof traceState.serialize === "function") {
const traceStateStr = traceState.serialize();
if (traceStateStr) {
autotelContext.trace_state = traceStateStr;
}
}
} catch {
}
}
if (eventsConfig.traceUrl && config) {
const traceUrl = eventsConfig.traceUrl({
traceId: spanContext.traceId,
spanId: spanContext.spanId,
correlationId,
serviceName: config.service,
environment: config.environment
});
if (traceUrl) {
autotelContext.trace_url = traceUrl;
}
}
} else {
if (eventsConfig.traceUrl && config) {
const traceUrl = eventsConfig.traceUrl({
correlationId,
serviceName: config.service,
environment: config.environment
});
if (traceUrl) {
autotelContext.trace_url = traceUrl;
}
}
}
return autotelContext;
}
function getOrCreateQueue() {
if (!isInitialized()) {
warnIfNotInitialized("track()");
return null;
}
if (!eventsQueue) {
const config = getConfig();
if (!config?.subscribers || config.subscribers.length === 0) {
return null;
}
eventsQueue = new EventQueue(config.subscribers);
}
return eventsQueue;
}
function track(event, data) {
const queue = getOrCreateQueue();
if (!queue) return;
const validationConfig = getValidationConfig();
const validated = validateEvent(event, data, validationConfig || void 0);
const span = trace.getActiveSpan();
const enrichedData = span ? {
...validated.attributes,
traceId: span.spanContext().traceId,
spanId: span.spanContext().spanId
} : validated.attributes;
const autotelContext = buildAutotelContext(span);
queue.enqueue({
name: validated.eventName,
attributes: enrichedData,
timestamp: Date.now(),
autotel: autotelContext
});
}
function getEventQueue() {
return eventsQueue;
}
function resetEventQueue() {
eventsQueue = null;
}
// src/trace-context.ts
var contextStorage = new AsyncLocalStorage();
function getContextStorage() {
return contextStorage;
}
function getActiveContextWithBaggage() {
const stored = contextStorage.getStore()?.value;
return stored ?? context.active();
}
function enterOrRun(storage, value) {
const existingStore = storage.getStore();
if (existingStore) {
existingStore.value = value;
return;
}
const boxedValue = { value };
try {
storage.enterWith(boxedValue);
} catch {
storage.run(boxedValue, () => {
});
}
}
function updateActiveContext(newContext) {
enterOrRun(contextStorage, newContext);
const contextWithManager = context;
const manager = contextWithManager._getContextManager?.();
if (!manager) return;
const asyncLocal = manager._asyncLocalStorage ?? void 0;
if (asyncLocal?.enterWith) {
asyncLocal.enterWith(newContext);
return;
}
if (typeof manager.with === "function") {
manager.with(newContext, () => {
});
}
}
function createTraceContext(span) {
const spanContext = span.spanContext();
const existingStored = contextStorage.getStore()?.value;
if (!existingStored) {
const activeContext = context.active();
enterOrRun(contextStorage, activeContext);
}
const baggageHelpers = {
getBaggage(key) {
const activeCtx = context.active();
let baggage = propagation.getBaggage(activeCtx);
if (!baggage) {
const storedContext = contextStorage.getStore()?.value;
if (storedContext) {
baggage = propagation.getBaggage(storedContext);
}
}
return baggage?.getEntry(key)?.value;
},
setBaggage(key, value) {
const activeCtx = context.active();
const storedContext = contextStorage.getStore()?.value;
const currentContext = storedContext ?? activeCtx;
const baggage = propagation.getBaggage(currentContext) ?? propagation.createBaggage();
const updated = baggage.setEntry(key, { value });
const newContext = propagation.setBaggage(currentContext, updated);
updateActiveContext(newContext);
return value;
},
deleteBaggage(key) {
const activeCtx = context.active();
const storedContext = contextStorage.getStore()?.value;
const currentContext = storedContext ?? activeCtx;
const baggage = propagation.getBaggage(currentContext);
if (baggage) {
const updated = baggage.removeEntry(key);
const newContext = propagation.setBaggage(currentContext, updated);
updateActiveContext(newContext);
}
},
getAllBaggage() {
const activeCtx = context.active();
let baggage = propagation.getBaggage(activeCtx);
if (!baggage) {
const storedContext = contextStorage.getStore()?.value;
if (storedContext) {
baggage = propagation.getBaggage(storedContext);
}
}
if (!baggage) {
return /* @__PURE__ */ new Map();
}
const entries = /* @__PURE__ */ new Map();
for (const [key, entry] of baggage.getAllEntries()) {
entries.set(key, entry);
}
return entries;
},
// Typed baggage helpers (used by defineBaggageSchema)
getTypedBaggage: ((namespace) => {
const activeCtx = context.active();
let baggage = propagation.getBaggage(activeCtx);
if (!baggage) {
const storedContext = contextStorage.getStore()?.value;
if (storedContext) {
baggage = propagation.getBaggage(storedContext);
}
}
if (!baggage) return;
const prefix = namespace ? `${namespace}.` : "";
const result = {};
for (const [key, entry] of baggage.getAllEntries()) {
if (namespace && key.startsWith(prefix)) {
const fieldName = key.slice(prefix.length);
result[fieldName] = entry.value;
} else if (!namespace) {
result[key] = entry.value;
}
}
return Object.keys(result).length > 0 ? result : void 0;
}),
setTypedBaggage: ((namespace, value) => {
const activeCtx = context.active();
const storedContext = contextStorage.getStore()?.value;
const currentContext = storedContext ?? activeCtx;
let baggage = propagation.getBaggage(currentContext) ?? propagation.createBaggage();
const prefix = namespace ? `${namespace}.` : "";
for (const [key, val] of Object.entries(value)) {
if (val !== void 0) {
const baggageKey = `${prefix}${key}`;
baggage = baggage.setEntry(baggageKey, { value: String(val) });
}
}
const newContext = propagation.setBaggage(currentContext, baggage);
updateActiveContext(newContext);
})
};
const traceCtx = {
traceId: spanContext.traceId,
spanId: spanContext.spanId,
correlationId: spanContext.traceId.slice(0, 16),
setAttribute: span.setAttribute.bind(span),
setAttributes: span.setAttributes.bind(span),
setStatus: span.setStatus.bind(span),
recordException: span.recordException.bind(span),
addEvent: span.addEvent.bind(span),
addLink: span.addLink.bind(span),
addLinks: span.addLinks.bind(span),
updateName: span.updateName.bind(span),
isRecording: span.isRecording.bind(span),
recordError: (error) => {
const err = error instanceof Error ? error : new Error(String(error));
recordStructuredError(traceCtx, err);
},
track: (event, data) => {
track(event, data);
},
...baggageHelpers
};
return traceCtx;
}
function defineBaggageSchema(namespace) {
return {
/**
* Get typed baggage from context
* @param ctx - Trace context
* @returns Partial baggage object or undefined if no baggage is set
*/
get: (ctx) => {
if (!ctx.getTypedBaggage) return void 0;
return ctx.getTypedBaggage(namespace);
},
/**
* Set typed baggage in context
*
* Note: For proper scoping across async boundaries, use the `with` method instead
*
* @param ctx - Trace context
* @param value - Partial baggage object to set
*/
set: (ctx, value) => {
if (!ctx.setTypedBaggage) return;
ctx.setTypedBaggage(namespace, value);
},
/**
* Run a function with typed baggage properly scoped
*
* This is the recommended way to set baggage as it ensures proper
* scoping across async boundaries.
*
* @param ctx - Trace context (can be omitted, will use active context)
* @param value - Partial baggage object to set
* @param fn - Function to execute with the baggage
*/
with: (ctxOrValue, valueOrFn, maybeFn) => {
const value = maybeFn ? valueOrFn : ctxOrValue;
const fn = maybeFn || valueOrFn;
const prefix = namespace ? `${namespace}.` : "";
const flatBaggage = {};
for (const [key, val] of Object.entries(value)) {
if (val !== void 0) {
flatBaggage[`${prefix}${key}`] = String(val);
}
}
const currentContext = context.active();
let baggage = propagation.getBaggage(currentContext) ?? propagation.createBaggage();
for (const [key, val] of Object.entries(flatBaggage)) {
baggage = baggage.setEntry(key, { value: val });
}
const newContext = propagation.setBaggage(currentContext, baggage);
return context.with(newContext, fn);
}
};
}
// src/correlation-id.ts
var correlationStorage = new AsyncLocalStorage();
var CORRELATION_ID_BAGGAGE_KEY = "autotel.correlation_id";
function generateCorrelationId() {
const bytes = new Uint8Array(8);
crypto.getRandomValues(bytes);
return [...bytes].map((b) => b.toString(16).padStart(2, "0")).join("");
}
function getCorrelationId() {
const storedId = correlationStorage.getStore()?.value;
if (storedId) {
return storedId;
}
const activeContext = context.active();
const baggage = propagation.getBaggage(activeContext);
const baggageEntry = baggage?.getEntry(CORRELATION_ID_BAGGAGE_KEY);
if (baggageEntry?.value) {
return baggageEntry.value;
}
const span = trace.getActiveSpan();
if (span) {
const spanContext = span.spanContext();
return spanContext.traceId.slice(0, 16);
}
return void 0;
}
function getOrCreateCorrelationId() {
return getCorrelationId() ?? generateCorrelationId();
}
function runWithCorrelationId(correlationId, fn) {
return correlationStorage.run({ value: correlationId }, fn);
}
function setCorrelationId(correlationId) {
enterOrRun(correlationStorage, correlationId);
}
function setCorrelationIdInBaggage(correlationId) {
const activeContext = context.active();
let baggage = propagation.getBaggage(activeContext) ?? propagation.createBaggage();
baggage = baggage.setEntry(CORRELATION_ID_BAGGAGE_KEY, {
value: correlationId
});
return propagation.setBaggage(activeContext, baggage);
}
function getCorrelationStorage() {
return correlationStorage;
}
export { CORRELATION_ID_BAGGAGE_KEY, createStructuredError, createTraceContext, defineBaggageSchema, enterOrRun, flattenToAttributes, generateCorrelationId, getActiveContextWithBaggage, getContextStorage, getCorrelationId, getCorrelationStorage, getEventQueue, getOrCreateCorrelationId, getStructuredErrorAttributes, recordStructuredError, resetEventQueue, runWithCorrelationId, setCorrelationId, setCorrelationIdInBaggage, structuredErrorToJSON, toAttributeValue, track, validateEvent };
//# sourceMappingURL=chunk-Z7VAOK5X.js.map
//# sourceMappingURL=chunk-Z7VAOK5X.js.map