autotel 4.1.0 → 4.2.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 (154) hide show
  1. package/package.json +1 -2
  2. package/src/attribute-redacting-processor.test.ts +0 -763
  3. package/src/attribute-redacting-processor.ts +0 -621
  4. package/src/attributes/attachers.ts +0 -161
  5. package/src/attributes/builders.ts +0 -529
  6. package/src/attributes/domains.ts +0 -42
  7. package/src/attributes/index.ts +0 -81
  8. package/src/attributes/registry.ts +0 -323
  9. package/src/attributes/types.ts +0 -211
  10. package/src/attributes/utils.ts +0 -64
  11. package/src/attributes/validators.ts +0 -266
  12. package/src/attributes.test.ts +0 -292
  13. package/src/auto.ts +0 -67
  14. package/src/autotel-logger.test.ts +0 -548
  15. package/src/autotel-logger.ts +0 -364
  16. package/src/baggage-span-processor.test.ts +0 -202
  17. package/src/baggage-span-processor.ts +0 -100
  18. package/src/business-baggage.test.ts +0 -500
  19. package/src/business-baggage.ts +0 -669
  20. package/src/circuit-breaker.test.ts +0 -341
  21. package/src/circuit-breaker.ts +0 -184
  22. package/src/config.test.ts +0 -94
  23. package/src/config.ts +0 -172
  24. package/src/correlated-events.test.ts +0 -151
  25. package/src/correlated-events.ts +0 -47
  26. package/src/correlation-id.test.ts +0 -163
  27. package/src/correlation-id.ts +0 -206
  28. package/src/db.test.ts +0 -252
  29. package/src/db.ts +0 -447
  30. package/src/decorators.test.ts +0 -153
  31. package/src/decorators.ts +0 -188
  32. package/src/define-event.test.ts +0 -41
  33. package/src/define-event.ts +0 -58
  34. package/src/devtools.ts +0 -60
  35. package/src/drain-pipeline.test.ts +0 -68
  36. package/src/drain-pipeline.ts +0 -199
  37. package/src/drain-toolkit.test.ts +0 -113
  38. package/src/drain-toolkit.ts +0 -129
  39. package/src/enricher-toolkit.test.ts +0 -67
  40. package/src/enricher-toolkit.ts +0 -79
  41. package/src/enrichers.test.ts +0 -150
  42. package/src/enrichers.ts +0 -145
  43. package/src/env-config.test.ts +0 -323
  44. package/src/env-config.ts +0 -309
  45. package/src/error-catalog.test.ts +0 -133
  46. package/src/error-catalog.ts +0 -262
  47. package/src/event-queue.test.ts +0 -864
  48. package/src/event-queue.ts +0 -699
  49. package/src/event-subscriber.ts +0 -262
  50. package/src/event-testing.ts +0 -197
  51. package/src/event.test.ts +0 -1104
  52. package/src/event.ts +0 -988
  53. package/src/events-config.ts +0 -235
  54. package/src/exporters.ts +0 -165
  55. package/src/filtering-span-processor.test.ts +0 -281
  56. package/src/filtering-span-processor.ts +0 -111
  57. package/src/flatten-attributes.test.ts +0 -76
  58. package/src/flatten-attributes.ts +0 -80
  59. package/src/functional.strict-types.typecheck.ts +0 -53
  60. package/src/functional.test.ts +0 -1464
  61. package/src/functional.ts +0 -2539
  62. package/src/functional.types.test.ts +0 -135
  63. package/src/hook.mjs +0 -15
  64. package/src/http.test.ts +0 -485
  65. package/src/http.ts +0 -424
  66. package/src/index.ts +0 -433
  67. package/src/init-auto-redactor.test.ts +0 -53
  68. package/src/init-redactor.test.ts +0 -8
  69. package/src/init.customization.test.ts +0 -665
  70. package/src/init.integrations.test.ts +0 -399
  71. package/src/init.openllmetry.test.ts +0 -194
  72. package/src/init.protocol.test.ts +0 -215
  73. package/src/init.ts +0 -2439
  74. package/src/instrumentation.test.ts +0 -108
  75. package/src/instrumentation.ts +0 -319
  76. package/src/logger.test.ts +0 -125
  77. package/src/logger.ts +0 -341
  78. package/src/messaging-adapters.test.ts +0 -595
  79. package/src/messaging-adapters.ts +0 -583
  80. package/src/messaging-testing.test.ts +0 -573
  81. package/src/messaging-testing.ts +0 -935
  82. package/src/messaging.test.ts +0 -1646
  83. package/src/messaging.ts +0 -2245
  84. package/src/metric-helpers.ts +0 -47
  85. package/src/metric-testing.ts +0 -197
  86. package/src/metric.ts +0 -446
  87. package/src/metrics.test.ts +0 -241
  88. package/src/node-require.ts +0 -123
  89. package/src/operation-context.ts +0 -93
  90. package/src/parse-error.test.ts +0 -73
  91. package/src/parse-error.ts +0 -112
  92. package/src/posthog-logs.test.ts +0 -115
  93. package/src/posthog-logs.ts +0 -77
  94. package/src/pretty-console-exporter.test.ts +0 -545
  95. package/src/pretty-console-exporter.ts +0 -413
  96. package/src/pretty-log-formatter.test.ts +0 -123
  97. package/src/pretty-log-formatter.ts +0 -210
  98. package/src/processors/canonical-log-line-processor.test.ts +0 -523
  99. package/src/processors/canonical-log-line-processor.ts +0 -396
  100. package/src/processors.ts +0 -152
  101. package/src/rate-limiter.test.ts +0 -199
  102. package/src/rate-limiter.ts +0 -98
  103. package/src/redact-values.test.ts +0 -90
  104. package/src/redact-values.ts +0 -34
  105. package/src/register.ts +0 -37
  106. package/src/request-logger.test.ts +0 -545
  107. package/src/request-logger.ts +0 -342
  108. package/src/sampling.test.ts +0 -1060
  109. package/src/sampling.ts +0 -737
  110. package/src/security-schema.test.ts +0 -45
  111. package/src/security-schema.ts +0 -107
  112. package/src/semantic-conventions.ts +0 -15
  113. package/src/semantic-helpers.test.ts +0 -226
  114. package/src/semantic-helpers.ts +0 -438
  115. package/src/shutdown.test.ts +0 -364
  116. package/src/shutdown.ts +0 -246
  117. package/src/span-name-normalizer.test.ts +0 -377
  118. package/src/span-name-normalizer.ts +0 -213
  119. package/src/stable-hash.ts +0 -27
  120. package/src/structured-error.test.ts +0 -191
  121. package/src/structured-error.ts +0 -157
  122. package/src/stub.integration.test.ts +0 -361
  123. package/src/tail-sampling-processor.test.ts +0 -230
  124. package/src/tail-sampling-processor.ts +0 -55
  125. package/src/test-span-collector.test.ts +0 -234
  126. package/src/test-span-collector.ts +0 -150
  127. package/src/testing.ts +0 -705
  128. package/src/trace-context.test.ts +0 -73
  129. package/src/trace-context.ts +0 -567
  130. package/src/trace-helpers.new.test.ts +0 -278
  131. package/src/trace-helpers.test.ts +0 -290
  132. package/src/trace-helpers.ts +0 -710
  133. package/src/trace-hybrid.test.ts +0 -42
  134. package/src/trace-hybrid.ts +0 -37
  135. package/src/tracer-provider.test.ts +0 -183
  136. package/src/tracer-provider.ts +0 -266
  137. package/src/track.test.ts +0 -154
  138. package/src/track.ts +0 -216
  139. package/src/validate.test.ts +0 -287
  140. package/src/validate.ts +0 -307
  141. package/src/validation-attributes.ts +0 -43
  142. package/src/validation.test.ts +0 -330
  143. package/src/validation.ts +0 -246
  144. package/src/variable-name-inference.test.ts +0 -178
  145. package/src/variable-name-inference.ts +0 -242
  146. package/src/webhook.test.ts +0 -649
  147. package/src/webhook.ts +0 -637
  148. package/src/workflow-distributed.test.ts +0 -786
  149. package/src/workflow-distributed.ts +0 -916
  150. package/src/workflow.async-safety.integration.test.ts +0 -345
  151. package/src/workflow.test.ts +0 -647
  152. package/src/workflow.ts +0 -810
  153. package/src/yaml-config.test.ts +0 -373
  154. package/src/yaml-config.ts +0 -351
package/src/event.ts DELETED
@@ -1,988 +0,0 @@
1
- /**
2
- * Events API for product events platforms
3
- *
4
- * Track user behavior, business events, and critical actions.
5
- * Sends to product events platforms (PostHog, Mixpanel, Amplitude) via subscribers.
6
- * For business people who think in events/funnels.
7
- *
8
- * For OpenTelemetry metrics (Prometheus/Grafana), use the Metrics class instead.
9
- *
10
- * @example Recommended: Configure subscribers in init(), use track() function
11
- * ```typescript
12
- * import { init, track } from 'autotel';
13
- * import { PostHogSubscriber } from 'autotel-subscribers/posthog';
14
- *
15
- * init({
16
- * service: 'my-app',
17
- * subscribers: [new PostHogSubscriber({ apiKey: 'phc_...' })]
18
- * });
19
- *
20
- * // Track events - uses subscribers from init()
21
- * track('application.submitted', { jobId: '123', userId: '456' });
22
- * ```
23
- *
24
- * @example Create Event instance (inherits subscribers from init)
25
- * ```typescript
26
- * import { Event } from 'autotel/event';
27
- *
28
- * // Uses subscribers configured in init()
29
- * const event = new Event('job-application');
30
- * event.trackEvent('application.submitted', { jobId: '123' });
31
- * ```
32
- *
33
- * @example Override subscribers for specific Event instance
34
- * ```typescript
35
- * import { Event } from 'autotel/event';
36
- * import { PostHogSubscriber } from 'autotel-subscribers/posthog';
37
- *
38
- * // Override: use different subscribers for this instance
39
- * const event = new Event('job-application', {
40
- * subscribers: [new PostHogSubscriber({ apiKey: 'phc_different_project' })]
41
- * });
42
- *
43
- * event.trackEvent('application.submitted', { jobId: '123' });
44
- * ```
45
- */
46
-
47
- import { trace, propagation, context, TraceFlags } from '@opentelemetry/api';
48
- import { type Logger } from './logger';
49
- import {
50
- getLogger,
51
- getValidationConfig,
52
- getConfig,
53
- getEventsConfig,
54
- } from './init';
55
- import {
56
- type EventSubscriber,
57
- type EventAttributes,
58
- type EventAttributesInput,
59
- type FunnelStatus,
60
- type OutcomeStatus,
61
- type AutotelEventContext,
62
- } from './event-subscriber';
63
- import { type EventCollector } from './event-testing';
64
- import { CircuitBreaker, CircuitOpenError } from './circuit-breaker';
65
- import { validateEvent } from './validation';
66
- import { getOperationContext } from './operation-context';
67
- import {
68
- type EnrichFromBaggageConfig,
69
- hashValue,
70
- hashLinkedTraceIds,
71
- } from './events-config';
72
- import { getOrCreateCorrelationId } from './correlation-id';
73
-
74
- // Re-export types for convenience
75
- export type {
76
- EventAttributes,
77
- EventAttributesInput,
78
- FunnelStatus,
79
- OutcomeStatus,
80
- } from './event-subscriber';
81
-
82
- /**
83
- * Events class for tracking user behavior and product events
84
- *
85
- * Track critical indicators such as:
86
- * - User events (signups, purchases, feature usage)
87
- * - Conversion funnels (signup → activation → purchase)
88
- * - Business outcomes (success/failure rates)
89
- * - Product metrics (revenue, engagement, retention)
90
- *
91
- * All events are sent to events platforms via subscribers (PostHog, Mixpanel, etc.).
92
- * For OpenTelemetry metrics, use the Metrics class instead.
93
- */
94
- /**
95
- * Events options
96
- */
97
- export interface EventsOptions {
98
- /** Optional logger for audit trail */
99
- logger?: Logger;
100
- /** Optional collector for testing (captures events in memory) */
101
- collector?: EventCollector;
102
- /**
103
- * Optional subscribers to send events to other platforms
104
- * (e.g., PostHog, Mixpanel, Amplitude)
105
- *
106
- * **Subscriber Resolution**:
107
- * - If provided → uses these subscribers (instance override)
108
- * - If not provided → falls back to subscribers from `init()` (global config)
109
- * - If neither → no subscribers (events logged only)
110
- *
111
- * Install `autotel-subscribers` package for ready-made subscribers
112
- */
113
- subscribers?: EventSubscriber[];
114
- }
115
-
116
- export class Event {
117
- private serviceName: string;
118
- private logger?: Logger;
119
- private collector?: EventCollector;
120
- private subscribers: EventSubscriber[];
121
- private hasSubscribers: boolean; // Cached for performance
122
- private circuitBreakers: Map<EventSubscriber, CircuitBreaker>; // One per subscriber
123
-
124
- /**
125
- * Create a new Event instance
126
- *
127
- * **Note**: Most users should use `init()` + `track()` instead of creating Event instances directly.
128
- *
129
- * **Subscriber Resolution**:
130
- * - If `subscribers` provided in options → uses those (instance override)
131
- * - If `subscribers` not provided → falls back to subscribers from `init()` (global config)
132
- * - If neither → no subscribers (events logged only)
133
- *
134
- * @param serviceName - Service name for identifying events
135
- * @param options - Optional configuration (logger, collector, subscribers)
136
- *
137
- * @example Recommended: Use track() with init()
138
- * ```typescript
139
- * import { init, track } from 'autotel';
140
- * import { PostHogSubscriber } from 'autotel-subscribers/posthog';
141
- *
142
- * init({
143
- * service: 'checkout',
144
- * subscribers: [new PostHogSubscriber({ apiKey: 'phc_...' })]
145
- * });
146
- *
147
- * track('purchase.completed', { amount: 99.99 });
148
- * ```
149
- *
150
- * @example Inherit subscribers from init()
151
- * ```typescript
152
- * // Uses subscribers configured in init()
153
- * const event = new Event('checkout');
154
- * event.trackEvent('purchase.completed', { amount: 99.99 });
155
- * ```
156
- *
157
- * @example Override subscribers for this instance
158
- * ```typescript
159
- * import { Event } from 'autotel/event';
160
- * import { PostHogSubscriber } from 'autotel-subscribers/posthog';
161
- *
162
- * // Override: use different subscribers for this instance only
163
- * const event = new Event('checkout', {
164
- * subscribers: [new PostHogSubscriber({ apiKey: 'phc_different_project' })]
165
- * });
166
- * ```
167
- */
168
- constructor(serviceName: string, options: EventsOptions = {}) {
169
- this.serviceName = serviceName;
170
- this.logger = options.logger;
171
- this.collector = options.collector;
172
-
173
- // Subscriber resolution: instance-level overrides global init() config
174
- // If subscribers provided to constructor, use those
175
- // Otherwise, fall back to subscribers from init()
176
- this.subscribers =
177
- options.subscribers === undefined
178
- ? getConfig()?.subscribers || []
179
- : options.subscribers;
180
-
181
- this.hasSubscribers = this.subscribers.length > 0; // Cache for hot path
182
-
183
- // Create circuit breaker for each subscriber
184
- this.circuitBreakers = new Map();
185
- for (const subscriber of this.subscribers) {
186
- const subscriberName = subscriber.name || 'Unknown';
187
- this.circuitBreakers.set(
188
- subscriber,
189
- new CircuitBreaker(subscriberName, {
190
- failureThreshold: 5,
191
- resetTimeout: 30_000, // 30s
192
- windowSize: 60_000, // 1min
193
- }),
194
- );
195
- }
196
- }
197
-
198
- /**
199
- * Automatically enrich attributes with all available telemetry context
200
- *
201
- * Auto-captures:
202
- * - Resource attributes: service.version, deployment.environment
203
- * - Trace context: traceId, spanId, correlationId
204
- * - Operation context: operation.name
205
- */
206
- private enrichWithTelemetryContext(
207
- attributes: EventAttributes = {},
208
- ): EventAttributes {
209
- const enriched: EventAttributes = {
210
- service: this.serviceName,
211
- ...attributes,
212
- };
213
-
214
- // 1. Resource attributes (service-level context)
215
- const config = getConfig();
216
- if (config) {
217
- if (config.version) {
218
- enriched['service.version'] = config.version;
219
- }
220
- if (config.environment) {
221
- enriched['deployment.environment'] = config.environment;
222
- }
223
- }
224
-
225
- // 2. Trace context (if inside a traced operation)
226
- const span = trace.getActiveSpan();
227
- const spanContext = span?.spanContext();
228
- if (spanContext) {
229
- enriched.traceId = spanContext.traceId;
230
- enriched.spanId = spanContext.spanId;
231
- // Add correlation ID (first 16 chars of trace ID) for easier log grouping
232
- enriched.correlationId = spanContext.traceId.slice(0, 16);
233
- }
234
-
235
- // 3. Operation context (if inside a trace/span)
236
- const operationContext = getOperationContext();
237
- if (operationContext) {
238
- enriched['operation.name'] = operationContext.name;
239
- }
240
-
241
- return enriched;
242
- }
243
-
244
- /**
245
- * Build autotel event context for trace correlation
246
- *
247
- * Works in 4 contexts:
248
- * 1. Inside a span → use current span's trace_id + span_id
249
- * 2. Outside span but in AsyncLocalStorage context → use trace_id + correlation_id
250
- * 3. Totally standalone → use correlation_id + service/env/version
251
- * 4. Batch/fan-in (multiple linked parents) → use count + hash or full array
252
- *
253
- * @returns AutotelEventContext or undefined if trace context is disabled
254
- */
255
- private buildAutotelContext(): AutotelEventContext | undefined {
256
- const eventsConfig = getEventsConfig();
257
-
258
- // Return undefined if trace context is not enabled
259
- if (!eventsConfig?.includeTraceContext) {
260
- // Still generate correlation_id even without full trace context
261
- // This provides a stable join key across events/logs/spans
262
- return {
263
- correlation_id: getOrCreateCorrelationId(),
264
- };
265
- }
266
-
267
- const config = getConfig();
268
- const span = trace.getActiveSpan();
269
- const spanContext = span?.spanContext();
270
-
271
- // Always generate a correlation_id
272
- const correlationId = getOrCreateCorrelationId();
273
-
274
- // Build base context
275
- const autotelContext: AutotelEventContext = {
276
- correlation_id: correlationId,
277
- };
278
-
279
- // Add trace context if inside a span
280
- if (spanContext) {
281
- autotelContext.trace_id = spanContext.traceId;
282
- autotelContext.span_id = spanContext.spanId;
283
-
284
- // Trace flags as 2-char hex string (canonical format)
285
- autotelContext.trace_flags = spanContext.traceFlags
286
- .toString(16)
287
- .padStart(2, '0');
288
-
289
- // Tracestate if present
290
- const traceState = spanContext.traceState;
291
- if (traceState) {
292
- // Convert TraceState to string representation safely
293
- let traceStateStr = '';
294
- try {
295
- if (typeof traceState.serialize === 'function') {
296
- traceStateStr = traceState.serialize();
297
- }
298
- } catch {
299
- // Silently ignore serialization errors - traceState is optional metadata
300
- }
301
- if (traceStateStr) {
302
- autotelContext.trace_state = traceStateStr;
303
- }
304
- }
305
-
306
- // Generate trace URL if configured
307
- if (eventsConfig.traceUrl) {
308
- const traceUrl = eventsConfig.traceUrl({
309
- traceId: spanContext.traceId,
310
- spanId: spanContext.spanId,
311
- correlationId,
312
- serviceName: config?.service || this.serviceName,
313
- environment: config?.environment,
314
- });
315
- if (traceUrl) {
316
- autotelContext.trace_url = traceUrl;
317
- }
318
- }
319
-
320
- // Handle linked spans (batch/fan-in scenarios)
321
- // Note: This would require access to span links which are not easily accessible
322
- // from the public OpenTelemetry API. For now, we skip this unless we have
323
- // explicit linked trace IDs passed in.
324
- } else {
325
- // Outside span but may still have trace URL generator
326
- if (eventsConfig.traceUrl && config) {
327
- const traceUrl = eventsConfig.traceUrl({
328
- correlationId,
329
- serviceName: config.service,
330
- environment: config.environment,
331
- });
332
- if (traceUrl) {
333
- autotelContext.trace_url = traceUrl;
334
- }
335
- }
336
- }
337
-
338
- return autotelContext;
339
- }
340
-
341
- /**
342
- * Enrich event attributes from baggage with guardrails
343
- *
344
- * @param attributes - Current event attributes
345
- * @returns Enriched attributes with baggage values
346
- */
347
- private enrichFromBaggage(attributes: EventAttributes): EventAttributes {
348
- const eventsConfig = getEventsConfig();
349
- const enrichConfig = eventsConfig?.enrichFromBaggage;
350
-
351
- if (!enrichConfig) {
352
- return attributes;
353
- }
354
-
355
- const enriched = { ...attributes };
356
- const activeContext = context.active();
357
- const baggage = propagation.getBaggage(activeContext);
358
-
359
- if (!baggage) {
360
- return enriched;
361
- }
362
-
363
- let keyCount = 0;
364
- let byteCount = 0;
365
- const maxKeys = enrichConfig.maxKeys ?? 10;
366
- const maxBytes = enrichConfig.maxBytes ?? 1024;
367
- const prefix = enrichConfig.prefix ?? '';
368
-
369
- // Get all baggage entries
370
- for (const [key, entry] of baggage.getAllEntries()) {
371
- // Check if key is allowed
372
- if (!this.isBaggageKeyAllowed(key, enrichConfig)) {
373
- continue;
374
- }
375
-
376
- // Check limits
377
- if (keyCount >= maxKeys) {
378
- break;
379
- }
380
-
381
- const value = entry.value;
382
-
383
- // Apply transform first so maxBytes is checked against transformed size (e.g. hash output)
384
- const transform = enrichConfig.transform?.[key];
385
- let transformedValue: string;
386
-
387
- if (transform === 'hash') {
388
- transformedValue = hashValue(value);
389
- } else if (transform === 'plain' || !transform) {
390
- transformedValue = value;
391
- } else if (typeof transform === 'function') {
392
- transformedValue = transform(value);
393
- } else {
394
- transformedValue = value;
395
- }
396
-
397
- const valueBytes = new TextEncoder().encode(transformedValue).length;
398
-
399
- if (byteCount + valueBytes > maxBytes) {
400
- continue; // Skip this entry if transformed value would exceed byte limit
401
- }
402
-
403
- // Add to enriched attributes with prefix
404
- const enrichedKey = `${prefix}${key}`;
405
- enriched[enrichedKey] = transformedValue;
406
-
407
- keyCount++;
408
- byteCount += valueBytes;
409
- }
410
-
411
- return enriched;
412
- }
413
-
414
- /**
415
- * Check if a baggage key is allowed based on config
416
- */
417
- private isBaggageKeyAllowed(
418
- key: string,
419
- config: EnrichFromBaggageConfig,
420
- ): boolean {
421
- // Check deny list first (takes precedence)
422
- if (config.deny) {
423
- for (const pattern of config.deny) {
424
- if (this.matchesBaggagePattern(key, pattern)) {
425
- return false;
426
- }
427
- }
428
- }
429
-
430
- // Check allow list
431
- for (const pattern of config.allow) {
432
- if (this.matchesBaggagePattern(key, pattern)) {
433
- return true;
434
- }
435
- }
436
-
437
- return false;
438
- }
439
-
440
- /**
441
- * Check if a key matches a baggage pattern
442
- * Supports exact matches and wildcard patterns (e.g., 'tenant.*')
443
- */
444
- private matchesBaggagePattern(key: string, pattern: string): boolean {
445
- if (pattern.endsWith('.*')) {
446
- const prefix = pattern.slice(0, -2);
447
- return key.startsWith(prefix + '.');
448
- }
449
- return key === pattern;
450
- }
451
-
452
- /**
453
- * Track a business event
454
- *
455
- * Use this for tracking user actions, business events, product usage:
456
- * - "user.signup"
457
- * - "order.completed"
458
- * - "feature.used"
459
- *
460
- * Events are sent to configured subscribers (PostHog, Mixpanel, etc.).
461
- *
462
- * @example
463
- * ```typescript
464
- * // Track user signup
465
- * events.trackEvent('user.signup', {
466
- * userId: '123',
467
- * plan: 'pro'
468
- * })
469
- *
470
- * // Track order
471
- * events.trackEvent('order.completed', {
472
- * orderId: 'ord_123',
473
- * amount: 99.99
474
- * })
475
- * ```
476
- */
477
- trackEvent(eventName: string, attributes?: EventAttributes): void {
478
- // Validate and sanitize input (with custom config if provided)
479
- const validationConfig = getValidationConfig();
480
- const validated = validateEvent(
481
- eventName,
482
- attributes,
483
- validationConfig || undefined,
484
- );
485
-
486
- // Auto-attach all available telemetry context
487
- const enrichedAttributes = this.enrichWithTelemetryContext(
488
- validated.attributes,
489
- );
490
-
491
- this.logger?.info(
492
- {
493
- event: validated.eventName,
494
- attributes: enrichedAttributes,
495
- },
496
- 'Event tracked',
497
- );
498
-
499
- // Record for testing
500
- this.collector?.recordEvent({
501
- event: validated.eventName,
502
- attributes: enrichedAttributes,
503
- service: this.serviceName,
504
- timestamp: Date.now(),
505
- });
506
-
507
- // Notify subscribers (zero overhead if no subscribers)
508
- // Run in background - don't block event recording
509
- if (this.hasSubscribers) {
510
- // Build autotel context for trace correlation
511
- const autotelContext = this.buildAutotelContext();
512
-
513
- // Enrich from baggage if configured
514
- const finalAttributes = this.enrichFromBaggage(enrichedAttributes);
515
-
516
- void this.notifySubscribers((subscriber) =>
517
- subscriber.trackEvent(validated.eventName, finalAttributes, {
518
- autotel: autotelContext,
519
- }),
520
- );
521
- }
522
- }
523
-
524
- /**
525
- * Notify all subscribers concurrently without blocking
526
- * Uses circuit breakers to protect against failing subscribers
527
- * Uses Promise.allSettled to prevent subscriber errors from affecting other subscribers
528
- */
529
- private async notifySubscribers(
530
- fn: (subscriber: EventSubscriber) => Promise<void>,
531
- ): Promise<void> {
532
- const promises = this.subscribers.map(async (subscriber) => {
533
- const circuitBreaker = this.circuitBreakers.get(subscriber);
534
- if (!circuitBreaker) return; // Should never happen
535
-
536
- try {
537
- // Execute with circuit breaker protection
538
- await circuitBreaker.execute(() => fn(subscriber));
539
- } catch (error) {
540
- // Handle circuit open errors (expected behavior when subscriber is down)
541
- if (error instanceof CircuitOpenError) {
542
- // Circuit is open - subscriber is down, log at warn level for visibility (same behavior in all environments)
543
- getLogger().warn(
544
- {
545
- subscriberName: subscriber.name || 'Unknown',
546
- },
547
- `[Events] ${error.message}`,
548
- );
549
- return;
550
- }
551
-
552
- // Log other subscriber errors but don't throw - event failures shouldn't break business logic
553
- getLogger().error(
554
- {
555
- err: error instanceof Error ? error : undefined,
556
- subscriberName: subscriber.name || 'Unknown',
557
- },
558
- `[Events] Subscriber ${subscriber.name || 'Unknown'} failed`,
559
- );
560
- }
561
- });
562
-
563
- // Wait for all subscribers (success or failure)
564
- await Promise.allSettled(promises);
565
- }
566
-
567
- /**
568
- * Track conversion funnel steps
569
- *
570
- * Monitor where users drop off in multi-step processes.
571
- *
572
- * @example
573
- * ```typescript
574
- * // Track signup funnel
575
- * events.trackFunnelStep('signup', 'started', { userId: '123' })
576
- * events.trackFunnelStep('signup', 'email_verified', { userId: '123' })
577
- * events.trackFunnelStep('signup', 'completed', { userId: '123' })
578
- *
579
- * // Track checkout flow
580
- * events.trackFunnelStep('checkout', 'started', { cartValue: 99.99 })
581
- * events.trackFunnelStep('checkout', 'payment_info', { cartValue: 99.99 })
582
- * events.trackFunnelStep('checkout', 'completed', { cartValue: 99.99 })
583
- * ```
584
- */
585
- trackFunnelStep(
586
- funnelName: string,
587
- status: FunnelStatus,
588
- attributes?: EventAttributes,
589
- ): void {
590
- // Auto-attach all available telemetry context
591
- const enrichedAttributes = this.enrichWithTelemetryContext(attributes);
592
-
593
- this.logger?.info(
594
- {
595
- funnel: funnelName,
596
- status,
597
- attributes: enrichedAttributes,
598
- },
599
- 'Funnel step tracked',
600
- );
601
-
602
- // Record for testing
603
- this.collector?.recordFunnelStep({
604
- funnel: funnelName,
605
- status,
606
- attributes: enrichedAttributes,
607
- service: this.serviceName,
608
- timestamp: Date.now(),
609
- });
610
-
611
- // Notify subscribers
612
- if (this.hasSubscribers) {
613
- const autotelContext = this.buildAutotelContext();
614
- const finalAttributes = this.enrichFromBaggage(enrichedAttributes);
615
-
616
- void this.notifySubscribers((subscriber) =>
617
- subscriber.trackFunnelStep(funnelName, status, finalAttributes, {
618
- autotel: autotelContext,
619
- }),
620
- );
621
- }
622
- }
623
-
624
- /**
625
- * Track outcomes (success/failure/partial)
626
- *
627
- * Monitor success rates of critical operations.
628
- *
629
- * @example
630
- * ```typescript
631
- * // Track email delivery
632
- * events.trackOutcome('email.delivery', 'success', {
633
- * recipientType: 'user',
634
- * emailType: 'welcome'
635
- * })
636
- *
637
- * events.trackOutcome('email.delivery', 'failure', {
638
- * recipientType: 'user',
639
- * errorCode: 'invalid_email'
640
- * })
641
- *
642
- * // Track payment processing
643
- * events.trackOutcome('payment.process', 'success', { amount: 99.99 })
644
- * events.trackOutcome('payment.process', 'failure', { error: 'insufficient_funds' })
645
- * ```
646
- */
647
- trackOutcome(
648
- operationName: string,
649
- status: OutcomeStatus,
650
- attributes?: EventAttributes,
651
- ): void {
652
- // Auto-attach all available telemetry context
653
- const enrichedAttributes = this.enrichWithTelemetryContext(attributes);
654
-
655
- this.logger?.info(
656
- {
657
- operation: operationName,
658
- status,
659
- attributes: enrichedAttributes,
660
- },
661
- 'Outcome tracked',
662
- );
663
-
664
- // Record for testing
665
- this.collector?.recordOutcome({
666
- operation: operationName,
667
- status,
668
- attributes: enrichedAttributes,
669
- service: this.serviceName,
670
- timestamp: Date.now(),
671
- });
672
-
673
- // Notify subscribers
674
- if (this.hasSubscribers) {
675
- const autotelContext = this.buildAutotelContext();
676
- const finalAttributes = this.enrichFromBaggage(enrichedAttributes);
677
-
678
- void this.notifySubscribers((subscriber) =>
679
- subscriber.trackOutcome(operationName, status, finalAttributes, {
680
- autotel: autotelContext,
681
- }),
682
- );
683
- }
684
- }
685
-
686
- /**
687
- * Track value metrics
688
- *
689
- * Record numerical values like revenue, transaction amounts,
690
- * item counts, processing times, engagement scores, etc.
691
- *
692
- * @example
693
- * ```typescript
694
- * // Track revenue
695
- * events.trackValue('order.revenue', 149.99, {
696
- * currency: 'USD',
697
- * productCategory: 'electronics'
698
- * })
699
- *
700
- * // Track items per cart
701
- * events.trackValue('cart.item_count', 5, {
702
- * userId: '123'
703
- * })
704
- *
705
- * // Track processing time
706
- * events.trackValue('api.response_time', 250, {
707
- * unit: 'ms',
708
- * endpoint: '/api/checkout'
709
- * })
710
- * ```
711
- */
712
- trackValue(
713
- metricName: string,
714
- value: number,
715
- attributes?: EventAttributes,
716
- ): void {
717
- // Auto-attach all available telemetry context
718
- const enrichedAttributes = this.enrichWithTelemetryContext({
719
- metric: metricName,
720
- ...attributes,
721
- });
722
-
723
- this.logger?.debug(
724
- {
725
- metric: metricName,
726
- value,
727
- attributes: enrichedAttributes,
728
- },
729
- 'Value tracked',
730
- );
731
-
732
- // Record for testing
733
- this.collector?.recordValue({
734
- metric: metricName,
735
- value,
736
- attributes: enrichedAttributes,
737
- service: this.serviceName,
738
- timestamp: Date.now(),
739
- });
740
-
741
- // Notify subscribers
742
- if (this.hasSubscribers) {
743
- const autotelContext = this.buildAutotelContext();
744
- const finalAttributes = this.enrichFromBaggage(enrichedAttributes);
745
-
746
- void this.notifySubscribers((subscriber) =>
747
- subscriber.trackValue(metricName, value, finalAttributes, {
748
- autotel: autotelContext,
749
- }),
750
- );
751
- }
752
- }
753
-
754
- /**
755
- * Flush all subscribers and wait for pending events
756
- *
757
- * Call this before shutdown to ensure all events are delivered.
758
- *
759
- * @example
760
- * ```typescript
761
- * const event =new Event('app', { subscribers: [...] });
762
- *
763
- * // Before shutdown
764
- * await events.flush();
765
- * ```
766
- */
767
- async flush(): Promise<void> {
768
- if (!this.hasSubscribers) return;
769
-
770
- const shutdownPromises = this.subscribers.map(async (subscriber) => {
771
- if (subscriber.shutdown) {
772
- try {
773
- await subscriber.shutdown();
774
- } catch (error) {
775
- getLogger().error(
776
- {
777
- err: error instanceof Error ? error : undefined,
778
- subscriberName: subscriber.name || 'Unknown',
779
- },
780
- `[Events] Failed to shutdown subscriber ${subscriber.name || 'Unknown'}`,
781
- );
782
- }
783
- }
784
- });
785
-
786
- await Promise.allSettled(shutdownPromises);
787
- }
788
-
789
- /**
790
- * Shutdown the Event instance and all subscribers
791
- *
792
- * Unlike `flush()`, this method:
793
- * - Shuts down all subscribers
794
- * - Prevents further event tracking (hasSubscribers becomes false)
795
- * - Should only be called once at application shutdown
796
- *
797
- * @example
798
- * ```typescript
799
- * // In Next.js API route with after()
800
- * import { after } from 'next/server';
801
- *
802
- * export async function POST(req: Request) {
803
- * const event = new Event('checkout', { subscribers: [...] });
804
- * event.trackEvent('order.completed', { orderId: '123' });
805
- *
806
- * after(async () => {
807
- * await event.shutdown();
808
- * });
809
- *
810
- * return Response.json({ success: true });
811
- * }
812
- * ```
813
- */
814
- async shutdown(): Promise<void> {
815
- if (!this.hasSubscribers) return;
816
-
817
- await Promise.allSettled(
818
- this.subscribers.map(async (subscriber) => {
819
- if (subscriber.shutdown) {
820
- try {
821
- await subscriber.shutdown();
822
- } catch (error) {
823
- getLogger().error(
824
- {
825
- err: error instanceof Error ? error : undefined,
826
- subscriberName: subscriber.name || 'Unknown',
827
- },
828
- `[Events] Failed to shutdown subscriber ${subscriber.name || 'Unknown'}`,
829
- );
830
- }
831
- }
832
- }),
833
- );
834
-
835
- // Prevent further tracking after shutdown
836
- this.hasSubscribers = false;
837
- }
838
-
839
- /**
840
- * Track funnel progression with custom step names
841
- *
842
- * Unlike trackFunnelStep which uses FunnelStatus enum values,
843
- * this method allows any string as the step name for flexible funnel tracking.
844
- *
845
- * @param funnelName - Name of the funnel (e.g., "checkout", "onboarding")
846
- * @param stepName - Custom step name (e.g., "cart_viewed", "payment_entered")
847
- * @param stepNumber - Optional numeric position in the funnel
848
- * @param attributes - Optional event attributes
849
- *
850
- * @example
851
- * ```typescript
852
- * // Track custom checkout steps
853
- * event.trackFunnelProgression('checkout', 'cart_viewed', 1);
854
- * event.trackFunnelProgression('checkout', 'shipping_selected', 2);
855
- * event.trackFunnelProgression('checkout', 'payment_entered', 3);
856
- * event.trackFunnelProgression('checkout', 'order_confirmed', 4);
857
- * ```
858
- */
859
- trackFunnelProgression(
860
- funnelName: string,
861
- stepName: string,
862
- stepNumber?: number,
863
- attributes?: EventAttributes,
864
- ): void {
865
- // Auto-attach all available telemetry context
866
- const enrichedAttributes = this.enrichWithTelemetryContext(attributes);
867
-
868
- this.logger?.info(
869
- {
870
- funnel: funnelName,
871
- stepName,
872
- stepNumber,
873
- attributes: enrichedAttributes,
874
- },
875
- 'Funnel progression tracked',
876
- );
877
-
878
- // Record for testing (as funnel step with custom name)
879
- this.collector?.recordFunnelStep({
880
- funnel: funnelName,
881
- status: stepName as FunnelStatus, // Cast for testing collector
882
- attributes: {
883
- ...enrichedAttributes,
884
- step_name: stepName,
885
- ...(stepNumber === undefined ? {} : { step_number: stepNumber }),
886
- },
887
- service: this.serviceName,
888
- timestamp: Date.now(),
889
- });
890
-
891
- // Notify subscribers that support trackFunnelProgression
892
- if (this.hasSubscribers) {
893
- const autotelContext = this.buildAutotelContext();
894
- const finalAttributes = this.enrichFromBaggage(enrichedAttributes);
895
-
896
- void this.notifySubscribers(async (subscriber) => {
897
- await (subscriber.trackFunnelProgression
898
- ? subscriber.trackFunnelProgression(
899
- funnelName,
900
- stepName,
901
- stepNumber,
902
- finalAttributes,
903
- { autotel: autotelContext },
904
- )
905
- : // Fall back to trackFunnelStep with step as custom name (cast)
906
- subscriber.trackFunnelStep(
907
- funnelName,
908
- stepName as FunnelStatus,
909
- {
910
- ...finalAttributes,
911
- step_name: stepName,
912
- ...(stepNumber === undefined
913
- ? {}
914
- : { step_number: stepNumber }),
915
- },
916
- { autotel: autotelContext },
917
- ));
918
- });
919
- }
920
- }
921
-
922
- /**
923
- * Track multiple events in a batch
924
- *
925
- * Useful for bulk event tracking with consistent timestamps.
926
- * Events are sent to subscribers individually but processed together.
927
- *
928
- * @param events - Array of events to track
929
- *
930
- * @example
931
- * ```typescript
932
- * event.trackBatch([
933
- * { name: 'item.viewed', attributes: { itemId: '1' } },
934
- * { name: 'item.viewed', attributes: { itemId: '2' } },
935
- * { name: 'cart.updated', attributes: { itemCount: 2 } },
936
- * ]);
937
- * ```
938
- */
939
- trackBatch(
940
- events: Array<{ name: string; attributes?: EventAttributesInput }>,
941
- ): void {
942
- // Filter attributes and track each event
943
- for (const event of events) {
944
- // Filter undefined/null values from attributes
945
- const filteredAttributes = event.attributes
946
- ? (Object.fromEntries(
947
- Object.entries(event.attributes).filter(
948
- ([, v]) => v !== undefined && v !== null,
949
- ),
950
- ) as EventAttributes)
951
- : undefined;
952
-
953
- this.trackEvent(event.name, filteredAttributes);
954
- }
955
- }
956
- }
957
-
958
- /**
959
- * Global events instances (singleton pattern)
960
- */
961
- const eventsInstances = new Map<string, Event>();
962
-
963
- /**
964
- * Get or create an Events instance for a service
965
- *
966
- * @param serviceName - Service name for identifying events
967
- * @param logger - Optional logger
968
- * @returns Events instance
969
- *
970
- * @example
971
- * ```typescript
972
- * const event =getEvents('job-application')
973
- * events.trackEvent('application.submitted', { jobId: '123' })
974
- * ```
975
- */
976
- export function getEvents(serviceName: string, logger?: Logger): Event {
977
- if (!eventsInstances.has(serviceName)) {
978
- eventsInstances.set(serviceName, new Event(serviceName, { logger }));
979
- }
980
- return eventsInstances.get(serviceName)!;
981
- }
982
-
983
- /**
984
- * Reset all events instances (mainly for testing)
985
- */
986
- export function resetEvents(): void {
987
- eventsInstances.clear();
988
- }