autotel 4.1.0 → 4.2.1

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 (253) hide show
  1. package/dist/auto.cjs +5 -3
  2. package/dist/auto.cjs.map +1 -1
  3. package/dist/auto.js +3 -3
  4. package/dist/auto.js.map +1 -1
  5. package/dist/chunk-C_NdSu1c.cjs +34 -0
  6. package/dist/correlation-id.cjs +1 -1
  7. package/dist/correlation-id.d.cts.map +1 -1
  8. package/dist/correlation-id.d.ts.map +1 -1
  9. package/dist/correlation-id.js +1 -1
  10. package/dist/decorators.cjs +1 -1
  11. package/dist/decorators.js +1 -1
  12. package/dist/{event-ByBTV9M2.js → event-531asIM6.js} +4 -4
  13. package/dist/{event-ByBTV9M2.js.map → event-531asIM6.js.map} +1 -1
  14. package/dist/{event-BhHREDJk.cjs → event-CcZYwp50.cjs} +4 -4
  15. package/dist/{event-BhHREDJk.cjs.map → event-CcZYwp50.cjs.map} +1 -1
  16. package/dist/event.cjs +1 -1
  17. package/dist/event.js +1 -1
  18. package/dist/{functional-zpzNLhky.cjs → functional-C8B0Qa7o.cjs} +10 -7
  19. package/dist/functional-C8B0Qa7o.cjs.map +1 -0
  20. package/dist/{functional-DtI0u4vx.js → functional-r-AUIRy_.js} +9 -9
  21. package/dist/functional-r-AUIRy_.js.map +1 -0
  22. package/dist/functional.cjs +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 +15 -13
  27. package/dist/index.cjs.map +1 -1
  28. package/dist/index.d.cts.map +1 -1
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +14 -14
  31. package/dist/index.js.map +1 -1
  32. package/dist/{init-D-jnNMix.js → init-BS2JVkrL.js} +2 -2
  33. package/dist/{init-D-jnNMix.js.map → init-BS2JVkrL.js.map} +1 -1
  34. package/dist/{init-BX7AmFRl.cjs → init-BXiuPK6j.cjs} +3 -3
  35. package/dist/{init-BX7AmFRl.cjs.map → init-BXiuPK6j.cjs.map} +1 -1
  36. package/dist/instrumentation.cjs +2 -2
  37. package/dist/instrumentation.js +2 -2
  38. package/dist/logger.cjs +236 -8
  39. package/dist/logger.cjs.map +1 -0
  40. package/dist/messaging.cjs +1 -1
  41. package/dist/messaging.js +1 -1
  42. package/dist/{node-require-DF5QBX6z.cjs → node-require-CZ_PU448.cjs} +6 -4
  43. package/dist/node-require-CZ_PU448.cjs.map +1 -0
  44. package/dist/{node-require-Db1oDpLj.js → node-require-vROmTeJ8.js} +5 -5
  45. package/dist/node-require-vROmTeJ8.js.map +1 -0
  46. package/dist/{operation-context-C-2hmmtP.js → operation-context-CKBoA4Qy.js} +3 -3
  47. package/dist/operation-context-CKBoA4Qy.js.map +1 -0
  48. package/dist/{operation-context-n4_obUwq.cjs → operation-context-D6LDf4W_.cjs} +3 -1
  49. package/dist/operation-context-D6LDf4W_.cjs.map +1 -0
  50. package/dist/register.cjs +3 -1
  51. package/dist/register.cjs.map +1 -1
  52. package/dist/register.js +2 -2
  53. package/dist/register.js.map +1 -1
  54. package/dist/semantic-helpers.cjs +1 -1
  55. package/dist/semantic-helpers.js +1 -1
  56. package/dist/{stable-hash-Cg5cT34Q.js → stable-hash-ChFBIhNt.js} +3 -3
  57. package/dist/stable-hash-ChFBIhNt.js.map +1 -0
  58. package/dist/{stable-hash-BNTMrmdB.cjs → stable-hash-brKISGf1.cjs} +4 -2
  59. package/dist/stable-hash-brKISGf1.cjs.map +1 -0
  60. package/dist/trace-context-Cijqoi6e.d.cts.map +1 -1
  61. package/dist/trace-context-Cijqoi6e.d.ts.map +1 -1
  62. package/dist/trace-helpers.cjs +1 -1
  63. package/dist/trace-helpers.js +1 -1
  64. package/dist/{track-wc0HafS_.js → track-COUuU48p.js} +5 -5
  65. package/dist/track-COUuU48p.js.map +1 -0
  66. package/dist/{track-D59FfpL0.cjs → track-Cb3Q4QmS.cjs} +4 -2
  67. package/dist/track-Cb3Q4QmS.cjs.map +1 -0
  68. package/dist/validate.cjs +1 -1
  69. package/dist/validate.js +1 -1
  70. package/dist/webhook.cjs +1 -1
  71. package/dist/webhook.js +1 -1
  72. package/dist/workflow-distributed.cjs +1 -1
  73. package/dist/workflow-distributed.js +1 -1
  74. package/dist/workflow.cjs +3 -1
  75. package/dist/workflow.cjs.map +1 -1
  76. package/dist/workflow.d.cts.map +1 -1
  77. package/dist/workflow.d.ts.map +1 -1
  78. package/dist/workflow.js +3 -3
  79. package/dist/workflow.js.map +1 -1
  80. package/dist/yaml-config.cjs +233 -4
  81. package/dist/yaml-config.cjs.map +1 -0
  82. package/dist/yaml-config.d.cts.map +1 -1
  83. package/dist/yaml-config.d.ts.map +1 -1
  84. package/dist/yaml-config.js +8 -7
  85. package/dist/yaml-config.js.map +1 -1
  86. package/package.json +1 -2
  87. package/dist/functional-DtI0u4vx.js.map +0 -1
  88. package/dist/functional-zpzNLhky.cjs.map +0 -1
  89. package/dist/logger-thMPLpOG.cjs +0 -487
  90. package/dist/logger-thMPLpOG.cjs.map +0 -1
  91. package/dist/node-require-DF5QBX6z.cjs.map +0 -1
  92. package/dist/node-require-Db1oDpLj.js.map +0 -1
  93. package/dist/operation-context-C-2hmmtP.js.map +0 -1
  94. package/dist/operation-context-n4_obUwq.cjs.map +0 -1
  95. package/dist/stable-hash-BNTMrmdB.cjs.map +0 -1
  96. package/dist/stable-hash-Cg5cT34Q.js.map +0 -1
  97. package/dist/track-D59FfpL0.cjs.map +0 -1
  98. package/dist/track-wc0HafS_.js.map +0 -1
  99. package/dist/yaml-config-Ck2uB0Dp.cjs +0 -273
  100. package/dist/yaml-config-Ck2uB0Dp.cjs.map +0 -1
  101. package/src/attribute-redacting-processor.test.ts +0 -763
  102. package/src/attribute-redacting-processor.ts +0 -621
  103. package/src/attributes/attachers.ts +0 -161
  104. package/src/attributes/builders.ts +0 -529
  105. package/src/attributes/domains.ts +0 -42
  106. package/src/attributes/index.ts +0 -81
  107. package/src/attributes/registry.ts +0 -323
  108. package/src/attributes/types.ts +0 -211
  109. package/src/attributes/utils.ts +0 -64
  110. package/src/attributes/validators.ts +0 -266
  111. package/src/attributes.test.ts +0 -292
  112. package/src/auto.ts +0 -67
  113. package/src/autotel-logger.test.ts +0 -548
  114. package/src/autotel-logger.ts +0 -364
  115. package/src/baggage-span-processor.test.ts +0 -202
  116. package/src/baggage-span-processor.ts +0 -100
  117. package/src/business-baggage.test.ts +0 -500
  118. package/src/business-baggage.ts +0 -669
  119. package/src/circuit-breaker.test.ts +0 -341
  120. package/src/circuit-breaker.ts +0 -184
  121. package/src/config.test.ts +0 -94
  122. package/src/config.ts +0 -172
  123. package/src/correlated-events.test.ts +0 -151
  124. package/src/correlated-events.ts +0 -47
  125. package/src/correlation-id.test.ts +0 -163
  126. package/src/correlation-id.ts +0 -206
  127. package/src/db.test.ts +0 -252
  128. package/src/db.ts +0 -447
  129. package/src/decorators.test.ts +0 -153
  130. package/src/decorators.ts +0 -188
  131. package/src/define-event.test.ts +0 -41
  132. package/src/define-event.ts +0 -58
  133. package/src/devtools.ts +0 -60
  134. package/src/drain-pipeline.test.ts +0 -68
  135. package/src/drain-pipeline.ts +0 -199
  136. package/src/drain-toolkit.test.ts +0 -113
  137. package/src/drain-toolkit.ts +0 -129
  138. package/src/enricher-toolkit.test.ts +0 -67
  139. package/src/enricher-toolkit.ts +0 -79
  140. package/src/enrichers.test.ts +0 -150
  141. package/src/enrichers.ts +0 -145
  142. package/src/env-config.test.ts +0 -323
  143. package/src/env-config.ts +0 -309
  144. package/src/error-catalog.test.ts +0 -133
  145. package/src/error-catalog.ts +0 -262
  146. package/src/event-queue.test.ts +0 -864
  147. package/src/event-queue.ts +0 -699
  148. package/src/event-subscriber.ts +0 -262
  149. package/src/event-testing.ts +0 -197
  150. package/src/event.test.ts +0 -1104
  151. package/src/event.ts +0 -988
  152. package/src/events-config.ts +0 -235
  153. package/src/exporters.ts +0 -165
  154. package/src/filtering-span-processor.test.ts +0 -281
  155. package/src/filtering-span-processor.ts +0 -111
  156. package/src/flatten-attributes.test.ts +0 -76
  157. package/src/flatten-attributes.ts +0 -80
  158. package/src/functional.strict-types.typecheck.ts +0 -53
  159. package/src/functional.test.ts +0 -1464
  160. package/src/functional.ts +0 -2539
  161. package/src/functional.types.test.ts +0 -135
  162. package/src/hook.mjs +0 -15
  163. package/src/http.test.ts +0 -485
  164. package/src/http.ts +0 -424
  165. package/src/index.ts +0 -433
  166. package/src/init-auto-redactor.test.ts +0 -53
  167. package/src/init-redactor.test.ts +0 -8
  168. package/src/init.customization.test.ts +0 -665
  169. package/src/init.integrations.test.ts +0 -399
  170. package/src/init.openllmetry.test.ts +0 -194
  171. package/src/init.protocol.test.ts +0 -215
  172. package/src/init.ts +0 -2439
  173. package/src/instrumentation.test.ts +0 -108
  174. package/src/instrumentation.ts +0 -319
  175. package/src/logger.test.ts +0 -125
  176. package/src/logger.ts +0 -341
  177. package/src/messaging-adapters.test.ts +0 -595
  178. package/src/messaging-adapters.ts +0 -583
  179. package/src/messaging-testing.test.ts +0 -573
  180. package/src/messaging-testing.ts +0 -935
  181. package/src/messaging.test.ts +0 -1646
  182. package/src/messaging.ts +0 -2245
  183. package/src/metric-helpers.ts +0 -47
  184. package/src/metric-testing.ts +0 -197
  185. package/src/metric.ts +0 -446
  186. package/src/metrics.test.ts +0 -241
  187. package/src/node-require.ts +0 -123
  188. package/src/operation-context.ts +0 -93
  189. package/src/parse-error.test.ts +0 -73
  190. package/src/parse-error.ts +0 -112
  191. package/src/posthog-logs.test.ts +0 -115
  192. package/src/posthog-logs.ts +0 -77
  193. package/src/pretty-console-exporter.test.ts +0 -545
  194. package/src/pretty-console-exporter.ts +0 -413
  195. package/src/pretty-log-formatter.test.ts +0 -123
  196. package/src/pretty-log-formatter.ts +0 -210
  197. package/src/processors/canonical-log-line-processor.test.ts +0 -523
  198. package/src/processors/canonical-log-line-processor.ts +0 -396
  199. package/src/processors.ts +0 -152
  200. package/src/rate-limiter.test.ts +0 -199
  201. package/src/rate-limiter.ts +0 -98
  202. package/src/redact-values.test.ts +0 -90
  203. package/src/redact-values.ts +0 -34
  204. package/src/register.ts +0 -37
  205. package/src/request-logger.test.ts +0 -545
  206. package/src/request-logger.ts +0 -342
  207. package/src/sampling.test.ts +0 -1060
  208. package/src/sampling.ts +0 -737
  209. package/src/security-schema.test.ts +0 -45
  210. package/src/security-schema.ts +0 -107
  211. package/src/semantic-conventions.ts +0 -15
  212. package/src/semantic-helpers.test.ts +0 -226
  213. package/src/semantic-helpers.ts +0 -438
  214. package/src/shutdown.test.ts +0 -364
  215. package/src/shutdown.ts +0 -246
  216. package/src/span-name-normalizer.test.ts +0 -377
  217. package/src/span-name-normalizer.ts +0 -213
  218. package/src/stable-hash.ts +0 -27
  219. package/src/structured-error.test.ts +0 -191
  220. package/src/structured-error.ts +0 -157
  221. package/src/stub.integration.test.ts +0 -361
  222. package/src/tail-sampling-processor.test.ts +0 -230
  223. package/src/tail-sampling-processor.ts +0 -55
  224. package/src/test-span-collector.test.ts +0 -234
  225. package/src/test-span-collector.ts +0 -150
  226. package/src/testing.ts +0 -705
  227. package/src/trace-context.test.ts +0 -73
  228. package/src/trace-context.ts +0 -567
  229. package/src/trace-helpers.new.test.ts +0 -278
  230. package/src/trace-helpers.test.ts +0 -290
  231. package/src/trace-helpers.ts +0 -710
  232. package/src/trace-hybrid.test.ts +0 -42
  233. package/src/trace-hybrid.ts +0 -37
  234. package/src/tracer-provider.test.ts +0 -183
  235. package/src/tracer-provider.ts +0 -266
  236. package/src/track.test.ts +0 -154
  237. package/src/track.ts +0 -216
  238. package/src/validate.test.ts +0 -287
  239. package/src/validate.ts +0 -307
  240. package/src/validation-attributes.ts +0 -43
  241. package/src/validation.test.ts +0 -330
  242. package/src/validation.ts +0 -246
  243. package/src/variable-name-inference.test.ts +0 -178
  244. package/src/variable-name-inference.ts +0 -242
  245. package/src/webhook.test.ts +0 -649
  246. package/src/webhook.ts +0 -637
  247. package/src/workflow-distributed.test.ts +0 -786
  248. package/src/workflow-distributed.ts +0 -916
  249. package/src/workflow.async-safety.integration.test.ts +0 -345
  250. package/src/workflow.test.ts +0 -647
  251. package/src/workflow.ts +0 -810
  252. package/src/yaml-config.test.ts +0 -373
  253. 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
- }