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,134 @@
1
+ /**
2
+ * Mixpanel Subscriber for autotel
3
+ *
4
+ * Send events to Mixpanel for product events.
5
+ *
6
+ * @example
7
+ * ```typescript
8
+ * import { Events } from 'autotel/events';
9
+ * import { MixpanelSubscriber } from 'autotel-subscribers/mixpanel';
10
+ *
11
+ * const events = new Events('checkout', {
12
+ * subscribers: [
13
+ * new MixpanelSubscriber({
14
+ * token: process.env.MIXPANEL_TOKEN!
15
+ * })
16
+ * ]
17
+ * });
18
+ *
19
+ * events.trackEvent('order.completed', { userId: '123', amount: 99.99 });
20
+ * ```
21
+ */
22
+
23
+ import type {
24
+ EventSubscriber,
25
+ EventAttributes,
26
+ FunnelStatus,
27
+ OutcomeStatus,
28
+ } from 'autotel/event-subscriber';
29
+
30
+ export interface MixpanelConfig {
31
+ /** Mixpanel project token */
32
+ token: string;
33
+ /** Enable/disable the subscriber */
34
+ enabled?: boolean;
35
+ }
36
+
37
+ export class MixpanelSubscriber implements EventSubscriber {
38
+ readonly name = 'MixpanelSubscriber';
39
+ readonly version = '1.0.0';
40
+
41
+ private mixpanel: any;
42
+ private enabled: boolean;
43
+ private config: MixpanelConfig;
44
+ private initPromise: Promise<void> | null = null;
45
+
46
+ constructor(config: MixpanelConfig) {
47
+ this.enabled = config.enabled ?? true;
48
+ this.config = config;
49
+
50
+ if (this.enabled) {
51
+ // Start initialization immediately but don't block constructor
52
+ this.initPromise = this.initialize();
53
+ }
54
+ }
55
+
56
+ private async initialize(): Promise<void> {
57
+ try {
58
+ // Dynamic import to avoid adding mixpanel as a hard dependency
59
+ const Mixpanel = await import('mixpanel');
60
+ this.mixpanel = Mixpanel.default.init(this.config.token);
61
+ } catch (error) {
62
+ console.error(
63
+ 'Mixpanel subscriber failed to initialize. Install mixpanel: pnpm add mixpanel',
64
+ error,
65
+ );
66
+ this.enabled = false;
67
+ }
68
+ }
69
+
70
+ private async ensureInitialized(): Promise<void> {
71
+ if (this.initPromise) {
72
+ await this.initPromise;
73
+ this.initPromise = null;
74
+ }
75
+ }
76
+
77
+ async trackEvent(name: string, attributes?: EventAttributes): Promise<void> {
78
+ if (!this.enabled) return;
79
+
80
+ await this.ensureInitialized();
81
+ const distinctId = attributes?.userId || attributes?.user_id || 'anonymous';
82
+ this.mixpanel?.track(name, {
83
+ distinct_id: distinctId,
84
+ ...attributes,
85
+ });
86
+ }
87
+
88
+ async trackFunnelStep(
89
+ funnelName: string,
90
+ step: FunnelStatus,
91
+ attributes?: EventAttributes,
92
+ ): Promise<void> {
93
+ if (!this.enabled) return;
94
+
95
+ await this.ensureInitialized();
96
+ const distinctId = attributes?.userId || attributes?.user_id || 'anonymous';
97
+ this.mixpanel?.track(`${funnelName}.${step}`, {
98
+ distinct_id: distinctId,
99
+ funnel: funnelName,
100
+ step,
101
+ ...attributes,
102
+ });
103
+ }
104
+
105
+ async trackOutcome(
106
+ operationName: string,
107
+ outcome: OutcomeStatus,
108
+ attributes?: EventAttributes,
109
+ ): Promise<void> {
110
+ if (!this.enabled) return;
111
+
112
+ await this.ensureInitialized();
113
+ const distinctId = attributes?.userId || attributes?.user_id || 'anonymous';
114
+ this.mixpanel?.track(`${operationName}.${outcome}`, {
115
+ distinct_id: distinctId,
116
+ operation: operationName,
117
+ outcome,
118
+ ...attributes,
119
+ });
120
+ }
121
+
122
+ async trackValue(name: string, value: number, attributes?: EventAttributes): Promise<void> {
123
+ if (!this.enabled) return;
124
+
125
+ await this.ensureInitialized();
126
+ const distinctId = attributes?.userId || attributes?.user_id || 'anonymous';
127
+ this.mixpanel?.track(name, {
128
+ distinct_id: distinctId,
129
+ value,
130
+ ...attributes,
131
+ });
132
+ }
133
+ }
134
+
@@ -0,0 +1,333 @@
1
+ /**
2
+ * Mock Events Subscriber for Testing
3
+ *
4
+ * In-memory subscriber that captures all events events for testing assertions.
5
+ * Useful for unit testing code that uses Events without making real API calls.
6
+ *
7
+ * @example Basic testing
8
+ * ```typescript
9
+ * import { Events } from 'autotel/events';
10
+ * import { MockEventSubscriber } from 'autotel-subscribers/mock-event-subscriber';
11
+ * import { describe, it, expect, beforeEach } from 'vitest';
12
+ *
13
+ * describe('CheckoutService', () => {
14
+ * let mockSubscriber: MockEventSubscriber;
15
+ * let events: Events;
16
+ *
17
+ * beforeEach(() => {
18
+ * mockSubscriber = new MockEventSubscriber();
19
+ * events = new Events('checkout', { subscribers: [mockSubscriber] });
20
+ * });
21
+ *
22
+ * it('should track order completion', async () => {
23
+ * const service = new CheckoutService(events);
24
+ * await service.completeOrder('ord_123', 99.99);
25
+ *
26
+ * expect(mockSubscriber.events).toHaveLength(1);
27
+ * expect(mockSubscriber.events[0]).toMatchObject({
28
+ * type: 'event',
29
+ * name: 'order.completed',
30
+ * attributes: { orderId: 'ord_123', amount: 99.99 }
31
+ * });
32
+ * });
33
+ *
34
+ * it('should track checkout funnel', () => {
35
+ * events.trackFunnelStep('checkout', 'started', { cartValue: 99.99 });
36
+ * events.trackFunnelStep('checkout', 'completed', { cartValue: 99.99 });
37
+ *
38
+ * const funnelEvents = mockSubscriber.getFunnelEvents('checkout');
39
+ * expect(funnelEvents).toHaveLength(2);
40
+ * expect(funnelEvents[0].step).toBe('started');
41
+ * expect(funnelEvents[1].step).toBe('completed');
42
+ * });
43
+ * });
44
+ * ```
45
+ *
46
+ * @example With Outbox Pattern
47
+ * ```typescript
48
+ * import { createPublisher } from 'autotel-outbox';
49
+ * import { MockOutboxStorage } from 'autotel-outbox/testing';
50
+ *
51
+ * it('should broadcast events to all subscribers', async () => {
52
+ * const mockOutbox = new MockOutboxStorage();
53
+ * const mockSubscriber = new MockEventSubscriber();
54
+ *
55
+ * // Add events to outbox
56
+ * await mockOutbox.writeEvent({
57
+ * id: '1',
58
+ * type: 'order.completed',
59
+ * aggregateId: 'ord_123',
60
+ * aggregateType: 'Order',
61
+ * payload: { amount: 99.99 },
62
+ * createdAt: new Date().toISOString(),
63
+ * publishedAt: null
64
+ * });
65
+ *
66
+ * // Run publisher
67
+ * const publisher = createPublisher(mockOutbox, [mockSubscriber]);
68
+ * await publisher();
69
+ *
70
+ * // Assert event was broadcast
71
+ * expect(mockSubscriber.events).toHaveLength(1);
72
+ * expect(mockSubscriber.events[0].name).toBe('order.completed');
73
+ * });
74
+ * ```
75
+ */
76
+
77
+ import type {
78
+ EventSubscriber,
79
+ EventAttributes,
80
+ FunnelStatus,
81
+ OutcomeStatus,
82
+ } from 'autotel/event-subscriber';
83
+
84
+ /**
85
+ * Captured event data
86
+ */
87
+ export interface CapturedEvent {
88
+ type: 'event' | 'funnel' | 'outcome' | 'value';
89
+ name: string;
90
+ attributes?: EventAttributes;
91
+ funnel?: string;
92
+ step?: FunnelStatus;
93
+ operation?: string;
94
+ outcome?: OutcomeStatus;
95
+ value?: number;
96
+ timestamp: string;
97
+ }
98
+
99
+ /**
100
+ * Mock subscriber for testing
101
+ *
102
+ * Captures all events calls in memory for test assertions.
103
+ * Does not make any real API calls.
104
+ */
105
+ export class MockEventSubscriber implements EventSubscriber {
106
+ readonly name = 'MockEventSubscriber';
107
+ readonly version = '1.0.0';
108
+
109
+ /**
110
+ * All captured events
111
+ */
112
+ public events: CapturedEvent[] = [];
113
+
114
+ /**
115
+ * Track if shutdown was called
116
+ */
117
+ public shutdownCalled = false;
118
+
119
+ async trackEvent(name: string, attributes?: EventAttributes): Promise<void> {
120
+ this.events.push({
121
+ type: 'event',
122
+ name,
123
+ attributes,
124
+ timestamp: new Date().toISOString(),
125
+ });
126
+ }
127
+
128
+ async trackFunnelStep(
129
+ funnelName: string,
130
+ step: FunnelStatus,
131
+ attributes?: EventAttributes,
132
+ ): Promise<void> {
133
+ this.events.push({
134
+ type: 'funnel',
135
+ name: `${funnelName}.${step}`,
136
+ funnel: funnelName,
137
+ step,
138
+ attributes,
139
+ timestamp: new Date().toISOString(),
140
+ });
141
+ }
142
+
143
+ async trackOutcome(
144
+ operationName: string,
145
+ outcome: OutcomeStatus,
146
+ attributes?: EventAttributes,
147
+ ): Promise<void> {
148
+ this.events.push({
149
+ type: 'outcome',
150
+ name: `${operationName}.${outcome}`,
151
+ operation: operationName,
152
+ outcome,
153
+ attributes,
154
+ timestamp: new Date().toISOString(),
155
+ });
156
+ }
157
+
158
+ async trackValue(
159
+ name: string,
160
+ value: number,
161
+ attributes?: EventAttributes,
162
+ ): Promise<void> {
163
+ this.events.push({
164
+ type: 'value',
165
+ name,
166
+ value,
167
+ attributes,
168
+ timestamp: new Date().toISOString(),
169
+ });
170
+ }
171
+
172
+ async shutdown(): Promise<void> {
173
+ this.shutdownCalled = true;
174
+ }
175
+
176
+ /**
177
+ * Reset captured events (useful between tests)
178
+ */
179
+ reset(): void {
180
+ this.events = [];
181
+ this.shutdownCalled = false;
182
+ }
183
+
184
+ /**
185
+ * Get events by type
186
+ */
187
+ getEventsByType(
188
+ type: 'event' | 'funnel' | 'outcome' | 'value',
189
+ ): CapturedEvent[] {
190
+ return this.events.filter((e) => e.type === type);
191
+ }
192
+
193
+ /**
194
+ * Get events by name
195
+ */
196
+ getEventsByName(name: string): CapturedEvent[] {
197
+ return this.events.filter((e) => e.name === name);
198
+ }
199
+
200
+ /**
201
+ * Get funnel events for a specific funnel
202
+ */
203
+ getFunnelEvents(funnelName: string): CapturedEvent[] {
204
+ return this.events.filter(
205
+ (e) => e.type === 'funnel' && e.funnel === funnelName,
206
+ );
207
+ }
208
+
209
+ /**
210
+ * Get outcome events for a specific operation
211
+ */
212
+ getOutcomeEvents(operationName: string): CapturedEvent[] {
213
+ return this.events.filter(
214
+ (e) => e.type === 'outcome' && e.operation === operationName,
215
+ );
216
+ }
217
+
218
+ /**
219
+ * Get value events by metric name
220
+ */
221
+ getValueEvents(metricName: string): CapturedEvent[] {
222
+ return this.events.filter(
223
+ (e) => e.type === 'value' && e.name === metricName,
224
+ );
225
+ }
226
+
227
+ /**
228
+ * Assert that an event was tracked
229
+ */
230
+ assertEventTracked(
231
+ name: string,
232
+ attributes?: Partial<EventAttributes>,
233
+ ): void {
234
+ const events = this.getEventsByName(name);
235
+
236
+ if (events.length === 0) {
237
+ throw new Error(`No events found with name: ${name}`);
238
+ }
239
+
240
+ if (attributes) {
241
+ const matchingEvent = events.find((event) => {
242
+ if (!event.attributes) return false;
243
+
244
+ return Object.entries(attributes).every(
245
+ ([key, value]) => event.attributes![key] === value,
246
+ );
247
+ });
248
+
249
+ if (!matchingEvent) {
250
+ throw new Error(
251
+ `Event "${name}" found, but no matching attributes: ${JSON.stringify(attributes)}`,
252
+ );
253
+ }
254
+ }
255
+ }
256
+
257
+ /**
258
+ * Assert that a funnel step was tracked
259
+ */
260
+ assertFunnelStepTracked(
261
+ funnelName: string,
262
+ step: FunnelStatus,
263
+ attributes?: Partial<EventAttributes>,
264
+ ): void {
265
+ const events = this.getFunnelEvents(funnelName);
266
+ const matchingEvent = events.find((e) => e.step === step);
267
+
268
+ if (!matchingEvent) {
269
+ throw new Error(
270
+ `Funnel "${funnelName}" step "${step}" was not tracked`,
271
+ );
272
+ }
273
+
274
+ if (attributes) {
275
+ const hasMatchingAttributes = Object.entries(attributes).every(
276
+ ([key, value]) => matchingEvent.attributes?.[key] === value,
277
+ );
278
+
279
+ if (!hasMatchingAttributes) {
280
+ throw new Error(
281
+ `Funnel step tracked, but attributes don't match: ${JSON.stringify(attributes)}`,
282
+ );
283
+ }
284
+ }
285
+ }
286
+
287
+ /**
288
+ * Assert that an outcome was tracked
289
+ */
290
+ assertOutcomeTracked(
291
+ operationName: string,
292
+ outcome: OutcomeStatus,
293
+ attributes?: Partial<EventAttributes>,
294
+ ): void {
295
+ const events = this.getOutcomeEvents(operationName);
296
+ const matchingEvent = events.find((e) => e.outcome === outcome);
297
+
298
+ if (!matchingEvent) {
299
+ throw new Error(
300
+ `Outcome "${operationName}.${outcome}" was not tracked`,
301
+ );
302
+ }
303
+
304
+ if (attributes) {
305
+ const hasMatchingAttributes = Object.entries(attributes).every(
306
+ ([key, value]) => matchingEvent.attributes?.[key] === value,
307
+ );
308
+
309
+ if (!hasMatchingAttributes) {
310
+ throw new Error(
311
+ `Outcome tracked, but attributes don't match: ${JSON.stringify(attributes)}`,
312
+ );
313
+ }
314
+ }
315
+ }
316
+
317
+ /**
318
+ * Pretty print all captured events (useful for debugging)
319
+ */
320
+ printEvents(): void {
321
+ console.log(`\n[${this.name}] Captured ${this.events.length} events:\n`);
322
+ for (const [index, event] of this.events.entries()) {
323
+ console.log(`${index + 1}. [${event.type}] ${event.name}`);
324
+ if (event.attributes) {
325
+ console.log(` Attributes:`, event.attributes);
326
+ }
327
+ if (event.value !== undefined) {
328
+ console.log(` Value: ${event.value}`);
329
+ }
330
+ console.log(` Timestamp: ${event.timestamp}\n`);
331
+ }
332
+ }
333
+ }