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.
- package/LICENSE +21 -0
- package/README.md +669 -0
- package/dist/amplitude.cjs +2486 -0
- package/dist/amplitude.cjs.map +1 -0
- package/dist/amplitude.d.cts +49 -0
- package/dist/amplitude.d.ts +49 -0
- package/dist/amplitude.js +2463 -0
- package/dist/amplitude.js.map +1 -0
- package/dist/event-subscriber-base-CnF3V56W.d.cts +182 -0
- package/dist/event-subscriber-base-CnF3V56W.d.ts +182 -0
- package/dist/factories.cjs +16660 -0
- package/dist/factories.cjs.map +1 -0
- package/dist/factories.d.cts +304 -0
- package/dist/factories.d.ts +304 -0
- package/dist/factories.js +16624 -0
- package/dist/factories.js.map +1 -0
- package/dist/index.cjs +16575 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +179 -0
- package/dist/index.d.ts +179 -0
- package/dist/index.js +16539 -0
- package/dist/index.js.map +1 -0
- package/dist/middleware.cjs +220 -0
- package/dist/middleware.cjs.map +1 -0
- package/dist/middleware.d.cts +227 -0
- package/dist/middleware.d.ts +227 -0
- package/dist/middleware.js +208 -0
- package/dist/middleware.js.map +1 -0
- package/dist/mixpanel.cjs +2940 -0
- package/dist/mixpanel.cjs.map +1 -0
- package/dist/mixpanel.d.cts +47 -0
- package/dist/mixpanel.d.ts +47 -0
- package/dist/mixpanel.js +2932 -0
- package/dist/mixpanel.js.map +1 -0
- package/dist/posthog.cjs +4115 -0
- package/dist/posthog.cjs.map +1 -0
- package/dist/posthog.d.cts +299 -0
- package/dist/posthog.d.ts +299 -0
- package/dist/posthog.js +4113 -0
- package/dist/posthog.js.map +1 -0
- package/dist/segment.cjs +6822 -0
- package/dist/segment.cjs.map +1 -0
- package/dist/segment.d.cts +49 -0
- package/dist/segment.d.ts +49 -0
- package/dist/segment.js +6794 -0
- package/dist/segment.js.map +1 -0
- package/dist/slack.cjs +368 -0
- package/dist/slack.cjs.map +1 -0
- package/dist/slack.d.cts +126 -0
- package/dist/slack.d.ts +126 -0
- package/dist/slack.js +366 -0
- package/dist/slack.js.map +1 -0
- package/dist/webhook.cjs +100 -0
- package/dist/webhook.cjs.map +1 -0
- package/dist/webhook.d.cts +53 -0
- package/dist/webhook.d.ts +53 -0
- package/dist/webhook.js +98 -0
- package/dist/webhook.js.map +1 -0
- package/examples/quickstart-custom-subscriber.ts +144 -0
- package/examples/subscriber-bigquery.ts +219 -0
- package/examples/subscriber-databricks.ts +280 -0
- package/examples/subscriber-kafka.ts +326 -0
- package/examples/subscriber-kinesis.ts +307 -0
- package/examples/subscriber-posthog.ts +421 -0
- package/examples/subscriber-pubsub.ts +336 -0
- package/examples/subscriber-snowflake.ts +232 -0
- package/package.json +141 -0
- package/src/amplitude.test.ts +231 -0
- package/src/amplitude.ts +148 -0
- package/src/event-subscriber-base.ts +325 -0
- package/src/factories.ts +197 -0
- package/src/index.ts +50 -0
- package/src/middleware.ts +489 -0
- package/src/mixpanel.test.ts +194 -0
- package/src/mixpanel.ts +134 -0
- package/src/mock-event-subscriber.ts +333 -0
- package/src/posthog.test.ts +629 -0
- package/src/posthog.ts +530 -0
- package/src/segment.test.ts +228 -0
- package/src/segment.ts +148 -0
- package/src/slack.ts +383 -0
- package/src/streaming-event-subscriber.ts +323 -0
- package/src/testing/index.ts +37 -0
- package/src/testing/mock-webhook-server.ts +242 -0
- package/src/testing/subscriber-test-harness.ts +365 -0
- package/src/webhook.test.ts +264 -0
- package/src/webhook.ts +158 -0
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { SegmentSubscriber } from './segment';
|
|
3
|
+
|
|
4
|
+
// Mock the @segment/analytics-node module
|
|
5
|
+
const mockTrack = vi.fn();
|
|
6
|
+
const mockCloseAndFlush = vi.fn(() => Promise.resolve());
|
|
7
|
+
|
|
8
|
+
// Create a mock Analytics class that returns instances with mocked methods
|
|
9
|
+
const MockAnalytics = vi.fn(function(this: any) {
|
|
10
|
+
this.track = mockTrack;
|
|
11
|
+
this.closeAndFlush = mockCloseAndFlush;
|
|
12
|
+
return this;
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
vi.mock('@segment/analytics-node', () => ({
|
|
16
|
+
Analytics: MockAnalytics,
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
describe('SegmentSubscriber', () => {
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
mockTrack.mockClear();
|
|
22
|
+
mockCloseAndFlush.mockClear();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe('initialization', () => {
|
|
26
|
+
it('should initialize with valid config', async () => {
|
|
27
|
+
const adapter = new SegmentSubscriber({
|
|
28
|
+
writeKey: 'test_write_key',
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
32
|
+
|
|
33
|
+
expect(adapter).toBeDefined();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('should not initialize when disabled', () => {
|
|
37
|
+
const adapter = new SegmentSubscriber({
|
|
38
|
+
writeKey: 'test_write_key',
|
|
39
|
+
enabled: false,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
expect(adapter).toBeDefined();
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe('trackEvent', () => {
|
|
47
|
+
it('should track event with attributes', async () => {
|
|
48
|
+
const adapter = new SegmentSubscriber({
|
|
49
|
+
writeKey: 'test_write_key',
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
53
|
+
|
|
54
|
+
await adapter.trackEvent('order.completed', {
|
|
55
|
+
userId: 'user-123',
|
|
56
|
+
amount: 99.99,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
60
|
+
|
|
61
|
+
expect(mockTrack).toHaveBeenCalledWith({
|
|
62
|
+
userId: 'user-123',
|
|
63
|
+
event: 'order.completed',
|
|
64
|
+
properties: {
|
|
65
|
+
userId: 'user-123',
|
|
66
|
+
amount: 99.99,
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('should use user_id if userId is not present', async () => {
|
|
72
|
+
const adapter = new SegmentSubscriber({
|
|
73
|
+
writeKey: 'test_write_key',
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
77
|
+
|
|
78
|
+
await adapter.trackEvent('order.completed', {
|
|
79
|
+
user_id: 'user-456',
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
83
|
+
|
|
84
|
+
expect(mockTrack).toHaveBeenCalledWith({
|
|
85
|
+
userId: 'user-456',
|
|
86
|
+
event: 'order.completed',
|
|
87
|
+
properties: {
|
|
88
|
+
user_id: 'user-456',
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('should use anonymous if no userId is present', async () => {
|
|
94
|
+
const adapter = new SegmentSubscriber({
|
|
95
|
+
writeKey: 'test_write_key',
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
99
|
+
|
|
100
|
+
await adapter.trackEvent('page.viewed');
|
|
101
|
+
|
|
102
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
103
|
+
|
|
104
|
+
expect(mockTrack).toHaveBeenCalledWith({
|
|
105
|
+
userId: 'anonymous',
|
|
106
|
+
event: 'page.viewed',
|
|
107
|
+
properties: undefined,
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('should not track when disabled', async () => {
|
|
112
|
+
const adapter = new SegmentSubscriber({
|
|
113
|
+
writeKey: 'test_write_key',
|
|
114
|
+
enabled: false,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
await adapter.trackEvent('order.completed', { userId: 'user-123' });
|
|
118
|
+
|
|
119
|
+
// Should not throw
|
|
120
|
+
expect(true).toBe(true);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
describe('trackFunnelStep', () => {
|
|
125
|
+
it('should track funnel step', async () => {
|
|
126
|
+
const adapter = new SegmentSubscriber({
|
|
127
|
+
writeKey: 'test_write_key',
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
131
|
+
|
|
132
|
+
await adapter.trackFunnelStep('checkout', 'started', {
|
|
133
|
+
userId: 'user-123',
|
|
134
|
+
cartValue: 150,
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
138
|
+
|
|
139
|
+
expect(mockTrack).toHaveBeenCalledWith({
|
|
140
|
+
userId: 'user-123',
|
|
141
|
+
event: 'checkout.started',
|
|
142
|
+
properties: {
|
|
143
|
+
funnel: 'checkout',
|
|
144
|
+
step: 'started',
|
|
145
|
+
userId: 'user-123',
|
|
146
|
+
cartValue: 150,
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
describe('trackOutcome', () => {
|
|
153
|
+
it('should track outcome', async () => {
|
|
154
|
+
const adapter = new SegmentSubscriber({
|
|
155
|
+
writeKey: 'test_write_key',
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
159
|
+
|
|
160
|
+
await adapter.trackOutcome('payment.processing', 'success', {
|
|
161
|
+
userId: 'user-123',
|
|
162
|
+
transactionId: 'txn-789',
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
166
|
+
|
|
167
|
+
expect(mockTrack).toHaveBeenCalledWith({
|
|
168
|
+
userId: 'user-123',
|
|
169
|
+
event: 'payment.processing.success',
|
|
170
|
+
properties: {
|
|
171
|
+
operation: 'payment.processing',
|
|
172
|
+
outcome: 'success',
|
|
173
|
+
userId: 'user-123',
|
|
174
|
+
transactionId: 'txn-789',
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
describe('trackValue', () => {
|
|
181
|
+
it('should track value', async () => {
|
|
182
|
+
const adapter = new SegmentSubscriber({
|
|
183
|
+
writeKey: 'test_write_key',
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
187
|
+
|
|
188
|
+
await adapter.trackValue('revenue', 99.99, {
|
|
189
|
+
userId: 'user-123',
|
|
190
|
+
currency: 'USD',
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
194
|
+
|
|
195
|
+
expect(mockTrack).toHaveBeenCalledWith({
|
|
196
|
+
userId: 'user-123',
|
|
197
|
+
event: 'revenue',
|
|
198
|
+
properties: {
|
|
199
|
+
value: 99.99,
|
|
200
|
+
userId: 'user-123',
|
|
201
|
+
currency: 'USD',
|
|
202
|
+
},
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
describe('shutdown', () => {
|
|
208
|
+
it('should call closeAndFlush on Events instance', async () => {
|
|
209
|
+
const adapter = new SegmentSubscriber({
|
|
210
|
+
writeKey: 'test_write_key',
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
214
|
+
await adapter.shutdown();
|
|
215
|
+
|
|
216
|
+
expect(mockCloseAndFlush).toHaveBeenCalled();
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('should not throw when shutting down disabled adapter', async () => {
|
|
220
|
+
const adapter = new SegmentSubscriber({
|
|
221
|
+
writeKey: 'test_write_key',
|
|
222
|
+
enabled: false,
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
await expect(adapter.shutdown()).resolves.not.toThrow();
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
});
|
package/src/segment.ts
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Segment Subscriber for autotel
|
|
3
|
+
*
|
|
4
|
+
* Send events to Segment (customer data platform).
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```typescript
|
|
8
|
+
* import { Events } from 'autotel/events';
|
|
9
|
+
* import { SegmentSubscriber } from 'autotel-subscribers/segment';
|
|
10
|
+
*
|
|
11
|
+
* const events = new Events('checkout', {
|
|
12
|
+
* subscribers: [
|
|
13
|
+
* new SegmentSubscriber({
|
|
14
|
+
* writeKey: process.env.SEGMENT_WRITE_KEY!
|
|
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 SegmentConfig {
|
|
31
|
+
/** Segment write key */
|
|
32
|
+
writeKey: string;
|
|
33
|
+
/** Enable/disable the subscriber */
|
|
34
|
+
enabled?: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export class SegmentSubscriber implements EventSubscriber {
|
|
38
|
+
readonly name = 'SegmentSubscriber';
|
|
39
|
+
readonly version = '1.0.0';
|
|
40
|
+
|
|
41
|
+
private events: any;
|
|
42
|
+
private enabled: boolean;
|
|
43
|
+
private config: SegmentConfig;
|
|
44
|
+
private initPromise: Promise<void> | null = null;
|
|
45
|
+
|
|
46
|
+
constructor(config: SegmentConfig) {
|
|
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 @segment/events-node as a hard dependency
|
|
59
|
+
const { Analytics } = await import('@segment/analytics-node');
|
|
60
|
+
this.events = new Analytics({ writeKey: this.config.writeKey });
|
|
61
|
+
} catch (error) {
|
|
62
|
+
console.error(
|
|
63
|
+
'Segment subscriber failed to initialize. Install @segment/events-node: pnpm add @segment/events-node',
|
|
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
|
+
this.events?.track({
|
|
82
|
+
userId: attributes?.userId || attributes?.user_id || 'anonymous',
|
|
83
|
+
event: name,
|
|
84
|
+
properties: 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
|
+
this.events?.track({
|
|
97
|
+
userId: attributes?.userId || attributes?.user_id || 'anonymous',
|
|
98
|
+
event: `${funnelName}.${step}`,
|
|
99
|
+
properties: {
|
|
100
|
+
funnel: funnelName,
|
|
101
|
+
step,
|
|
102
|
+
...attributes,
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async trackOutcome(
|
|
108
|
+
operationName: string,
|
|
109
|
+
outcome: OutcomeStatus,
|
|
110
|
+
attributes?: EventAttributes,
|
|
111
|
+
): Promise<void> {
|
|
112
|
+
if (!this.enabled) return;
|
|
113
|
+
|
|
114
|
+
await this.ensureInitialized();
|
|
115
|
+
this.events?.track({
|
|
116
|
+
userId: attributes?.userId || attributes?.user_id || 'anonymous',
|
|
117
|
+
event: `${operationName}.${outcome}`,
|
|
118
|
+
properties: {
|
|
119
|
+
operation: operationName,
|
|
120
|
+
outcome,
|
|
121
|
+
...attributes,
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async trackValue(name: string, value: number, attributes?: EventAttributes): Promise<void> {
|
|
127
|
+
if (!this.enabled) return;
|
|
128
|
+
|
|
129
|
+
await this.ensureInitialized();
|
|
130
|
+
this.events?.track({
|
|
131
|
+
userId: attributes?.userId || attributes?.user_id || 'anonymous',
|
|
132
|
+
event: name,
|
|
133
|
+
properties: {
|
|
134
|
+
value,
|
|
135
|
+
...attributes,
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Flush pending events before shutdown */
|
|
141
|
+
async shutdown(): Promise<void> {
|
|
142
|
+
await this.ensureInitialized();
|
|
143
|
+
if (this.events) {
|
|
144
|
+
await this.events.closeAndFlush();
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|