autotel-subscribers 4.0.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 (87) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +669 -0
  3. package/dist/amplitude.cjs +2486 -0
  4. package/dist/amplitude.cjs.map +1 -0
  5. package/dist/amplitude.d.cts +49 -0
  6. package/dist/amplitude.d.ts +49 -0
  7. package/dist/amplitude.js +2463 -0
  8. package/dist/amplitude.js.map +1 -0
  9. package/dist/event-subscriber-base-CnF3V56W.d.cts +182 -0
  10. package/dist/event-subscriber-base-CnF3V56W.d.ts +182 -0
  11. package/dist/factories.cjs +16660 -0
  12. package/dist/factories.cjs.map +1 -0
  13. package/dist/factories.d.cts +304 -0
  14. package/dist/factories.d.ts +304 -0
  15. package/dist/factories.js +16624 -0
  16. package/dist/factories.js.map +1 -0
  17. package/dist/index.cjs +16575 -0
  18. package/dist/index.cjs.map +1 -0
  19. package/dist/index.d.cts +179 -0
  20. package/dist/index.d.ts +179 -0
  21. package/dist/index.js +16539 -0
  22. package/dist/index.js.map +1 -0
  23. package/dist/middleware.cjs +220 -0
  24. package/dist/middleware.cjs.map +1 -0
  25. package/dist/middleware.d.cts +227 -0
  26. package/dist/middleware.d.ts +227 -0
  27. package/dist/middleware.js +208 -0
  28. package/dist/middleware.js.map +1 -0
  29. package/dist/mixpanel.cjs +2940 -0
  30. package/dist/mixpanel.cjs.map +1 -0
  31. package/dist/mixpanel.d.cts +47 -0
  32. package/dist/mixpanel.d.ts +47 -0
  33. package/dist/mixpanel.js +2932 -0
  34. package/dist/mixpanel.js.map +1 -0
  35. package/dist/posthog.cjs +4115 -0
  36. package/dist/posthog.cjs.map +1 -0
  37. package/dist/posthog.d.cts +299 -0
  38. package/dist/posthog.d.ts +299 -0
  39. package/dist/posthog.js +4113 -0
  40. package/dist/posthog.js.map +1 -0
  41. package/dist/segment.cjs +6822 -0
  42. package/dist/segment.cjs.map +1 -0
  43. package/dist/segment.d.cts +49 -0
  44. package/dist/segment.d.ts +49 -0
  45. package/dist/segment.js +6794 -0
  46. package/dist/segment.js.map +1 -0
  47. package/dist/slack.cjs +368 -0
  48. package/dist/slack.cjs.map +1 -0
  49. package/dist/slack.d.cts +126 -0
  50. package/dist/slack.d.ts +126 -0
  51. package/dist/slack.js +366 -0
  52. package/dist/slack.js.map +1 -0
  53. package/dist/webhook.cjs +100 -0
  54. package/dist/webhook.cjs.map +1 -0
  55. package/dist/webhook.d.cts +53 -0
  56. package/dist/webhook.d.ts +53 -0
  57. package/dist/webhook.js +98 -0
  58. package/dist/webhook.js.map +1 -0
  59. package/examples/quickstart-custom-subscriber.ts +144 -0
  60. package/examples/subscriber-bigquery.ts +219 -0
  61. package/examples/subscriber-databricks.ts +280 -0
  62. package/examples/subscriber-kafka.ts +326 -0
  63. package/examples/subscriber-kinesis.ts +307 -0
  64. package/examples/subscriber-posthog.ts +421 -0
  65. package/examples/subscriber-pubsub.ts +336 -0
  66. package/examples/subscriber-snowflake.ts +232 -0
  67. package/package.json +141 -0
  68. package/src/amplitude.test.ts +231 -0
  69. package/src/amplitude.ts +148 -0
  70. package/src/event-subscriber-base.ts +325 -0
  71. package/src/factories.ts +197 -0
  72. package/src/index.ts +50 -0
  73. package/src/middleware.ts +489 -0
  74. package/src/mixpanel.test.ts +194 -0
  75. package/src/mixpanel.ts +134 -0
  76. package/src/mock-event-subscriber.ts +333 -0
  77. package/src/posthog.test.ts +629 -0
  78. package/src/posthog.ts +530 -0
  79. package/src/segment.test.ts +228 -0
  80. package/src/segment.ts +148 -0
  81. package/src/slack.ts +383 -0
  82. package/src/streaming-event-subscriber.ts +323 -0
  83. package/src/testing/index.ts +37 -0
  84. package/src/testing/mock-webhook-server.ts +242 -0
  85. package/src/testing/subscriber-test-harness.ts +365 -0
  86. package/src/webhook.test.ts +264 -0
  87. package/src/webhook.ts +158 -0
@@ -0,0 +1,489 @@
1
+ /**
2
+ * Middleware System for Events Subscribers
3
+ *
4
+ * Compose subscriber behaviors using middleware (like Redux/Express).
5
+ * Add retry logic, sampling, enrichment, logging, and more without modifying subscriber code.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * import { applyMiddleware, retryMiddleware, loggingMiddleware } from 'autotel-subscribers/middleware';
10
+ *
11
+ * const subscriber = applyMiddleware(
12
+ * new PostHogSubscriber({ apiKey: '...' }),
13
+ * [
14
+ * retryMiddleware({ maxRetries: 3 }),
15
+ * loggingMiddleware()
16
+ * ]
17
+ * );
18
+ * ```
19
+ */
20
+
21
+ import type { EventSubscriber, EventAttributes, FunnelStatus, OutcomeStatus } from 'autotel/event-subscriber';
22
+
23
+ /**
24
+ * Unified event type for middleware
25
+ */
26
+ export type EventsEvent =
27
+ | {
28
+ type: 'event';
29
+ name: string;
30
+ attributes?: EventAttributes;
31
+ }
32
+ | {
33
+ type: 'funnel';
34
+ funnel: string;
35
+ step: FunnelStatus;
36
+ attributes?: EventAttributes;
37
+ }
38
+ | {
39
+ type: 'outcome';
40
+ operation: string;
41
+ outcome: OutcomeStatus;
42
+ attributes?: EventAttributes;
43
+ }
44
+ | {
45
+ type: 'value';
46
+ name: string;
47
+ value: number;
48
+ attributes?: EventAttributes;
49
+ };
50
+
51
+ /**
52
+ * Middleware function signature.
53
+ *
54
+ * Like Express middleware: `(event, next) => Promise<void>`
55
+ */
56
+ export type SubscriberMiddleware = (
57
+ event: EventsEvent,
58
+ next: (event: EventsEvent) => Promise<void>
59
+ ) => Promise<void>;
60
+
61
+ /**
62
+ * Apply middleware to an subscriber.
63
+ *
64
+ * Middleware is executed in order. Each middleware can:
65
+ * - Transform the event before passing to next()
66
+ * - Add side effects (logging, metrics)
67
+ * - Skip calling next() (filtering)
68
+ * - Handle errors
69
+ *
70
+ * @example
71
+ * ```typescript
72
+ * const subscriber = applyMiddleware(
73
+ * new WebhookSubscriber({ url: '...' }),
74
+ * [
75
+ * loggingMiddleware(),
76
+ * retryMiddleware({ maxRetries: 3 }),
77
+ * samplingMiddleware(0.1) // Only 10% of events
78
+ * ]
79
+ * );
80
+ * ```
81
+ */
82
+ export function applyMiddleware(
83
+ subscriber: EventSubscriber,
84
+ middlewares: SubscriberMiddleware[]
85
+ ): EventSubscriber {
86
+ // Convert subscriber methods to event format
87
+ const trackEvent = async (event: EventsEvent): Promise<void> => {
88
+ switch (event.type) {
89
+ case 'event': {
90
+ await subscriber.trackEvent(event.name, event.attributes);
91
+ break;
92
+ }
93
+ case 'funnel': {
94
+ await subscriber.trackFunnelStep(event.funnel, event.step, event.attributes);
95
+ break;
96
+ }
97
+ case 'outcome': {
98
+ await subscriber.trackOutcome(event.operation, event.outcome, event.attributes);
99
+ break;
100
+ }
101
+ case 'value': {
102
+ await subscriber.trackValue(event.name, event.value, event.attributes);
103
+ break;
104
+ }
105
+ }
106
+ };
107
+
108
+ // Build middleware chain
109
+ type ChainFunction = (event: EventsEvent) => Promise<void>;
110
+ const reversedMiddlewares = middlewares.toReversed();
111
+ let chain: ChainFunction = trackEvent;
112
+ for (const middleware of reversedMiddlewares) {
113
+ const next = chain;
114
+ chain = (event: EventsEvent) => middleware(event, next);
115
+ }
116
+
117
+ // Wrap subscriber with middleware chain
118
+ return {
119
+ name: `${subscriber.name}(middleware)`,
120
+ version: subscriber.version,
121
+
122
+ async trackEvent(name: string, attributes?: EventAttributes): Promise<void> {
123
+ await chain({ type: 'event', name, attributes });
124
+ },
125
+
126
+ async trackFunnelStep(funnel: string, step: FunnelStatus, attributes?: EventAttributes): Promise<void> {
127
+ await chain({ type: 'funnel', funnel, step, attributes });
128
+ },
129
+
130
+ async trackOutcome(operation: string, outcome: OutcomeStatus, attributes?: EventAttributes): Promise<void> {
131
+ await chain({ type: 'outcome', operation, outcome, attributes });
132
+ },
133
+
134
+ async trackValue(name: string, value: number, attributes?: EventAttributes): Promise<void> {
135
+ await chain({ type: 'value', name, value, attributes });
136
+ },
137
+
138
+ async shutdown(): Promise<void> {
139
+ await subscriber.shutdown?.();
140
+ },
141
+ };
142
+ }
143
+
144
+ // ============================================================================
145
+ // Built-in Middleware
146
+ // ============================================================================
147
+
148
+ /**
149
+ * Retry failed requests with exponential backoff.
150
+ *
151
+ * @example
152
+ * ```typescript
153
+ * const subscriber = applyMiddleware(adapter, [
154
+ * retryMiddleware({ maxRetries: 3, delayMs: 1000 })
155
+ * ]);
156
+ * ```
157
+ */
158
+ export function retryMiddleware(options: {
159
+ maxRetries?: number;
160
+ delayMs?: number;
161
+ onRetry?: (attempt: number, error: Error) => void;
162
+ }): SubscriberMiddleware {
163
+ const { maxRetries = 3, delayMs = 1000, onRetry } = options;
164
+
165
+ return async (event, next) => {
166
+ let lastError: Error | undefined;
167
+
168
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
169
+ try {
170
+ await next(event);
171
+ return; // Success!
172
+ } catch (error) {
173
+ lastError = error as Error;
174
+
175
+ if (attempt < maxRetries) {
176
+ onRetry?.(attempt, lastError);
177
+ // Exponential backoff: 1s, 2s, 4s, 8s...
178
+ await new Promise((resolve) => setTimeout(resolve, delayMs * Math.pow(2, attempt - 1)));
179
+ }
180
+ }
181
+ }
182
+
183
+ throw lastError;
184
+ };
185
+ }
186
+
187
+ /**
188
+ * Sample events (only send a percentage).
189
+ *
190
+ * @example
191
+ * ```typescript
192
+ * // Only send 10% of events (reduce costs)
193
+ * const subscriber = applyMiddleware(adapter, [
194
+ * samplingMiddleware(0.1)
195
+ * ]);
196
+ * ```
197
+ */
198
+ export function samplingMiddleware(rate: number): SubscriberMiddleware {
199
+ if (rate < 0 || rate > 1) {
200
+ throw new Error('Sample rate must be between 0 and 1');
201
+ }
202
+
203
+ return async (event, next) => {
204
+ if (Math.random() < rate) {
205
+ await next(event);
206
+ }
207
+ // Else: skip this event
208
+ };
209
+ }
210
+
211
+ /**
212
+ * Enrich events with additional attributes.
213
+ *
214
+ * @example
215
+ * ```typescript
216
+ * const subscriber = applyMiddleware(adapter, [
217
+ * enrichmentMiddleware((event) => ({
218
+ * ...event,
219
+ * attributes: {
220
+ * ...event.attributes,
221
+ * environment: process.env.NODE_ENV,
222
+ * timestamp: Date.now()
223
+ * }
224
+ * }))
225
+ * ]);
226
+ * ```
227
+ */
228
+ export function enrichmentMiddleware(
229
+ enricher: (event: EventsEvent) => EventsEvent
230
+ ): SubscriberMiddleware {
231
+ return async (event, next) => {
232
+ const enriched = enricher(event);
233
+ await next(enriched);
234
+ };
235
+ }
236
+
237
+ /**
238
+ * Log events to console.
239
+ *
240
+ * @example
241
+ * ```typescript
242
+ * const subscriber = applyMiddleware(adapter, [
243
+ * loggingMiddleware({ prefix: '[Events]', logAttributes: true })
244
+ * ]);
245
+ * ```
246
+ */
247
+ export function loggingMiddleware(options: {
248
+ prefix?: string;
249
+ logAttributes?: boolean;
250
+ } = {}): SubscriberMiddleware {
251
+ const { prefix = '[Events]', logAttributes = false } = options;
252
+
253
+ return async (event, next) => {
254
+ if (logAttributes) {
255
+ console.log(prefix, event.type, event);
256
+ } else {
257
+ // Just log event type and name
258
+ const eventName = 'name' in event ? event.name : `${(event as any).funnel || (event as any).operation}`;
259
+ console.log(prefix, event.type, eventName);
260
+ }
261
+
262
+ await next(event);
263
+ };
264
+ }
265
+
266
+ /**
267
+ * Filter events based on a predicate.
268
+ *
269
+ * @example
270
+ * ```typescript
271
+ * // Only send 'order' events
272
+ * const subscriber = applyMiddleware(adapter, [
273
+ * filterMiddleware((event) =>
274
+ * event.type === 'event' && event.name.startsWith('order.')
275
+ * )
276
+ * ]);
277
+ * ```
278
+ */
279
+ export function filterMiddleware(
280
+ predicate: (event: EventsEvent) => boolean
281
+ ): SubscriberMiddleware {
282
+ return async (event, next) => {
283
+ if (predicate(event)) {
284
+ await next(event);
285
+ }
286
+ };
287
+ }
288
+
289
+ /**
290
+ * Transform events.
291
+ *
292
+ * @example
293
+ * ```typescript
294
+ * // Lowercase all event names
295
+ * const subscriber = applyMiddleware(adapter, [
296
+ * transformMiddleware((event) => {
297
+ * if (event.type === 'event') {
298
+ * return { ...event, name: event.name.toLowerCase() };
299
+ * }
300
+ * return event;
301
+ * })
302
+ * ]);
303
+ * ```
304
+ */
305
+ export function transformMiddleware(
306
+ transformer: (event: EventsEvent) => EventsEvent
307
+ ): SubscriberMiddleware {
308
+ return async (event, next) => {
309
+ const transformed = transformer(event);
310
+ await next(transformed);
311
+ };
312
+ }
313
+
314
+ /**
315
+ * Batch events and flush periodically.
316
+ *
317
+ * @example
318
+ * ```typescript
319
+ * const subscriber = applyMiddleware(adapter, [
320
+ * batchingMiddleware({ batchSize: 100, flushInterval: 5000 })
321
+ * ]);
322
+ * ```
323
+ */
324
+ export function batchingMiddleware(options: {
325
+ batchSize?: number;
326
+ flushInterval?: number;
327
+ }): SubscriberMiddleware {
328
+ const { batchSize = 100, flushInterval = 5000 } = options;
329
+ const buffer: Array<{ event: EventsEvent; next: (event: EventsEvent) => Promise<void> }> = [];
330
+ let flushTimer: NodeJS.Timeout | null = null;
331
+
332
+ const flush = async () => {
333
+ const batch = [...buffer];
334
+ buffer.length = 0;
335
+
336
+ await Promise.all(batch.map(({ event, next }) => next(event)));
337
+ };
338
+
339
+ const scheduleFlush = () => {
340
+ if (flushTimer) return;
341
+ flushTimer = setTimeout(() => {
342
+ flush().catch(console.error);
343
+ flushTimer = null;
344
+ }, flushInterval);
345
+ };
346
+
347
+ return async (event, next) => {
348
+ buffer.push({ event, next });
349
+
350
+ if (buffer.length >= batchSize) {
351
+ await flush();
352
+ } else {
353
+ scheduleFlush();
354
+ }
355
+ };
356
+ }
357
+
358
+ /**
359
+ * Rate limit events (throttle).
360
+ *
361
+ * @example
362
+ * ```typescript
363
+ * // Max 100 events per second
364
+ * const subscriber = applyMiddleware(adapter, [
365
+ * rateLimitMiddleware({ requestsPerSecond: 100 })
366
+ * ]);
367
+ * ```
368
+ */
369
+ export function rateLimitMiddleware(options: {
370
+ requestsPerSecond: number;
371
+ }): SubscriberMiddleware {
372
+ const { requestsPerSecond } = options;
373
+ const intervalMs = 1000 / requestsPerSecond;
374
+ let lastCallTime = 0;
375
+ const queue: Array<() => void> = [];
376
+ let processing = false;
377
+
378
+ const processQueue = async () => {
379
+ if (processing) return;
380
+ processing = true;
381
+
382
+ while (queue.length > 0) {
383
+ const now = Date.now();
384
+ const timeSinceLastCall = now - lastCallTime;
385
+
386
+ if (timeSinceLastCall < intervalMs) {
387
+ await new Promise((resolve) => setTimeout(resolve, intervalMs - timeSinceLastCall));
388
+ }
389
+
390
+ const fn = queue.shift();
391
+ if (fn) {
392
+ lastCallTime = Date.now();
393
+ fn();
394
+ }
395
+ }
396
+
397
+ processing = false;
398
+ };
399
+
400
+ return async (event, next) => {
401
+ return new Promise<void>((resolve, reject) => {
402
+ queue.push(() => {
403
+ next(event).then(resolve).catch(reject);
404
+ });
405
+ processQueue().catch(reject);
406
+ });
407
+ };
408
+ }
409
+
410
+ /**
411
+ * Circuit breaker pattern.
412
+ *
413
+ * Opens circuit after N failures, prevents further requests for a timeout period.
414
+ *
415
+ * @example
416
+ * ```typescript
417
+ * const subscriber = applyMiddleware(adapter, [
418
+ * circuitBreakerMiddleware({
419
+ * failureThreshold: 5,
420
+ * timeout: 60000 // 1 minute
421
+ * })
422
+ * ]);
423
+ * ```
424
+ */
425
+ export function circuitBreakerMiddleware(options: {
426
+ failureThreshold?: number;
427
+ timeout?: number;
428
+ onOpen?: () => void;
429
+ onClose?: () => void;
430
+ }): SubscriberMiddleware {
431
+ const { failureThreshold = 5, timeout = 60_000, onOpen, onClose } = options;
432
+ let failureCount = 0;
433
+ let lastFailureTime = 0;
434
+ let circuitOpen = false;
435
+
436
+ return async (event, next) => {
437
+ // Check if circuit should close
438
+ if (circuitOpen) {
439
+ const now = Date.now();
440
+ if (now - lastFailureTime > timeout) {
441
+ circuitOpen = false;
442
+ failureCount = 0;
443
+ onClose?.();
444
+ } else {
445
+ throw new Error('Circuit breaker is open');
446
+ }
447
+ }
448
+
449
+ try {
450
+ await next(event);
451
+ // Success resets failure count
452
+ failureCount = 0;
453
+ } catch (error) {
454
+ failureCount++;
455
+ lastFailureTime = Date.now();
456
+
457
+ if (failureCount >= failureThreshold) {
458
+ circuitOpen = true;
459
+ onOpen?.();
460
+ }
461
+
462
+ throw error;
463
+ }
464
+ };
465
+ }
466
+
467
+ /**
468
+ * Add timeout to events.
469
+ *
470
+ * @example
471
+ * ```typescript
472
+ * const subscriber = applyMiddleware(adapter, [
473
+ * timeoutMiddleware({ timeoutMs: 5000 })
474
+ * ]);
475
+ * ```
476
+ */
477
+ export function timeoutMiddleware(options: {
478
+ timeoutMs: number;
479
+ }): SubscriberMiddleware {
480
+ const { timeoutMs } = options;
481
+
482
+ return async (event, next) => {
483
+ const timeoutPromise = new Promise<never>((_, reject) => {
484
+ setTimeout(() => reject(new Error(`Timeout after ${timeoutMs}ms`)), timeoutMs);
485
+ });
486
+
487
+ await Promise.race([next(event), timeoutPromise]);
488
+ };
489
+ }
@@ -0,0 +1,194 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import { MixpanelSubscriber } from './mixpanel';
3
+
4
+ // Mock the mixpanel module
5
+ vi.mock('mixpanel', () => ({
6
+ default: {
7
+ init: vi.fn().mockReturnValue({
8
+ track: vi.fn(),
9
+ }),
10
+ },
11
+ }));
12
+
13
+ describe('MixpanelSubscriber', () => {
14
+ beforeEach(() => {
15
+ vi.clearAllMocks();
16
+ });
17
+
18
+ describe('initialization', () => {
19
+ it('should initialize with valid config', async () => {
20
+ const adapter = new MixpanelSubscriber({
21
+ token: 'test_token',
22
+ });
23
+
24
+ await new Promise((resolve) => setTimeout(resolve, 100));
25
+
26
+ expect(adapter).toBeDefined();
27
+ });
28
+
29
+ it('should not initialize when disabled', () => {
30
+ const adapter = new MixpanelSubscriber({
31
+ token: 'test_token',
32
+ enabled: false,
33
+ });
34
+
35
+ expect(adapter).toBeDefined();
36
+ });
37
+ });
38
+
39
+ describe('trackEvent', () => {
40
+ it('should track event with attributes', async () => {
41
+ const Mixpanel = await import('mixpanel');
42
+ const adapter = new MixpanelSubscriber({
43
+ token: 'test_token',
44
+ });
45
+
46
+ await new Promise((resolve) => setTimeout(resolve, 100));
47
+
48
+ adapter.trackEvent('order.completed', {
49
+ userId: 'user-123',
50
+ amount: 99.99,
51
+ });
52
+
53
+ await new Promise((resolve) => setTimeout(resolve, 100));
54
+
55
+ const mockInstance = (Mixpanel.default.init as any).mock.results[0].value;
56
+ expect(mockInstance.track).toHaveBeenCalledWith('order.completed', {
57
+ distinct_id: 'user-123',
58
+ userId: 'user-123',
59
+ amount: 99.99,
60
+ });
61
+ });
62
+
63
+ it('should use user_id if userId is not present', async () => {
64
+ const Mixpanel = await import('mixpanel');
65
+ const adapter = new MixpanelSubscriber({
66
+ token: 'test_token',
67
+ });
68
+
69
+ await new Promise((resolve) => setTimeout(resolve, 100));
70
+
71
+ adapter.trackEvent('order.completed', {
72
+ user_id: 'user-456',
73
+ });
74
+
75
+ await new Promise((resolve) => setTimeout(resolve, 100));
76
+
77
+ const mockInstance = (Mixpanel.default.init as any).mock.results[0].value;
78
+ expect(mockInstance.track).toHaveBeenCalledWith('order.completed', {
79
+ distinct_id: 'user-456',
80
+ user_id: 'user-456',
81
+ });
82
+ });
83
+
84
+ it('should use anonymous if no userId is present', async () => {
85
+ const Mixpanel = await import('mixpanel');
86
+ const adapter = new MixpanelSubscriber({
87
+ token: 'test_token',
88
+ });
89
+
90
+ await new Promise((resolve) => setTimeout(resolve, 100));
91
+
92
+ adapter.trackEvent('page.viewed');
93
+
94
+ await new Promise((resolve) => setTimeout(resolve, 100));
95
+
96
+ const mockInstance = (Mixpanel.default.init as any).mock.results[0].value;
97
+ expect(mockInstance.track).toHaveBeenCalledWith('page.viewed', {
98
+ distinct_id: 'anonymous',
99
+ });
100
+ });
101
+
102
+ it('should not track when disabled', () => {
103
+ const adapter = new MixpanelSubscriber({
104
+ token: 'test_token',
105
+ enabled: false,
106
+ });
107
+
108
+ adapter.trackEvent('order.completed', { userId: 'user-123' });
109
+
110
+ // Should not throw
111
+ expect(true).toBe(true);
112
+ });
113
+ });
114
+
115
+ describe('trackFunnelStep', () => {
116
+ it('should track funnel step', async () => {
117
+ const Mixpanel = await import('mixpanel');
118
+ const adapter = new MixpanelSubscriber({
119
+ token: 'test_token',
120
+ });
121
+
122
+ await new Promise((resolve) => setTimeout(resolve, 100));
123
+
124
+ adapter.trackFunnelStep('checkout', 'started', {
125
+ userId: 'user-123',
126
+ cartValue: 150,
127
+ });
128
+
129
+ await new Promise((resolve) => setTimeout(resolve, 100));
130
+
131
+ const mockInstance = (Mixpanel.default.init as any).mock.results[0].value;
132
+ expect(mockInstance.track).toHaveBeenCalledWith('checkout.started', {
133
+ distinct_id: 'user-123',
134
+ funnel: 'checkout',
135
+ step: 'started',
136
+ userId: 'user-123',
137
+ cartValue: 150,
138
+ });
139
+ });
140
+ });
141
+
142
+ describe('trackOutcome', () => {
143
+ it('should track outcome', async () => {
144
+ const Mixpanel = await import('mixpanel');
145
+ const adapter = new MixpanelSubscriber({
146
+ token: 'test_token',
147
+ });
148
+
149
+ await new Promise((resolve) => setTimeout(resolve, 100));
150
+
151
+ adapter.trackOutcome('payment.processing', 'success', {
152
+ userId: 'user-123',
153
+ transactionId: 'txn-789',
154
+ });
155
+
156
+ await new Promise((resolve) => setTimeout(resolve, 100));
157
+
158
+ const mockInstance = (Mixpanel.default.init as any).mock.results[0].value;
159
+ expect(mockInstance.track).toHaveBeenCalledWith('payment.processing.success', {
160
+ distinct_id: 'user-123',
161
+ operation: 'payment.processing',
162
+ outcome: 'success',
163
+ userId: 'user-123',
164
+ transactionId: 'txn-789',
165
+ });
166
+ });
167
+ });
168
+
169
+ describe('trackValue', () => {
170
+ it('should track value', async () => {
171
+ const Mixpanel = await import('mixpanel');
172
+ const adapter = new MixpanelSubscriber({
173
+ token: 'test_token',
174
+ });
175
+
176
+ await new Promise((resolve) => setTimeout(resolve, 100));
177
+
178
+ adapter.trackValue('revenue', 99.99, {
179
+ userId: 'user-123',
180
+ currency: 'USD',
181
+ });
182
+
183
+ await new Promise((resolve) => setTimeout(resolve, 100));
184
+
185
+ const mockInstance = (Mixpanel.default.init as any).mock.results[0].value;
186
+ expect(mockInstance.track).toHaveBeenCalledWith('revenue', {
187
+ distinct_id: 'user-123',
188
+ value: 99.99,
189
+ userId: 'user-123',
190
+ currency: 'USD',
191
+ });
192
+ });
193
+ });
194
+ });