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,264 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import { WebhookSubscriber } from './webhook';
3
+
4
+ // Mock fetch globally
5
+ globalThis.fetch = vi.fn();
6
+
7
+ describe('WebhookSubscriber', () => {
8
+ beforeEach(() => {
9
+ vi.clearAllMocks();
10
+ (globalThis.fetch as any).mockResolvedValue({
11
+ ok: true,
12
+ status: 200,
13
+ statusText: 'OK',
14
+ });
15
+ });
16
+
17
+ describe('initialization', () => {
18
+ it('should initialize with valid config', () => {
19
+ const adapter = new WebhookSubscriber({
20
+ url: 'https://hooks.example.com/webhook',
21
+ });
22
+
23
+ expect(adapter).toBeDefined();
24
+ });
25
+
26
+ it('should initialize with custom headers', () => {
27
+ const adapter = new WebhookSubscriber({
28
+ url: 'https://hooks.example.com/webhook',
29
+ headers: { 'X-API-Key': 'secret' },
30
+ });
31
+
32
+ expect(adapter).toBeDefined();
33
+ });
34
+
35
+ it('should not send when disabled', () => {
36
+ const adapter = new WebhookSubscriber({
37
+ url: 'https://hooks.example.com/webhook',
38
+ enabled: false,
39
+ });
40
+
41
+ expect(adapter).toBeDefined();
42
+ });
43
+ });
44
+
45
+ describe('trackEvent', () => {
46
+ it('should send event to webhook', async () => {
47
+ const adapter = new WebhookSubscriber({
48
+ url: 'https://hooks.example.com/webhook',
49
+ });
50
+
51
+ adapter.trackEvent('order.completed', {
52
+ userId: 'user-123',
53
+ amount: 99.99,
54
+ });
55
+
56
+ await new Promise((resolve) => setTimeout(resolve, 100));
57
+
58
+ expect(globalThis.fetch).toHaveBeenCalledWith(
59
+ 'https://hooks.example.com/webhook',
60
+ expect.objectContaining({
61
+ method: 'POST',
62
+ headers: {
63
+ 'Content-Type': 'application/json',
64
+ },
65
+ body: expect.stringContaining('order.completed'),
66
+ }),
67
+ );
68
+ });
69
+
70
+ it('should include custom headers', async () => {
71
+ const adapter = new WebhookSubscriber({
72
+ url: 'https://hooks.example.com/webhook',
73
+ headers: { 'X-API-Key': 'secret' },
74
+ });
75
+
76
+ adapter.trackEvent('order.completed', { userId: 'user-123' });
77
+
78
+ await new Promise((resolve) => setTimeout(resolve, 100));
79
+
80
+ expect(globalThis.fetch).toHaveBeenCalledWith(
81
+ 'https://hooks.example.com/webhook',
82
+ expect.objectContaining({
83
+ headers: {
84
+ 'Content-Type': 'application/json',
85
+ 'X-API-Key': 'secret',
86
+ },
87
+ }),
88
+ );
89
+ });
90
+
91
+ it('should not send when disabled', async () => {
92
+ const adapter = new WebhookSubscriber({
93
+ url: 'https://hooks.example.com/webhook',
94
+ enabled: false,
95
+ });
96
+
97
+ adapter.trackEvent('order.completed', { userId: 'user-123' });
98
+
99
+ await new Promise((resolve) => setTimeout(resolve, 100));
100
+
101
+ expect(globalThis.fetch).not.toHaveBeenCalled();
102
+ });
103
+ });
104
+
105
+ describe('trackFunnelStep', () => {
106
+ it('should send funnel step to webhook', async () => {
107
+ const adapter = new WebhookSubscriber({
108
+ url: 'https://hooks.example.com/webhook',
109
+ });
110
+
111
+ adapter.trackFunnelStep('checkout', 'started', {
112
+ userId: 'user-123',
113
+ cartValue: 150,
114
+ });
115
+
116
+ await new Promise((resolve) => setTimeout(resolve, 100));
117
+
118
+ expect(globalThis.fetch).toHaveBeenCalledWith(
119
+ 'https://hooks.example.com/webhook',
120
+ expect.objectContaining({
121
+ method: 'POST',
122
+ body: expect.stringContaining('funnel'),
123
+ }),
124
+ );
125
+
126
+ const callBody = JSON.parse((globalThis.fetch as any).mock.calls[0][1].body);
127
+ expect(callBody).toMatchObject({
128
+ type: 'funnel',
129
+ funnel: 'checkout',
130
+ step: 'started',
131
+ });
132
+ });
133
+ });
134
+
135
+ describe('trackOutcome', () => {
136
+ it('should send outcome to webhook', async () => {
137
+ const adapter = new WebhookSubscriber({
138
+ url: 'https://hooks.example.com/webhook',
139
+ });
140
+
141
+ adapter.trackOutcome('payment.processing', 'success', {
142
+ userId: 'user-123',
143
+ transactionId: 'txn-789',
144
+ });
145
+
146
+ await new Promise((resolve) => setTimeout(resolve, 100));
147
+
148
+ expect(globalThis.fetch).toHaveBeenCalledWith(
149
+ 'https://hooks.example.com/webhook',
150
+ expect.objectContaining({
151
+ method: 'POST',
152
+ body: expect.stringContaining('outcome'),
153
+ }),
154
+ );
155
+
156
+ const callBody = JSON.parse((globalThis.fetch as any).mock.calls[0][1].body);
157
+ expect(callBody).toMatchObject({
158
+ type: 'outcome',
159
+ operation: 'payment.processing',
160
+ outcome: 'success',
161
+ });
162
+ });
163
+ });
164
+
165
+ describe('trackValue', () => {
166
+ it('should send value to webhook', async () => {
167
+ const adapter = new WebhookSubscriber({
168
+ url: 'https://hooks.example.com/webhook',
169
+ });
170
+
171
+ adapter.trackValue('revenue', 99.99, {
172
+ userId: 'user-123',
173
+ currency: 'USD',
174
+ });
175
+
176
+ await new Promise((resolve) => setTimeout(resolve, 100));
177
+
178
+ expect(globalThis.fetch).toHaveBeenCalledWith(
179
+ 'https://hooks.example.com/webhook',
180
+ expect.objectContaining({
181
+ method: 'POST',
182
+ body: expect.stringContaining('value'),
183
+ }),
184
+ );
185
+
186
+ const callBody = JSON.parse((globalThis.fetch as any).mock.calls[0][1].body);
187
+ expect(callBody).toMatchObject({
188
+ type: 'value',
189
+ name: 'revenue',
190
+ value: 99.99,
191
+ });
192
+ });
193
+ });
194
+
195
+ describe('retry logic', () => {
196
+ it(
197
+ 'should retry on failure',
198
+ async () => {
199
+ (globalThis.fetch as any)
200
+ .mockRejectedValueOnce(new Error('Network error'))
201
+ .mockRejectedValueOnce(new Error('Network error'))
202
+ .mockResolvedValueOnce({
203
+ ok: true,
204
+ status: 200,
205
+ statusText: 'OK',
206
+ });
207
+
208
+ const adapter = new WebhookSubscriber({
209
+ url: 'https://hooks.example.com/webhook',
210
+ maxRetries: 3,
211
+ });
212
+
213
+ adapter.trackEvent('order.completed', { userId: 'user-123' });
214
+
215
+ await new Promise((resolve) => setTimeout(resolve, 5000));
216
+
217
+ expect(globalThis.fetch).toHaveBeenCalledTimes(3);
218
+ },
219
+ 10_000,
220
+ );
221
+
222
+ it(
223
+ 'should respect maxRetries setting',
224
+ async () => {
225
+ (globalThis.fetch as any).mockRejectedValue(new Error('Network error'));
226
+
227
+ const adapter = new WebhookSubscriber({
228
+ url: 'https://hooks.example.com/webhook',
229
+ maxRetries: 2,
230
+ });
231
+
232
+ adapter.trackEvent('order.completed', { userId: 'user-123' });
233
+
234
+ await new Promise((resolve) => setTimeout(resolve, 5000));
235
+
236
+ expect(globalThis.fetch).toHaveBeenCalledTimes(2);
237
+ },
238
+ 10_000,
239
+ );
240
+ });
241
+
242
+ describe('shutdown', () => {
243
+ it('should wait for pending requests', async () => {
244
+ const adapter = new WebhookSubscriber({
245
+ url: 'https://hooks.example.com/webhook',
246
+ });
247
+
248
+ adapter.trackEvent('order.completed', { userId: 'user-123' });
249
+ adapter.trackEvent('order.completed', { userId: 'user-456' });
250
+
251
+ await adapter.shutdown();
252
+
253
+ expect(globalThis.fetch).toHaveBeenCalledTimes(2);
254
+ });
255
+
256
+ it('should not throw when no pending requests', async () => {
257
+ const adapter = new WebhookSubscriber({
258
+ url: 'https://hooks.example.com/webhook',
259
+ });
260
+
261
+ await expect(adapter.shutdown()).resolves.not.toThrow();
262
+ });
263
+ });
264
+ });
package/src/webhook.ts ADDED
@@ -0,0 +1,158 @@
1
+ /**
2
+ * Webhook Subscriber for autotel
3
+ *
4
+ * Send events to any webhook endpoint (custom integrations, Zapier, Make.com, etc.).
5
+ *
6
+ * @example
7
+ * ```typescript
8
+ * import { Events } from 'autotel/events';
9
+ * import { WebhookSubscriber } from 'autotel-subscribers/webhook';
10
+ *
11
+ * const events = new Events('checkout', {
12
+ * subscribers: [
13
+ * new WebhookSubscriber({
14
+ * url: 'https://hooks.zapier.com/hooks/catch/...',
15
+ * headers: { 'X-API-Key': 'secret' }
16
+ * })
17
+ * ]
18
+ * });
19
+ *
20
+ * events.trackEvent('order.completed', { userId: '123', amount: 99.99 });
21
+ * ```
22
+ */
23
+
24
+ import type {
25
+ EventSubscriber,
26
+ EventAttributes,
27
+ FunnelStatus,
28
+ OutcomeStatus,
29
+ } from 'autotel/event-subscriber';
30
+
31
+ export interface WebhookConfig {
32
+ /** Webhook URL */
33
+ url: string;
34
+ /** Optional headers (e.g., API keys) */
35
+ headers?: Record<string, string>;
36
+ /** Enable/disable the subscriber */
37
+ enabled?: boolean;
38
+ /** Retry failed requests (default: 3) */
39
+ maxRetries?: number;
40
+ }
41
+
42
+ export class WebhookSubscriber implements EventSubscriber {
43
+ readonly name = 'WebhookSubscriber';
44
+ readonly version = '1.0.0';
45
+
46
+ private config: WebhookConfig;
47
+ private enabled: boolean;
48
+ private pendingRequests: Set<Promise<void>> = new Set();
49
+
50
+ constructor(config: WebhookConfig) {
51
+ this.config = config;
52
+ this.enabled = config.enabled ?? true;
53
+ }
54
+
55
+ private async send(payload: any): Promise<void> {
56
+ if (!this.enabled) return;
57
+
58
+ const maxRetries = this.config.maxRetries ?? 3;
59
+ let lastError: Error | undefined;
60
+
61
+ for (let attempt = 0; attempt < maxRetries; attempt++) {
62
+ try {
63
+ const response = await fetch(this.config.url, {
64
+ method: 'POST',
65
+ headers: {
66
+ 'Content-Type': 'application/json',
67
+ ...this.config.headers,
68
+ },
69
+ body: JSON.stringify(payload),
70
+ });
71
+
72
+ if (!response.ok) {
73
+ throw new Error(`Webhook returned ${response.status}: ${response.statusText}`);
74
+ }
75
+
76
+ return; // Success
77
+ } catch (error) {
78
+ lastError = error as Error;
79
+ if (attempt < maxRetries - 1) {
80
+ // Exponential backoff
81
+ await new Promise((resolve) => setTimeout(resolve, Math.pow(2, attempt) * 1000));
82
+ }
83
+ }
84
+ }
85
+
86
+ console.error(`Webhook subscriber failed after ${maxRetries} attempts:`, lastError);
87
+ }
88
+
89
+ async trackEvent(name: string, attributes?: EventAttributes): Promise<void> {
90
+ const request = this.send({
91
+ type: 'event',
92
+ name,
93
+ attributes,
94
+ timestamp: new Date().toISOString(),
95
+ });
96
+ this.trackRequest(request);
97
+ await request;
98
+ }
99
+
100
+ async trackFunnelStep(
101
+ funnelName: string,
102
+ step: FunnelStatus,
103
+ attributes?: EventAttributes,
104
+ ): Promise<void> {
105
+ const request = this.send({
106
+ type: 'funnel',
107
+ funnel: funnelName,
108
+ step,
109
+ attributes,
110
+ timestamp: new Date().toISOString(),
111
+ });
112
+ this.trackRequest(request);
113
+ await request;
114
+ }
115
+
116
+ async trackOutcome(
117
+ operationName: string,
118
+ outcome: OutcomeStatus,
119
+ attributes?: EventAttributes,
120
+ ): Promise<void> {
121
+ const request = this.send({
122
+ type: 'outcome',
123
+ operation: operationName,
124
+ outcome,
125
+ attributes,
126
+ timestamp: new Date().toISOString(),
127
+ });
128
+ this.trackRequest(request);
129
+ await request;
130
+ }
131
+
132
+ async trackValue(name: string, value: number, attributes?: EventAttributes): Promise<void> {
133
+ const request = this.send({
134
+ type: 'value',
135
+ name,
136
+ value,
137
+ attributes,
138
+ timestamp: new Date().toISOString(),
139
+ });
140
+ this.trackRequest(request);
141
+ await request;
142
+ }
143
+
144
+ private trackRequest(request: Promise<void>): void {
145
+ this.pendingRequests.add(request);
146
+ void request.finally(() => {
147
+ this.pendingRequests.delete(request);
148
+ });
149
+ }
150
+
151
+ /** Wait for all pending webhook requests to complete */
152
+ async shutdown(): Promise<void> {
153
+ if (this.pendingRequests.size > 0) {
154
+ await Promise.allSettled(this.pendingRequests);
155
+ }
156
+ }
157
+ }
158
+