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.
- package/LICENSE +21 -0
- package/README.md +1946 -0
- package/dist/chunk-2LNRY4QK.js +273 -0
- package/dist/chunk-2LNRY4QK.js.map +1 -0
- package/dist/chunk-3HENGDW2.js +587 -0
- package/dist/chunk-3HENGDW2.js.map +1 -0
- package/dist/chunk-4OAT42CA.cjs +73 -0
- package/dist/chunk-4OAT42CA.cjs.map +1 -0
- package/dist/chunk-5GWX5LFW.js +70 -0
- package/dist/chunk-5GWX5LFW.js.map +1 -0
- package/dist/chunk-5R2M36QB.js +195 -0
- package/dist/chunk-5R2M36QB.js.map +1 -0
- package/dist/chunk-5ZN622AO.js +73 -0
- package/dist/chunk-5ZN622AO.js.map +1 -0
- package/dist/chunk-77MSMAUQ.cjs +498 -0
- package/dist/chunk-77MSMAUQ.cjs.map +1 -0
- package/dist/chunk-ABPEQ6RK.cjs +596 -0
- package/dist/chunk-ABPEQ6RK.cjs.map +1 -0
- package/dist/chunk-BWYGJKRB.js +95 -0
- package/dist/chunk-BWYGJKRB.js.map +1 -0
- package/dist/chunk-BZHG5IZ4.js +73 -0
- package/dist/chunk-BZHG5IZ4.js.map +1 -0
- package/dist/chunk-G7VZBCD6.cjs +35 -0
- package/dist/chunk-G7VZBCD6.cjs.map +1 -0
- package/dist/chunk-GVLK7YUU.cjs +30 -0
- package/dist/chunk-GVLK7YUU.cjs.map +1 -0
- package/dist/chunk-HCCXC7XG.js +205 -0
- package/dist/chunk-HCCXC7XG.js.map +1 -0
- package/dist/chunk-HE6T6FIX.cjs +203 -0
- package/dist/chunk-HE6T6FIX.cjs.map +1 -0
- package/dist/chunk-KIXWPOCO.cjs +100 -0
- package/dist/chunk-KIXWPOCO.cjs.map +1 -0
- package/dist/chunk-KVGNW3FC.js +87 -0
- package/dist/chunk-KVGNW3FC.js.map +1 -0
- package/dist/chunk-LITNXTTT.js +3 -0
- package/dist/chunk-LITNXTTT.js.map +1 -0
- package/dist/chunk-M4ANN7RL.js +114 -0
- package/dist/chunk-M4ANN7RL.js.map +1 -0
- package/dist/chunk-NC52UBR2.cjs +32 -0
- package/dist/chunk-NC52UBR2.cjs.map +1 -0
- package/dist/chunk-NHCNRQD3.cjs +212 -0
- package/dist/chunk-NHCNRQD3.cjs.map +1 -0
- package/dist/chunk-NZ72VDNY.cjs +4 -0
- package/dist/chunk-NZ72VDNY.cjs.map +1 -0
- package/dist/chunk-P6JUDYNO.js +57 -0
- package/dist/chunk-P6JUDYNO.js.map +1 -0
- package/dist/chunk-RJYY7BWX.js +1349 -0
- package/dist/chunk-RJYY7BWX.js.map +1 -0
- package/dist/chunk-TRI4V5BF.cjs +126 -0
- package/dist/chunk-TRI4V5BF.cjs.map +1 -0
- package/dist/chunk-UL33I6IS.js +139 -0
- package/dist/chunk-UL33I6IS.js.map +1 -0
- package/dist/chunk-URRW6M2C.cjs +61 -0
- package/dist/chunk-URRW6M2C.cjs.map +1 -0
- package/dist/chunk-UY3UYPBZ.cjs +77 -0
- package/dist/chunk-UY3UYPBZ.cjs.map +1 -0
- package/dist/chunk-W3253FGB.cjs +277 -0
- package/dist/chunk-W3253FGB.cjs.map +1 -0
- package/dist/chunk-W7LHZVQF.js +26 -0
- package/dist/chunk-W7LHZVQF.js.map +1 -0
- package/dist/chunk-WBWNM6LB.cjs +1360 -0
- package/dist/chunk-WBWNM6LB.cjs.map +1 -0
- package/dist/chunk-WFJ7L2RV.js +494 -0
- package/dist/chunk-WFJ7L2RV.js.map +1 -0
- package/dist/chunk-X4RMFFMR.js +28 -0
- package/dist/chunk-X4RMFFMR.js.map +1 -0
- package/dist/chunk-Y4Y2S7BM.cjs +92 -0
- package/dist/chunk-Y4Y2S7BM.cjs.map +1 -0
- package/dist/chunk-YLPNXZFI.cjs +143 -0
- package/dist/chunk-YLPNXZFI.cjs.map +1 -0
- package/dist/chunk-YTXEZ4SD.cjs +77 -0
- package/dist/chunk-YTXEZ4SD.cjs.map +1 -0
- package/dist/chunk-Z6ZWNWWR.js +30 -0
- package/dist/chunk-Z6ZWNWWR.js.map +1 -0
- package/dist/config.cjs +26 -0
- package/dist/config.cjs.map +1 -0
- package/dist/config.d.cts +75 -0
- package/dist/config.d.ts +75 -0
- package/dist/config.js +5 -0
- package/dist/config.js.map +1 -0
- package/dist/db.cjs +233 -0
- package/dist/db.cjs.map +1 -0
- package/dist/db.d.cts +123 -0
- package/dist/db.d.ts +123 -0
- package/dist/db.js +228 -0
- package/dist/db.js.map +1 -0
- package/dist/decorators.cjs +67 -0
- package/dist/decorators.cjs.map +1 -0
- package/dist/decorators.d.cts +91 -0
- package/dist/decorators.d.ts +91 -0
- package/dist/decorators.js +65 -0
- package/dist/decorators.js.map +1 -0
- package/dist/event-subscriber.cjs +6 -0
- package/dist/event-subscriber.cjs.map +1 -0
- package/dist/event-subscriber.d.cts +116 -0
- package/dist/event-subscriber.d.ts +116 -0
- package/dist/event-subscriber.js +3 -0
- package/dist/event-subscriber.js.map +1 -0
- package/dist/event-testing.cjs +21 -0
- package/dist/event-testing.cjs.map +1 -0
- package/dist/event-testing.d.cts +110 -0
- package/dist/event-testing.d.ts +110 -0
- package/dist/event-testing.js +4 -0
- package/dist/event-testing.js.map +1 -0
- package/dist/event.cjs +30 -0
- package/dist/event.cjs.map +1 -0
- package/dist/event.d.cts +282 -0
- package/dist/event.d.ts +282 -0
- package/dist/event.js +13 -0
- package/dist/event.js.map +1 -0
- package/dist/exporters.cjs +17 -0
- package/dist/exporters.cjs.map +1 -0
- package/dist/exporters.d.cts +1 -0
- package/dist/exporters.d.ts +1 -0
- package/dist/exporters.js +4 -0
- package/dist/exporters.js.map +1 -0
- package/dist/functional.cjs +46 -0
- package/dist/functional.cjs.map +1 -0
- package/dist/functional.d.cts +478 -0
- package/dist/functional.d.ts +478 -0
- package/dist/functional.js +13 -0
- package/dist/functional.js.map +1 -0
- package/dist/http.cjs +189 -0
- package/dist/http.cjs.map +1 -0
- package/dist/http.d.cts +169 -0
- package/dist/http.d.ts +169 -0
- package/dist/http.js +184 -0
- package/dist/http.js.map +1 -0
- package/dist/index.cjs +333 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +758 -0
- package/dist/index.d.ts +758 -0
- package/dist/index.js +143 -0
- package/dist/index.js.map +1 -0
- package/dist/instrumentation.cjs +182 -0
- package/dist/instrumentation.cjs.map +1 -0
- package/dist/instrumentation.d.cts +49 -0
- package/dist/instrumentation.d.ts +49 -0
- package/dist/instrumentation.js +179 -0
- package/dist/instrumentation.js.map +1 -0
- package/dist/logger.cjs +19 -0
- package/dist/logger.cjs.map +1 -0
- package/dist/logger.d.cts +146 -0
- package/dist/logger.d.ts +146 -0
- package/dist/logger.js +6 -0
- package/dist/logger.js.map +1 -0
- package/dist/metric-helpers.cjs +31 -0
- package/dist/metric-helpers.cjs.map +1 -0
- package/dist/metric-helpers.d.cts +13 -0
- package/dist/metric-helpers.d.ts +13 -0
- package/dist/metric-helpers.js +6 -0
- package/dist/metric-helpers.js.map +1 -0
- package/dist/metric-testing.cjs +21 -0
- package/dist/metric-testing.cjs.map +1 -0
- package/dist/metric-testing.d.cts +110 -0
- package/dist/metric-testing.d.ts +110 -0
- package/dist/metric-testing.js +4 -0
- package/dist/metric-testing.js.map +1 -0
- package/dist/metric.cjs +26 -0
- package/dist/metric.cjs.map +1 -0
- package/dist/metric.d.cts +240 -0
- package/dist/metric.d.ts +240 -0
- package/dist/metric.js +9 -0
- package/dist/metric.js.map +1 -0
- package/dist/processors.cjs +17 -0
- package/dist/processors.cjs.map +1 -0
- package/dist/processors.d.cts +1 -0
- package/dist/processors.d.ts +1 -0
- package/dist/processors.js +4 -0
- package/dist/processors.js.map +1 -0
- package/dist/sampling.cjs +40 -0
- package/dist/sampling.cjs.map +1 -0
- package/dist/sampling.d.cts +260 -0
- package/dist/sampling.d.ts +260 -0
- package/dist/sampling.js +7 -0
- package/dist/sampling.js.map +1 -0
- package/dist/semantic-helpers.cjs +35 -0
- package/dist/semantic-helpers.cjs.map +1 -0
- package/dist/semantic-helpers.d.cts +442 -0
- package/dist/semantic-helpers.d.ts +442 -0
- package/dist/semantic-helpers.js +14 -0
- package/dist/semantic-helpers.js.map +1 -0
- package/dist/tail-sampling-processor.cjs +13 -0
- package/dist/tail-sampling-processor.cjs.map +1 -0
- package/dist/tail-sampling-processor.d.cts +27 -0
- package/dist/tail-sampling-processor.d.ts +27 -0
- package/dist/tail-sampling-processor.js +4 -0
- package/dist/tail-sampling-processor.js.map +1 -0
- package/dist/testing.cjs +286 -0
- package/dist/testing.cjs.map +1 -0
- package/dist/testing.d.cts +291 -0
- package/dist/testing.d.ts +291 -0
- package/dist/testing.js +263 -0
- package/dist/testing.js.map +1 -0
- package/dist/trace-context-DRZdUvVY.d.cts +181 -0
- package/dist/trace-context-DRZdUvVY.d.ts +181 -0
- package/dist/trace-helpers.cjs +54 -0
- package/dist/trace-helpers.cjs.map +1 -0
- package/dist/trace-helpers.d.cts +524 -0
- package/dist/trace-helpers.d.ts +524 -0
- package/dist/trace-helpers.js +5 -0
- package/dist/trace-helpers.js.map +1 -0
- package/dist/tracer-provider.cjs +21 -0
- package/dist/tracer-provider.cjs.map +1 -0
- package/dist/tracer-provider.d.cts +169 -0
- package/dist/tracer-provider.d.ts +169 -0
- package/dist/tracer-provider.js +4 -0
- package/dist/tracer-provider.js.map +1 -0
- package/package.json +280 -0
- package/src/baggage-span-processor.test.ts +202 -0
- package/src/baggage-span-processor.ts +98 -0
- package/src/circuit-breaker.test.ts +341 -0
- package/src/circuit-breaker.ts +184 -0
- package/src/config.test.ts +94 -0
- package/src/config.ts +169 -0
- package/src/db.test.ts +252 -0
- package/src/db.ts +447 -0
- package/src/decorators.test.ts +203 -0
- package/src/decorators.ts +188 -0
- package/src/env-config.test.ts +246 -0
- package/src/env-config.ts +158 -0
- package/src/event-queue.test.ts +222 -0
- package/src/event-queue.ts +203 -0
- package/src/event-subscriber.ts +136 -0
- package/src/event-testing.ts +197 -0
- package/src/event.test.ts +718 -0
- package/src/event.ts +556 -0
- package/src/exporters.ts +96 -0
- package/src/functional.test.ts +1059 -0
- package/src/functional.ts +2295 -0
- package/src/http.test.ts +487 -0
- package/src/http.ts +424 -0
- package/src/index.ts +158 -0
- package/src/init.customization.test.ts +210 -0
- package/src/init.integrations.test.ts +366 -0
- package/src/init.openllmetry.test.ts +282 -0
- package/src/init.protocol.test.ts +215 -0
- package/src/init.ts +1426 -0
- package/src/instrumentation.test.ts +108 -0
- package/src/instrumentation.ts +308 -0
- package/src/logger.test.ts +117 -0
- package/src/logger.ts +246 -0
- package/src/metric-helpers.ts +47 -0
- package/src/metric-testing.ts +197 -0
- package/src/metric.ts +434 -0
- package/src/metrics.test.ts +205 -0
- package/src/operation-context.ts +93 -0
- package/src/processors.ts +106 -0
- package/src/rate-limiter.test.ts +199 -0
- package/src/rate-limiter.ts +98 -0
- package/src/sampling.test.ts +513 -0
- package/src/sampling.ts +428 -0
- package/src/semantic-helpers.test.ts +311 -0
- package/src/semantic-helpers.ts +584 -0
- package/src/shutdown.test.ts +311 -0
- package/src/shutdown.ts +222 -0
- package/src/stub.integration.test.ts +361 -0
- package/src/tail-sampling-processor.test.ts +226 -0
- package/src/tail-sampling-processor.ts +51 -0
- package/src/testing.ts +670 -0
- package/src/trace-context.ts +470 -0
- package/src/trace-helpers.new.test.ts +278 -0
- package/src/trace-helpers.test.ts +242 -0
- package/src/trace-helpers.ts +690 -0
- package/src/tracer-provider.test.ts +183 -0
- package/src/tracer-provider.ts +266 -0
- package/src/track.test.ts +153 -0
- package/src/track.ts +120 -0
- package/src/validation.test.ts +306 -0
- package/src/validation.ts +239 -0
- package/src/variable-name-inference.test.ts +178 -0
- 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
|
+
}
|