autotel 2.1.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 (272) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1946 -0
  3. package/dist/chunk-2LNRY4QK.js +273 -0
  4. package/dist/chunk-2LNRY4QK.js.map +1 -0
  5. package/dist/chunk-3HENGDW2.js +587 -0
  6. package/dist/chunk-3HENGDW2.js.map +1 -0
  7. package/dist/chunk-4OAT42CA.cjs +73 -0
  8. package/dist/chunk-4OAT42CA.cjs.map +1 -0
  9. package/dist/chunk-5GWX5LFW.js +70 -0
  10. package/dist/chunk-5GWX5LFW.js.map +1 -0
  11. package/dist/chunk-5R2M36QB.js +195 -0
  12. package/dist/chunk-5R2M36QB.js.map +1 -0
  13. package/dist/chunk-5ZN622AO.js +73 -0
  14. package/dist/chunk-5ZN622AO.js.map +1 -0
  15. package/dist/chunk-77MSMAUQ.cjs +498 -0
  16. package/dist/chunk-77MSMAUQ.cjs.map +1 -0
  17. package/dist/chunk-ABPEQ6RK.cjs +596 -0
  18. package/dist/chunk-ABPEQ6RK.cjs.map +1 -0
  19. package/dist/chunk-BWYGJKRB.js +95 -0
  20. package/dist/chunk-BWYGJKRB.js.map +1 -0
  21. package/dist/chunk-BZHG5IZ4.js +73 -0
  22. package/dist/chunk-BZHG5IZ4.js.map +1 -0
  23. package/dist/chunk-G7VZBCD6.cjs +35 -0
  24. package/dist/chunk-G7VZBCD6.cjs.map +1 -0
  25. package/dist/chunk-GVLK7YUU.cjs +30 -0
  26. package/dist/chunk-GVLK7YUU.cjs.map +1 -0
  27. package/dist/chunk-HCCXC7XG.js +205 -0
  28. package/dist/chunk-HCCXC7XG.js.map +1 -0
  29. package/dist/chunk-HE6T6FIX.cjs +203 -0
  30. package/dist/chunk-HE6T6FIX.cjs.map +1 -0
  31. package/dist/chunk-KIXWPOCO.cjs +100 -0
  32. package/dist/chunk-KIXWPOCO.cjs.map +1 -0
  33. package/dist/chunk-KVGNW3FC.js +87 -0
  34. package/dist/chunk-KVGNW3FC.js.map +1 -0
  35. package/dist/chunk-LITNXTTT.js +3 -0
  36. package/dist/chunk-LITNXTTT.js.map +1 -0
  37. package/dist/chunk-M4ANN7RL.js +114 -0
  38. package/dist/chunk-M4ANN7RL.js.map +1 -0
  39. package/dist/chunk-NC52UBR2.cjs +32 -0
  40. package/dist/chunk-NC52UBR2.cjs.map +1 -0
  41. package/dist/chunk-NHCNRQD3.cjs +212 -0
  42. package/dist/chunk-NHCNRQD3.cjs.map +1 -0
  43. package/dist/chunk-NZ72VDNY.cjs +4 -0
  44. package/dist/chunk-NZ72VDNY.cjs.map +1 -0
  45. package/dist/chunk-P6JUDYNO.js +57 -0
  46. package/dist/chunk-P6JUDYNO.js.map +1 -0
  47. package/dist/chunk-RJYY7BWX.js +1349 -0
  48. package/dist/chunk-RJYY7BWX.js.map +1 -0
  49. package/dist/chunk-TRI4V5BF.cjs +126 -0
  50. package/dist/chunk-TRI4V5BF.cjs.map +1 -0
  51. package/dist/chunk-UL33I6IS.js +139 -0
  52. package/dist/chunk-UL33I6IS.js.map +1 -0
  53. package/dist/chunk-URRW6M2C.cjs +61 -0
  54. package/dist/chunk-URRW6M2C.cjs.map +1 -0
  55. package/dist/chunk-UY3UYPBZ.cjs +77 -0
  56. package/dist/chunk-UY3UYPBZ.cjs.map +1 -0
  57. package/dist/chunk-W3253FGB.cjs +277 -0
  58. package/dist/chunk-W3253FGB.cjs.map +1 -0
  59. package/dist/chunk-W7LHZVQF.js +26 -0
  60. package/dist/chunk-W7LHZVQF.js.map +1 -0
  61. package/dist/chunk-WBWNM6LB.cjs +1360 -0
  62. package/dist/chunk-WBWNM6LB.cjs.map +1 -0
  63. package/dist/chunk-WFJ7L2RV.js +494 -0
  64. package/dist/chunk-WFJ7L2RV.js.map +1 -0
  65. package/dist/chunk-X4RMFFMR.js +28 -0
  66. package/dist/chunk-X4RMFFMR.js.map +1 -0
  67. package/dist/chunk-Y4Y2S7BM.cjs +92 -0
  68. package/dist/chunk-Y4Y2S7BM.cjs.map +1 -0
  69. package/dist/chunk-YLPNXZFI.cjs +143 -0
  70. package/dist/chunk-YLPNXZFI.cjs.map +1 -0
  71. package/dist/chunk-YTXEZ4SD.cjs +77 -0
  72. package/dist/chunk-YTXEZ4SD.cjs.map +1 -0
  73. package/dist/chunk-Z6ZWNWWR.js +30 -0
  74. package/dist/chunk-Z6ZWNWWR.js.map +1 -0
  75. package/dist/config.cjs +26 -0
  76. package/dist/config.cjs.map +1 -0
  77. package/dist/config.d.cts +75 -0
  78. package/dist/config.d.ts +75 -0
  79. package/dist/config.js +5 -0
  80. package/dist/config.js.map +1 -0
  81. package/dist/db.cjs +233 -0
  82. package/dist/db.cjs.map +1 -0
  83. package/dist/db.d.cts +123 -0
  84. package/dist/db.d.ts +123 -0
  85. package/dist/db.js +228 -0
  86. package/dist/db.js.map +1 -0
  87. package/dist/decorators.cjs +67 -0
  88. package/dist/decorators.cjs.map +1 -0
  89. package/dist/decorators.d.cts +91 -0
  90. package/dist/decorators.d.ts +91 -0
  91. package/dist/decorators.js +65 -0
  92. package/dist/decorators.js.map +1 -0
  93. package/dist/event-subscriber.cjs +6 -0
  94. package/dist/event-subscriber.cjs.map +1 -0
  95. package/dist/event-subscriber.d.cts +116 -0
  96. package/dist/event-subscriber.d.ts +116 -0
  97. package/dist/event-subscriber.js +3 -0
  98. package/dist/event-subscriber.js.map +1 -0
  99. package/dist/event-testing.cjs +21 -0
  100. package/dist/event-testing.cjs.map +1 -0
  101. package/dist/event-testing.d.cts +110 -0
  102. package/dist/event-testing.d.ts +110 -0
  103. package/dist/event-testing.js +4 -0
  104. package/dist/event-testing.js.map +1 -0
  105. package/dist/event.cjs +30 -0
  106. package/dist/event.cjs.map +1 -0
  107. package/dist/event.d.cts +282 -0
  108. package/dist/event.d.ts +282 -0
  109. package/dist/event.js +13 -0
  110. package/dist/event.js.map +1 -0
  111. package/dist/exporters.cjs +17 -0
  112. package/dist/exporters.cjs.map +1 -0
  113. package/dist/exporters.d.cts +1 -0
  114. package/dist/exporters.d.ts +1 -0
  115. package/dist/exporters.js +4 -0
  116. package/dist/exporters.js.map +1 -0
  117. package/dist/functional.cjs +46 -0
  118. package/dist/functional.cjs.map +1 -0
  119. package/dist/functional.d.cts +478 -0
  120. package/dist/functional.d.ts +478 -0
  121. package/dist/functional.js +13 -0
  122. package/dist/functional.js.map +1 -0
  123. package/dist/http.cjs +189 -0
  124. package/dist/http.cjs.map +1 -0
  125. package/dist/http.d.cts +169 -0
  126. package/dist/http.d.ts +169 -0
  127. package/dist/http.js +184 -0
  128. package/dist/http.js.map +1 -0
  129. package/dist/index.cjs +333 -0
  130. package/dist/index.cjs.map +1 -0
  131. package/dist/index.d.cts +758 -0
  132. package/dist/index.d.ts +758 -0
  133. package/dist/index.js +143 -0
  134. package/dist/index.js.map +1 -0
  135. package/dist/instrumentation.cjs +182 -0
  136. package/dist/instrumentation.cjs.map +1 -0
  137. package/dist/instrumentation.d.cts +49 -0
  138. package/dist/instrumentation.d.ts +49 -0
  139. package/dist/instrumentation.js +179 -0
  140. package/dist/instrumentation.js.map +1 -0
  141. package/dist/logger.cjs +19 -0
  142. package/dist/logger.cjs.map +1 -0
  143. package/dist/logger.d.cts +146 -0
  144. package/dist/logger.d.ts +146 -0
  145. package/dist/logger.js +6 -0
  146. package/dist/logger.js.map +1 -0
  147. package/dist/metric-helpers.cjs +31 -0
  148. package/dist/metric-helpers.cjs.map +1 -0
  149. package/dist/metric-helpers.d.cts +13 -0
  150. package/dist/metric-helpers.d.ts +13 -0
  151. package/dist/metric-helpers.js +6 -0
  152. package/dist/metric-helpers.js.map +1 -0
  153. package/dist/metric-testing.cjs +21 -0
  154. package/dist/metric-testing.cjs.map +1 -0
  155. package/dist/metric-testing.d.cts +110 -0
  156. package/dist/metric-testing.d.ts +110 -0
  157. package/dist/metric-testing.js +4 -0
  158. package/dist/metric-testing.js.map +1 -0
  159. package/dist/metric.cjs +26 -0
  160. package/dist/metric.cjs.map +1 -0
  161. package/dist/metric.d.cts +240 -0
  162. package/dist/metric.d.ts +240 -0
  163. package/dist/metric.js +9 -0
  164. package/dist/metric.js.map +1 -0
  165. package/dist/processors.cjs +17 -0
  166. package/dist/processors.cjs.map +1 -0
  167. package/dist/processors.d.cts +1 -0
  168. package/dist/processors.d.ts +1 -0
  169. package/dist/processors.js +4 -0
  170. package/dist/processors.js.map +1 -0
  171. package/dist/sampling.cjs +40 -0
  172. package/dist/sampling.cjs.map +1 -0
  173. package/dist/sampling.d.cts +260 -0
  174. package/dist/sampling.d.ts +260 -0
  175. package/dist/sampling.js +7 -0
  176. package/dist/sampling.js.map +1 -0
  177. package/dist/semantic-helpers.cjs +35 -0
  178. package/dist/semantic-helpers.cjs.map +1 -0
  179. package/dist/semantic-helpers.d.cts +442 -0
  180. package/dist/semantic-helpers.d.ts +442 -0
  181. package/dist/semantic-helpers.js +14 -0
  182. package/dist/semantic-helpers.js.map +1 -0
  183. package/dist/tail-sampling-processor.cjs +13 -0
  184. package/dist/tail-sampling-processor.cjs.map +1 -0
  185. package/dist/tail-sampling-processor.d.cts +27 -0
  186. package/dist/tail-sampling-processor.d.ts +27 -0
  187. package/dist/tail-sampling-processor.js +4 -0
  188. package/dist/tail-sampling-processor.js.map +1 -0
  189. package/dist/testing.cjs +286 -0
  190. package/dist/testing.cjs.map +1 -0
  191. package/dist/testing.d.cts +291 -0
  192. package/dist/testing.d.ts +291 -0
  193. package/dist/testing.js +263 -0
  194. package/dist/testing.js.map +1 -0
  195. package/dist/trace-context-DRZdUvVY.d.cts +181 -0
  196. package/dist/trace-context-DRZdUvVY.d.ts +181 -0
  197. package/dist/trace-helpers.cjs +54 -0
  198. package/dist/trace-helpers.cjs.map +1 -0
  199. package/dist/trace-helpers.d.cts +524 -0
  200. package/dist/trace-helpers.d.ts +524 -0
  201. package/dist/trace-helpers.js +5 -0
  202. package/dist/trace-helpers.js.map +1 -0
  203. package/dist/tracer-provider.cjs +21 -0
  204. package/dist/tracer-provider.cjs.map +1 -0
  205. package/dist/tracer-provider.d.cts +169 -0
  206. package/dist/tracer-provider.d.ts +169 -0
  207. package/dist/tracer-provider.js +4 -0
  208. package/dist/tracer-provider.js.map +1 -0
  209. package/package.json +280 -0
  210. package/src/baggage-span-processor.test.ts +202 -0
  211. package/src/baggage-span-processor.ts +98 -0
  212. package/src/circuit-breaker.test.ts +341 -0
  213. package/src/circuit-breaker.ts +184 -0
  214. package/src/config.test.ts +94 -0
  215. package/src/config.ts +169 -0
  216. package/src/db.test.ts +252 -0
  217. package/src/db.ts +447 -0
  218. package/src/decorators.test.ts +203 -0
  219. package/src/decorators.ts +188 -0
  220. package/src/env-config.test.ts +246 -0
  221. package/src/env-config.ts +158 -0
  222. package/src/event-queue.test.ts +222 -0
  223. package/src/event-queue.ts +203 -0
  224. package/src/event-subscriber.ts +136 -0
  225. package/src/event-testing.ts +197 -0
  226. package/src/event.test.ts +718 -0
  227. package/src/event.ts +556 -0
  228. package/src/exporters.ts +96 -0
  229. package/src/functional.test.ts +1059 -0
  230. package/src/functional.ts +2295 -0
  231. package/src/http.test.ts +487 -0
  232. package/src/http.ts +424 -0
  233. package/src/index.ts +158 -0
  234. package/src/init.customization.test.ts +210 -0
  235. package/src/init.integrations.test.ts +366 -0
  236. package/src/init.openllmetry.test.ts +282 -0
  237. package/src/init.protocol.test.ts +215 -0
  238. package/src/init.ts +1426 -0
  239. package/src/instrumentation.test.ts +108 -0
  240. package/src/instrumentation.ts +308 -0
  241. package/src/logger.test.ts +117 -0
  242. package/src/logger.ts +246 -0
  243. package/src/metric-helpers.ts +47 -0
  244. package/src/metric-testing.ts +197 -0
  245. package/src/metric.ts +434 -0
  246. package/src/metrics.test.ts +205 -0
  247. package/src/operation-context.ts +93 -0
  248. package/src/processors.ts +106 -0
  249. package/src/rate-limiter.test.ts +199 -0
  250. package/src/rate-limiter.ts +98 -0
  251. package/src/sampling.test.ts +513 -0
  252. package/src/sampling.ts +428 -0
  253. package/src/semantic-helpers.test.ts +311 -0
  254. package/src/semantic-helpers.ts +584 -0
  255. package/src/shutdown.test.ts +311 -0
  256. package/src/shutdown.ts +222 -0
  257. package/src/stub.integration.test.ts +361 -0
  258. package/src/tail-sampling-processor.test.ts +226 -0
  259. package/src/tail-sampling-processor.ts +51 -0
  260. package/src/testing.ts +670 -0
  261. package/src/trace-context.ts +470 -0
  262. package/src/trace-helpers.new.test.ts +278 -0
  263. package/src/trace-helpers.test.ts +242 -0
  264. package/src/trace-helpers.ts +690 -0
  265. package/src/tracer-provider.test.ts +183 -0
  266. package/src/tracer-provider.ts +266 -0
  267. package/src/track.test.ts +153 -0
  268. package/src/track.ts +120 -0
  269. package/src/validation.test.ts +306 -0
  270. package/src/validation.ts +239 -0
  271. package/src/variable-name-inference.test.ts +178 -0
  272. package/src/variable-name-inference.ts +242 -0
@@ -0,0 +1,222 @@
1
+ /**
2
+ * Tests for events queue guardrails
3
+ */
4
+
5
+ import { describe, it, expect, beforeEach } from 'vitest';
6
+ import { EventQueue } from './event-queue';
7
+
8
+ // Mock adapter for testing
9
+ type MockEvent = {
10
+ name: string;
11
+ attributes?: Record<string, unknown>;
12
+ };
13
+
14
+ class MockAdapter {
15
+ public events: MockEvent[] = [];
16
+ public callCount = 0;
17
+ public shouldFail = false;
18
+
19
+ async trackEvent(
20
+ name: string,
21
+ attributes?: Record<string, unknown>,
22
+ ): Promise<void> {
23
+ this.callCount++;
24
+ if (this.shouldFail) {
25
+ throw new Error('Adapter failed');
26
+ }
27
+ this.events.push({ name, attributes });
28
+ }
29
+
30
+ async trackFunnelStep(): Promise<void> {}
31
+ async trackOutcome(): Promise<void> {}
32
+ async trackValue(): Promise<void> {}
33
+ }
34
+
35
+ describe('EventQueue', () => {
36
+ let mockAdapter: MockAdapter;
37
+ let queue: EventQueue;
38
+
39
+ beforeEach(() => {
40
+ mockAdapter = new MockAdapter();
41
+ queue = new EventQueue([mockAdapter], {
42
+ maxSize: 10,
43
+ batchSize: 3,
44
+ flushInterval: 100,
45
+ maxRetries: 2,
46
+ });
47
+ });
48
+
49
+ describe('Batching', () => {
50
+ it('should enqueue events without immediate sending', () => {
51
+ queue.enqueue({ name: 'test1', attributes: {}, timestamp: Date.now() });
52
+ queue.enqueue({ name: 'test2', attributes: {}, timestamp: Date.now() });
53
+
54
+ expect(queue.size()).toBe(2);
55
+ expect(mockAdapter.callCount).toBe(0); // Not sent yet
56
+ });
57
+
58
+ it('should flush after interval', async () => {
59
+ queue.enqueue({ name: 'test1', attributes: {}, timestamp: Date.now() });
60
+ queue.enqueue({ name: 'test2', attributes: {}, timestamp: Date.now() });
61
+
62
+ // Wait for flush interval
63
+ await new Promise((resolve) => setTimeout(resolve, 150));
64
+
65
+ expect(queue.size()).toBe(0);
66
+ expect(mockAdapter.events.length).toBeGreaterThan(0);
67
+ });
68
+
69
+ it('should batch events efficiently', async () => {
70
+ // Enqueue 5 events (batch size is 3)
71
+ for (let i = 0; i < 5; i++) {
72
+ queue.enqueue({
73
+ name: `test${i}`,
74
+ attributes: {},
75
+ timestamp: Date.now(),
76
+ });
77
+ }
78
+
79
+ // Manual flush
80
+ await queue.flush();
81
+
82
+ expect(queue.size()).toBe(0);
83
+ expect(mockAdapter.events.length).toBe(5);
84
+ });
85
+ });
86
+
87
+ describe('Backpressure', () => {
88
+ it('should drop oldest when queue is full in production', () => {
89
+ const originalEnv = process.env.NODE_ENV;
90
+ process.env.NODE_ENV = 'production';
91
+
92
+ // Fill queue to max (10 events)
93
+ for (let i = 0; i < 12; i++) {
94
+ queue.enqueue({
95
+ name: `test${i}`,
96
+ attributes: { index: i },
97
+ timestamp: Date.now(),
98
+ });
99
+ }
100
+
101
+ // Queue should be at max size
102
+ expect(queue.size()).toBe(10);
103
+
104
+ process.env.NODE_ENV = originalEnv;
105
+ });
106
+
107
+ it('should drop oldest and log when queue is full (consistent behavior)', () => {
108
+ // Fill queue to max
109
+ for (let i = 0; i < 10; i++) {
110
+ queue.enqueue({
111
+ name: `test${i}`,
112
+ attributes: {},
113
+ timestamp: Date.now(),
114
+ });
115
+ }
116
+
117
+ // Next enqueue should drop oldest (not throw)
118
+ expect(() => {
119
+ queue.enqueue({
120
+ name: 'test11',
121
+ attributes: {},
122
+ timestamp: Date.now(),
123
+ });
124
+ }).not.toThrow();
125
+
126
+ // Verify queue is still at max size
127
+ expect(queue['queue']).toHaveLength(10);
128
+ });
129
+ });
130
+
131
+ describe('Retry logic', () => {
132
+ it('should retry on failure', async () => {
133
+ mockAdapter.shouldFail = true;
134
+
135
+ queue.enqueue({ name: 'test1', attributes: {}, timestamp: Date.now() });
136
+
137
+ await queue.flush();
138
+
139
+ // Should have tried maxRetries + 1 times (initial + 2 retries = 3)
140
+ expect(mockAdapter.callCount).toBeGreaterThanOrEqual(3);
141
+ });
142
+
143
+ it('should succeed after transient failure', async () => {
144
+ let failCount = 0;
145
+ mockAdapter.trackEvent = async () => {
146
+ if (failCount < 2) {
147
+ failCount++;
148
+ throw new Error('Transient failure');
149
+ }
150
+ mockAdapter.events.push({ name: 'test', attributes: {} });
151
+ };
152
+
153
+ queue.enqueue({ name: 'test1', attributes: {}, timestamp: Date.now() });
154
+
155
+ await queue.flush();
156
+
157
+ // Should eventually succeed
158
+ expect(mockAdapter.events.length).toBe(1);
159
+ });
160
+ });
161
+
162
+ describe('Graceful flush', () => {
163
+ it('should flush all remaining events', async () => {
164
+ for (let i = 0; i < 5; i++) {
165
+ queue.enqueue({
166
+ name: `test${i}`,
167
+ attributes: {},
168
+ timestamp: Date.now(),
169
+ });
170
+ }
171
+
172
+ expect(queue.size()).toBe(5);
173
+
174
+ await queue.flush();
175
+
176
+ expect(queue.size()).toBe(0);
177
+ expect(mockAdapter.events.length).toBe(5);
178
+ });
179
+
180
+ it('should handle empty queue flush', async () => {
181
+ await expect(queue.flush()).resolves.not.toThrow();
182
+ });
183
+ });
184
+
185
+ describe('Multiple adapters', () => {
186
+ it('should send to all adapters', async () => {
187
+ const adapter1 = new MockAdapter();
188
+ const adapter2 = new MockAdapter();
189
+ const multiQueue = new EventQueue([adapter1, adapter2]);
190
+
191
+ multiQueue.enqueue({
192
+ name: 'test1',
193
+ attributes: {},
194
+ timestamp: Date.now(),
195
+ });
196
+
197
+ await multiQueue.flush();
198
+
199
+ expect(adapter1.events.length).toBe(1);
200
+ expect(adapter2.events.length).toBe(1);
201
+ });
202
+
203
+ it('should handle partial adapter failures', async () => {
204
+ const adapter1 = new MockAdapter();
205
+ const adapter2 = new MockAdapter();
206
+ adapter1.shouldFail = true; // One adapter fails
207
+
208
+ const multiQueue = new EventQueue([adapter1, adapter2], {
209
+ maxRetries: 1,
210
+ });
211
+
212
+ multiQueue.enqueue({
213
+ name: 'test1',
214
+ attributes: {},
215
+ timestamp: Date.now(),
216
+ });
217
+
218
+ // Should not throw, just log error
219
+ await expect(multiQueue.flush()).resolves.not.toThrow();
220
+ });
221
+ });
222
+ });
@@ -0,0 +1,203 @@
1
+ /**
2
+ * Events event queue with batching, backpressure, retry logic, and rate limiting
3
+ */
4
+
5
+ import type { EventSubscriber, EventAttributes } from './event-subscriber';
6
+ import { getLogger } from './init';
7
+ import { TokenBucketRateLimiter, type RateLimiterConfig } from './rate-limiter';
8
+
9
+ export interface EventData {
10
+ name: string;
11
+ attributes?: EventAttributes;
12
+ timestamp: number;
13
+ }
14
+
15
+ export interface QueueConfig {
16
+ maxSize: number; // Max events in queue (default: 50,000)
17
+ batchSize: number; // Events per batch (default: 100)
18
+ flushInterval: number; // Flush interval in ms (default: 10,000)
19
+ maxRetries: number; // Max retry attempts (default: 3)
20
+ rateLimit?: RateLimiterConfig; // Optional rate limiting (default: 100 events/sec)
21
+ }
22
+
23
+ const DEFAULT_CONFIG: QueueConfig = {
24
+ maxSize: 50_000,
25
+ batchSize: 100,
26
+ flushInterval: 10_000,
27
+ maxRetries: 3,
28
+ rateLimit: {
29
+ maxEventsPerSecond: 100,
30
+ burstCapacity: 200,
31
+ },
32
+ };
33
+
34
+ /**
35
+ * Events queue with batching and backpressure
36
+ *
37
+ * Features:
38
+ * - Batches events for efficient sending
39
+ * - Bounded queue with drop-oldest policy (prod) or blocking (dev)
40
+ * - Exponential backoff retry
41
+ * - Rate limiting to prevent overwhelming subscribers
42
+ * - Graceful flush on shutdown
43
+ */
44
+ export class EventQueue {
45
+ private queue: EventData[] = [];
46
+ private flushTimer: NodeJS.Timeout | null = null;
47
+ private readonly config: QueueConfig;
48
+ private readonly subscribers: EventSubscriber[];
49
+ private readonly rateLimiter: TokenBucketRateLimiter | null;
50
+ private flushing = false;
51
+
52
+ constructor(subscribers: EventSubscriber[], config?: Partial<QueueConfig>) {
53
+ this.subscribers = subscribers;
54
+ this.config = { ...DEFAULT_CONFIG, ...config };
55
+
56
+ // Initialize rate limiter if configured
57
+ this.rateLimiter = this.config.rateLimit
58
+ ? new TokenBucketRateLimiter(this.config.rateLimit)
59
+ : null;
60
+ }
61
+
62
+ /**
63
+ * Enqueue an event for sending
64
+ *
65
+ * Backpressure policy:
66
+ * - Drops oldest event and logs warning if queue is full (same behavior in all environments)
67
+ */
68
+ enqueue(event: EventData): void {
69
+ // Check queue size
70
+ if (this.queue.length >= this.config.maxSize) {
71
+ // Drop oldest event and log warning (same behavior in all environments)
72
+ const droppedEvent = this.queue.shift();
73
+ getLogger().warn(
74
+ `[autotel] Events queue full (${this.config.maxSize} events). ` +
75
+ 'Dropping oldest event. Events are being produced faster than they can be sent. ' +
76
+ 'Check your subscribers or reduce tracking frequency.',
77
+ { droppedEvent: droppedEvent?.name },
78
+ );
79
+ }
80
+
81
+ this.queue.push(event);
82
+ this.scheduleBatchFlush();
83
+ }
84
+
85
+ /**
86
+ * Schedule a batch flush if not already scheduled
87
+ */
88
+ private scheduleBatchFlush(): void {
89
+ if (this.flushTimer || this.flushing) return;
90
+
91
+ this.flushTimer = setTimeout(() => {
92
+ this.flushTimer = null;
93
+ void this.flushBatch();
94
+ }, this.config.flushInterval);
95
+ }
96
+
97
+ /**
98
+ * Flush a batch of events
99
+ */
100
+ private async flushBatch(): Promise<void> {
101
+ if (this.queue.length === 0 || this.flushing) return;
102
+
103
+ this.flushing = true;
104
+
105
+ try {
106
+ const batch = this.queue.splice(0, this.config.batchSize);
107
+ await this.sendWithRetry(batch, this.config.maxRetries);
108
+ } finally {
109
+ this.flushing = false;
110
+
111
+ // Schedule next flush if more events
112
+ if (this.queue.length > 0) {
113
+ this.scheduleBatchFlush();
114
+ }
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Send events with exponential backoff retry
120
+ */
121
+ private async sendWithRetry(
122
+ events: EventData[],
123
+ retriesLeft: number,
124
+ ): Promise<void> {
125
+ try {
126
+ await this.sendToSubscribers(events);
127
+ } catch (error) {
128
+ if (retriesLeft > 0) {
129
+ // Exponential backoff: 1s, 2s, 4s
130
+ const delay = Math.pow(2, this.config.maxRetries - retriesLeft) * 1000;
131
+ await new Promise((resolve) => setTimeout(resolve, delay));
132
+ return this.sendWithRetry(events, retriesLeft - 1);
133
+ } else {
134
+ // Give up after max retries
135
+ // Always log failed retries to maintain visibility (same behavior in all environments)
136
+ getLogger().error(
137
+ '[autotel] Failed to send events after retries',
138
+ error instanceof Error ? error : undefined,
139
+ { retriesAttempted: this.config.maxRetries },
140
+ );
141
+ }
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Send events to all configured subscribers with rate limiting
147
+ */
148
+ private async sendToSubscribers(events: EventData[]): Promise<void> {
149
+ // If rate limiting is disabled, send all at once
150
+ if (!this.rateLimiter) {
151
+ const promises = events.map((event) =>
152
+ Promise.all(
153
+ this.subscribers.map((subscriber) =>
154
+ subscriber.trackEvent(event.name, event.attributes),
155
+ ),
156
+ ),
157
+ );
158
+ await Promise.all(promises);
159
+ return;
160
+ }
161
+
162
+ // With rate limiting: wait for token before sending each event
163
+ for (const event of events) {
164
+ // Wait for rate limiter token (smooth traffic)
165
+ await this.rateLimiter.waitForToken();
166
+
167
+ // Send to all subscribers concurrently
168
+ await Promise.all(
169
+ this.subscribers.map((subscriber) =>
170
+ subscriber.trackEvent(event.name, event.attributes),
171
+ ),
172
+ );
173
+ }
174
+ }
175
+
176
+ /**
177
+ * Flush all remaining events (for shutdown)
178
+ */
179
+ async flush(): Promise<void> {
180
+ // Cancel any pending timer
181
+ if (this.flushTimer) {
182
+ clearTimeout(this.flushTimer);
183
+ this.flushTimer = null;
184
+ }
185
+
186
+ // Wait for any in-progress flush to complete
187
+ while (this.flushing) {
188
+ await new Promise((resolve) => setTimeout(resolve, 10));
189
+ }
190
+
191
+ // Flush all batches
192
+ while (this.queue.length > 0) {
193
+ await this.flushBatch();
194
+ }
195
+ }
196
+
197
+ /**
198
+ * Get queue size (for testing/debugging)
199
+ */
200
+ size(): number {
201
+ return this.queue.length;
202
+ }
203
+ }
@@ -0,0 +1,136 @@
1
+ /**
2
+ * Event Subscriber Interface (Type-only)
3
+ *
4
+ * Import this interface to create custom subscribers without importing implementations.
5
+ * Keeps core package focused on OpenTelemetry with zero extra dependencies.
6
+ *
7
+ * For ready-made subscribers (PostHog, Mixpanel, Amplitude, Segment),
8
+ * see the separate `autotel-subscribers` package.
9
+ *
10
+ * @example Custom subscriber
11
+ * ```typescript
12
+ * import { EventSubscriber } from 'autotel/event-subscriber';
13
+ *
14
+ * class MyCustomSubscriber implements EventSubscriber {
15
+ * trackEvent(name: string, attributes?: Record<string, any>): void {
16
+ * // Send to your events platform
17
+ * }
18
+ * // ... implement other methods
19
+ * }
20
+ * ```
21
+ *
22
+ * @example Use pre-built subscribers
23
+ * ```typescript
24
+ * import { Events } from 'autotel/events';
25
+ * import { PostHogSubscriber } from 'autotel-subscribers/posthog';
26
+ * import { MixpanelSubscriber } from 'autotel-subscribers/mixpanel';
27
+ *
28
+ * const event =new Event('checkout', {
29
+ * subscribers: [
30
+ * new PostHogSubscriber({ apiKey: 'phc_...' }),
31
+ * new MixpanelSubscriber({ token: '...' })
32
+ * ]
33
+ * });
34
+ * ```
35
+ */
36
+
37
+ /**
38
+ * Event attributes (supports any JSON-serializable values)
39
+ */
40
+ export type EventAttributes = Record<string, string | number | boolean>;
41
+
42
+ /**
43
+ * Funnel step status
44
+ */
45
+ export type FunnelStatus = 'started' | 'completed' | 'abandoned' | 'failed';
46
+
47
+ /**
48
+ * Outcome status
49
+ */
50
+ export type OutcomeStatus = 'success' | 'failure' | 'partial';
51
+
52
+ /**
53
+ * Event subscriber interface
54
+ *
55
+ * Implement this to send events to any platform.
56
+ * Zero runtime dependencies - just types.
57
+ *
58
+ * All tracking methods are async to support:
59
+ * - Backpressure signaling (buffer full)
60
+ * - Streaming platforms (Kafka, Kinesis, Pub/Sub)
61
+ * - Await delivery confirmation
62
+ * - Proper error propagation
63
+ */
64
+ export interface EventSubscriber {
65
+ /**
66
+ * Track an event (e.g., "user.registered", "order.created")
67
+ *
68
+ * @returns Promise that resolves when event is sent (or buffered)
69
+ */
70
+ trackEvent(name: string, attributes?: EventAttributes): Promise<void>;
71
+
72
+ /**
73
+ * Track a funnel step (e.g., checkout: started → completed)
74
+ *
75
+ * @returns Promise that resolves when event is sent (or buffered)
76
+ */
77
+ trackFunnelStep(
78
+ funnelName: string,
79
+ step: FunnelStatus,
80
+ attributes?: EventAttributes,
81
+ ): Promise<void>;
82
+
83
+ /**
84
+ * Track an outcome (e.g., "payment.processing" → success/failure)
85
+ *
86
+ * @returns Promise that resolves when event is sent (or buffered)
87
+ */
88
+ trackOutcome(
89
+ operationName: string,
90
+ outcome: OutcomeStatus,
91
+ attributes?: EventAttributes,
92
+ ): Promise<void>;
93
+
94
+ /**
95
+ * Track a value/metric (e.g., revenue, cart value)
96
+ *
97
+ * @returns Promise that resolves when event is sent (or buffered)
98
+ */
99
+ trackValue(
100
+ name: string,
101
+ value: number,
102
+ attributes?: EventAttributes,
103
+ ): Promise<void>;
104
+
105
+ /**
106
+ * Optional: Flush pending events and clean up resources
107
+ *
108
+ * Implement this if your subscriber buffers events, maintains connections,
109
+ * or needs cleanup before shutdown. Called during graceful shutdown.
110
+ *
111
+ * @example
112
+ * ```typescript
113
+ * class MySubscriber implements EventSubscriber {
114
+ * async shutdown(): Promise<void> {
115
+ * await this.flushBuffer();
116
+ * await this.closeConnections();
117
+ * }
118
+ * }
119
+ * ```
120
+ */
121
+ shutdown?(): Promise<void>;
122
+
123
+ /**
124
+ * Optional: Subscriber name for debugging and error reporting
125
+ *
126
+ * @example "PostHogSubscriber", "SnowflakeSubscriber", "CustomWebhookSubscriber"
127
+ */
128
+ readonly name?: string;
129
+
130
+ /**
131
+ * Optional: Subscriber version for debugging
132
+ *
133
+ * @example "1.0.0"
134
+ */
135
+ readonly version?: string;
136
+ }