autotel 7.1.0 → 7.3.0

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.
Files changed (62) hide show
  1. package/README.md +41 -14
  2. package/dist/analysis.cjs +17 -0
  3. package/dist/analysis.d.cts +9 -1
  4. package/dist/analysis.d.ts +9 -1
  5. package/dist/analysis.js +17 -1
  6. package/dist/auto.cjs +2 -3
  7. package/dist/auto.js +1 -2
  8. package/dist/correlation-id.cjs +1 -1
  9. package/dist/correlation-id.js +1 -1
  10. package/dist/decorators.cjs +1 -1
  11. package/dist/decorators.d.cts +1 -1
  12. package/dist/decorators.d.ts +1 -1
  13. package/dist/decorators.js +1 -1
  14. package/dist/event.cjs +1 -1
  15. package/dist/event.js +1 -1
  16. package/dist/{functional-Cub1ku-C.js → functional-C4js2vTM.js} +44 -7
  17. package/dist/{functional-CJn2zY4o.d.cts → functional-CgWcRXGX.d.cts} +12 -5
  18. package/dist/{functional-BhhP4Er_.cjs → functional-DHlD7V0x.cjs} +47 -10
  19. package/dist/{functional-DXy1cHFF.d.ts → functional-RFFpQzAV.d.ts} +12 -5
  20. package/dist/functional.cjs +1 -1
  21. package/dist/functional.d.cts +1 -1
  22. package/dist/functional.d.ts +1 -1
  23. package/dist/functional.js +1 -1
  24. package/dist/http.cjs +1 -1
  25. package/dist/http.js +1 -1
  26. package/dist/index.cjs +53 -15
  27. package/dist/index.d.cts +24 -4
  28. package/dist/index.d.ts +24 -4
  29. package/dist/index.js +40 -5
  30. package/dist/{init-DrlL1eis.d.cts → init-8GSy2bza.d.cts} +1 -1
  31. package/dist/{init-DDyaLzeg.js → init-CsKK9vZO.js} +2196 -1298
  32. package/dist/{init-C5aHRqi2.cjs → init-DmM7dprX.cjs} +2356 -1300
  33. package/dist/{init-BgnYi2sm.d.ts → init-UnKrXhpq.d.ts} +1 -1
  34. package/dist/instrumentation.cjs +2 -3
  35. package/dist/instrumentation.js +1 -2
  36. package/dist/messaging.cjs +5 -5
  37. package/dist/messaging.js +2 -2
  38. package/dist/{sampling-CM5S2Lp3.d.ts → sampling-DBng4Foa.d.ts} +20 -1
  39. package/dist/{sampling-CmRdnXUi.d.cts → sampling-Er217Mge.d.cts} +20 -1
  40. package/dist/sampling.cjs +22 -601
  41. package/dist/sampling.d.cts +2 -2
  42. package/dist/sampling.d.ts +2 -2
  43. package/dist/sampling.js +2 -585
  44. package/dist/semantic-helpers.cjs +1 -1
  45. package/dist/semantic-helpers.js +1 -1
  46. package/dist/tail-sampling-processor.cjs +2 -26
  47. package/dist/tail-sampling-processor.js +1 -25
  48. package/dist/testing.cjs +32 -0
  49. package/dist/testing.d.cts +26 -1
  50. package/dist/testing.d.ts +26 -1
  51. package/dist/testing.js +32 -1
  52. package/dist/webhook.cjs +1 -1
  53. package/dist/webhook.js +1 -1
  54. package/dist/workflow-distributed.cjs +1 -1
  55. package/dist/workflow-distributed.js +1 -1
  56. package/dist/workflow.cjs +1 -1
  57. package/dist/workflow.js +1 -1
  58. package/dist/yaml-config.cjs +4 -248
  59. package/dist/yaml-config.d.cts +2 -2
  60. package/dist/yaml-config.d.ts +2 -2
  61. package/dist/yaml-config.js +1 -242
  62. package/package.json +1 -1
@@ -1,694 +1,1046 @@
1
- import { _ as readProperty, f as describeValue, h as isFunction, i as asNumber, l as asString, r as asFunction, s as asRecord, t as asBoolean } from "./values-BenyvWKz.js";
1
+ import { _ as readProperty, d as asStringRecord, f as describeValue, h as isFunction, i as asNumber, l as asString, r as asFunction, s as asRecord, t as asBoolean } from "./values-BenyvWKz.js";
2
2
  import { getForceFlushableProvider } from "./tracer-provider.js";
3
3
  import { getConfig as getConfig$1 } from "./config.js";
4
- import { AUTOTEL_SAMPLING_RATE, resolveSamplingPreset, samplingPresets } from "./sampling.js";
4
+ import { r as recordStructuredError } from "./structured-error-YIuka-FE.js";
5
5
  import { n as safeRequire, t as requireModule } from "./node-require-DSPjaw_l.js";
6
- import { TailSamplingSpanProcessor } from "./tail-sampling-processor.js";
7
6
  import { FilteringSpanProcessor } from "./filtering-span-processor.js";
8
7
  import { PolicyLogRecordProcessor, policySpanFilter, setPolicies, watchPolicyFile } from "./policy.js";
9
8
  import { SpanNameNormalizingProcessor } from "./span-name-normalizer.js";
10
9
  import { AttributeRedactingProcessor, REDACTOR_PRESETS, normalizeAttributeRedactorConfig } from "./attribute-redacting-processor.js";
11
10
  import { t as PrettyConsoleExporter } from "./pretty-console-exporter-DqKl_q9z.js";
12
- import { loadYamlConfig } from "./yaml-config.js";
13
11
  import { t as CanonicalLogLineProcessor } from "./canonical-log-line-processor-hFUmS__K.js";
14
- import { r as recordStructuredError } from "./structured-error-YIuka-FE.js";
15
12
  import { resetMetrics } from "./metric.js";
16
13
  import { NodeSDK } from "@opentelemetry/sdk-node";
17
14
  import { AlwaysOffSampler, AlwaysOnSampler, BatchSpanProcessor, ConsoleSpanExporter, ParentBasedSampler, SamplingDecision, SimpleSpanProcessor, TraceIdRatioBasedSampler } from "@opentelemetry/sdk-trace-base";
18
15
  import { resourceFromAttributes } from "@opentelemetry/resources";
19
16
  import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from "@opentelemetry/semantic-conventions";
20
- import { context, propagation, trace } from "@opentelemetry/api";
17
+ import { TraceFlags, context, propagation, trace } from "@opentelemetry/api";
18
+ import * as nodeAsyncHooks from "node:async_hooks";
21
19
  import { PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics";
22
20
  import { BatchLogRecordProcessor } from "@opentelemetry/sdk-logs";
21
+ import * as nodeFs from "node:fs";
22
+ import path from "node:path";
23
23
  import { constants } from "node:os";
24
- import * as nodeAsyncHooks from "node:async_hooks";
25
24
  import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-http";
26
25
  import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
27
26
  import { OTLPLogExporter } from "@opentelemetry/exporter-logs-otlp-http";
28
27
 
29
- //#region src/init-logger.ts
30
- const LOG_LEVELS = {
31
- debug: 0,
32
- info: 1,
33
- warn: 2,
34
- error: 3
35
- };
36
- /** Silent logger used until and unless the application opts into diagnostics. */
37
- const silentLogger = {
38
- info: () => {},
39
- warn: () => {},
40
- error: () => {},
41
- debug: () => {}
42
- };
43
- /** Apply the resolved silence and minimum-level settings to a logger. */
44
- function wrapLogger(base, silent, minLevel) {
45
- if (silent) return silentLogger;
46
- const threshold = LOG_LEVELS[minLevel];
47
- const wrap = (fn, level) => {
48
- if (LOG_LEVELS[level] < threshold) return (() => {});
49
- return ((...args) => fn(...args));
50
- };
51
- return {
52
- debug: wrap(base.debug, "debug"),
53
- info: wrap(base.info, "info"),
54
- warn: wrap(base.warn, "warn"),
55
- error: wrap(base.error, "error")
56
- };
57
- }
58
-
59
- //#endregion
60
- //#region src/posthog-logs.ts
61
- var RedactingLogRecordProcessor = class {
62
- wrapped;
63
- redact;
64
- constructor(wrapped, redact) {
65
- this.wrapped = wrapped;
66
- this.redact = redact;
28
+ //#region src/rate-limiter.ts
29
+ /**
30
+ * Token bucket rate limiter
31
+ *
32
+ * Allows bursts up to burstCapacity, then smooths to maxEventsPerSecond.
33
+ * Thread-safe for async operations.
34
+ */
35
+ var TokenBucketRateLimiter = class {
36
+ tokens;
37
+ maxTokens;
38
+ refillRate;
39
+ lastRefill;
40
+ constructor(config) {
41
+ this.maxTokens = config.burstCapacity || config.maxEventsPerSecond * 2;
42
+ this.tokens = this.maxTokens;
43
+ this.refillRate = config.maxEventsPerSecond / 1e3;
44
+ this.lastRefill = Date.now();
67
45
  }
68
- onEmit(logRecord, context) {
69
- const body = asString(logRecord.body);
70
- if (body) logRecord.body = this.redact(body);
71
- if (logRecord.attributes) for (const [key, value] of Object.entries(logRecord.attributes)) {
72
- const text = asString(value);
73
- if (text !== void 0) logRecord.attributes[key] = this.redact(text);
74
- else if (Array.isArray(value)) logRecord.attributes[key] = value.map((item) => {
75
- const entry = asString(item);
76
- return entry === void 0 ? item : this.redact(entry);
77
- });
46
+ /**
47
+ * Try to consume a token (allow an event)
48
+ * Returns true if allowed, false if rate limit exceeded
49
+ */
50
+ tryConsume(count = 1) {
51
+ this.refill();
52
+ if (this.tokens >= count) {
53
+ this.tokens -= count;
54
+ return true;
78
55
  }
79
- this.wrapped.onEmit(logRecord, context);
56
+ return false;
80
57
  }
81
- shutdown() {
82
- return this.wrapped.shutdown();
58
+ /**
59
+ * Wait until a token is available (async rate limiting)
60
+ * Returns a promise that resolves when the event can be processed
61
+ */
62
+ async waitForToken(count = 1) {
63
+ this.refill();
64
+ if (this.tokens >= count) {
65
+ this.tokens -= count;
66
+ return;
67
+ }
68
+ const tokensNeeded = count - this.tokens;
69
+ const waitMs = Math.ceil(tokensNeeded / this.refillRate);
70
+ await new Promise((resolve) => setTimeout(resolve, waitMs));
71
+ return this.waitForToken(count);
83
72
  }
84
- forceFlush() {
85
- return this.wrapped.forceFlush();
73
+ /**
74
+ * Refill tokens based on elapsed time
75
+ */
76
+ refill() {
77
+ const now = Date.now();
78
+ const tokensToAdd = (now - this.lastRefill) * this.refillRate;
79
+ this.tokens = Math.min(this.maxTokens, this.tokens + tokensToAdd);
80
+ this.lastRefill = now;
81
+ }
82
+ /**
83
+ * Get current available tokens (for testing/debugging)
84
+ */
85
+ getAvailableTokens() {
86
+ this.refill();
87
+ return Math.floor(this.tokens);
88
+ }
89
+ /**
90
+ * Reset the rate limiter (for testing)
91
+ */
92
+ reset() {
93
+ this.tokens = this.maxTokens;
94
+ this.lastRefill = Date.now();
86
95
  }
87
96
  };
88
- /**
89
- * Build log record processors for PostHog OTLP logs integration.
90
- *
91
- * Resolution order:
92
- * 1. config.url if provided
93
- * 2. POSTHOG_LOGS_URL env var
94
- * 3. Empty array (disabled)
95
- */
96
- function buildPostHogLogProcessors(config, stringRedactor) {
97
- const url = config?.url || process.env.POSTHOG_LOGS_URL;
98
- if (!url) return [];
99
- const sdkLogs = safeRequire("@opentelemetry/sdk-logs");
100
- const exporterModule = safeRequire("@opentelemetry/exporter-logs-otlp-http");
101
- if (!sdkLogs || !exporterModule) return [];
102
- const exporter = new exporterModule.OTLPLogExporter({ url });
103
- let processor = new sdkLogs.BatchLogRecordProcessor({ exporter });
104
- if (stringRedactor) processor = new RedactingLogRecordProcessor(processor, stringRedactor);
105
- return [processor];
106
- }
107
97
 
108
98
  //#endregion
109
- //#region src/baggage-span-processor.ts
99
+ //#region src/correlation-id.ts
110
100
  /**
111
- * Span processor that automatically copies baggage entries to span attributes
101
+ * Correlation ID utilities for event-driven observability
112
102
  *
113
- * This makes baggage visible in trace UIs (Jaeger, Grafana, DataDog, etc.)
114
- * without manually calling ctx.setAttribute() for each baggage entry.
103
+ * Provides a stable join key across events, logs, and spans even when traces fragment.
104
+ * Format: 16 hex chars (64 bits), crypto-random, URL-safe.
115
105
  *
116
- * @example Enable in init()
106
+ * Lifecycle:
107
+ * 1. Generated at boundary root (HTTP server span, message process span, cron job span)
108
+ * 2. Reused within context (nested work shares it via AsyncLocalStorage)
109
+ * 3. Propagated via baggage (optional, default OFF to avoid header bloat)
110
+ *
111
+ * @example Basic usage
117
112
  * ```typescript
118
- * init({
119
- * service: 'my-app',
120
- * baggage: true // Uses default 'baggage.' prefix
121
- * });
113
+ * import { generateCorrelationId, getCorrelationId } from 'autotel/correlation-id';
122
114
  *
123
- * // Now baggage automatically appears as span attributes
124
- * await withBaggage({
125
- * baggage: { 'tenant.id': 't1', 'user.id': 'u1' },
126
- * fn: async () => {
127
- * // Span has baggage.tenant.id and baggage.user.id attributes!
128
- * }
129
- * });
130
- * ```
115
+ * // Generate a new correlation ID
116
+ * const id = generateCorrelationId();
117
+ * // Returns: 'a1b2c3d4e5f67890'
131
118
  *
132
- * @example Custom prefix
133
- * ```typescript
134
- * init({
135
- * service: 'my-app',
136
- * baggage: 'ctx' // Uses 'ctx.' prefix
137
- * });
138
- * // Creates attributes: ctx.tenant.id, ctx.user.id
119
+ * // Get current correlation ID from context
120
+ * const currentId = getCorrelationId();
139
121
  * ```
140
122
  */
141
- var BaggageSpanProcessor = class {
142
- prefix;
143
- constructor(options = {}) {
144
- this.prefix = options.prefix ?? "baggage.";
145
- }
146
- onStart(span, parentContext) {
147
- let baggage = propagation.getBaggage(parentContext);
148
- if (!baggage) baggage = propagation.getBaggage(context.active());
149
- if (!baggage) try {
150
- const { getActiveContextWithBaggage } = requireModule("./trace-context");
151
- const storedContext = getActiveContextWithBaggage();
152
- baggage = propagation.getBaggage(storedContext);
153
- } catch {}
154
- if (!baggage) return;
155
- for (const [key, entry] of baggage.getAllEntries()) span.setAttribute(`${this.prefix}${key}`, entry.value);
156
- }
157
- onEnd(_span) {}
158
- async shutdown() {}
159
- async forceFlush() {}
160
- };
161
-
162
- //#endregion
163
- //#region src/redact-values.ts
164
- /** Standalone string redaction for use outside the span processor pipeline. */
165
- function createStringRedactor(config) {
166
- const resolved = typeof config === "string" ? REDACTOR_PRESETS[config] : config;
167
- const valuePatterns = resolved.valuePatterns ?? [];
168
- const defaultReplacement = resolved.replacement ?? "[REDACTED]";
169
- return (value) => {
170
- let result = value;
171
- for (const { pattern, replacement, mask } of valuePatterns) {
172
- pattern.lastIndex = 0;
173
- result = mask ? result.replaceAll(pattern, (match) => mask(match)) : result.replaceAll(pattern, replacement ?? defaultReplacement);
174
- }
175
- return result;
176
- };
177
- }
178
-
179
- //#endregion
180
- //#region src/env-config.ts
181
123
  /**
182
- * Validate URL format
124
+ * AsyncLocalStorage for storing correlation ID
125
+ * This allows correlation IDs to persist across async boundaries
183
126
  */
184
- function isValidUrl(urlString) {
185
- try {
186
- const url = new URL(urlString);
187
- return url.protocol === "http:" || url.protocol === "https:";
188
- } catch {
189
- return false;
190
- }
191
- }
127
+ const correlationStorage = new nodeAsyncHooks.AsyncLocalStorage();
192
128
  /**
193
- * Resolve OpenTelemetry environment variables from process.env
129
+ * Baggage key for correlation ID propagation
194
130
  */
195
- function resolveOtelEnv() {
196
- const env = {};
197
- if (process.env.OTEL_SERVICE_NAME) {
198
- const value = process.env.OTEL_SERVICE_NAME.trim();
199
- if (value) env.OTEL_SERVICE_NAME = value;
200
- }
201
- if (process.env.OTEL_EXPORTER_OTLP_ENDPOINT) {
202
- const value = process.env.OTEL_EXPORTER_OTLP_ENDPOINT.trim();
203
- if (value && isValidUrl(value)) env.OTEL_EXPORTER_OTLP_ENDPOINT = value;
204
- }
205
- if (process.env.OTEL_EXPORTER_OTLP_HEADERS) {
206
- const value = process.env.OTEL_EXPORTER_OTLP_HEADERS.trim();
207
- if (value) env.OTEL_EXPORTER_OTLP_HEADERS = value;
208
- }
209
- if (process.env.OTEL_RESOURCE_ATTRIBUTES) {
210
- const value = process.env.OTEL_RESOURCE_ATTRIBUTES.trim();
211
- if (value) env.OTEL_RESOURCE_ATTRIBUTES = value;
212
- }
213
- if (process.env.OTEL_EXPORTER_OTLP_PROTOCOL) {
214
- const value = process.env.OTEL_EXPORTER_OTLP_PROTOCOL.trim().toLowerCase();
215
- if (value === "http" || value === "http/json") env.OTEL_EXPORTER_OTLP_PROTOCOL = "http";
216
- else if (value === "http/protobuf" || value === "grpc") env.OTEL_EXPORTER_OTLP_PROTOCOL = value;
217
- }
218
- if (process.env.OTEL_TRACES_SAMPLER) {
219
- const value = process.env.OTEL_TRACES_SAMPLER.trim();
220
- if (value) env.OTEL_TRACES_SAMPLER = value;
221
- }
222
- if (process.env.OTEL_TRACES_SAMPLER_ARG) {
223
- const value = process.env.OTEL_TRACES_SAMPLER_ARG.trim();
224
- if (value) env.OTEL_TRACES_SAMPLER_ARG = value;
225
- }
226
- return env;
227
- }
228
- function parseRatioSamplerArg(samplerName, samplerArg) {
229
- if (samplerArg === void 0) return 1;
230
- const ratio = Number(samplerArg);
231
- if (!Number.isFinite(ratio) || ratio < 0 || ratio > 1) {
232
- console.error(`[autotel] Invalid OTEL_TRACES_SAMPLER_ARG="${samplerArg}" for ${samplerName}. Expected a number in [0..1]. Falling back to 1.0.`);
233
- return 1;
234
- }
235
- return ratio;
236
- }
237
- function warnOnUnusedSamplerArg(samplerName, samplerArg) {
238
- if (samplerArg !== void 0) console.error(`[autotel] OTEL_TRACES_SAMPLER_ARG is not used by OTEL_TRACES_SAMPLER="${samplerName}". Ignoring value "${samplerArg}".`);
131
+ const CORRELATION_ID_BAGGAGE_KEY = "autotel.correlation_id";
132
+ /**
133
+ * Generate a new correlation ID
134
+ *
135
+ * Format: 16 hex chars (64 bits), crypto-random, URL-safe
136
+ *
137
+ * @returns A new correlation ID
138
+ *
139
+ * @example
140
+ * ```typescript
141
+ * const id = generateCorrelationId();
142
+ * // Returns: 'a1b2c3d4e5f67890'
143
+ * ```
144
+ */
145
+ function generateCorrelationId() {
146
+ const bytes = /* @__PURE__ */ new Uint8Array(8);
147
+ crypto.getRandomValues(bytes);
148
+ return [...bytes].map((b) => b.toString(16).padStart(2, "0")).join("");
239
149
  }
240
- function createSamplerFromEnv(env) {
241
- const samplerName = env.OTEL_TRACES_SAMPLER;
242
- if (!samplerName) return;
243
- switch (samplerName) {
244
- case "always_on":
245
- warnOnUnusedSamplerArg(samplerName, env.OTEL_TRACES_SAMPLER_ARG);
246
- return new AlwaysOnSampler();
247
- case "always_off":
248
- warnOnUnusedSamplerArg(samplerName, env.OTEL_TRACES_SAMPLER_ARG);
249
- return new AlwaysOffSampler();
250
- case "traceidratio": return new TraceIdRatioBasedSampler(parseRatioSamplerArg(samplerName, env.OTEL_TRACES_SAMPLER_ARG));
251
- case "parentbased_always_on":
252
- warnOnUnusedSamplerArg(samplerName, env.OTEL_TRACES_SAMPLER_ARG);
253
- return new ParentBasedSampler({ root: new AlwaysOnSampler() });
254
- case "parentbased_always_off":
255
- warnOnUnusedSamplerArg(samplerName, env.OTEL_TRACES_SAMPLER_ARG);
256
- return new ParentBasedSampler({ root: new AlwaysOffSampler() });
257
- case "parentbased_traceidratio": return new ParentBasedSampler({ root: new TraceIdRatioBasedSampler(parseRatioSamplerArg(samplerName, env.OTEL_TRACES_SAMPLER_ARG)) });
258
- case "jaeger_remote":
259
- case "parentbased_jaeger_remote":
260
- case "xray":
261
- console.error(`[autotel] OTEL_TRACES_SAMPLER="${samplerName}" is not supported yet by autotel. Falling back to the next sampler source.`);
262
- return;
263
- default:
264
- console.error(`[autotel] Unknown OTEL_TRACES_SAMPLER="${samplerName}". Falling back to the next sampler source.`);
265
- return;
266
- }
150
+ /**
151
+ * Get the current correlation ID from context
152
+ *
153
+ * Resolution order:
154
+ * 1. AsyncLocalStorage (from explicit setCorrelationId or runWithCorrelationId)
155
+ * 2. Baggage (if propagated from upstream)
156
+ * 3. Active span's trace ID (first 16 chars as fallback)
157
+ * 4. undefined (if not in any context)
158
+ *
159
+ * @returns Current correlation ID or undefined
160
+ *
161
+ * @example
162
+ * ```typescript
163
+ * const id = getCorrelationId();
164
+ * if (id) {
165
+ * console.log('Correlation ID:', id);
166
+ * }
167
+ * ```
168
+ */
169
+ function getCorrelationId() {
170
+ const storedId = correlationStorage.getStore()?.value;
171
+ if (storedId) return storedId;
172
+ const activeContext = context.active();
173
+ const baggageEntry = propagation.getBaggage(activeContext)?.getEntry(CORRELATION_ID_BAGGAGE_KEY);
174
+ if (baggageEntry?.value) return baggageEntry.value;
175
+ const span = trace.getActiveSpan();
176
+ if (span) return span.spanContext().traceId.slice(0, 16);
267
177
  }
268
178
  /**
269
- * Parse OTEL_RESOURCE_ATTRIBUTES from comma-separated key=value pairs
270
- * Example: "service.version=1.0.0,deployment.environment=production"
179
+ * Get or create a correlation ID
180
+ *
181
+ * If a correlation ID exists in the current context, returns it.
182
+ * Otherwise, generates a new one.
183
+ *
184
+ * @returns Existing or new correlation ID
185
+ *
186
+ * @example
187
+ * ```typescript
188
+ * const id = getOrCreateCorrelationId();
189
+ * // Always returns a valid correlation ID
190
+ * ```
271
191
  */
272
- function parseResourceAttributes(input) {
273
- if (!input || input.trim() === "") return {};
274
- const attributes = {};
275
- const pairs = input.split(",");
276
- for (const pair of pairs) {
277
- const trimmedPair = pair.trim();
278
- if (!trimmedPair) continue;
279
- const equalIndex = trimmedPair.indexOf("=");
280
- if (equalIndex === -1) continue;
281
- const key = trimmedPair.slice(0, equalIndex).trim();
282
- const value = trimmedPair.slice(equalIndex + 1).trim();
283
- if (key && value) attributes[key] = value;
284
- }
285
- return attributes;
192
+ function getOrCreateCorrelationId() {
193
+ return getCorrelationId() ?? generateCorrelationId();
286
194
  }
287
195
  /**
288
- * Parse OTEL_EXPORTER_OTLP_HEADERS from comma-separated key=value pairs
289
- * Example: "api-key=secret123,x-custom-header=value"
196
+ * Run a function with a specific correlation ID in context
197
+ *
198
+ * The correlation ID will be available via getCorrelationId() throughout
199
+ * the execution of the function and any async operations it spawns.
200
+ *
201
+ * @param correlationId - Correlation ID to use
202
+ * @param fn - Function to execute
203
+ * @returns The return value of the function
204
+ *
205
+ * @example
206
+ * ```typescript
207
+ * await runWithCorrelationId('abc123', async () => {
208
+ * // getCorrelationId() returns 'abc123' here
209
+ * await processRequest();
210
+ * });
211
+ * ```
290
212
  */
291
- function parseOtlpHeaders(input) {
292
- if (!input || input.trim() === "") return {};
293
- const headers = {};
294
- const pairs = input.split(",");
295
- for (const pair of pairs) {
296
- const trimmedPair = pair.trim();
297
- if (!trimmedPair) continue;
298
- const equalIndex = trimmedPair.indexOf("=");
299
- if (equalIndex === -1) continue;
300
- const key = trimmedPair.slice(0, equalIndex).trim();
301
- const value = trimmedPair.slice(equalIndex + 1).trim();
302
- if (key && value) headers[key] = value;
303
- }
304
- return headers;
213
+ function runWithCorrelationId(correlationId, fn) {
214
+ return correlationStorage.run({ value: correlationId }, fn);
305
215
  }
306
216
  /**
307
- * Convert resolved environment variables to config
217
+ * Set correlation ID in the current context (mutates context)
218
+ *
219
+ * Note: This updates the AsyncLocalStorage context. For proper scoping
220
+ * across async boundaries, prefer runWithCorrelationId() instead.
221
+ *
222
+ * @param correlationId - Correlation ID to set
223
+ *
224
+ * @example
225
+ * ```typescript
226
+ * setCorrelationId('abc123');
227
+ * // Now getCorrelationId() returns 'abc123'
228
+ * ```
308
229
  */
309
- function envToConfig(env) {
310
- const config = {};
311
- if (env.OTEL_SERVICE_NAME) config.service = env.OTEL_SERVICE_NAME;
312
- if (env.OTEL_EXPORTER_OTLP_ENDPOINT) config.endpoint = env.OTEL_EXPORTER_OTLP_ENDPOINT;
313
- if (env.OTEL_EXPORTER_OTLP_PROTOCOL) config.protocol = env.OTEL_EXPORTER_OTLP_PROTOCOL;
314
- if (env.OTEL_EXPORTER_OTLP_HEADERS) config.headers = parseOtlpHeaders(env.OTEL_EXPORTER_OTLP_HEADERS);
315
- const resourceAttrs = parseResourceAttributes(env.OTEL_RESOURCE_ATTRIBUTES);
316
- if (Object.keys(resourceAttrs).length > 0) config.resourceAttributes = resourceAttrs;
317
- const sampler = createSamplerFromEnv(env);
318
- if (sampler) config.otelSampler = sampler;
319
- return config;
230
+ function setCorrelationId(correlationId) {
231
+ enterOrRun(correlationStorage, correlationId);
320
232
  }
321
233
  /**
322
- * Main function to resolve config from environment variables
234
+ * Set correlation ID in baggage for propagation
235
+ *
236
+ * This adds the correlation ID to the W3C baggage header, allowing it
237
+ * to be propagated to downstream services.
238
+ *
239
+ * Note: Only use this when you explicitly want cross-service propagation.
240
+ * Default is OFF to avoid header bloat.
241
+ *
242
+ * @param correlationId - Correlation ID to propagate
243
+ * @returns New context with baggage set
244
+ *
245
+ * @example
246
+ * ```typescript
247
+ * const newContext = setCorrelationIdInBaggage('abc123');
248
+ * context.with(newContext, () => {
249
+ * // Baggage will be propagated in outgoing requests
250
+ * });
251
+ * ```
323
252
  */
324
- function resolveConfigFromEnv() {
325
- return envToConfig(resolveOtelEnv());
253
+ function setCorrelationIdInBaggage(correlationId) {
254
+ const activeContext = context.active();
255
+ let baggage = propagation.getBaggage(activeContext) ?? propagation.createBaggage();
256
+ baggage = baggage.setEntry(CORRELATION_ID_BAGGAGE_KEY, { value: correlationId });
257
+ return propagation.setBaggage(activeContext, baggage);
326
258
  }
327
-
328
- //#endregion
329
- //#region src/devtools.ts
330
- const defaultHost = "127.0.0.1";
331
- const defaultPort = 4318;
332
- function resolveDevtoolsConfig(config) {
333
- if (!config) return {
334
- enabled: false,
335
- endpoint: void 0,
336
- embedded: false,
337
- host: defaultHost,
338
- port: defaultPort,
339
- verbose: false
340
- };
341
- if (config === true) return {
342
- enabled: true,
343
- endpoint: `http://${defaultHost}:${defaultPort}`,
344
- embedded: false,
345
- host: defaultHost,
346
- port: defaultPort,
347
- verbose: false
348
- };
349
- const enabled = config.enabled ?? true;
350
- const host = config.host ?? defaultHost;
351
- const port = config.port ?? defaultPort;
352
- const endpoint = config.endpoint ?? `http://${host}:${port}`;
353
- return {
354
- enabled,
355
- endpoint: enabled ? endpoint : void 0,
356
- embedded: enabled && (config.embedded ?? false),
357
- host,
358
- port,
359
- verbose: config.verbose ?? false
360
- };
259
+ /**
260
+ * Get the correlation storage instance (for internal use in init/shutdown)
261
+ */
262
+ function getCorrelationStorage() {
263
+ return correlationStorage;
361
264
  }
362
265
 
363
266
  //#endregion
364
- //#region src/process-handlers.ts
365
- let removeOwnedHandlers = [];
366
- /**
367
- * Tracked separately from `removeOwnedHandlers` because the exit flush outlives
368
- * a call to `installProcessHandlers`, which clears that list before installing
369
- * its own signal listeners.
370
- */
371
- let removeExitFlush;
267
+ //#region src/event-queue.ts
268
+ const DEFAULT_CONFIG$2 = {
269
+ maxSize: 5e4,
270
+ batchSize: 100,
271
+ flushInterval: 1e4,
272
+ maxRetries: 3,
273
+ rateLimit: {
274
+ maxEventsPerSecond: 100,
275
+ burstCapacity: 200
276
+ }
277
+ };
372
278
  /**
373
- * Shared across every path that can end the process: signals, fatal errors and
374
- * a clean exit. They can overlap — a container stopping a job that has just
375
- * finished its work sends SIGTERM while the exit flush is still draining — and
376
- * a second shutdown would tear down queues the first is still using.
279
+ * Get subscriber name for metrics (stable, low-cardinality)
280
+ *
281
+ * Priority:
282
+ * 1. Explicit config: subscriber.name
283
+ * 2. Class static property (if available)
284
+ * 3. Fallback: lowercase class name without "Subscriber" suffix
377
285
  */
378
- let shutdownInFlight;
286
+ function getSubscriberName(subscriber) {
287
+ if (subscriber.name) return subscriber.name.toLowerCase();
288
+ return (subscriber.constructor?.name || "unknown").replace(/Subscriber$/i, "").toLowerCase();
289
+ }
379
290
  /**
380
- * The clean-exit flush, while it runs. Doubles as the latch that keeps a
381
- * re-emitted `beforeExit` from flushing twice, and as the thing a shutdown
382
- * waits on rather than tearing down queues mid-drain.
291
+ * Subscribers whose `shutdown()` has run. Terminal for the client they wrap, so
292
+ * a queue rebuilt from the same config must not reuse them.
383
293
  */
384
- let exitFlushInFlight;
385
- const DEFAULT_SHUTDOWN_TIMEOUT_MS = 2e3;
386
- function runShutdownOnce(shutdown, timeoutMs) {
387
- if (shutdownInFlight) return shutdownInFlight;
388
- let timeoutHandle;
389
- const shutdownAttempt = Promise.resolve(exitFlushInFlight).then(shutdown).catch(() => void 0);
390
- const timeout = new Promise((resolve) => {
391
- timeoutHandle = setTimeout(resolve, timeoutMs);
392
- timeoutHandle.unref();
393
- });
394
- shutdownInFlight = Promise.race([shutdownAttempt, timeout]).then(() => {
395
- if (timeoutHandle) clearTimeout(timeoutHandle);
396
- });
397
- return shutdownInFlight;
398
- }
399
- const DEFAULT_SIGNALS = ["SIGTERM", "SIGINT"];
400
- function signalExitCode(signal) {
401
- return 128 + constants.signals[signal];
402
- }
294
+ const shutDownSubscribers = /* @__PURE__ */ new WeakSet();
403
295
  /**
404
- * Surface a fatal error before shutting down.
296
+ * Events queue with batching and backpressure
405
297
  *
406
- * Registering an `uncaughtException` / `unhandledRejection` listener overrides
407
- * Node's default of printing the stack to stderr, so without this a crash under
408
- * `fatalErrors` would exit silently. Autotel's own logger is silent by default,
409
- * so we print to stderr directly to guarantee the crash stays visible.
298
+ * Features:
299
+ * - Batches events for efficient sending
300
+ * - Bounded queue with drop-oldest policy (prod) or blocking (dev)
301
+ * - Exponential backoff retry
302
+ * - Rate limiting to prevent overwhelming subscribers
303
+ * - Graceful flush on shutdown
410
304
  */
411
- function reportFatalError(error, event) {
412
- const err = error instanceof Error ? error : new Error(String(error));
413
- console.error(`[autotel] ${event}, flushing telemetry then exiting`, err);
414
- }
415
- function installProcessHandlers(config, shutdown) {
416
- uninstallProcessHandlers();
417
- const timeoutMs = config.shutdownTimeoutMs ?? DEFAULT_SHUTDOWN_TIMEOUT_MS;
418
- let exitScheduled = false;
419
- let resolvedExitCode = 0;
420
- let fatalLatched = false;
421
- const shutdownAndExit = (exitCode, fatal) => {
422
- if (fatal) {
423
- if (!fatalLatched) {
424
- resolvedExitCode = exitCode;
425
- fatalLatched = true;
426
- }
427
- } else if (!exitScheduled && !fatalLatched) resolvedExitCode = exitCode;
428
- if (exitScheduled) return;
429
- exitScheduled = true;
430
- runShutdownOnce(shutdown, timeoutMs).then(() => {
431
- process.exit(resolvedExitCode);
432
- });
433
- };
434
- for (const signal of config.signals ?? DEFAULT_SIGNALS) {
435
- const listener = () => {
436
- shutdownAndExit(signalExitCode(signal), false);
305
+ var EventQueue = class {
306
+ queue = [];
307
+ flushTimer = null;
308
+ config;
309
+ subscribers;
310
+ rateLimiter;
311
+ flushPromise = null;
312
+ isShuttingDown = false;
313
+ metrics = null;
314
+ observableCleanups = [];
315
+ subscriberHealthy = /* @__PURE__ */ new Map();
316
+ constructor(subscribers, config) {
317
+ const live = subscribers.filter((s) => !shutDownSubscribers.has(s));
318
+ if (live.length < subscribers.length) getLogger().warn({ subscribers: subscribers.filter((s) => shutDownSubscribers.has(s)).map((s) => getSubscriberName(s)) }, "[autotel] Subscriber already shut down, events will not be delivered; pass new subscriber instances to init()");
319
+ this.subscribers = live;
320
+ this.config = {
321
+ ...DEFAULT_CONFIG$2,
322
+ ...config
437
323
  };
438
- process.on(signal, listener);
439
- removeOwnedHandlers.push(() => {
440
- process.removeListener(signal, listener);
441
- });
324
+ this.rateLimiter = this.config.rateLimit ? new TokenBucketRateLimiter(this.config.rateLimit) : null;
325
+ for (const subscriber of this.subscribers) {
326
+ const name = getSubscriberName(subscriber);
327
+ this.subscriberHealthy.set(name, true);
328
+ }
329
+ this.initMetrics();
442
330
  }
443
- if (config.fatalErrors ?? true) {
444
- const uncaughtExceptionListener = (error) => {
445
- reportFatalError(error, "uncaughtException");
446
- shutdownAndExit(1, true);
331
+ /**
332
+ * Initialize OTel metrics for queue observability
333
+ */
334
+ initMetrics() {
335
+ const meter = getConfig$1().meter;
336
+ const queueSize = meter.createObservableGauge("autotel.event_delivery.queue.size", {
337
+ description: "Current number of events in the delivery queue",
338
+ unit: "count"
339
+ });
340
+ const queueSizeCallback = (observableResult) => {
341
+ observableResult.observe(this.queue.length);
447
342
  };
448
- const unhandledRejectionListener = (reason) => {
449
- reportFatalError(reason, "unhandledRejection");
450
- shutdownAndExit(1, true);
343
+ queueSize.addCallback(queueSizeCallback);
344
+ this.observableCleanups.push(() => queueSize.removeCallback(queueSizeCallback));
345
+ const oldestAge = meter.createObservableGauge("autotel.event_delivery.queue.oldest_age_ms", {
346
+ description: "Age of the oldest event in the queue in milliseconds",
347
+ unit: "ms"
348
+ });
349
+ const oldestAgeCallback = (observableResult) => {
350
+ if (this.queue.length > 0) {
351
+ const oldest = this.queue[0];
352
+ const ageMs = Date.now() - oldest.timestamp;
353
+ observableResult.observe(ageMs);
354
+ } else observableResult.observe(0);
451
355
  };
452
- process.on("uncaughtException", uncaughtExceptionListener);
453
- process.on("unhandledRejection", unhandledRejectionListener);
454
- removeOwnedHandlers.push(() => {
455
- process.removeListener("uncaughtException", uncaughtExceptionListener);
456
- }, () => {
457
- process.removeListener("unhandledRejection", unhandledRejectionListener);
356
+ oldestAge.addCallback(oldestAgeCallback);
357
+ this.observableCleanups.push(() => oldestAge.removeCallback(oldestAgeCallback));
358
+ const delivered = meter.createCounter("autotel.event_delivery.queue.delivered", {
359
+ description: "Number of events successfully delivered to subscribers",
360
+ unit: "count"
458
361
  });
459
- }
460
- }
461
- /**
462
- * Flush telemetry when the process runs to completion.
463
- *
464
- * The signal and fatal-error handlers above cover a process that is stopped or
465
- * that crashes. Neither fires when a script simply finishes: the event loop
466
- * drains and Node exits, taking whatever the batch span processor was still
467
- * holding with it. `beforeExit` is the only hook for that case, and it is the
468
- * one that matters for CLIs, cron jobs, CI steps and serverless handlers.
469
- *
470
- * A flush, never a shutdown: `beforeExit` fires on *any* event-loop drain, not
471
- * only the final one, so tearing the SDK down here would silently kill
472
- * telemetry in a process that goes on to do more work.
473
- */
474
- function installExitFlush(flushTelemetry, timeoutMs = DEFAULT_SHUTDOWN_TIMEOUT_MS) {
475
- removeExitFlush?.();
476
- exitFlushInFlight = void 0;
477
- const listener = (code) => {
478
- if (exitFlushInFlight || shutdownInFlight) return;
479
- const deadline = setTimeout(() => {
480
- process.exit(process.exitCode ?? code);
481
- }, timeoutMs);
482
- exitFlushInFlight = flushTelemetry().catch(() => void 0).finally(() => {
483
- clearTimeout(deadline);
362
+ const failed = meter.createCounter("autotel.event_delivery.queue.failed", {
363
+ description: "Number of events that failed delivery after all retry attempts",
364
+ unit: "count"
484
365
  });
485
- };
486
- process.on("beforeExit", listener);
487
- removeExitFlush = () => {
488
- process.removeListener("beforeExit", listener);
489
- removeExitFlush = void 0;
490
- };
491
- }
492
- function uninstallProcessHandlers() {
493
- for (const removeHandler of removeOwnedHandlers) removeHandler();
494
- removeOwnedHandlers = [];
495
- removeExitFlush?.();
496
- shutdownInFlight = void 0;
497
- exitFlushInFlight = void 0;
498
- }
499
-
500
- //#endregion
501
- //#region src/rate-limiter.ts
502
- /**
503
- * Token bucket rate limiter
504
- *
505
- * Allows bursts up to burstCapacity, then smooths to maxEventsPerSecond.
506
- * Thread-safe for async operations.
507
- */
508
- var TokenBucketRateLimiter = class {
509
- tokens;
510
- maxTokens;
511
- refillRate;
512
- lastRefill;
513
- constructor(config) {
514
- this.maxTokens = config.burstCapacity || config.maxEventsPerSecond * 2;
515
- this.tokens = this.maxTokens;
516
- this.refillRate = config.maxEventsPerSecond / 1e3;
517
- this.lastRefill = Date.now();
366
+ const dropped = meter.createCounter("autotel.event_delivery.queue.dropped", {
367
+ description: "Number of events dropped from the queue",
368
+ unit: "count"
369
+ });
370
+ const latency = meter.createHistogram("autotel.event_delivery.queue.latency_ms", {
371
+ description: "Event delivery latency from enqueue to successful send",
372
+ unit: "ms"
373
+ });
374
+ const subscriberHealth = meter.createObservableGauge("autotel.event_delivery.subscriber.health", {
375
+ description: "Subscriber health status (1=healthy, 0=unhealthy)",
376
+ unit: "1"
377
+ });
378
+ const subscriberHealthCallback = (observableResult) => {
379
+ for (const [subscriberName, isHealthy] of this.subscriberHealthy) observableResult.observe(isHealthy ? 1 : 0, { subscriber: subscriberName });
380
+ };
381
+ subscriberHealth.addCallback(subscriberHealthCallback);
382
+ this.observableCleanups.push(() => subscriberHealth.removeCallback(subscriberHealthCallback));
383
+ this.metrics = {
384
+ queueSize,
385
+ oldestAge,
386
+ delivered,
387
+ failed,
388
+ dropped,
389
+ latency,
390
+ subscriberHealth
391
+ };
518
392
  }
519
393
  /**
520
- * Try to consume a token (allow an event)
521
- * Returns true if allowed, false if rate limit exceeded
394
+ * Record a dropped event with reason and emit debug breadcrumb
522
395
  */
523
- tryConsume(count = 1) {
524
- this.refill();
525
- if (this.tokens >= count) {
526
- this.tokens -= count;
527
- return true;
528
- }
529
- return false;
530
- }
396
+ recordDropped(reason, event, subscriberName) {
397
+ const attrs = { reason };
398
+ if (subscriberName) attrs.subscriber = subscriberName;
399
+ this.metrics?.dropped.add(1, attrs);
400
+ const logLevel = reason === "payload_invalid" ? "error" : "warn";
401
+ const logger = getLogger();
402
+ if (logLevel === "error") logger.error({
403
+ eventName: event?.name,
404
+ subscriber: subscriberName,
405
+ reason,
406
+ correlationId: event?._correlationId,
407
+ traceId: event?._traceId
408
+ }, `[autotel] Event dropped: ${reason}`);
409
+ else logger.warn({
410
+ eventName: event?.name,
411
+ subscriber: subscriberName,
412
+ reason,
413
+ correlationId: event?._correlationId,
414
+ traceId: event?._traceId
415
+ }, `[autotel] Event dropped: ${reason}`);
416
+ }
531
417
  /**
532
- * Wait until a token is available (async rate limiting)
533
- * Returns a promise that resolves when the event can be processed
418
+ * Record permanent delivery failure (after all retries exhausted)
419
+ * Increments failed counter and logs error
534
420
  */
535
- async waitForToken(count = 1) {
536
- this.refill();
537
- if (this.tokens >= count) {
538
- this.tokens -= count;
539
- return;
540
- }
541
- const tokensNeeded = count - this.tokens;
542
- const waitMs = Math.ceil(tokensNeeded / this.refillRate);
543
- await new Promise((resolve) => setTimeout(resolve, waitMs));
544
- return this.waitForToken(count);
421
+ recordFailed(event, subscriberName, error) {
422
+ this.metrics?.failed.add(1, { subscriber: subscriberName });
423
+ this.subscriberHealthy.set(subscriberName, false);
424
+ getLogger().error({
425
+ eventName: event.name,
426
+ subscriber: subscriberName,
427
+ correlationId: event._correlationId,
428
+ traceId: event._traceId,
429
+ err: error
430
+ }, `[autotel] Event delivery failed after all retries`);
545
431
  }
546
432
  /**
547
- * Refill tokens based on elapsed time
433
+ * Mark subscriber as unhealthy on transient failure (without incrementing failed counter)
434
+ * Used during retry attempts - only recordFailed should increment the counter
548
435
  */
549
- refill() {
550
- const now = Date.now();
551
- const tokensToAdd = (now - this.lastRefill) * this.refillRate;
552
- this.tokens = Math.min(this.maxTokens, this.tokens + tokensToAdd);
553
- this.lastRefill = now;
436
+ markSubscriberUnhealthy(subscriberName) {
437
+ this.subscriberHealthy.set(subscriberName, false);
554
438
  }
555
439
  /**
556
- * Get current available tokens (for testing/debugging)
440
+ * Record successful delivery
557
441
  */
558
- getAvailableTokens() {
559
- this.refill();
560
- return Math.floor(this.tokens);
442
+ recordDelivered(event, subscriberName, startTime) {
443
+ const latencyMs = Date.now() - startTime;
444
+ this.metrics?.delivered.add(1, { subscriber: subscriberName });
445
+ this.metrics?.latency.record(latencyMs, { subscriber: subscriberName });
446
+ this.subscriberHealthy.set(subscriberName, true);
561
447
  }
562
448
  /**
563
- * Reset the rate limiter (for testing)
449
+ * Enqueue an event for sending
450
+ *
451
+ * Backpressure policy:
452
+ * - Drops oldest event and logs warning if queue is full (same behavior in all environments)
564
453
  */
565
- reset() {
566
- this.tokens = this.maxTokens;
567
- this.lastRefill = Date.now();
568
- }
569
- };
570
-
571
- //#endregion
572
- //#region src/trace-context.ts
573
- const spansWithExplicitStatus = /* @__PURE__ */ new WeakSet();
574
- /** Whether a TraceContext consumer explicitly chose this span's status. */
575
- function hasExplicitSpanStatus(span) {
576
- return spansWithExplicitStatus.has(span);
577
- }
578
- /**
579
- * AsyncLocalStorage for storing the active context with baggage
580
- * This allows setters to update the context and have it persist
581
- */
582
- const contextStorage = new nodeAsyncHooks.AsyncLocalStorage();
583
- /**
584
- * Get the context storage instance (for initialization in functional.ts)
585
- */
586
- function getContextStorage() {
587
- return contextStorage;
588
- }
589
- /**
590
- * Get the active OTel context with the latest stored baggage overlaid.
591
- * Span identity always comes from the active OTel scope.
592
- */
593
- function getActiveContextWithBaggage() {
594
- const activeContext = context.active();
595
- const stored = contextStorage.getStore()?.value;
596
- if (!stored) return activeContext;
597
- const storedBaggage = propagation.getBaggage(stored);
598
- return storedBaggage ? propagation.setBaggage(activeContext, storedBaggage) : activeContext;
599
- }
600
- /**
601
- * Set a value in AsyncLocalStorage, preferring enterWith() when available
602
- * (Node.js) and falling back to run() for environments that only support
603
- * run() (e.g. Cloudflare Workers).
604
- *
605
- * On runtimes without enterWith() we mutate the existing run() scope when one
606
- * exists. This is what allows baggage/correlation updates to remain visible
607
- * for the rest of the traced callback in Workers.
608
- */
609
- function enterOrRun(storage, value) {
610
- const existingStore = storage.getStore();
611
- if (existingStore) {
612
- existingStore.value = value;
613
- return;
454
+ enqueue(event) {
455
+ if (this.isShuttingDown) {
456
+ this.recordDropped("shutdown", event);
457
+ return;
458
+ }
459
+ if (this.queue.length >= this.config.maxSize) {
460
+ const droppedEvent = this.queue.shift();
461
+ this.recordDropped("rate_limit", droppedEvent);
462
+ 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.`);
463
+ }
464
+ const enrichedEvent = {
465
+ ...event,
466
+ _correlationId: event._correlationId || getOrCreateCorrelationId()
467
+ };
468
+ this.queue.push(enrichedEvent);
469
+ this.scheduleBatchFlush();
614
470
  }
615
- const boxedValue = { value };
616
- try {
617
- storage.enterWith(boxedValue);
618
- } catch {
619
- storage.run(boxedValue, () => {});
471
+ /**
472
+ * Schedule a batch flush if not already scheduled
473
+ */
474
+ scheduleBatchFlush() {
475
+ if (this.flushTimer || this.flushPromise) return;
476
+ this.flushTimer = setTimeout(() => {
477
+ this.flushTimer = null;
478
+ this.flushBatch();
479
+ }, this.config.flushInterval);
620
480
  }
621
- }
622
- function updateActiveContext(newContext) {
623
- enterOrRun(contextStorage, newContext);
624
- const manager = context._getContextManager?.();
625
- if (!manager) return;
626
- const asyncLocal = manager._asyncLocalStorage ?? void 0;
627
- if (asyncLocal?.enterWith) {
628
- asyncLocal.enterWith(newContext);
629
- return;
481
+ /**
482
+ * Flush a batch of events
483
+ * Uses promise-based concurrency control to prevent race conditions
484
+ */
485
+ async flushBatch() {
486
+ if (this.queue.length === 0) return;
487
+ if (this.flushPromise) {
488
+ await this.flushPromise;
489
+ return;
490
+ }
491
+ this.flushPromise = this.doFlushBatch();
492
+ try {
493
+ await this.flushPromise;
494
+ } finally {
495
+ this.flushPromise = null;
496
+ if (this.queue.length > 0) this.scheduleBatchFlush();
497
+ }
630
498
  }
631
- if (isFunction(manager.with)) manager.with(newContext, () => {});
632
- }
633
- /**
634
- * Create a TraceContext from an OpenTelemetry Span
635
- *
636
- * This utility extracts trace context information from a span
637
- * and provides span manipulation methods and baggage operations in a consistent format.
638
- *
639
- * Note: Baggage methods always operate on the currently active context,
640
- * which may differ from the context when createTraceContext was called.
641
- */
642
- function createTraceContext(span) {
643
- const spanContext = span.spanContext();
644
- if (!contextStorage.getStore()?.value) {
645
- const activeContext = context.active();
646
- enterOrRun(contextStorage, activeContext);
499
+ /**
500
+ * Internal flush implementation
501
+ */
502
+ async doFlushBatch() {
503
+ const batch = this.queue.splice(0, this.config.batchSize);
504
+ await this.sendWithRetry(batch, this.config.maxRetries);
647
505
  }
648
- const traceCtx = {
649
- traceId: spanContext.traceId,
650
- spanId: spanContext.spanId,
651
- correlationId: spanContext.traceId.slice(0, 16),
652
- setAttribute: span.setAttribute.bind(span),
653
- setAttributes: span.setAttributes.bind(span),
654
- setStatus: (status) => {
655
- spansWithExplicitStatus.add(span);
656
- span.setStatus(status);
657
- },
658
- recordException: span.recordException.bind(span),
659
- addEvent: span.addEvent.bind(span),
660
- addLink: span.addLink.bind(span),
661
- addLinks: span.addLinks.bind(span),
662
- updateName: span.updateName.bind(span),
663
- isRecording: span.isRecording.bind(span),
664
- recordError: (cause) => {
665
- const err = cause instanceof Error ? cause : new Error(String(cause));
666
- recordStructuredError(traceCtx, err);
667
- },
668
- track: (event, data) => {
669
- track(event, data);
670
- },
671
- getBaggage(key) {
672
- const activeCtx = context.active();
673
- let baggage = propagation.getBaggage(activeCtx);
674
- if (!baggage) {
675
- const storedContext = contextStorage.getStore()?.value;
676
- if (storedContext) baggage = propagation.getBaggage(storedContext);
677
- }
678
- return baggage?.getEntry(key)?.value;
679
- },
680
- setBaggage(key, value) {
681
- const currentContext = getActiveContextWithBaggage();
682
- const updated = (propagation.getBaggage(currentContext) ?? propagation.createBaggage()).setEntry(key, { value });
683
- updateActiveContext(propagation.setBaggage(currentContext, updated));
684
- return value;
685
- },
686
- deleteBaggage(key) {
687
- const currentContext = getActiveContextWithBaggage();
688
- const baggage = propagation.getBaggage(currentContext);
689
- if (baggage) {
690
- const updated = baggage.removeEntry(key);
691
- updateActiveContext(propagation.setBaggage(currentContext, updated));
506
+ /**
507
+ * Send events with exponential backoff retry
508
+ * Tracks per-event, per-subscriber failures so failed counter reflects actual failed deliveries.
509
+ * On retry, only failed (event, subscriber) pairs are re-sent to avoid double-counting delivered.
510
+ */
511
+ async sendWithRetry(events, retriesLeft, subscribersByEventIndex) {
512
+ const failedDeliveries = await this.sendToSubscribers(events, subscribersByEventIndex);
513
+ if (failedDeliveries.length > 0) if (retriesLeft > 0) {
514
+ const failedEventIndicesOrdered = [...new Set(failedDeliveries.map((f) => f.eventIndex))].toSorted((a, b) => a - b);
515
+ const eventsToRetry = failedEventIndicesOrdered.map((i) => events[i]);
516
+ const failedSubscribersByRetryIndex = /* @__PURE__ */ new Map();
517
+ for (const [j, origIndex] of failedEventIndicesOrdered.entries()) {
518
+ const set = /* @__PURE__ */ new Set();
519
+ for (const { eventIndex, subscriberName } of failedDeliveries) if (eventIndex === origIndex) set.add(subscriberName);
520
+ failedSubscribersByRetryIndex.set(j, set);
521
+ }
522
+ const delay = Math.pow(2, this.config.maxRetries - retriesLeft) * 1e3;
523
+ await new Promise((resolve) => setTimeout(resolve, delay));
524
+ return this.sendWithRetry(eventsToRetry, retriesLeft - 1, failedSubscribersByRetryIndex);
525
+ } else {
526
+ for (const { eventIndex, subscriberName, error } of failedDeliveries) {
527
+ const event = events[eventIndex];
528
+ if (event) this.recordFailed(event, subscriberName, error);
529
+ }
530
+ const failedSubscriberNames = [...new Set(failedDeliveries.map((f) => f.subscriberName))];
531
+ getLogger().error({
532
+ failedSubscribers: failedSubscriberNames,
533
+ retriesAttempted: this.config.maxRetries
534
+ }, "[autotel] Failed to send events after retries");
535
+ }
536
+ }
537
+ /**
538
+ * Send events to configured subscribers with rate limiting and metrics.
539
+ * When subscribersByEventIndex is provided (retry path), only those subscribers are tried per event.
540
+ * Returns per-event, per-subscriber failures (empty if all succeeded).
541
+ */
542
+ async sendToSubscribers(events, subscribersByEventIndex) {
543
+ const failedDeliveries = [];
544
+ const sendOne = async (event, eventIndex) => {
545
+ const subscriberNames = subscribersByEventIndex?.get(eventIndex);
546
+ const failures = await this.sendEventToSubscribers(event, subscriberNames ?? void 0);
547
+ for (const failure of failures) failedDeliveries.push({
548
+ eventIndex,
549
+ subscriberName: failure.subscriberName,
550
+ error: failure.error
551
+ });
552
+ };
553
+ if (!this.rateLimiter) {
554
+ for (const [i, event] of events.entries()) if (event) await sendOne(event, i);
555
+ return failedDeliveries;
556
+ }
557
+ for (const [i, event] of events.entries()) {
558
+ await this.rateLimiter.waitForToken();
559
+ if (event) await sendOne(event, i);
560
+ }
561
+ return failedDeliveries;
562
+ }
563
+ /**
564
+ * Send a single event to subscribers.
565
+ * - When subscriberNames is undefined (initial attempt): send to all subscribers.
566
+ * - When subscriberNames is provided (retry): send only to those subscribers (never re-send to healthy ones).
567
+ * Returns list of subscribers that failed (empty if all succeeded).
568
+ */
569
+ async sendEventToSubscribers(event, subscriberNames) {
570
+ const startTime = event.timestamp;
571
+ const failures = [];
572
+ const subscribersToTry = subscriberNames === void 0 ? this.subscribers : this.subscribers.filter((s) => subscriberNames.has(getSubscriberName(s)));
573
+ const results = await Promise.allSettled(subscribersToTry.map(async (subscriber) => {
574
+ const subscriberName = getSubscriberName(subscriber);
575
+ try {
576
+ await subscriber.trackEvent(event.name, event.attributes, {
577
+ autotel: event.autotel,
578
+ schema: event.schema
579
+ });
580
+ this.recordDelivered(event, subscriberName, startTime);
581
+ return {
582
+ subscriberName,
583
+ success: true
584
+ };
585
+ } catch (error) {
586
+ this.markSubscriberUnhealthy(subscriberName);
587
+ return {
588
+ subscriberName,
589
+ success: false,
590
+ error: error instanceof Error ? error : void 0
591
+ };
592
+ }
593
+ }));
594
+ for (const result of results) if (result.status === "fulfilled" && !result.value.success) failures.push({
595
+ subscriberName: result.value.subscriberName,
596
+ error: result.value.error
597
+ });
598
+ return failures;
599
+ }
600
+ /**
601
+ * Flush all remaining events. Queue remains usable after flush (e.g. for
602
+ * auto-flush at root span end). Use shutdown() when tearing down the queue.
603
+ */
604
+ async flush() {
605
+ if (this.flushTimer) {
606
+ clearTimeout(this.flushTimer);
607
+ this.flushTimer = null;
608
+ }
609
+ if (this.flushPromise) await this.flushPromise;
610
+ while (this.queue.length > 0) await this.doFlushBatch();
611
+ }
612
+ /**
613
+ * Flush remaining events and permanently disable the queue (reject new events).
614
+ * Use for process/SDK shutdown; use flush() for periodic or span-end drain.
615
+ */
616
+ async shutdown() {
617
+ this.isShuttingDown = true;
618
+ await this.flush();
619
+ const drains = await Promise.allSettled(this.subscribers.map(async (subscriber) => {
620
+ if (!subscriber.shutdown) return;
621
+ shutDownSubscribers.add(subscriber);
622
+ return subscriber.shutdown();
623
+ }));
624
+ for (const [index, drain] of drains.entries()) {
625
+ if (drain.status !== "rejected") continue;
626
+ const subscriberName = getSubscriberName(this.subscribers[index]);
627
+ this.subscriberHealthy.set(subscriberName, false);
628
+ getLogger().error({
629
+ subscriber: subscriberName,
630
+ err: drain.reason
631
+ }, "[autotel] Subscriber shutdown failed, buffered events may be lost");
632
+ }
633
+ }
634
+ /**
635
+ * Cleanup observable metric callbacks to prevent memory leaks
636
+ * Call this when destroying the EventQueue instance
637
+ */
638
+ cleanup() {
639
+ for (const cleanupFn of this.observableCleanups) try {
640
+ cleanupFn();
641
+ } catch {}
642
+ this.observableCleanups = [];
643
+ }
644
+ /**
645
+ * Get queue size (for testing/debugging)
646
+ */
647
+ size() {
648
+ return this.queue.length;
649
+ }
650
+ /**
651
+ * Get subscriber health status (for testing/debugging)
652
+ */
653
+ getSubscriberHealth() {
654
+ return new Map(this.subscriberHealthy);
655
+ }
656
+ /**
657
+ * Check if a specific subscriber is healthy
658
+ */
659
+ isSubscriberHealthy(subscriberName) {
660
+ return this.subscriberHealthy.get(subscriberName.toLowerCase()) ?? true;
661
+ }
662
+ /**
663
+ * Manually mark a subscriber as healthy or unhealthy
664
+ * (used for circuit breaker integration)
665
+ */
666
+ setSubscriberHealth(subscriberName, healthy) {
667
+ this.subscriberHealthy.set(subscriberName.toLowerCase(), healthy);
668
+ }
669
+ };
670
+
671
+ //#endregion
672
+ //#region src/validation.ts
673
+ const DEFAULT_CONFIG$1 = {
674
+ maxEventNameLength: 100,
675
+ maxAttributeKeyLength: 100,
676
+ maxAttributeValueLength: 1e3,
677
+ maxAttributeCount: 50,
678
+ maxNestingDepth: 3,
679
+ sensitivePatterns: [
680
+ /password/i,
681
+ /secret/i,
682
+ /token/i,
683
+ /api[_-]?key/i,
684
+ /access[_-]?key/i,
685
+ /private[_-]?key/i,
686
+ /auth/i,
687
+ /credential/i,
688
+ /ssn/i,
689
+ /credit[_-]?card/i
690
+ ]
691
+ };
692
+ var ValidationError = class extends Error {
693
+ constructor(message) {
694
+ super(message);
695
+ this.name = "ValidationError";
696
+ }
697
+ };
698
+ /**
699
+ * Validate and sanitize event name
700
+ * Throws ValidationError if invalid
701
+ */
702
+ function validateEventName(eventName, config = DEFAULT_CONFIG$1) {
703
+ if (asString(eventName) === void 0) throw new ValidationError(`Event name must be a string, got ${describeValue(eventName)}`);
704
+ const trimmed = eventName.trim();
705
+ if (trimmed.length === 0) throw new ValidationError("Event name cannot be empty");
706
+ if (trimmed.length > config.maxEventNameLength) throw new ValidationError(`Event name too long (${trimmed.length} chars). Max: ${config.maxEventNameLength}`);
707
+ if (!/^[a-zA-Z0-9._-]+$/.test(trimmed)) throw new ValidationError(`Event name contains invalid characters: "${trimmed}". Use only letters, numbers, dots, underscores, and hyphens.`);
708
+ return trimmed;
709
+ }
710
+ /**
711
+ * Validate and sanitize attributes
712
+ * Returns sanitized attributes (sensitive data redacted)
713
+ */
714
+ function validateAttributes(attributes, config = DEFAULT_CONFIG$1) {
715
+ if (attributes === void 0 || attributes === null) return;
716
+ if (!asRecord(attributes)) throw new ValidationError("Attributes must be an object");
717
+ const keys = Object.keys(attributes);
718
+ if (keys.length > config.maxAttributeCount) throw new ValidationError(`Too many attributes (${keys.length}). Max: ${config.maxAttributeCount}`);
719
+ const sanitized = {};
720
+ for (const key of keys) {
721
+ if (key.length > config.maxAttributeKeyLength) throw new ValidationError(`Attribute key too long: "${key.slice(0, 20)}..." (${key.length} chars). Max: ${config.maxAttributeKeyLength}`);
722
+ const value = attributes[key];
723
+ if (isSensitiveString(key, value, config)) {
724
+ sanitized[key] = "[REDACTED]";
725
+ continue;
726
+ }
727
+ sanitized[key] = sanitizeValue(value, config, 1);
728
+ }
729
+ return sanitized;
730
+ }
731
+ /**
732
+ * Whether this key/value pair is a credential to redact. Strings only:
733
+ * numeric and boolean values are not credentials, and replacing them with the
734
+ * literal '[REDACTED]' leaks no signal while breaking every consumer that
735
+ * treats them as a number.
736
+ */
737
+ function isSensitiveString(key, value, config) {
738
+ return asString(value) !== void 0 && config.sensitivePatterns.some((pattern) => pattern.test(key));
739
+ }
740
+ /**
741
+ * Sanitize attribute value (recursive)
742
+ */
743
+ function sanitizeValue(value, config, depth) {
744
+ if (depth > config.maxNestingDepth) return "[MAX_DEPTH_EXCEEDED]";
745
+ if (value === null || value === void 0) return value;
746
+ const text = asString(value);
747
+ if (text !== void 0) return text.length > config.maxAttributeValueLength ? text.slice(0, config.maxAttributeValueLength) + "..." : text;
748
+ const scalar = asNumber(value) ?? asBoolean(value);
749
+ if (scalar !== void 0) return scalar;
750
+ if (Array.isArray(value)) return value.map((item) => sanitizeValue(item, config, depth + 1));
751
+ const record = asRecord(value);
752
+ if (record) try {
753
+ JSON.stringify(record);
754
+ const sanitized = {};
755
+ for (const [key, nested] of Object.entries(record)) sanitized[key] = isSensitiveString(key, nested, config) ? "[REDACTED]" : sanitizeValue(nested, config, depth + 1);
756
+ return sanitized;
757
+ } catch {
758
+ return "[CIRCULAR]";
759
+ }
760
+ return `[${describeValue(value)}]`;
761
+ }
762
+ /**
763
+ * Validate and sanitize an events event
764
+ * Returns { eventName, attributes } with sanitized values
765
+ */
766
+ function validateEvent(eventName, attributes, config) {
767
+ const fullConfig = {
768
+ ...DEFAULT_CONFIG$1,
769
+ ...config
770
+ };
771
+ return {
772
+ eventName: validateEventName(eventName, fullConfig),
773
+ attributes: validateAttributes(attributes, fullConfig)
774
+ };
775
+ }
776
+
777
+ //#endregion
778
+ //#region src/track.ts
779
+ /**
780
+ * Global track() function for business events
781
+ *
782
+ * Simple, no instantiation needed, auto-attaches trace context
783
+ */
784
+ let eventsQueue = null;
785
+ let warnedNoSubscribers = false;
786
+ /**
787
+ * Build autotel event context for trace correlation
788
+ *
789
+ * Works in multiple contexts:
790
+ * 1. Inside a span → use current span's trace_id + span_id
791
+ * 2. Outside span → use correlation_id only
792
+ * 3. With trace URL config → include clickable trace URL
793
+ */
794
+ function buildAutotelContext(span) {
795
+ const eventsConfig = getEventsConfig();
796
+ const config = getConfig();
797
+ const correlationId = getOrCreateCorrelationId();
798
+ if (!eventsConfig?.includeTraceContext) return { correlation_id: correlationId };
799
+ const autotelContext = { correlation_id: correlationId };
800
+ const spanContext = span?.spanContext();
801
+ if (spanContext) {
802
+ autotelContext.trace_id = spanContext.traceId;
803
+ autotelContext.span_id = spanContext.spanId;
804
+ autotelContext.trace_flags = spanContext.traceFlags.toString(16).padStart(2, "0");
805
+ const traceState = spanContext.traceState;
806
+ if (traceState) try {
807
+ if (typeof traceState.serialize === "function") {
808
+ const traceStateStr = traceState.serialize();
809
+ if (traceStateStr) autotelContext.trace_state = traceStateStr;
810
+ }
811
+ } catch {}
812
+ if (eventsConfig.traceUrl && config) {
813
+ const traceUrl = eventsConfig.traceUrl({
814
+ traceId: spanContext.traceId,
815
+ spanId: spanContext.spanId,
816
+ correlationId,
817
+ serviceName: config.service,
818
+ environment: config.environment
819
+ });
820
+ if (traceUrl) autotelContext.trace_url = traceUrl;
821
+ }
822
+ } else if (eventsConfig.traceUrl && config) {
823
+ const traceUrl = eventsConfig.traceUrl({
824
+ correlationId,
825
+ serviceName: config.service,
826
+ environment: config.environment
827
+ });
828
+ if (traceUrl) autotelContext.trace_url = traceUrl;
829
+ }
830
+ return autotelContext;
831
+ }
832
+ /**
833
+ * Initialize events queue lazily
834
+ */
835
+ function getOrCreateQueue() {
836
+ if (!isInitialized()) {
837
+ warnIfNotInitialized("track()");
838
+ return null;
839
+ }
840
+ if (!eventsQueue) {
841
+ const config = getConfig();
842
+ if (!config?.subscribers || config.subscribers.length === 0) {
843
+ if (!warnedNoSubscribers) {
844
+ warnedNoSubscribers = true;
845
+ getLogger().warn({}, "[autotel] track() dropped an event: init() configured no subscribers. Events (including gen_ai.evaluation.result and audit events) reach a backend through init({ subscribers: [...] }).");
846
+ }
847
+ return null;
848
+ }
849
+ eventsQueue = new EventQueue(config.subscribers);
850
+ }
851
+ return eventsQueue;
852
+ }
853
+ /**
854
+ * Track a business events event
855
+ *
856
+ * Features:
857
+ * - Auto-attaches traceId and spanId if in active span
858
+ * - Batched sending with retry
859
+ * - Type-safe with optional generic
860
+ * - No-op if init() not called or no subscribers configured
861
+ *
862
+ * @example Basic usage
863
+ * ```typescript
864
+ * track('user.signup', { userId: '123', plan: 'pro' })
865
+ * ```
866
+ *
867
+ * @example With type safety
868
+ * ```typescript
869
+ * interface EventDatas {
870
+ * 'user.signup': { userId: string; plan: string }
871
+ * 'plan.upgraded': { userId: string; revenue: number }
872
+ * }
873
+ *
874
+ * track<EventDatas>('user.signup', { userId: '123', plan: 'pro' })
875
+ * ```
876
+ *
877
+ * @example Trace correlation (automatic)
878
+ * ```typescript
879
+ * @Instrumented()
880
+ * class UserService {
881
+ * async createUser(data: CreateUserData) {
882
+ * // This track call automatically includes traceId + spanId
883
+ * track('user.signup', { userId: data.id })
884
+ * }
885
+ * }
886
+ * ```
887
+ */
888
+ function track(event, data, options) {
889
+ const queue = getOrCreateQueue();
890
+ if (!queue) return;
891
+ const validated = validateEvent(event, data, getValidationConfig() || void 0);
892
+ const span = trace.getActiveSpan();
893
+ const enrichedData = span ? {
894
+ ...validated.attributes,
895
+ traceId: span.spanContext().traceId,
896
+ spanId: span.spanContext().spanId
897
+ } : validated.attributes;
898
+ const autotelContext = buildAutotelContext(span);
899
+ queue.enqueue({
900
+ name: validated.eventName,
901
+ attributes: enrichedData,
902
+ timestamp: Date.now(),
903
+ autotel: autotelContext,
904
+ schema: options?.schema
905
+ });
906
+ }
907
+ /**
908
+ * Get events queue (for flush/shutdown)
909
+ * @internal
910
+ */
911
+ function getEventQueue() {
912
+ return eventsQueue;
913
+ }
914
+ /**
915
+ * Reset events queue (for shutdown/cleanup)
916
+ * @internal
917
+ */
918
+ function resetEventQueue() {
919
+ eventsQueue = null;
920
+ warnedNoSubscribers = false;
921
+ }
922
+
923
+ //#endregion
924
+ //#region src/trace-context.ts
925
+ const spansWithExplicitStatus = /* @__PURE__ */ new WeakSet();
926
+ /** Whether a TraceContext consumer explicitly chose this span's status. */
927
+ function hasExplicitSpanStatus(span) {
928
+ return spansWithExplicitStatus.has(span);
929
+ }
930
+ /**
931
+ * AsyncLocalStorage for storing the active context with baggage
932
+ * This allows setters to update the context and have it persist
933
+ */
934
+ const contextStorage = new nodeAsyncHooks.AsyncLocalStorage();
935
+ /**
936
+ * Get the context storage instance (for initialization in functional.ts)
937
+ */
938
+ function getContextStorage() {
939
+ return contextStorage;
940
+ }
941
+ /**
942
+ * Get the active OTel context with the latest stored baggage overlaid.
943
+ * Span identity always comes from the active OTel scope.
944
+ */
945
+ function getActiveContextWithBaggage() {
946
+ const activeContext = context.active();
947
+ const stored = contextStorage.getStore()?.value;
948
+ if (!stored) return activeContext;
949
+ const storedBaggage = propagation.getBaggage(stored);
950
+ return storedBaggage ? propagation.setBaggage(activeContext, storedBaggage) : activeContext;
951
+ }
952
+ /**
953
+ * Set a value in AsyncLocalStorage, preferring enterWith() when available
954
+ * (Node.js) and falling back to run() for environments that only support
955
+ * run() (e.g. Cloudflare Workers).
956
+ *
957
+ * On runtimes without enterWith() we mutate the existing run() scope when one
958
+ * exists. This is what allows baggage/correlation updates to remain visible
959
+ * for the rest of the traced callback in Workers.
960
+ */
961
+ function enterOrRun(storage, value) {
962
+ const existingStore = storage.getStore();
963
+ if (existingStore) {
964
+ existingStore.value = value;
965
+ return;
966
+ }
967
+ const boxedValue = { value };
968
+ try {
969
+ storage.enterWith(boxedValue);
970
+ } catch {
971
+ storage.run(boxedValue, () => {});
972
+ }
973
+ }
974
+ function updateActiveContext(newContext) {
975
+ enterOrRun(contextStorage, newContext);
976
+ const manager = context._getContextManager?.();
977
+ if (!manager) return;
978
+ const asyncLocal = manager._asyncLocalStorage ?? void 0;
979
+ if (asyncLocal?.enterWith) {
980
+ asyncLocal.enterWith(newContext);
981
+ return;
982
+ }
983
+ if (isFunction(manager.with)) manager.with(newContext, () => {});
984
+ }
985
+ /**
986
+ * Create a TraceContext from an OpenTelemetry Span
987
+ *
988
+ * This utility extracts trace context information from a span
989
+ * and provides span manipulation methods and baggage operations in a consistent format.
990
+ *
991
+ * Note: Baggage methods always operate on the currently active context,
992
+ * which may differ from the context when createTraceContext was called.
993
+ */
994
+ function createTraceContext(span) {
995
+ const spanContext = span.spanContext();
996
+ if (!contextStorage.getStore()?.value) {
997
+ const activeContext = context.active();
998
+ enterOrRun(contextStorage, activeContext);
999
+ }
1000
+ const traceCtx = {
1001
+ traceId: spanContext.traceId,
1002
+ spanId: spanContext.spanId,
1003
+ correlationId: spanContext.traceId.slice(0, 16),
1004
+ setAttribute: span.setAttribute.bind(span),
1005
+ setAttributes: span.setAttributes.bind(span),
1006
+ setStatus: (status) => {
1007
+ spansWithExplicitStatus.add(span);
1008
+ span.setStatus(status);
1009
+ },
1010
+ recordException: span.recordException.bind(span),
1011
+ addEvent: span.addEvent.bind(span),
1012
+ addLink: span.addLink.bind(span),
1013
+ addLinks: span.addLinks.bind(span),
1014
+ updateName: span.updateName.bind(span),
1015
+ isRecording: span.isRecording.bind(span),
1016
+ recordError: (cause) => {
1017
+ const err = cause instanceof Error ? cause : new Error(String(cause));
1018
+ recordStructuredError(traceCtx, err);
1019
+ },
1020
+ track: (event, data) => {
1021
+ track(event, data);
1022
+ },
1023
+ getBaggage(key) {
1024
+ const activeCtx = context.active();
1025
+ let baggage = propagation.getBaggage(activeCtx);
1026
+ if (!baggage) {
1027
+ const storedContext = contextStorage.getStore()?.value;
1028
+ if (storedContext) baggage = propagation.getBaggage(storedContext);
1029
+ }
1030
+ return baggage?.getEntry(key)?.value;
1031
+ },
1032
+ setBaggage(key, value) {
1033
+ const currentContext = getActiveContextWithBaggage();
1034
+ const updated = (propagation.getBaggage(currentContext) ?? propagation.createBaggage()).setEntry(key, { value });
1035
+ updateActiveContext(propagation.setBaggage(currentContext, updated));
1036
+ return value;
1037
+ },
1038
+ deleteBaggage(key) {
1039
+ const currentContext = getActiveContextWithBaggage();
1040
+ const baggage = propagation.getBaggage(currentContext);
1041
+ if (baggage) {
1042
+ const updated = baggage.removeEntry(key);
1043
+ updateActiveContext(propagation.setBaggage(currentContext, updated));
692
1044
  }
693
1045
  },
694
1046
  getAllBaggage() {
@@ -818,820 +1170,1345 @@ function defineBaggageSchema(namespace) {
818
1170
  }
819
1171
 
820
1172
  //#endregion
821
- //#region src/correlation-id.ts
822
- /**
823
- * Correlation ID utilities for event-driven observability
824
- *
825
- * Provides a stable join key across events, logs, and spans even when traces fragment.
826
- * Format: 16 hex chars (64 bits), crypto-random, URL-safe.
827
- *
828
- * Lifecycle:
829
- * 1. Generated at boundary root (HTTP server span, message process span, cron job span)
830
- * 2. Reused within context (nested work shares it via AsyncLocalStorage)
831
- * 3. Propagated via baggage (optional, default OFF to avoid header bloat)
832
- *
833
- * @example Basic usage
834
- * ```typescript
835
- * import { generateCorrelationId, getCorrelationId } from 'autotel/correlation-id';
836
- *
837
- * // Generate a new correlation ID
838
- * const id = generateCorrelationId();
839
- * // Returns: 'a1b2c3d4e5f67890'
840
- *
841
- * // Get current correlation ID from context
842
- * const currentId = getCorrelationId();
843
- * ```
844
- */
845
- /**
846
- * AsyncLocalStorage for storing correlation ID
847
- * This allows correlation IDs to persist across async boundaries
848
- */
849
- const correlationStorage = new nodeAsyncHooks.AsyncLocalStorage();
1173
+ //#region src/sampling.ts
850
1174
  /**
851
- * Baggage key for correlation ID propagation
1175
+ * Tail sampling attribute keys (autotel-internal, not OTel semconv)
852
1176
  */
853
- const CORRELATION_ID_BAGGAGE_KEY = "autotel.correlation_id";
1177
+ const AUTOTEL_SAMPLING_TAIL_KEEP = "autotel.sampling.tail.keep";
1178
+ const AUTOTEL_SAMPLING_TAIL_EVALUATED = "autotel.sampling.tail.evaluated";
854
1179
  /**
855
- * Generate a new correlation ID
856
- *
857
- * Format: 16 hex chars (64 bits), crypto-random, URL-safe
858
- *
859
- * @returns A new correlation ID
1180
+ * How many events each kept event stands for, expressed as "1 in N".
860
1181
  *
861
- * @example
862
- * ```typescript
863
- * const id = generateCorrelationId();
864
- * // Returns: 'a1b2c3d4e5f67890'
865
- * ```
1182
+ * A query that counts sampled spans undercounts the population. Multiplying
1183
+ * each kept event by this rate restores the estimate. Autotel records the
1184
+ * attribute only when N exceeds 1, so fully captured spans stay clean.
866
1185
  */
867
- function generateCorrelationId() {
868
- const bytes = /* @__PURE__ */ new Uint8Array(8);
869
- crypto.getRandomValues(bytes);
870
- return [...bytes].map((b) => b.toString(16).padStart(2, "0")).join("");
871
- }
1186
+ const AUTOTEL_SAMPLING_RATE = "autotel.sampling.rate";
872
1187
  /**
873
- * Get the current correlation ID from context
874
- *
875
- * Resolution order:
876
- * 1. AsyncLocalStorage (from explicit setCorrelationId or runWithCorrelationId)
877
- * 2. Baggage (if propagated from upstream)
878
- * 3. Active span's trace ID (first 16 chars as fallback)
879
- * 4. undefined (if not in any context)
880
- *
881
- * @returns Current correlation ID or undefined
1188
+ * Spans an explicit `forceKeep()` claimed.
882
1189
  *
883
- * @example
884
- * ```typescript
885
- * const id = getCorrelationId();
886
- * if (id) {
887
- * console.log('Correlation ID:', id);
888
- * }
889
- * ```
1190
+ * The tracing wrapper writes the sampler's tail verdict once the body has
1191
+ * run, which is after any `forceKeep()` inside it. Without this the verdict
1192
+ * overwrites the override and the span the caller insisted on is dropped.
890
1193
  */
891
- function getCorrelationId() {
892
- const storedId = correlationStorage.getStore()?.value;
893
- if (storedId) return storedId;
894
- const activeContext = context.active();
895
- const baggageEntry = propagation.getBaggage(activeContext)?.getEntry(CORRELATION_ID_BAGGAGE_KEY);
896
- if (baggageEntry?.value) return baggageEntry.value;
897
- const span = trace.getActiveSpan();
898
- if (span) return span.spanContext().traceId.slice(0, 16);
899
- }
900
1194
  /**
901
- * Get or create a correlation ID
902
- *
903
- * If a correlation ID exists in the current context, returns it.
904
- * Otherwise, generates a new one.
905
- *
906
- * @returns Existing or new correlation ID
1195
+ * Baggage key that turns on full-fidelity capture for a request.
907
1196
  *
908
- * @example
909
- * ```typescript
910
- * const id = getOrCreateCorrelationId();
911
- * // Always returns a valid correlation ID
912
- * ```
1197
+ * Baggage arrives on the request and propagates, so a gateway, a proxy, a
1198
+ * feature flag or a curl can turn this on for one user and it follows them
1199
+ * across every service. Nobody deploys anything to debug a live problem.
913
1200
  */
914
- function getOrCreateCorrelationId() {
915
- return getCorrelationId() ?? generateCorrelationId();
1201
+ const AUTOTEL_DEBUG_BAGGAGE_KEY = "autotel.debug";
1202
+ const forceKeptSpans = /* @__PURE__ */ new WeakSet();
1203
+ function markForceKept(span) {
1204
+ forceKeptSpans.add(span);
1205
+ }
1206
+ function isForceKept(span) {
1207
+ return forceKeptSpans.has(span);
916
1208
  }
917
1209
  /**
918
- * Run a function with a specific correlation ID in context
919
- *
920
- * The correlation ID will be available via getCorrelationId() throughout
921
- * the execution of the function and any async operations it spawns.
922
- *
923
- * @param correlationId - Correlation ID to use
924
- * @param fn - Function to execute
925
- * @returns The return value of the function
1210
+ * Convert a keep probability into the "1 in N" rate reported on spans.
926
1211
  *
927
- * @example
928
- * ```typescript
929
- * await runWithCorrelationId('abc123', async () => {
930
- * // getCorrelationId() returns 'abc123' here
931
- * await processRequest();
932
- * });
933
- * ```
1212
+ * Keeping 10% of traces means each survivor stands for 10, so the two numbers
1213
+ * are reciprocals and easy to publish the wrong way round.
934
1214
  */
935
- function runWithCorrelationId(correlationId, fn) {
936
- return correlationStorage.run({ value: correlationId }, fn);
1215
+ function toSampleRate(probability) {
1216
+ return probability > 0 ? 1 / probability : 0;
937
1217
  }
938
1218
  /**
939
- * Set correlation ID in the current context (mutates context)
1219
+ * Map a string to a stable, evenly spread position in the unit interval.
940
1220
  *
941
- * Note: This updates the AsyncLocalStorage context. For proper scoping
942
- * across async boundaries, prefer runWithCorrelationId() instead.
1221
+ * Two processes that hash the same key reach the same number, which is what
1222
+ * lets independent services agree on one trace's sampling decision.
943
1223
  *
944
- * @param correlationId - Correlation ID to set
1224
+ * The spread matters as much as the stability. Real sampling keys share long
1225
+ * prefixes: `user_1000`, `user_1001`, `checkout-trace-0001`. A plain
1226
+ * multiply-and-add hash lets that shared prefix dominate the high bits, so a
1227
+ * whole key family lands in one narrow band and a rate of 0.1 keeps all of
1228
+ * them or none of them. FNV-1a followed by the murmur3 finalizer mixes the
1229
+ * low bits back through the word, so keys that differ in one character land
1230
+ * far apart.
1231
+ */
1232
+ function hashUnitInterval(value) {
1233
+ let hash = 2166136261;
1234
+ for (let i = 0; i < value.length; i++) {
1235
+ hash ^= value.codePointAt(i) ?? 0;
1236
+ hash = Math.imul(hash, 16777619);
1237
+ }
1238
+ hash ^= hash >>> 16;
1239
+ hash = Math.imul(hash, 2246822507);
1240
+ hash ^= hash >>> 13;
1241
+ hash = Math.imul(hash, 3266489909);
1242
+ hash ^= hash >>> 16;
1243
+ return (hash >>> 0) / 4294967296;
1244
+ }
1245
+ /**
1246
+ * Simple random sampler
945
1247
  *
946
1248
  * @example
947
1249
  * ```typescript
948
- * setCorrelationId('abc123');
949
- * // Now getCorrelationId() returns 'abc123'
1250
+ * new RandomSampler(0.1) // Sample 10% of requests
950
1251
  * ```
951
1252
  */
952
- function setCorrelationId(correlationId) {
953
- enterOrRun(correlationStorage, correlationId);
954
- }
1253
+ var RandomSampler = class {
1254
+ rate;
1255
+ constructor(rate) {
1256
+ this.rate = rate;
1257
+ if (rate < 0 || rate > 1) throw new Error("Sample rate must be between 0 and 1");
1258
+ }
1259
+ shouldSample(_context) {
1260
+ return Math.random() < this.rate;
1261
+ }
1262
+ sampleRate() {
1263
+ return toSampleRate(this.rate);
1264
+ }
1265
+ };
955
1266
  /**
956
- * Set correlation ID in baggage for propagation
1267
+ * Always sample (100% tracing)
1268
+ */
1269
+ var AlwaysSampler = class {
1270
+ shouldSample(_context) {
1271
+ return true;
1272
+ }
1273
+ };
1274
+ /**
1275
+ * Never sample (0% tracing)
1276
+ */
1277
+ var NeverSampler = class {
1278
+ shouldSample(_context) {
1279
+ return false;
1280
+ }
1281
+ };
1282
+ /**
1283
+ * Adaptive sampler that always traces errors and slow requests
957
1284
  *
958
- * This adds the correlation ID to the W3C baggage header, allowing it
959
- * to be propagated to downstream services.
1285
+ * This is the recommended sampler for production use.
1286
+ * It ensures you never miss critical issues while keeping costs down.
960
1287
  *
961
- * Note: Only use this when you explicitly want cross-service propagation.
962
- * Default is OFF to avoid header bloat.
1288
+ * Strategy:
1289
+ * - Always trace errors (critical for debugging)
1290
+ * - Always trace slow requests (performance issues)
1291
+ * - Use baseline sample rate for successful fast requests
963
1292
  *
964
- * @param correlationId - Correlation ID to propagate
965
- * @returns New context with baggage set
1293
+ * **IMPORTANT - Tail Sampling Requirement:**
1294
+ * This sampler uses tail sampling (makes decisions AFTER execution).
1295
+ * You MUST use TailSamplingSpanProcessor for it to work correctly:
1296
+ *
1297
+ * - If using initInstrumentation(): TailSamplingSpanProcessor is auto-configured
1298
+ * - If using custom TracerProvider: You MUST manually register TailSamplingSpanProcessor
1299
+ *
1300
+ * Without TailSamplingSpanProcessor, ALL spans are exported (defeating the cost savings).
1301
+ *
1302
+ * @see TailSamplingSpanProcessor
1303
+ * @see README.md "Tail Sampling with Custom Providers" section
966
1304
  *
967
1305
  * @example
968
1306
  * ```typescript
969
- * const newContext = setCorrelationIdInBaggage('abc123');
970
- * context.with(newContext, () => {
971
- * // Baggage will be propagated in outgoing requests
972
- * });
1307
+ * new AdaptiveSampler({
1308
+ * baselineSampleRate: 0.1, // 10% of normal requests
1309
+ * slowThresholdMs: 1000, // Requests > 1s are "slow"
1310
+ * alwaysSampleErrors: true, // Always trace errors
1311
+ * alwaysSampleSlow: true // Always trace slow requests
1312
+ * })
973
1313
  * ```
974
1314
  */
975
- function setCorrelationIdInBaggage(correlationId) {
976
- const activeContext = context.active();
977
- let baggage = propagation.getBaggage(activeContext) ?? propagation.createBaggage();
978
- baggage = baggage.setEntry(CORRELATION_ID_BAGGAGE_KEY, { value: correlationId });
979
- return propagation.setBaggage(activeContext, baggage);
980
- }
981
- /**
982
- * Get the correlation storage instance (for internal use in init/shutdown)
983
- */
984
- function getCorrelationStorage() {
985
- return correlationStorage;
986
- }
987
-
988
- //#endregion
989
- //#region src/event-queue.ts
990
- const DEFAULT_CONFIG$2 = {
991
- maxSize: 5e4,
992
- batchSize: 100,
993
- flushInterval: 1e4,
994
- maxRetries: 3,
995
- rateLimit: {
996
- maxEventsPerSecond: 100,
997
- burstCapacity: 200
1315
+ var AdaptiveSampler = class {
1316
+ baselineSampleRate;
1317
+ slowThresholdMs;
1318
+ alwaysSampleErrors;
1319
+ alwaysSampleSlow;
1320
+ linksBased;
1321
+ linksRate;
1322
+ logger;
1323
+ samplingDecisions = /* @__PURE__ */ new WeakMap();
1324
+ operationResults = /* @__PURE__ */ new WeakMap();
1325
+ constructor(options = {}) {
1326
+ this.baselineSampleRate = options.baselineSampleRate ?? .1;
1327
+ this.slowThresholdMs = options.slowThresholdMs ?? 1e3;
1328
+ this.alwaysSampleErrors = options.alwaysSampleErrors ?? true;
1329
+ this.alwaysSampleSlow = options.alwaysSampleSlow ?? true;
1330
+ this.linksBased = options.linksBased ?? false;
1331
+ this.linksRate = options.linksRate ?? 1;
1332
+ this.logger = options.logger;
1333
+ if (this.baselineSampleRate < 0 || this.baselineSampleRate > 1) throw new Error("Baseline sample rate must be between 0 and 1");
1334
+ if (this.linksRate < 0 || this.linksRate > 1) throw new Error("Links rate must be between 0 and 1");
1335
+ }
1336
+ needsTailSampling() {
1337
+ return true;
1338
+ }
1339
+ shouldSample(context) {
1340
+ const baselineDecision = Math.random() < this.baselineSampleRate;
1341
+ this.samplingDecisions.set(context.args, baselineDecision);
1342
+ return true;
1343
+ }
1344
+ /**
1345
+ * Check if any links point to sampled spans.
1346
+ *
1347
+ * A span is considered linked to a sampled span if any of its links
1348
+ * have trace_flags with the sampled bit set (0x01).
1349
+ *
1350
+ * @param links - Array of span links to check
1351
+ * @returns true if any linked span is sampled, false otherwise
1352
+ */
1353
+ hasSampledLink(links) {
1354
+ if (!links || links.length === 0) return false;
1355
+ return links.some((link) => link.context && (link.context.traceFlags & TraceFlags.SAMPLED) !== 0);
1356
+ }
1357
+ /**
1358
+ * Re-evaluate sampling decision after operation completes
1359
+ *
1360
+ * This allows us to always capture errors and slow requests,
1361
+ * even if they weren't initially sampled.
1362
+ *
1363
+ * @param context - Sampling context
1364
+ * @param result - Operation result
1365
+ * @returns true if this operation should be kept (not discarded)
1366
+ */
1367
+ shouldKeepTrace(context, result) {
1368
+ const baselineDecision = this.samplingDecisions.get(context.args) ?? false;
1369
+ if (this.alwaysSampleErrors && !result.success) {
1370
+ if (!baselineDecision) this.logger?.debug({
1371
+ operation: context.operationName,
1372
+ error: result.error?.message
1373
+ }, "Adaptive sampling: Keeping error trace");
1374
+ return true;
1375
+ }
1376
+ if (this.alwaysSampleSlow && result.duration >= this.slowThresholdMs) {
1377
+ if (!baselineDecision) this.logger?.debug({
1378
+ operation: context.operationName,
1379
+ duration: result.duration
1380
+ }, "Adaptive sampling: Keeping slow trace");
1381
+ return true;
1382
+ }
1383
+ if (this.linksBased && context.links && this.hasSampledLink(context.links)) {
1384
+ const keepLinked = Math.random() < this.linksRate;
1385
+ if (keepLinked && !baselineDecision) this.logger?.debug({
1386
+ operation: context.operationName,
1387
+ linkCount: context.links.length
1388
+ }, "Adaptive sampling: Keeping trace due to sampled link");
1389
+ return keepLinked;
1390
+ }
1391
+ return baselineDecision;
998
1392
  }
999
1393
  };
1000
1394
  /**
1001
- * Get subscriber name for metrics (stable, low-cardinality)
1395
+ * User-based sampler for consistent tracing
1002
1396
  *
1003
- * Priority:
1004
- * 1. Explicit config: subscriber.name
1005
- * 2. Class static property (if available)
1006
- * 3. Fallback: lowercase class name without "Subscriber" suffix
1007
- */
1008
- function getSubscriberName(subscriber) {
1009
- if (subscriber.name) return subscriber.name.toLowerCase();
1010
- return (subscriber.constructor?.name || "unknown").replace(/Subscriber$/i, "").toLowerCase();
1011
- }
1012
- /**
1013
- * Subscribers whose `shutdown()` has run. Terminal for the client they wrap, so
1014
- * a queue rebuilt from the same config must not reuse them.
1015
- */
1016
- const shutDownSubscribers = /* @__PURE__ */ new WeakSet();
1017
- /**
1018
- * Events queue with batching and backpressure
1397
+ * Always samples requests from specific user IDs.
1398
+ * Useful for debugging specific user issues or monitoring VIP users.
1019
1399
  *
1020
- * Features:
1021
- * - Batches events for efficient sending
1022
- * - Bounded queue with drop-oldest policy (prod) or blocking (dev)
1023
- * - Exponential backoff retry
1024
- * - Rate limiting to prevent overwhelming subscribers
1025
- * - Graceful flush on shutdown
1400
+ * @example
1401
+ * ```typescript
1402
+ * new UserIdSampler({
1403
+ * baselineSampleRate: 0.01, // 1% of normal users
1404
+ * alwaysSampleUsers: ['vip_123'], // Always trace VIP users
1405
+ * extractUserId: (args) => args[0]?.userId // Extract user ID from first arg
1406
+ * })
1407
+ * ```
1026
1408
  */
1027
- var EventQueue = class {
1028
- queue = [];
1029
- flushTimer = null;
1030
- config;
1031
- subscribers;
1032
- rateLimiter;
1033
- flushPromise = null;
1034
- isShuttingDown = false;
1035
- metrics = null;
1036
- observableCleanups = [];
1037
- subscriberHealthy = /* @__PURE__ */ new Map();
1038
- constructor(subscribers, config) {
1039
- const live = subscribers.filter((s) => !shutDownSubscribers.has(s));
1040
- if (live.length < subscribers.length) getLogger().warn({ subscribers: subscribers.filter((s) => shutDownSubscribers.has(s)).map((s) => getSubscriberName(s)) }, "[autotel] Subscriber already shut down, events will not be delivered; pass new subscriber instances to init()");
1041
- this.subscribers = live;
1042
- this.config = {
1043
- ...DEFAULT_CONFIG$2,
1044
- ...config
1045
- };
1046
- this.rateLimiter = this.config.rateLimit ? new TokenBucketRateLimiter(this.config.rateLimit) : null;
1047
- for (const subscriber of this.subscribers) {
1048
- const name = getSubscriberName(subscriber);
1049
- this.subscriberHealthy.set(name, true);
1409
+ var UserIdSampler = class {
1410
+ baselineSampleRate;
1411
+ alwaysSampleUsers;
1412
+ extractUserId;
1413
+ logger;
1414
+ constructor(options) {
1415
+ this.baselineSampleRate = options.baselineSampleRate ?? .1;
1416
+ this.alwaysSampleUsers = new Set(options.alwaysSampleUsers || []);
1417
+ this.extractUserId = options.extractUserId;
1418
+ this.logger = options.logger;
1419
+ }
1420
+ shouldSample(context) {
1421
+ const userId = this.extractUserId(context.args);
1422
+ if (userId && this.alwaysSampleUsers.has(userId)) {
1423
+ this.logger?.debug({
1424
+ operation: context.operationName,
1425
+ userId
1426
+ }, "Sampling user request");
1427
+ return true;
1050
1428
  }
1051
- this.initMetrics();
1429
+ if (userId) return this.hashString(userId) < this.baselineSampleRate;
1430
+ return Math.random() < this.baselineSampleRate;
1052
1431
  }
1053
1432
  /**
1054
- * Initialize OTel metrics for queue observability
1433
+ * Add user IDs to always-sample list
1055
1434
  */
1056
- initMetrics() {
1057
- const meter = getConfig$1().meter;
1058
- const queueSize = meter.createObservableGauge("autotel.event_delivery.queue.size", {
1059
- description: "Current number of events in the delivery queue",
1060
- unit: "count"
1061
- });
1062
- const queueSizeCallback = (observableResult) => {
1063
- observableResult.observe(this.queue.length);
1064
- };
1065
- queueSize.addCallback(queueSizeCallback);
1066
- this.observableCleanups.push(() => queueSize.removeCallback(queueSizeCallback));
1067
- const oldestAge = meter.createObservableGauge("autotel.event_delivery.queue.oldest_age_ms", {
1068
- description: "Age of the oldest event in the queue in milliseconds",
1069
- unit: "ms"
1070
- });
1071
- const oldestAgeCallback = (observableResult) => {
1072
- if (this.queue.length > 0) {
1073
- const oldest = this.queue[0];
1074
- const ageMs = Date.now() - oldest.timestamp;
1075
- observableResult.observe(ageMs);
1076
- } else observableResult.observe(0);
1077
- };
1078
- oldestAge.addCallback(oldestAgeCallback);
1079
- this.observableCleanups.push(() => oldestAge.removeCallback(oldestAgeCallback));
1080
- const delivered = meter.createCounter("autotel.event_delivery.queue.delivered", {
1081
- description: "Number of events successfully delivered to subscribers",
1082
- unit: "count"
1083
- });
1084
- const failed = meter.createCounter("autotel.event_delivery.queue.failed", {
1085
- description: "Number of events that failed delivery after all retry attempts",
1086
- unit: "count"
1087
- });
1088
- const dropped = meter.createCounter("autotel.event_delivery.queue.dropped", {
1089
- description: "Number of events dropped from the queue",
1090
- unit: "count"
1091
- });
1092
- const latency = meter.createHistogram("autotel.event_delivery.queue.latency_ms", {
1093
- description: "Event delivery latency from enqueue to successful send",
1094
- unit: "ms"
1095
- });
1096
- const subscriberHealth = meter.createObservableGauge("autotel.event_delivery.subscriber.health", {
1097
- description: "Subscriber health status (1=healthy, 0=unhealthy)",
1098
- unit: "1"
1099
- });
1100
- const subscriberHealthCallback = (observableResult) => {
1101
- for (const [subscriberName, isHealthy] of this.subscriberHealthy) observableResult.observe(isHealthy ? 1 : 0, { subscriber: subscriberName });
1102
- };
1103
- subscriberHealth.addCallback(subscriberHealthCallback);
1104
- this.observableCleanups.push(() => subscriberHealth.removeCallback(subscriberHealthCallback));
1105
- this.metrics = {
1106
- queueSize,
1107
- oldestAge,
1108
- delivered,
1109
- failed,
1110
- dropped,
1111
- latency,
1112
- subscriberHealth
1113
- };
1435
+ addAlwaysSampleUsers(...userIds) {
1436
+ for (const userId of userIds) this.alwaysSampleUsers.add(userId);
1114
1437
  }
1115
1438
  /**
1116
- * Record a dropped event with reason and emit debug breadcrumb
1439
+ * Remove user IDs from always-sample list
1117
1440
  */
1118
- recordDropped(reason, event, subscriberName) {
1119
- const attrs = { reason };
1120
- if (subscriberName) attrs.subscriber = subscriberName;
1121
- this.metrics?.dropped.add(1, attrs);
1122
- const logLevel = reason === "payload_invalid" ? "error" : "warn";
1123
- const logger = getLogger();
1124
- if (logLevel === "error") logger.error({
1125
- eventName: event?.name,
1126
- subscriber: subscriberName,
1127
- reason,
1128
- correlationId: event?._correlationId,
1129
- traceId: event?._traceId
1130
- }, `[autotel] Event dropped: ${reason}`);
1131
- else logger.warn({
1132
- eventName: event?.name,
1133
- subscriber: subscriberName,
1134
- reason,
1135
- correlationId: event?._correlationId,
1136
- traceId: event?._traceId
1137
- }, `[autotel] Event dropped: ${reason}`);
1441
+ removeAlwaysSampleUsers(...userIds) {
1442
+ for (const userId of userIds) this.alwaysSampleUsers.delete(userId);
1443
+ }
1444
+ /**
1445
+ * Simple hash function for consistent user sampling
1446
+ */
1447
+ hashString(str) {
1448
+ return hashUnitInterval(str);
1449
+ }
1450
+ };
1451
+ /**
1452
+ * Consistent sampler: every service reaches the same verdict for one trace.
1453
+ *
1454
+ * `RandomSampler` rolls the dice per process, so an upstream service can keep
1455
+ * a trace that its downstream drops, leaving a waterfall with holes in it.
1456
+ * Hashing a key that travels with the request removes the disagreement. Pass
1457
+ * the trace id, or any identifier every hop already shares.
1458
+ *
1459
+ * @example
1460
+ * ```typescript
1461
+ * new DeterministicSampler({
1462
+ * sampleRate: 0.1,
1463
+ * key: (context) => trace.getActiveSpan()?.spanContext().traceId,
1464
+ * })
1465
+ * ```
1466
+ */
1467
+ var DeterministicSampler = class {
1468
+ rate;
1469
+ key;
1470
+ constructor(options) {
1471
+ if (options.sampleRate < 0 || options.sampleRate > 1) throw new Error("Sample rate must be between 0 and 1");
1472
+ this.rate = options.sampleRate;
1473
+ this.key = options.key;
1474
+ }
1475
+ shouldSample(context) {
1476
+ const key = this.key(context);
1477
+ if (key === void 0) return Math.random() < this.rate;
1478
+ return hashUnitInterval(key) < this.rate;
1479
+ }
1480
+ sampleRate() {
1481
+ return toSampleRate(this.rate);
1482
+ }
1483
+ };
1484
+ /** Bucket for keys seen after the tracked map fills up. */
1485
+ const OVERFLOW_KEY = "__overflow__";
1486
+ /**
1487
+ * Per-key target-rate sampler for workloads with uneven traffic.
1488
+ *
1489
+ * A single rate serves a skewed workload badly: 1% floods storage with the
1490
+ * busiest endpoint and still loses the rare tenant whose failures you need.
1491
+ * This sampler counts traffic per key over a rolling window, then sets each
1492
+ * key its own rate so every key contributes roughly `targetPerKey` events.
1493
+ * Quiet keys survive intact; loud keys get thinned.
1494
+ *
1495
+ * The first window keeps everything, because no traffic history exists yet.
1496
+ * Rates take effect from the second window onward.
1497
+ *
1498
+ * @example
1499
+ * ```typescript
1500
+ * new KeyTargetRateSampler({
1501
+ * key: (context) => context.operationName,
1502
+ * targetPerKey: 10, // ~10 events per key per window
1503
+ * windowMs: 30_000,
1504
+ * })
1505
+ * ```
1506
+ */
1507
+ var KeyTargetRateSampler = class {
1508
+ key;
1509
+ targetPerKey;
1510
+ windowMs;
1511
+ maxKeys;
1512
+ counts = /* @__PURE__ */ new Map();
1513
+ rates = /* @__PURE__ */ new Map();
1514
+ windowStart = Date.now();
1515
+ constructor(options) {
1516
+ this.key = options.key;
1517
+ this.targetPerKey = options.targetPerKey ?? 10;
1518
+ this.windowMs = options.windowMs ?? 3e4;
1519
+ this.maxKeys = options.maxKeys ?? 1e3;
1520
+ if (this.targetPerKey <= 0) throw new Error("Target per key must be greater than 0");
1521
+ if (this.windowMs <= 0) throw new Error("Window must be greater than 0");
1522
+ }
1523
+ /** Turn the window's observed counts into the next window's rates. */
1524
+ roll(now) {
1525
+ if (now - this.windowStart < this.windowMs) return;
1526
+ const rates = /* @__PURE__ */ new Map();
1527
+ for (const [key, count] of this.counts) rates.set(key, Math.max(1, count / this.targetPerKey));
1528
+ this.rates = rates;
1529
+ this.counts = /* @__PURE__ */ new Map();
1530
+ this.windowStart = now;
1138
1531
  }
1139
1532
  /**
1140
- * Record permanent delivery failure (after all retries exhausted)
1141
- * Increments failed counter and logs error
1533
+ * Resolve the key, collapsing into one bucket once the map is full.
1534
+ *
1535
+ * An unbounded key function would otherwise grow the map without limit,
1536
+ * which turns a sampler meant to cut cost into a memory leak.
1142
1537
  */
1143
- recordFailed(event, subscriberName, error) {
1144
- this.metrics?.failed.add(1, { subscriber: subscriberName });
1145
- this.subscriberHealthy.set(subscriberName, false);
1146
- getLogger().error({
1147
- eventName: event.name,
1148
- subscriber: subscriberName,
1149
- correlationId: event._correlationId,
1150
- traceId: event._traceId,
1151
- err: error
1152
- }, `[autotel] Event delivery failed after all retries`);
1538
+ resolveKey(context) {
1539
+ const key = this.key(context) ?? OVERFLOW_KEY;
1540
+ if (this.counts.has(key) || this.counts.size < this.maxKeys) return key;
1541
+ return OVERFLOW_KEY;
1542
+ }
1543
+ shouldSample(context) {
1544
+ this.roll(Date.now());
1545
+ const key = this.resolveKey(context);
1546
+ this.counts.set(key, (this.counts.get(key) ?? 0) + 1);
1547
+ const rate = this.rates.get(key) ?? 1;
1548
+ return rate <= 1 || Math.random() < 1 / rate;
1549
+ }
1550
+ sampleRate(context) {
1551
+ return this.rates.get(this.resolveKey(context)) ?? 1;
1552
+ }
1553
+ };
1554
+ /**
1555
+ * Composite sampler that combines multiple samplers
1556
+ *
1557
+ * Samples if ANY of the child samplers returns true.
1558
+ *
1559
+ * @example
1560
+ * ```typescript
1561
+ * new CompositeSampler([
1562
+ * new UserIdSampler({ extractUserId: (args) => args[0]?.userId }),
1563
+ * new AdaptiveSampler({ baselineSampleRate: 0.1 })
1564
+ * ])
1565
+ * ```
1566
+ */
1567
+ var CompositeSampler = class {
1568
+ samplers;
1569
+ constructor(samplers) {
1570
+ this.samplers = samplers;
1571
+ if (samplers.length === 0) throw new Error("CompositeSampler requires at least one child sampler");
1572
+ }
1573
+ shouldSample(context) {
1574
+ return this.samplers.some((sampler) => sampler.shouldSample(context));
1575
+ }
1576
+ };
1577
+ /**
1578
+ * Feature flag sampler
1579
+ *
1580
+ * Always samples requests with specific feature flags enabled.
1581
+ * Perfect for correlating A/B test experiments with metrics.
1582
+ *
1583
+ * @example
1584
+ * ```typescript
1585
+ * new FeatureFlagSampler({
1586
+ * baselineSampleRate: 0.01,
1587
+ * alwaysSampleFlags: ['new_checkout', 'experimental_ui'],
1588
+ * extractFlags: (args, metadata) => metadata?.featureFlags
1589
+ * })
1590
+ * ```
1591
+ */
1592
+ var FeatureFlagSampler = class {
1593
+ baselineSampleRate;
1594
+ alwaysSampleFlags;
1595
+ extractFlags;
1596
+ logger;
1597
+ constructor(options) {
1598
+ this.baselineSampleRate = options.baselineSampleRate ?? .1;
1599
+ this.alwaysSampleFlags = new Set(options.alwaysSampleFlags || []);
1600
+ this.extractFlags = options.extractFlags;
1601
+ this.logger = options.logger;
1602
+ }
1603
+ shouldSample(context) {
1604
+ const flags = this.extractFlags(context.args, context.metadata);
1605
+ if (flags && flags.some((flag) => this.alwaysSampleFlags.has(flag))) {
1606
+ this.logger?.debug({
1607
+ operation: context.operationName,
1608
+ flags
1609
+ }, "Sampling feature flag request");
1610
+ return true;
1611
+ }
1612
+ return Math.random() < this.baselineSampleRate;
1153
1613
  }
1154
1614
  /**
1155
- * Mark subscriber as unhealthy on transient failure (without incrementing failed counter)
1156
- * Used during retry attempts - only recordFailed should increment the counter
1615
+ * Add feature flags to always-sample list
1157
1616
  */
1158
- markSubscriberUnhealthy(subscriberName) {
1159
- this.subscriberHealthy.set(subscriberName, false);
1617
+ addAlwaysSampleFlags(...flags) {
1618
+ for (const flag of flags) this.alwaysSampleFlags.add(flag);
1160
1619
  }
1161
1620
  /**
1162
- * Record successful delivery
1621
+ * Remove feature flags from always-sample list
1163
1622
  */
1164
- recordDelivered(event, subscriberName, startTime) {
1165
- const latencyMs = Date.now() - startTime;
1166
- this.metrics?.delivered.add(1, { subscriber: subscriberName });
1167
- this.metrics?.latency.record(latencyMs, { subscriber: subscriberName });
1168
- this.subscriberHealthy.set(subscriberName, true);
1623
+ removeAlwaysSampleFlags(...flags) {
1624
+ for (const flag of flags) this.alwaysSampleFlags.delete(flag);
1169
1625
  }
1626
+ };
1627
+ /**
1628
+ * Sampling preset factories.
1629
+ *
1630
+ * For most users, the string shorthand on `init()` is simpler:
1631
+ * ```typescript
1632
+ * init({ service: 'my-app', sampling: 'production' })
1633
+ * ```
1634
+ *
1635
+ * Use factories when you need to customize:
1636
+ * ```typescript
1637
+ * init({ service: 'my-app', sampler: samplingPresets.production({ baselineSampleRate: 0.05 }) })
1638
+ * ```
1639
+ */
1640
+ const samplingPresets = {
1641
+ /** Capture everything — best for local development and debugging */
1642
+ development: () => new AlwaysSampler(),
1643
+ /** Only bad outcomes — zero baseline, errors always kept */
1644
+ errorsOnly: () => new AdaptiveSampler({
1645
+ baselineSampleRate: 0,
1646
+ alwaysSampleErrors: true
1647
+ }),
1170
1648
  /**
1171
- * Enqueue an event for sending
1172
- *
1173
- * Backpressure policy:
1174
- * - Drops oldest event and logs warning if queue is full (same behavior in all environments)
1649
+ * Balanced production defaults — 10% baseline + errors + slow traces.
1650
+ * Pass overrides to tune (uses the same option names as AdaptiveSampler).
1175
1651
  */
1176
- enqueue(event) {
1177
- if (this.isShuttingDown) {
1178
- this.recordDropped("shutdown", event);
1179
- return;
1652
+ production: (overrides) => new AdaptiveSampler({
1653
+ baselineSampleRate: .1,
1654
+ alwaysSampleErrors: true,
1655
+ alwaysSampleSlow: true,
1656
+ slowThresholdMs: 1e3,
1657
+ ...overrides
1658
+ }),
1659
+ /** Disable sampling entirely */
1660
+ off: () => new NeverSampler()
1661
+ };
1662
+ /**
1663
+ * Resolve a preset string to a Sampler instance.
1664
+ * Used internally by `init()` when `sampling` string is provided.
1665
+ *
1666
+ * @throws Error if preset is not recognized
1667
+ */
1668
+ function resolveSamplingPreset(preset) {
1669
+ switch (preset) {
1670
+ case "development": return samplingPresets.development();
1671
+ case "errors-only": return samplingPresets.errorsOnly();
1672
+ case "production": return samplingPresets.production();
1673
+ case "off": return samplingPresets.off();
1674
+ default: throw new Error(`Unknown sampling preset: "${preset}". Valid presets: development, errors-only, production, off`);
1675
+ }
1676
+ }
1677
+ /**
1678
+ * Create a Link from W3C trace context headers (e.g., from a message queue).
1679
+ *
1680
+ * This is useful for message consumers that need to link to the producer span.
1681
+ * The headers should contain at least a `traceparent` header in W3C format.
1682
+ *
1683
+ * @param headers - Dictionary containing traceparent/tracestate headers
1684
+ * @param attributes - Optional attributes for the link
1685
+ * @returns Link object if context could be extracted, null otherwise
1686
+ *
1687
+ * @example
1688
+ * ```typescript
1689
+ * // In a Kafka consumer
1690
+ * const headers = { traceparent: '00-abc123...-def456...-01' };
1691
+ * const link = createLinkFromHeaders(headers);
1692
+ * if (link) {
1693
+ * // Use with tracer.startActiveSpan options or ctx.addLink()
1694
+ * tracer.startActiveSpan('process.message', { links: [link] }, span => { ... });
1695
+ * }
1696
+ * ```
1697
+ */
1698
+ function createLinkFromHeaders(headers, attributes) {
1699
+ const traceparent = headers.traceparent || headers["traceparent"];
1700
+ if (!traceparent) return null;
1701
+ const spanContext = parseTraceparent(traceparent);
1702
+ if (!spanContext || !isValidSpanContext(spanContext)) return null;
1703
+ return {
1704
+ context: spanContext,
1705
+ attributes: attributes ?? {}
1706
+ };
1707
+ }
1708
+ /**
1709
+ * Extract Links from a batch of messages for fan-in scenarios.
1710
+ *
1711
+ * Useful for batch processing where multiple producer spans should be linked.
1712
+ * This enables tracing causality in event-driven architectures where a single
1713
+ * consumer processes messages from multiple producers.
1714
+ *
1715
+ * @param messages - List of message objects
1716
+ * @param headersKey - Key in each message containing trace headers (default: 'headers')
1717
+ * @returns List of Link objects for all valid trace contexts
1718
+ *
1719
+ * @example
1720
+ * ```typescript
1721
+ * // Processing a batch of SQS/Kafka messages
1722
+ * const messages = [
1723
+ * { body: '...', headers: { traceparent: '...' } },
1724
+ * { body: '...', headers: { traceparent: '...' } },
1725
+ * ];
1726
+ * const links = extractLinksFromBatch(messages);
1727
+ *
1728
+ * tracer.startActiveSpan('process.batch', { links }, span => {
1729
+ * for (const msg of messages) {
1730
+ * processMessage(msg);
1731
+ * }
1732
+ * });
1733
+ * ```
1734
+ */
1735
+ function extractLinksFromBatch(messages, headersKey = "headers") {
1736
+ const links = [];
1737
+ for (const msg of messages) {
1738
+ const msgHeaders = asStringRecord(msg[headersKey]);
1739
+ if (msgHeaders) {
1740
+ const link = createLinkFromHeaders(msgHeaders, { "messaging.batch.message_index": links.length });
1741
+ if (link) links.push(link);
1180
1742
  }
1181
- if (this.queue.length >= this.config.maxSize) {
1182
- const droppedEvent = this.queue.shift();
1183
- this.recordDropped("rate_limit", droppedEvent);
1184
- 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.`);
1743
+ }
1744
+ return links;
1745
+ }
1746
+ /**
1747
+ * Parse W3C traceparent header into SpanContext
1748
+ * Format: version-traceId-spanId-traceFlags (e.g., 00-abc123...-def456...-01)
1749
+ *
1750
+ * @see https://www.w3.org/TR/trace-context/#traceparent-header
1751
+ */
1752
+ function parseTraceparent(traceparent) {
1753
+ const match = traceparent.match(/^([0-9a-f]{2})-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$/i);
1754
+ if (!match || match.length < 5) return null;
1755
+ const version = match[1];
1756
+ const traceId = match[2];
1757
+ const spanId = match[3];
1758
+ const flags = match[4];
1759
+ if (!version || !traceId || !spanId || !flags) return null;
1760
+ if (version === "ff") return null;
1761
+ return {
1762
+ traceId,
1763
+ spanId,
1764
+ traceFlags: Number.parseInt(flags, 16),
1765
+ isRemote: true
1766
+ };
1767
+ }
1768
+ /**
1769
+ * Check if a SpanContext is valid (has non-zero trace and span IDs)
1770
+ */
1771
+ function isValidSpanContext(spanContext) {
1772
+ if (!spanContext) return false;
1773
+ return spanContext.traceId !== "00000000000000000000000000000000" && spanContext.spanId !== "0000000000000000";
1774
+ }
1775
+ /** True when the request in flight asked for full-fidelity capture. */
1776
+ function debugCaptureRequested() {
1777
+ const entry = propagation.getBaggage(getActiveContextWithBaggage())?.getEntry(AUTOTEL_DEBUG_BAGGAGE_KEY);
1778
+ return entry !== void 0 && entry.value !== "" && entry.value !== "0";
1779
+ }
1780
+
1781
+ //#endregion
1782
+ //#region src/init-logger.ts
1783
+ const LOG_LEVELS = {
1784
+ debug: 0,
1785
+ info: 1,
1786
+ warn: 2,
1787
+ error: 3
1788
+ };
1789
+ /** Silent logger used until and unless the application opts into diagnostics. */
1790
+ const silentLogger = {
1791
+ info: () => {},
1792
+ warn: () => {},
1793
+ error: () => {},
1794
+ debug: () => {}
1795
+ };
1796
+ /** Apply the resolved silence and minimum-level settings to a logger. */
1797
+ function wrapLogger(base, silent, minLevel) {
1798
+ if (silent) return silentLogger;
1799
+ const threshold = LOG_LEVELS[minLevel];
1800
+ const wrap = (fn, level) => {
1801
+ if (LOG_LEVELS[level] < threshold) return (() => {});
1802
+ return ((...args) => fn(...args));
1803
+ };
1804
+ return {
1805
+ debug: wrap(base.debug, "debug"),
1806
+ info: wrap(base.info, "info"),
1807
+ warn: wrap(base.warn, "warn"),
1808
+ error: wrap(base.error, "error")
1809
+ };
1810
+ }
1811
+
1812
+ //#endregion
1813
+ //#region src/posthog-logs.ts
1814
+ var RedactingLogRecordProcessor = class {
1815
+ wrapped;
1816
+ redact;
1817
+ constructor(wrapped, redact) {
1818
+ this.wrapped = wrapped;
1819
+ this.redact = redact;
1820
+ }
1821
+ onEmit(logRecord, context) {
1822
+ const body = asString(logRecord.body);
1823
+ if (body) logRecord.body = this.redact(body);
1824
+ if (logRecord.attributes) for (const [key, value] of Object.entries(logRecord.attributes)) {
1825
+ const text = asString(value);
1826
+ if (text !== void 0) logRecord.attributes[key] = this.redact(text);
1827
+ else if (Array.isArray(value)) logRecord.attributes[key] = value.map((item) => {
1828
+ const entry = asString(item);
1829
+ return entry === void 0 ? item : this.redact(entry);
1830
+ });
1185
1831
  }
1186
- const enrichedEvent = {
1187
- ...event,
1188
- _correlationId: event._correlationId || getOrCreateCorrelationId()
1189
- };
1190
- this.queue.push(enrichedEvent);
1191
- this.scheduleBatchFlush();
1832
+ this.wrapped.onEmit(logRecord, context);
1192
1833
  }
1193
- /**
1194
- * Schedule a batch flush if not already scheduled
1195
- */
1196
- scheduleBatchFlush() {
1197
- if (this.flushTimer || this.flushPromise) return;
1198
- this.flushTimer = setTimeout(() => {
1199
- this.flushTimer = null;
1200
- this.flushBatch();
1201
- }, this.config.flushInterval);
1834
+ shutdown() {
1835
+ return this.wrapped.shutdown();
1836
+ }
1837
+ forceFlush() {
1838
+ return this.wrapped.forceFlush();
1839
+ }
1840
+ };
1841
+ /**
1842
+ * Build log record processors for PostHog OTLP logs integration.
1843
+ *
1844
+ * Resolution order:
1845
+ * 1. config.url if provided
1846
+ * 2. POSTHOG_LOGS_URL env var
1847
+ * 3. Empty array (disabled)
1848
+ */
1849
+ function buildPostHogLogProcessors(config, stringRedactor) {
1850
+ const url = config?.url || process.env.POSTHOG_LOGS_URL;
1851
+ if (!url) return [];
1852
+ const sdkLogs = safeRequire("@opentelemetry/sdk-logs");
1853
+ const exporterModule = safeRequire("@opentelemetry/exporter-logs-otlp-http");
1854
+ if (!sdkLogs || !exporterModule) return [];
1855
+ const exporter = new exporterModule.OTLPLogExporter({ url });
1856
+ let processor = new sdkLogs.BatchLogRecordProcessor({ exporter });
1857
+ if (stringRedactor) processor = new RedactingLogRecordProcessor(processor, stringRedactor);
1858
+ return [processor];
1859
+ }
1860
+
1861
+ //#endregion
1862
+ //#region src/tail-sampling-processor.ts
1863
+ var TailSamplingSpanProcessor = class {
1864
+ wrappedProcessor;
1865
+ constructor(wrappedProcessor) {
1866
+ this.wrappedProcessor = wrappedProcessor;
1867
+ }
1868
+ onStart(span, parentContext) {
1869
+ this.wrappedProcessor.onStart(span, parentContext);
1870
+ }
1871
+ onEnd(span) {
1872
+ const tailEvaluated = span.attributes[AUTOTEL_SAMPLING_TAIL_EVALUATED];
1873
+ const shouldKeep = span.attributes[AUTOTEL_SAMPLING_TAIL_KEEP];
1874
+ if (tailEvaluated === true && shouldKeep === false) return;
1875
+ this.wrappedProcessor.onEnd(span);
1876
+ }
1877
+ forceFlush() {
1878
+ return this.wrappedProcessor.forceFlush();
1879
+ }
1880
+ shutdown() {
1881
+ return this.wrappedProcessor.shutdown();
1882
+ }
1883
+ };
1884
+
1885
+ //#endregion
1886
+ //#region src/baggage-span-processor.ts
1887
+ /**
1888
+ * Span processor that automatically copies baggage entries to span attributes
1889
+ *
1890
+ * This makes baggage visible in trace UIs (Jaeger, Grafana, DataDog, etc.)
1891
+ * without manually calling ctx.setAttribute() for each baggage entry.
1892
+ *
1893
+ * @example Enable in init()
1894
+ * ```typescript
1895
+ * init({
1896
+ * service: 'my-app',
1897
+ * baggage: true // Uses default 'baggage.' prefix
1898
+ * });
1899
+ *
1900
+ * // Now baggage automatically appears as span attributes
1901
+ * await withBaggage({
1902
+ * baggage: { 'tenant.id': 't1', 'user.id': 'u1' },
1903
+ * fn: async () => {
1904
+ * // Span has baggage.tenant.id and baggage.user.id attributes!
1905
+ * }
1906
+ * });
1907
+ * ```
1908
+ *
1909
+ * @example Custom prefix
1910
+ * ```typescript
1911
+ * init({
1912
+ * service: 'my-app',
1913
+ * baggage: 'ctx' // Uses 'ctx.' prefix
1914
+ * });
1915
+ * // Creates attributes: ctx.tenant.id, ctx.user.id
1916
+ * ```
1917
+ */
1918
+ var BaggageSpanProcessor = class {
1919
+ prefix;
1920
+ constructor(options = {}) {
1921
+ this.prefix = options.prefix ?? "baggage.";
1202
1922
  }
1203
- /**
1204
- * Flush a batch of events
1205
- * Uses promise-based concurrency control to prevent race conditions
1206
- */
1207
- async flushBatch() {
1208
- if (this.queue.length === 0) return;
1209
- if (this.flushPromise) {
1210
- await this.flushPromise;
1211
- return;
1212
- }
1213
- this.flushPromise = this.doFlushBatch();
1214
- try {
1215
- await this.flushPromise;
1216
- } finally {
1217
- this.flushPromise = null;
1218
- if (this.queue.length > 0) this.scheduleBatchFlush();
1923
+ onStart(span, parentContext) {
1924
+ let baggage = propagation.getBaggage(parentContext);
1925
+ if (!baggage) baggage = propagation.getBaggage(context.active());
1926
+ if (!baggage) try {
1927
+ const { getActiveContextWithBaggage } = requireModule("./trace-context");
1928
+ const storedContext = getActiveContextWithBaggage();
1929
+ baggage = propagation.getBaggage(storedContext);
1930
+ } catch {}
1931
+ if (!baggage) return;
1932
+ for (const [key, entry] of baggage.getAllEntries()) span.setAttribute(`${this.prefix}${key}`, entry.value);
1933
+ }
1934
+ onEnd(_span) {}
1935
+ async shutdown() {}
1936
+ async forceFlush() {}
1937
+ };
1938
+
1939
+ //#endregion
1940
+ //#region src/redact-values.ts
1941
+ /** Standalone string redaction for use outside the span processor pipeline. */
1942
+ function createStringRedactor(config) {
1943
+ const resolved = typeof config === "string" ? REDACTOR_PRESETS[config] : config;
1944
+ const valuePatterns = resolved.valuePatterns ?? [];
1945
+ const defaultReplacement = resolved.replacement ?? "[REDACTED]";
1946
+ return (value) => {
1947
+ let result = value;
1948
+ for (const { pattern, replacement, mask } of valuePatterns) {
1949
+ pattern.lastIndex = 0;
1950
+ result = mask ? result.replaceAll(pattern, (match) => mask(match)) : result.replaceAll(pattern, replacement ?? defaultReplacement);
1219
1951
  }
1952
+ return result;
1953
+ };
1954
+ }
1955
+
1956
+ //#endregion
1957
+ //#region src/env-config.ts
1958
+ /**
1959
+ * Validate URL format
1960
+ */
1961
+ function isValidUrl(urlString) {
1962
+ try {
1963
+ const url = new URL(urlString);
1964
+ return url.protocol === "http:" || url.protocol === "https:";
1965
+ } catch {
1966
+ return false;
1220
1967
  }
1221
- /**
1222
- * Internal flush implementation
1223
- */
1224
- async doFlushBatch() {
1225
- const batch = this.queue.splice(0, this.config.batchSize);
1226
- await this.sendWithRetry(batch, this.config.maxRetries);
1968
+ }
1969
+ /**
1970
+ * Resolve OpenTelemetry environment variables from process.env
1971
+ */
1972
+ function resolveOtelEnv() {
1973
+ const env = {};
1974
+ if (process.env.OTEL_SERVICE_NAME) {
1975
+ const value = process.env.OTEL_SERVICE_NAME.trim();
1976
+ if (value) env.OTEL_SERVICE_NAME = value;
1227
1977
  }
1228
- /**
1229
- * Send events with exponential backoff retry
1230
- * Tracks per-event, per-subscriber failures so failed counter reflects actual failed deliveries.
1231
- * On retry, only failed (event, subscriber) pairs are re-sent to avoid double-counting delivered.
1232
- */
1233
- async sendWithRetry(events, retriesLeft, subscribersByEventIndex) {
1234
- const failedDeliveries = await this.sendToSubscribers(events, subscribersByEventIndex);
1235
- if (failedDeliveries.length > 0) if (retriesLeft > 0) {
1236
- const failedEventIndicesOrdered = [...new Set(failedDeliveries.map((f) => f.eventIndex))].toSorted((a, b) => a - b);
1237
- const eventsToRetry = failedEventIndicesOrdered.map((i) => events[i]);
1238
- const failedSubscribersByRetryIndex = /* @__PURE__ */ new Map();
1239
- for (const [j, origIndex] of failedEventIndicesOrdered.entries()) {
1240
- const set = /* @__PURE__ */ new Set();
1241
- for (const { eventIndex, subscriberName } of failedDeliveries) if (eventIndex === origIndex) set.add(subscriberName);
1242
- failedSubscribersByRetryIndex.set(j, set);
1243
- }
1244
- const delay = Math.pow(2, this.config.maxRetries - retriesLeft) * 1e3;
1245
- await new Promise((resolve) => setTimeout(resolve, delay));
1246
- return this.sendWithRetry(eventsToRetry, retriesLeft - 1, failedSubscribersByRetryIndex);
1247
- } else {
1248
- for (const { eventIndex, subscriberName, error } of failedDeliveries) {
1249
- const event = events[eventIndex];
1250
- if (event) this.recordFailed(event, subscriberName, error);
1251
- }
1252
- const failedSubscriberNames = [...new Set(failedDeliveries.map((f) => f.subscriberName))];
1253
- getLogger().error({
1254
- failedSubscribers: failedSubscriberNames,
1255
- retriesAttempted: this.config.maxRetries
1256
- }, "[autotel] Failed to send events after retries");
1257
- }
1978
+ if (process.env.OTEL_EXPORTER_OTLP_ENDPOINT) {
1979
+ const value = process.env.OTEL_EXPORTER_OTLP_ENDPOINT.trim();
1980
+ if (value && isValidUrl(value)) env.OTEL_EXPORTER_OTLP_ENDPOINT = value;
1258
1981
  }
1259
- /**
1260
- * Send events to configured subscribers with rate limiting and metrics.
1261
- * When subscribersByEventIndex is provided (retry path), only those subscribers are tried per event.
1262
- * Returns per-event, per-subscriber failures (empty if all succeeded).
1263
- */
1264
- async sendToSubscribers(events, subscribersByEventIndex) {
1265
- const failedDeliveries = [];
1266
- const sendOne = async (event, eventIndex) => {
1267
- const subscriberNames = subscribersByEventIndex?.get(eventIndex);
1268
- const failures = await this.sendEventToSubscribers(event, subscriberNames ?? void 0);
1269
- for (const failure of failures) failedDeliveries.push({
1270
- eventIndex,
1271
- subscriberName: failure.subscriberName,
1272
- error: failure.error
1273
- });
1274
- };
1275
- if (!this.rateLimiter) {
1276
- for (const [i, event] of events.entries()) if (event) await sendOne(event, i);
1277
- return failedDeliveries;
1278
- }
1279
- for (const [i, event] of events.entries()) {
1280
- await this.rateLimiter.waitForToken();
1281
- if (event) await sendOne(event, i);
1282
- }
1283
- return failedDeliveries;
1982
+ if (process.env.OTEL_EXPORTER_OTLP_HEADERS) {
1983
+ const value = process.env.OTEL_EXPORTER_OTLP_HEADERS.trim();
1984
+ if (value) env.OTEL_EXPORTER_OTLP_HEADERS = value;
1284
1985
  }
1285
- /**
1286
- * Send a single event to subscribers.
1287
- * - When subscriberNames is undefined (initial attempt): send to all subscribers.
1288
- * - When subscriberNames is provided (retry): send only to those subscribers (never re-send to healthy ones).
1289
- * Returns list of subscribers that failed (empty if all succeeded).
1290
- */
1291
- async sendEventToSubscribers(event, subscriberNames) {
1292
- const startTime = event.timestamp;
1293
- const failures = [];
1294
- const subscribersToTry = subscriberNames === void 0 ? this.subscribers : this.subscribers.filter((s) => subscriberNames.has(getSubscriberName(s)));
1295
- const results = await Promise.allSettled(subscribersToTry.map(async (subscriber) => {
1296
- const subscriberName = getSubscriberName(subscriber);
1297
- try {
1298
- await subscriber.trackEvent(event.name, event.attributes, {
1299
- autotel: event.autotel,
1300
- schema: event.schema
1301
- });
1302
- this.recordDelivered(event, subscriberName, startTime);
1303
- return {
1304
- subscriberName,
1305
- success: true
1306
- };
1307
- } catch (error) {
1308
- this.markSubscriberUnhealthy(subscriberName);
1309
- return {
1310
- subscriberName,
1311
- success: false,
1312
- error: error instanceof Error ? error : void 0
1313
- };
1314
- }
1315
- }));
1316
- for (const result of results) if (result.status === "fulfilled" && !result.value.success) failures.push({
1317
- subscriberName: result.value.subscriberName,
1318
- error: result.value.error
1319
- });
1320
- return failures;
1986
+ if (process.env.OTEL_RESOURCE_ATTRIBUTES) {
1987
+ const value = process.env.OTEL_RESOURCE_ATTRIBUTES.trim();
1988
+ if (value) env.OTEL_RESOURCE_ATTRIBUTES = value;
1321
1989
  }
1322
- /**
1323
- * Flush all remaining events. Queue remains usable after flush (e.g. for
1324
- * auto-flush at root span end). Use shutdown() when tearing down the queue.
1325
- */
1326
- async flush() {
1327
- if (this.flushTimer) {
1328
- clearTimeout(this.flushTimer);
1329
- this.flushTimer = null;
1330
- }
1331
- if (this.flushPromise) await this.flushPromise;
1332
- while (this.queue.length > 0) await this.doFlushBatch();
1990
+ if (process.env.OTEL_EXPORTER_OTLP_PROTOCOL) {
1991
+ const value = process.env.OTEL_EXPORTER_OTLP_PROTOCOL.trim().toLowerCase();
1992
+ if (value === "http" || value === "http/json") env.OTEL_EXPORTER_OTLP_PROTOCOL = "http";
1993
+ else if (value === "http/protobuf" || value === "grpc") env.OTEL_EXPORTER_OTLP_PROTOCOL = value;
1333
1994
  }
1334
- /**
1335
- * Flush remaining events and permanently disable the queue (reject new events).
1336
- * Use for process/SDK shutdown; use flush() for periodic or span-end drain.
1337
- */
1338
- async shutdown() {
1339
- this.isShuttingDown = true;
1340
- await this.flush();
1341
- const drains = await Promise.allSettled(this.subscribers.map(async (subscriber) => {
1342
- if (!subscriber.shutdown) return;
1343
- shutDownSubscribers.add(subscriber);
1344
- return subscriber.shutdown();
1345
- }));
1346
- for (const [index, drain] of drains.entries()) {
1347
- if (drain.status !== "rejected") continue;
1348
- const subscriberName = getSubscriberName(this.subscribers[index]);
1349
- this.subscriberHealthy.set(subscriberName, false);
1350
- getLogger().error({
1351
- subscriber: subscriberName,
1352
- err: drain.reason
1353
- }, "[autotel] Subscriber shutdown failed, buffered events may be lost");
1354
- }
1995
+ if (process.env.OTEL_TRACES_SAMPLER) {
1996
+ const value = process.env.OTEL_TRACES_SAMPLER.trim();
1997
+ if (value) env.OTEL_TRACES_SAMPLER = value;
1355
1998
  }
1356
- /**
1357
- * Cleanup observable metric callbacks to prevent memory leaks
1358
- * Call this when destroying the EventQueue instance
1359
- */
1360
- cleanup() {
1361
- for (const cleanupFn of this.observableCleanups) try {
1362
- cleanupFn();
1363
- } catch {}
1364
- this.observableCleanups = [];
1999
+ if (process.env.OTEL_TRACES_SAMPLER_ARG) {
2000
+ const value = process.env.OTEL_TRACES_SAMPLER_ARG.trim();
2001
+ if (value) env.OTEL_TRACES_SAMPLER_ARG = value;
1365
2002
  }
1366
- /**
1367
- * Get queue size (for testing/debugging)
1368
- */
1369
- size() {
1370
- return this.queue.length;
2003
+ return env;
2004
+ }
2005
+ function parseRatioSamplerArg(samplerName, samplerArg) {
2006
+ if (samplerArg === void 0) return 1;
2007
+ const ratio = Number(samplerArg);
2008
+ if (!Number.isFinite(ratio) || ratio < 0 || ratio > 1) {
2009
+ console.error(`[autotel] Invalid OTEL_TRACES_SAMPLER_ARG="${samplerArg}" for ${samplerName}. Expected a number in [0..1]. Falling back to 1.0.`);
2010
+ return 1;
1371
2011
  }
1372
- /**
1373
- * Get subscriber health status (for testing/debugging)
1374
- */
1375
- getSubscriberHealth() {
1376
- return new Map(this.subscriberHealthy);
2012
+ return ratio;
2013
+ }
2014
+ function warnOnUnusedSamplerArg(samplerName, samplerArg) {
2015
+ if (samplerArg !== void 0) console.error(`[autotel] OTEL_TRACES_SAMPLER_ARG is not used by OTEL_TRACES_SAMPLER="${samplerName}". Ignoring value "${samplerArg}".`);
2016
+ }
2017
+ function createSamplerFromEnv(env) {
2018
+ const samplerName = env.OTEL_TRACES_SAMPLER;
2019
+ if (!samplerName) return;
2020
+ switch (samplerName) {
2021
+ case "always_on":
2022
+ warnOnUnusedSamplerArg(samplerName, env.OTEL_TRACES_SAMPLER_ARG);
2023
+ return new AlwaysOnSampler();
2024
+ case "always_off":
2025
+ warnOnUnusedSamplerArg(samplerName, env.OTEL_TRACES_SAMPLER_ARG);
2026
+ return new AlwaysOffSampler();
2027
+ case "traceidratio": return new TraceIdRatioBasedSampler(parseRatioSamplerArg(samplerName, env.OTEL_TRACES_SAMPLER_ARG));
2028
+ case "parentbased_always_on":
2029
+ warnOnUnusedSamplerArg(samplerName, env.OTEL_TRACES_SAMPLER_ARG);
2030
+ return new ParentBasedSampler({ root: new AlwaysOnSampler() });
2031
+ case "parentbased_always_off":
2032
+ warnOnUnusedSamplerArg(samplerName, env.OTEL_TRACES_SAMPLER_ARG);
2033
+ return new ParentBasedSampler({ root: new AlwaysOffSampler() });
2034
+ case "parentbased_traceidratio": return new ParentBasedSampler({ root: new TraceIdRatioBasedSampler(parseRatioSamplerArg(samplerName, env.OTEL_TRACES_SAMPLER_ARG)) });
2035
+ case "jaeger_remote":
2036
+ case "parentbased_jaeger_remote":
2037
+ case "xray":
2038
+ console.error(`[autotel] OTEL_TRACES_SAMPLER="${samplerName}" is not supported yet by autotel. Falling back to the next sampler source.`);
2039
+ return;
2040
+ default:
2041
+ console.error(`[autotel] Unknown OTEL_TRACES_SAMPLER="${samplerName}". Falling back to the next sampler source.`);
2042
+ return;
1377
2043
  }
1378
- /**
1379
- * Check if a specific subscriber is healthy
1380
- */
1381
- isSubscriberHealthy(subscriberName) {
1382
- return this.subscriberHealthy.get(subscriberName.toLowerCase()) ?? true;
2044
+ }
2045
+ /**
2046
+ * Parse OTEL_RESOURCE_ATTRIBUTES from comma-separated key=value pairs
2047
+ * Example: "service.version=1.0.0,deployment.environment=production"
2048
+ */
2049
+ function parseResourceAttributes(input) {
2050
+ if (!input || input.trim() === "") return {};
2051
+ const attributes = {};
2052
+ const pairs = input.split(",");
2053
+ for (const pair of pairs) {
2054
+ const trimmedPair = pair.trim();
2055
+ if (!trimmedPair) continue;
2056
+ const equalIndex = trimmedPair.indexOf("=");
2057
+ if (equalIndex === -1) continue;
2058
+ const key = trimmedPair.slice(0, equalIndex).trim();
2059
+ const value = trimmedPair.slice(equalIndex + 1).trim();
2060
+ if (key && value) attributes[key] = value;
1383
2061
  }
1384
- /**
1385
- * Manually mark a subscriber as healthy or unhealthy
1386
- * (used for circuit breaker integration)
1387
- */
1388
- setSubscriberHealth(subscriberName, healthy) {
1389
- this.subscriberHealthy.set(subscriberName.toLowerCase(), healthy);
2062
+ return attributes;
2063
+ }
2064
+ /**
2065
+ * Parse OTEL_EXPORTER_OTLP_HEADERS from comma-separated key=value pairs
2066
+ * Example: "api-key=secret123,x-custom-header=value"
2067
+ */
2068
+ function parseOtlpHeaders(input) {
2069
+ if (!input || input.trim() === "") return {};
2070
+ const headers = {};
2071
+ const pairs = input.split(",");
2072
+ for (const pair of pairs) {
2073
+ const trimmedPair = pair.trim();
2074
+ if (!trimmedPair) continue;
2075
+ const equalIndex = trimmedPair.indexOf("=");
2076
+ if (equalIndex === -1) continue;
2077
+ const key = trimmedPair.slice(0, equalIndex).trim();
2078
+ const value = trimmedPair.slice(equalIndex + 1).trim();
2079
+ if (key && value) headers[key] = value;
1390
2080
  }
1391
- };
2081
+ return headers;
2082
+ }
2083
+ /**
2084
+ * Convert resolved environment variables to config
2085
+ */
2086
+ function envToConfig(env) {
2087
+ const config = {};
2088
+ if (env.OTEL_SERVICE_NAME) config.service = env.OTEL_SERVICE_NAME;
2089
+ if (env.OTEL_EXPORTER_OTLP_ENDPOINT) config.endpoint = env.OTEL_EXPORTER_OTLP_ENDPOINT;
2090
+ if (env.OTEL_EXPORTER_OTLP_PROTOCOL) config.protocol = env.OTEL_EXPORTER_OTLP_PROTOCOL;
2091
+ if (env.OTEL_EXPORTER_OTLP_HEADERS) config.headers = parseOtlpHeaders(env.OTEL_EXPORTER_OTLP_HEADERS);
2092
+ const resourceAttrs = parseResourceAttributes(env.OTEL_RESOURCE_ATTRIBUTES);
2093
+ if (Object.keys(resourceAttrs).length > 0) config.resourceAttributes = resourceAttrs;
2094
+ const sampler = createSamplerFromEnv(env);
2095
+ if (sampler) config.otelSampler = sampler;
2096
+ return config;
2097
+ }
2098
+ /**
2099
+ * Main function to resolve config from environment variables
2100
+ */
2101
+ function resolveConfigFromEnv() {
2102
+ return envToConfig(resolveOtelEnv());
2103
+ }
1392
2104
 
1393
2105
  //#endregion
1394
- //#region src/validation.ts
1395
- const DEFAULT_CONFIG$1 = {
1396
- maxEventNameLength: 100,
1397
- maxAttributeKeyLength: 100,
1398
- maxAttributeValueLength: 1e3,
1399
- maxAttributeCount: 50,
1400
- maxNestingDepth: 3,
1401
- sensitivePatterns: [
1402
- /password/i,
1403
- /secret/i,
1404
- /token/i,
1405
- /api[_-]?key/i,
1406
- /access[_-]?key/i,
1407
- /private[_-]?key/i,
1408
- /auth/i,
1409
- /credential/i,
1410
- /ssn/i,
1411
- /credit[_-]?card/i
1412
- ]
1413
- };
1414
- var ValidationError = class extends Error {
1415
- constructor(message) {
1416
- super(message);
1417
- this.name = "ValidationError";
1418
- }
1419
- };
2106
+ //#region src/yaml-config.ts
1420
2107
  /**
1421
- * Validate and sanitize event name
1422
- * Throws ValidationError if invalid
2108
+ * YAML configuration loader for autotel
2109
+ *
2110
+ * Supports:
2111
+ * - Auto-discovery of autotel.yaml in cwd
2112
+ * - AUTOTEL_CONFIG_FILE env var override
2113
+ * - Environment variable substitution: ${env:VAR} and ${env:VAR:-default}
2114
+ *
2115
+ * @example Auto-discovery
2116
+ * ```yaml
2117
+ * # autotel.yaml in project root
2118
+ * service:
2119
+ * name: my-service
2120
+ * exporter:
2121
+ * endpoint: ${env:OTEL_EXPORTER_OTLP_ENDPOINT:-http://localhost:4318}
2122
+ * ```
2123
+ *
2124
+ * @example Explicit path
2125
+ * ```bash
2126
+ * AUTOTEL_CONFIG_FILE=./config/otel.yaml tsx --import autotel/auto src/index.ts
2127
+ * ```
1423
2128
  */
1424
- function validateEventName(eventName, config = DEFAULT_CONFIG$1) {
1425
- if (asString(eventName) === void 0) throw new ValidationError(`Event name must be a string, got ${describeValue(eventName)}`);
1426
- const trimmed = eventName.trim();
1427
- if (trimmed.length === 0) throw new ValidationError("Event name cannot be empty");
1428
- if (trimmed.length > config.maxEventNameLength) throw new ValidationError(`Event name too long (${trimmed.length} chars). Max: ${config.maxEventNameLength}`);
1429
- if (!/^[a-zA-Z0-9._-]+$/.test(trimmed)) throw new ValidationError(`Event name contains invalid characters: "${trimmed}". Use only letters, numbers, dots, underscores, and hyphens.`);
1430
- return trimmed;
1431
- }
1432
2129
  /**
1433
- * Validate and sanitize attributes
1434
- * Returns sanitized attributes (sensitive data redacted)
2130
+ * Lazy-load yaml parser (optional peer dependency)
2131
+ * Only loads when a YAML config file is actually found
1435
2132
  */
1436
- function validateAttributes(attributes, config = DEFAULT_CONFIG$1) {
1437
- if (attributes === void 0 || attributes === null) return;
1438
- if (!asRecord(attributes)) throw new ValidationError("Attributes must be an object");
1439
- const keys = Object.keys(attributes);
1440
- if (keys.length > config.maxAttributeCount) throw new ValidationError(`Too many attributes (${keys.length}). Max: ${config.maxAttributeCount}`);
1441
- const sanitized = {};
1442
- for (const key of keys) {
1443
- if (key.length > config.maxAttributeKeyLength) throw new ValidationError(`Attribute key too long: "${key.slice(0, 20)}..." (${key.length} chars). Max: ${config.maxAttributeKeyLength}`);
1444
- const value = attributes[key];
1445
- if (isSensitiveString(key, value, config)) {
1446
- sanitized[key] = "[REDACTED]";
1447
- continue;
1448
- }
1449
- sanitized[key] = sanitizeValue(value, config, 1);
2133
+ function loadYamlParser() {
2134
+ try {
2135
+ return requireModule("yaml").parse;
2136
+ } catch {
2137
+ throw new Error("YAML parser not found. Install with: pnpm add yaml");
1450
2138
  }
1451
- return sanitized;
1452
2139
  }
1453
2140
  /**
1454
- * Whether this key/value pair is a credential to redact. Strings only:
1455
- * numeric and boolean values are not credentials, and replacing them with the
1456
- * literal '[REDACTED]' leaks no signal while breaking every consumer that
1457
- * treats them as a number.
2141
+ * Environment variable substitution regex
2142
+ * Matches ${env:VAR_NAME} and ${env:VAR_NAME:-default}
1458
2143
  */
1459
- function isSensitiveString(key, value, config) {
1460
- return asString(value) !== void 0 && config.sensitivePatterns.some((pattern) => pattern.test(key));
2144
+ const ENV_VAR_PATTERN = /\$\{env:([A-Za-z_][A-Za-z0-9_]*)(?::-([^}]*))?\}/g;
2145
+ /**
2146
+ * Substitute ${env:VAR} and ${env:VAR:-default} in a string
2147
+ *
2148
+ * @param value - String potentially containing env var references
2149
+ * @returns String with env vars substituted
2150
+ *
2151
+ * @example
2152
+ * substituteEnvVars('${env:NODE_ENV:-development}')
2153
+ * // Returns 'production' if NODE_ENV=production, else 'development'
2154
+ */
2155
+ function substituteEnvVars(value) {
2156
+ return value.replaceAll(ENV_VAR_PATTERN, (_match, varName, defaultValue) => {
2157
+ const envValue = process.env[varName];
2158
+ if (envValue !== void 0) return envValue;
2159
+ if (defaultValue !== void 0) return defaultValue;
2160
+ console.warn(`[autotel] Environment variable ${varName} not set and no default provided`);
2161
+ return "";
2162
+ });
1461
2163
  }
1462
2164
  /**
1463
- * Sanitize attribute value (recursive)
2165
+ * Recursively substitute env vars in an object
2166
+ *
2167
+ * @param obj - Object to process
2168
+ * @returns Object with all string values having env vars substituted
1464
2169
  */
1465
- function sanitizeValue(value, config, depth) {
1466
- if (depth > config.maxNestingDepth) return "[MAX_DEPTH_EXCEEDED]";
1467
- if (value === null || value === void 0) return value;
1468
- const text = asString(value);
1469
- if (text !== void 0) return text.length > config.maxAttributeValueLength ? text.slice(0, config.maxAttributeValueLength) + "..." : text;
1470
- const scalar = asNumber(value) ?? asBoolean(value);
1471
- if (scalar !== void 0) return scalar;
1472
- if (Array.isArray(value)) return value.map((item) => sanitizeValue(item, config, depth + 1));
1473
- const record = asRecord(value);
1474
- if (record) try {
1475
- JSON.stringify(record);
1476
- const sanitized = {};
1477
- for (const [key, nested] of Object.entries(record)) sanitized[key] = isSensitiveString(key, nested, config) ? "[REDACTED]" : sanitizeValue(nested, config, depth + 1);
1478
- return sanitized;
1479
- } catch {
1480
- return "[CIRCULAR]";
2170
+ function substituteEnvVarsDeep(value) {
2171
+ if (Array.isArray(value)) return value.map(substituteEnvVarsDeep);
2172
+ const mapping = asYamlMapping(value);
2173
+ if (mapping) {
2174
+ const result = {};
2175
+ for (const [key, entry] of Object.entries(mapping)) result[key] = substituteEnvVarsDeep(entry);
2176
+ return result;
1481
2177
  }
1482
- return `[${describeValue(value)}]`;
2178
+ const text = asString(value);
2179
+ return text === void 0 ? value : substituteEnvVars(text);
2180
+ }
2181
+ /** The mapping a YAML value is, when it is one. */
2182
+ function asYamlMapping(value) {
2183
+ return asRecord(value) === void 0 ? void 0 : value;
1483
2184
  }
1484
2185
  /**
1485
- * Validate and sanitize an events event
1486
- * Returns { eventName, attributes } with sanitized values
2186
+ * A YAML document read into the config shape, with `${env:...}` placeholders
2187
+ * resolved.
2188
+ *
2189
+ * This is the boundary: the file is text until here. Every field of
2190
+ * YamlConfig is optional, so any document satisfies it, and
2191
+ * `yamlToAutotelConfig` picks out only the fields it understands.
1487
2192
  */
1488
- function validateEvent(eventName, attributes, config) {
1489
- const fullConfig = {
1490
- ...DEFAULT_CONFIG$1,
1491
- ...config
1492
- };
1493
- return {
1494
- eventName: validateEventName(eventName, fullConfig),
1495
- attributes: validateAttributes(attributes, fullConfig)
1496
- };
2193
+ function parseYamlConfig(content) {
2194
+ return asYamlMapping(substituteEnvVarsDeep(loadYamlParser()(content))) ?? {};
1497
2195
  }
1498
-
1499
- //#endregion
1500
- //#region src/track.ts
1501
2196
  /**
1502
- * Global track() function for business events
2197
+ * Find YAML config file path
1503
2198
  *
1504
- * Simple, no instantiation needed, auto-attaches trace context
2199
+ * Priority:
2200
+ * 1. AUTOTEL_CONFIG_FILE env var (explicit path)
2201
+ * 2. autotel.yaml in cwd (convention)
2202
+ * 3. autotel.yml in cwd (alternative extension)
2203
+ *
2204
+ * @returns File path if found, null otherwise
1505
2205
  */
1506
- let eventsQueue = null;
2206
+ function findConfigFile() {
2207
+ const envPath = process.env.AUTOTEL_CONFIG_FILE;
2208
+ if (envPath) {
2209
+ const resolved = path.resolve(envPath);
2210
+ if (nodeFs.existsSync(resolved)) return resolved;
2211
+ console.warn(`[autotel] Config file not found: ${envPath}`);
2212
+ return null;
2213
+ }
2214
+ const conventionPath = path.resolve(process.cwd(), "autotel.yaml");
2215
+ if (nodeFs.existsSync(conventionPath)) return conventionPath;
2216
+ const altPath = path.resolve(process.cwd(), "autotel.yml");
2217
+ if (nodeFs.existsSync(altPath)) return altPath;
2218
+ return null;
2219
+ }
1507
2220
  /**
1508
- * Build autotel event context for trace correlation
2221
+ * Convert YAML config structure to AutotelConfig
1509
2222
  *
1510
- * Works in multiple contexts:
1511
- * 1. Inside a span → use current span's trace_id + span_id
1512
- * 2. Outside span → use correlation_id only
1513
- * 3. With trace URL config → include clickable trace URL
2223
+ * @param yaml - Parsed and env-substituted YAML config
2224
+ * @returns Partial AutotelConfig ready for merging
1514
2225
  */
1515
- function buildAutotelContext(span) {
1516
- const eventsConfig = getEventsConfig();
1517
- const config = getConfig();
1518
- const correlationId = getOrCreateCorrelationId();
1519
- if (!eventsConfig?.includeTraceContext) return { correlation_id: correlationId };
1520
- const autotelContext = { correlation_id: correlationId };
1521
- const spanContext = span?.spanContext();
1522
- if (spanContext) {
1523
- autotelContext.trace_id = spanContext.traceId;
1524
- autotelContext.span_id = spanContext.spanId;
1525
- autotelContext.trace_flags = spanContext.traceFlags.toString(16).padStart(2, "0");
1526
- const traceState = spanContext.traceState;
1527
- if (traceState) try {
1528
- if (typeof traceState.serialize === "function") {
1529
- const traceStateStr = traceState.serialize();
1530
- if (traceStateStr) autotelContext.trace_state = traceStateStr;
1531
- }
1532
- } catch {}
1533
- if (eventsConfig.traceUrl && config) {
1534
- const traceUrl = eventsConfig.traceUrl({
1535
- traceId: spanContext.traceId,
1536
- spanId: spanContext.spanId,
1537
- correlationId,
1538
- serviceName: config.service,
1539
- environment: config.environment
1540
- });
1541
- if (traceUrl) autotelContext.trace_url = traceUrl;
1542
- }
1543
- } else if (eventsConfig.traceUrl && config) {
1544
- const traceUrl = eventsConfig.traceUrl({
1545
- correlationId,
1546
- serviceName: config.service,
1547
- environment: config.environment
1548
- });
1549
- if (traceUrl) autotelContext.trace_url = traceUrl;
2226
+ function yamlToAutotelConfig(yaml) {
2227
+ const config = {};
2228
+ if (yaml.service?.name) config.service = yaml.service.name;
2229
+ if (yaml.service?.version) config.version = yaml.service.version;
2230
+ if (yaml.service?.environment) config.environment = yaml.service.environment;
2231
+ if (yaml.exporter?.endpoint) config.endpoint = yaml.exporter.endpoint;
2232
+ if (yaml.exporter?.protocol) config.protocol = yaml.exporter.protocol;
2233
+ if (yaml.exporter?.headers) config.headers = yaml.exporter.headers;
2234
+ if (yaml.exporter?.destinations) config.destinations = yaml.exporter.destinations;
2235
+ if (yaml.resource) config.resourceAttributes = yaml.resource;
2236
+ if (yaml.autoInstrumentations) config.autoInstrumentations = yaml.autoInstrumentations;
2237
+ if (yaml.policies) config.policies = yaml.policies;
2238
+ if (yaml.debug !== void 0) config.debug = yaml.debug;
2239
+ if (yaml.sampling?.preset) {
2240
+ warnOnIgnoredPresetOverrides(yaml.sampling);
2241
+ config.sampling = yaml.sampling.preset;
2242
+ } else {
2243
+ const sampler = createSamplerFromYaml(yaml.sampling);
2244
+ if (sampler) config.sampler = sampler;
2245
+ }
2246
+ return config;
2247
+ }
2248
+ function createSamplerFromYaml(sampling) {
2249
+ if (!sampling) return void 0;
2250
+ if (sampling.preset) return void 0;
2251
+ const type = sampling.type ?? "adaptive";
2252
+ try {
2253
+ switch (type) {
2254
+ case "adaptive": return new AdaptiveSampler({
2255
+ baselineSampleRate: sampling.baseline_rate,
2256
+ alwaysSampleErrors: sampling.always_sample_errors,
2257
+ alwaysSampleSlow: sampling.always_sample_slow,
2258
+ slowThresholdMs: sampling.slow_threshold_ms
2259
+ });
2260
+ case "always_on": return new AlwaysSampler();
2261
+ case "always_off": return new NeverSampler();
2262
+ case "ratio":
2263
+ if (sampling.ratio === void 0) {
2264
+ console.warn("[autotel] sampling.ratio missing in YAML sampling config. Falling back to adaptive sampler.");
2265
+ return new AdaptiveSampler();
2266
+ }
2267
+ return new RandomSampler(sampling.ratio);
2268
+ default:
2269
+ console.warn(`[autotel] Unknown sampling type "${type}" in YAML config. Falling back to defaults.`);
2270
+ return;
2271
+ }
2272
+ } catch (error) {
2273
+ console.warn(`[autotel] Failed to configure sampling from YAML: ${error instanceof Error ? error.message : String(error)}`);
2274
+ return;
1550
2275
  }
1551
- return autotelContext;
2276
+ }
2277
+ function warnOnIgnoredPresetOverrides(sampling) {
2278
+ const ignoredFields = [
2279
+ "type",
2280
+ "ratio",
2281
+ "baseline_rate",
2282
+ "always_sample_errors",
2283
+ "always_sample_slow",
2284
+ "slow_threshold_ms"
2285
+ ].filter((field) => sampling[field] !== void 0);
2286
+ if (ignoredFields.length === 0) return;
2287
+ console.warn(`[autotel] sampling.preset="${sampling.preset}" ignores these YAML fields: ${ignoredFields.join(", ")}. Use the programmatic API with sampler or samplingPresets.*(...) for tuned presets.`);
1552
2288
  }
1553
2289
  /**
1554
- * Initialize events queue lazily
2290
+ * Load and parse YAML config file (auto-discovery)
2291
+ *
2292
+ * Automatically finds and loads autotel.yaml or uses AUTOTEL_CONFIG_FILE.
2293
+ * Returns null if no config file found (not an error - YAML config is optional).
2294
+ *
2295
+ * @returns Partial AutotelConfig or null if no config file found
2296
+ *
2297
+ * @example
2298
+ * const yamlConfig = loadYamlConfig();
2299
+ * if (yamlConfig) {
2300
+ * init({ ...yamlConfig, debug: true });
2301
+ * }
1555
2302
  */
1556
- function getOrCreateQueue() {
1557
- if (!isInitialized()) {
1558
- warnIfNotInitialized("track()");
2303
+ function loadYamlConfig() {
2304
+ const filePath = findConfigFile();
2305
+ if (!filePath) return null;
2306
+ try {
2307
+ return yamlToAutotelConfig(parseYamlConfig(nodeFs.readFileSync(filePath, "utf8")));
2308
+ } catch (error) {
2309
+ console.error(`[autotel] Failed to load YAML config from ${filePath}:`, error);
1559
2310
  return null;
1560
2311
  }
1561
- if (!eventsQueue) {
1562
- const config = getConfig();
1563
- if (!config?.subscribers || config.subscribers.length === 0) return null;
1564
- eventsQueue = new EventQueue(config.subscribers);
1565
- }
1566
- return eventsQueue;
1567
2312
  }
1568
2313
  /**
1569
- * Track a business events event
2314
+ * Load YAML config from a specific file path
1570
2315
  *
1571
- * Features:
1572
- * - Auto-attaches traceId and spanId if in active span
1573
- * - Batched sending with retry
1574
- * - Type-safe with optional generic
1575
- * - No-op if init() not called or no subscribers configured
2316
+ * Unlike loadYamlConfig(), this throws if the file cannot be read.
1576
2317
  *
1577
- * @example Basic usage
1578
- * ```typescript
1579
- * track('user.signup', { userId: '123', plan: 'pro' })
1580
- * ```
2318
+ * @param filePath - Path to YAML config file
2319
+ * @returns Partial AutotelConfig
2320
+ * @throws Error if file cannot be read or parsed
1581
2321
  *
1582
- * @example With type safety
1583
- * ```typescript
1584
- * interface EventDatas {
1585
- * 'user.signup': { userId: string; plan: string }
1586
- * 'plan.upgraded': { userId: string; revenue: number }
1587
- * }
2322
+ * @example
2323
+ * import { loadYamlConfigFromFile } from 'autotel/yaml';
2324
+ * import { init } from 'autotel';
1588
2325
  *
1589
- * track<EventDatas>('user.signup', { userId: '123', plan: 'pro' })
1590
- * ```
2326
+ * const config = loadYamlConfigFromFile('./config/otel.yaml');
2327
+ * init({ ...config, debug: true });
2328
+ */
2329
+ function loadYamlConfigFromFile(filePath) {
2330
+ const resolved = path.resolve(filePath);
2331
+ return yamlToAutotelConfig(parseYamlConfig(nodeFs.readFileSync(resolved, "utf8")));
2332
+ }
2333
+ /**
2334
+ * Check if a YAML config file exists (without loading it)
1591
2335
  *
1592
- * @example Trace correlation (automatic)
1593
- * ```typescript
1594
- * @Instrumented()
1595
- * class UserService {
1596
- * async createUser(data: CreateUserData) {
1597
- * // This track call automatically includes traceId + spanId
1598
- * track('user.signup', { userId: data.id })
1599
- * }
1600
- * }
1601
- * ```
2336
+ * @returns true if a config file would be found by loadYamlConfig()
1602
2337
  */
1603
- function track(event, data, options) {
1604
- const queue = getOrCreateQueue();
1605
- if (!queue) return;
1606
- const validated = validateEvent(event, data, getValidationConfig() || void 0);
1607
- const span = trace.getActiveSpan();
1608
- const enrichedData = span ? {
1609
- ...validated.attributes,
1610
- traceId: span.spanContext().traceId,
1611
- spanId: span.spanContext().spanId
1612
- } : validated.attributes;
1613
- const autotelContext = buildAutotelContext(span);
1614
- queue.enqueue({
1615
- name: validated.eventName,
1616
- attributes: enrichedData,
1617
- timestamp: Date.now(),
1618
- autotel: autotelContext,
1619
- schema: options?.schema
2338
+ function hasYamlConfig() {
2339
+ return findConfigFile() !== null;
2340
+ }
2341
+
2342
+ //#endregion
2343
+ //#region src/devtools.ts
2344
+ const defaultHost = "127.0.0.1";
2345
+ const defaultPort = 4318;
2346
+ function resolveDevtoolsConfig(config) {
2347
+ if (!config) return {
2348
+ enabled: false,
2349
+ endpoint: void 0,
2350
+ embedded: false,
2351
+ host: defaultHost,
2352
+ port: defaultPort,
2353
+ verbose: false
2354
+ };
2355
+ if (config === true) return {
2356
+ enabled: true,
2357
+ endpoint: `http://${defaultHost}:${defaultPort}`,
2358
+ embedded: false,
2359
+ host: defaultHost,
2360
+ port: defaultPort,
2361
+ verbose: false
2362
+ };
2363
+ const enabled = config.enabled ?? true;
2364
+ const host = config.host ?? defaultHost;
2365
+ const port = config.port ?? defaultPort;
2366
+ const endpoint = config.endpoint ?? `http://${host}:${port}`;
2367
+ return {
2368
+ enabled,
2369
+ endpoint: enabled ? endpoint : void 0,
2370
+ embedded: enabled && (config.embedded ?? false),
2371
+ host,
2372
+ port,
2373
+ verbose: config.verbose ?? false
2374
+ };
2375
+ }
2376
+
2377
+ //#endregion
2378
+ //#region src/process-handlers.ts
2379
+ let removeOwnedHandlers = [];
2380
+ /**
2381
+ * Tracked separately from `removeOwnedHandlers` because the exit flush outlives
2382
+ * a call to `installProcessHandlers`, which clears that list before installing
2383
+ * its own signal listeners.
2384
+ */
2385
+ let removeExitFlush;
2386
+ /**
2387
+ * Shared across every path that can end the process: signals, fatal errors and
2388
+ * a clean exit. They can overlap — a container stopping a job that has just
2389
+ * finished its work sends SIGTERM while the exit flush is still draining — and
2390
+ * a second shutdown would tear down queues the first is still using.
2391
+ */
2392
+ let shutdownInFlight;
2393
+ /**
2394
+ * The clean-exit flush, while it runs. Doubles as the latch that keeps a
2395
+ * re-emitted `beforeExit` from flushing twice, and as the thing a shutdown
2396
+ * waits on rather than tearing down queues mid-drain.
2397
+ */
2398
+ let exitFlushInFlight;
2399
+ const DEFAULT_SHUTDOWN_TIMEOUT_MS = 2e3;
2400
+ function runShutdownOnce(shutdown, timeoutMs) {
2401
+ if (shutdownInFlight) return shutdownInFlight;
2402
+ let timeoutHandle;
2403
+ const shutdownAttempt = Promise.resolve(exitFlushInFlight).then(shutdown).catch(() => void 0);
2404
+ const timeout = new Promise((resolve) => {
2405
+ timeoutHandle = setTimeout(resolve, timeoutMs);
2406
+ timeoutHandle.unref();
2407
+ });
2408
+ shutdownInFlight = Promise.race([shutdownAttempt, timeout]).then(() => {
2409
+ if (timeoutHandle) clearTimeout(timeoutHandle);
1620
2410
  });
2411
+ return shutdownInFlight;
2412
+ }
2413
+ const DEFAULT_SIGNALS = ["SIGTERM", "SIGINT"];
2414
+ function signalExitCode(signal) {
2415
+ return 128 + constants.signals[signal];
1621
2416
  }
1622
2417
  /**
1623
- * Get events queue (for flush/shutdown)
1624
- * @internal
2418
+ * Surface a fatal error before shutting down.
2419
+ *
2420
+ * Registering an `uncaughtException` / `unhandledRejection` listener overrides
2421
+ * Node's default of printing the stack to stderr, so without this a crash under
2422
+ * `fatalErrors` would exit silently. Autotel's own logger is silent by default,
2423
+ * so we print to stderr directly to guarantee the crash stays visible.
1625
2424
  */
1626
- function getEventQueue() {
1627
- return eventsQueue;
2425
+ function reportFatalError(error, event) {
2426
+ const err = error instanceof Error ? error : new Error(String(error));
2427
+ console.error(`[autotel] ${event}, flushing telemetry then exiting`, err);
2428
+ }
2429
+ function installProcessHandlers(config, shutdown) {
2430
+ uninstallProcessHandlers();
2431
+ const timeoutMs = config.shutdownTimeoutMs ?? DEFAULT_SHUTDOWN_TIMEOUT_MS;
2432
+ let exitScheduled = false;
2433
+ let resolvedExitCode = 0;
2434
+ let fatalLatched = false;
2435
+ const shutdownAndExit = (exitCode, fatal) => {
2436
+ if (fatal) {
2437
+ if (!fatalLatched) {
2438
+ resolvedExitCode = exitCode;
2439
+ fatalLatched = true;
2440
+ }
2441
+ } else if (!exitScheduled && !fatalLatched) resolvedExitCode = exitCode;
2442
+ if (exitScheduled) return;
2443
+ exitScheduled = true;
2444
+ runShutdownOnce(shutdown, timeoutMs).then(() => {
2445
+ process.exit(resolvedExitCode);
2446
+ });
2447
+ };
2448
+ for (const signal of config.signals ?? DEFAULT_SIGNALS) {
2449
+ const listener = () => {
2450
+ shutdownAndExit(signalExitCode(signal), false);
2451
+ };
2452
+ process.on(signal, listener);
2453
+ removeOwnedHandlers.push(() => {
2454
+ process.removeListener(signal, listener);
2455
+ });
2456
+ }
2457
+ if (config.fatalErrors ?? true) {
2458
+ const uncaughtExceptionListener = (error) => {
2459
+ reportFatalError(error, "uncaughtException");
2460
+ shutdownAndExit(1, true);
2461
+ };
2462
+ const unhandledRejectionListener = (reason) => {
2463
+ reportFatalError(reason, "unhandledRejection");
2464
+ shutdownAndExit(1, true);
2465
+ };
2466
+ process.on("uncaughtException", uncaughtExceptionListener);
2467
+ process.on("unhandledRejection", unhandledRejectionListener);
2468
+ removeOwnedHandlers.push(() => {
2469
+ process.removeListener("uncaughtException", uncaughtExceptionListener);
2470
+ }, () => {
2471
+ process.removeListener("unhandledRejection", unhandledRejectionListener);
2472
+ });
2473
+ }
1628
2474
  }
1629
2475
  /**
1630
- * Reset events queue (for shutdown/cleanup)
1631
- * @internal
2476
+ * Flush telemetry when the process runs to completion.
2477
+ *
2478
+ * The signal and fatal-error handlers above cover a process that is stopped or
2479
+ * that crashes. Neither fires when a script simply finishes: the event loop
2480
+ * drains and Node exits, taking whatever the batch span processor was still
2481
+ * holding with it. `beforeExit` is the only hook for that case, and it is the
2482
+ * one that matters for CLIs, cron jobs, CI steps and serverless handlers.
2483
+ *
2484
+ * A flush, never a shutdown: `beforeExit` fires on *any* event-loop drain, not
2485
+ * only the final one, so tearing the SDK down here would silently kill
2486
+ * telemetry in a process that goes on to do more work.
1632
2487
  */
1633
- function resetEventQueue() {
1634
- eventsQueue = null;
2488
+ function installExitFlush(flushTelemetry, timeoutMs = DEFAULT_SHUTDOWN_TIMEOUT_MS) {
2489
+ removeExitFlush?.();
2490
+ exitFlushInFlight = void 0;
2491
+ const listener = (code) => {
2492
+ if (exitFlushInFlight || shutdownInFlight) return;
2493
+ const deadline = setTimeout(() => {
2494
+ process.exit(process.exitCode ?? code);
2495
+ }, timeoutMs);
2496
+ exitFlushInFlight = flushTelemetry().catch(() => void 0).finally(() => {
2497
+ clearTimeout(deadline);
2498
+ });
2499
+ };
2500
+ process.on("beforeExit", listener);
2501
+ removeExitFlush = () => {
2502
+ process.removeListener("beforeExit", listener);
2503
+ removeExitFlush = void 0;
2504
+ };
2505
+ }
2506
+ function uninstallProcessHandlers() {
2507
+ for (const removeHandler of removeOwnedHandlers) removeHandler();
2508
+ removeOwnedHandlers = [];
2509
+ removeExitFlush?.();
2510
+ shutdownInFlight = void 0;
2511
+ exitFlushInFlight = void 0;
1635
2512
  }
1636
2513
 
1637
2514
  //#endregion
@@ -2470,6 +3347,15 @@ function errorCode(cause) {
2470
3347
  return asString(readProperty(cause, "code"));
2471
3348
  }
2472
3349
  /**
3350
+ * A bare "Flush timeout" left you with a stack trace into bundled internals and
3351
+ * nothing to act on. Name how long we waited and where we were sending, since
3352
+ * the usual cause is that nothing is listening at that endpoint.
3353
+ */
3354
+ function flushTimeoutMessage(timeout) {
3355
+ const endpoint = getConfig()?.endpoint;
3356
+ return `Flush timeout after ${timeout}ms${endpoint === void 0 ? "" : ` sending to ${endpoint}`}. No collector acknowledged the export; check that one is running and reachable.`;
3357
+ }
3358
+ /**
2473
3359
  * True when the error (or every error it wraps) is an unreachable-endpoint
2474
3360
  * failure. Traverses `AggregateError.errors` and the `cause` chain, since the
2475
3361
  * SDK often wraps the underlying network error.
@@ -2526,7 +3412,7 @@ async function flush(options) {
2526
3412
  await Promise.race([doFlush().finally(() => {
2527
3413
  if (timeoutHandle) clearTimeout(timeoutHandle);
2528
3414
  }), new Promise((_, reject) => {
2529
- timeoutHandle = setTimeout(() => reject(/* @__PURE__ */ new Error("Flush timeout")), timeout);
3415
+ timeoutHandle = setTimeout(() => reject(new Error(flushTimeoutMessage(timeout))), timeout);
2530
3416
  timeoutHandle.unref();
2531
3417
  })]);
2532
3418
  } catch (error) {
@@ -3334,6 +4220,18 @@ function warnIfNotInitialized(context) {
3334
4220
  }
3335
4221
  }
3336
4222
  /**
4223
+ * The sampler `init()` was configured with, or undefined when it was not.
4224
+ *
4225
+ * Separate from {@link getDefaultSampler} on purpose: the tracing wrapper needs
4226
+ * to know whether a sampler was *chosen*. Falling back to the production preset
4227
+ * there would start dropping spans for every app that never asked to sample.
4228
+ *
4229
+ * @internal
4230
+ */
4231
+ function getConfiguredSampler() {
4232
+ return config?.sampler;
4233
+ }
4234
+ /**
3337
4235
  * @internal Close embedded devtools if running.
3338
4236
  */
3339
4237
  async function _closeEmbeddedDevtools() {
@@ -3350,4 +4248,4 @@ function getSdk() {
3350
4248
  }
3351
4249
 
3352
4250
  //#endregion
3353
- export { createStringRedactor as A, setCorrelationId as C, getActiveContextWithBaggage as D, defineBaggageSchema as E, getContextStorage as O, runWithCorrelationId as S, createTraceContext as T, CORRELATION_ID_BAGGAGE_KEY as _, isInitialized as a, getCorrelationStorage as b, flush as c, getEvents as d, resetEvents as f, track as g, getEventQueue as h, init as i, BaggageSpanProcessor as j, hasExplicitSpanStatus as k, shutdown as l, runInOperationContext as m, getLogger as n, isLoggerLocked as o, getOperationContext as p, getSdk as r, lockLogger as s, getConfig as t, Event as u, generateCorrelationId as v, setCorrelationIdInBaggage as w, getOrCreateCorrelationId as x, getCorrelationId as y };
4251
+ export { getCorrelationStorage as $, FeatureFlagSampler as A, markForceKept as B, AUTOTEL_SAMPLING_RATE as C, AlwaysSampler as D, AdaptiveSampler as E, createLinkFromHeaders as F, getActiveContextWithBaggage as G, samplingPresets as H, debugCaptureRequested as I, getEventQueue as J, getContextStorage as K, extractLinksFromBatch as L, NeverSampler as M, RandomSampler as N, CompositeSampler as O, UserIdSampler as P, getCorrelationId as Q, hashUnitInterval as R, AUTOTEL_DEBUG_BAGGAGE_KEY as S, AUTOTEL_SAMPLING_TAIL_KEEP as T, createTraceContext as U, resolveSamplingPreset as V, defineBaggageSchema as W, CORRELATION_ID_BAGGAGE_KEY as X, track as Y, generateCorrelationId as Z, loadYamlConfig as _, init as a, BaggageSpanProcessor as b, lockLogger as c, Event as d, getOrCreateCorrelationId as et, getEvents as f, hasYamlConfig as g, runInOperationContext as h, getSdk as i, KeyTargetRateSampler as j, DeterministicSampler as k, flush as l, getOperationContext as m, getConfiguredSampler as n, setCorrelationId as nt, isInitialized as o, resetEvents as p, hasExplicitSpanStatus as q, getLogger as r, setCorrelationIdInBaggage as rt, isLoggerLocked as s, getConfig as t, runWithCorrelationId as tt, shutdown as u, loadYamlConfigFromFile as v, AUTOTEL_SAMPLING_TAIL_EVALUATED as w, TailSamplingSpanProcessor as x, createStringRedactor as y, isForceKept as z };