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,336 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Google Cloud Pub/Sub Streaming Subscriber Example
|
|
3
|
+
*
|
|
4
|
+
* Production-ready Pub/Sub subscriber for GCP-native event streaming.
|
|
5
|
+
*
|
|
6
|
+
* Installation:
|
|
7
|
+
* ```bash
|
|
8
|
+
* pnpm add @google-cloud/pubsub
|
|
9
|
+
* ```
|
|
10
|
+
*
|
|
11
|
+
* Features:
|
|
12
|
+
* - Ordered delivery with ordering keys
|
|
13
|
+
* - Automatic batching and flow control
|
|
14
|
+
* - Backpressure handling
|
|
15
|
+
* - Message deduplication
|
|
16
|
+
* - Dead letter queue support
|
|
17
|
+
* - Graceful shutdown
|
|
18
|
+
*
|
|
19
|
+
* Setup:
|
|
20
|
+
* ```bash
|
|
21
|
+
* # Create topic
|
|
22
|
+
* gcloud pubsub topics create events-events \
|
|
23
|
+
* --message-retention-duration=7d
|
|
24
|
+
*
|
|
25
|
+
* # Create subscription (for consumers)
|
|
26
|
+
* gcloud pubsub subscriptions create events-events-sub \
|
|
27
|
+
* --topic=events-events \
|
|
28
|
+
* --ack-deadline=60 \
|
|
29
|
+
* --message-retention-duration=7d
|
|
30
|
+
* ```
|
|
31
|
+
*
|
|
32
|
+
* Usage:
|
|
33
|
+
* ```typescript
|
|
34
|
+
* import { Events } from 'autotel/events';
|
|
35
|
+
* import { PubSubSubscriber } from './adapter-pubsub';
|
|
36
|
+
*
|
|
37
|
+
* const events = new Events('app', {
|
|
38
|
+
* subscribers: [
|
|
39
|
+
* new PubSubSubscriber({
|
|
40
|
+
* projectId: 'my-gcp-project',
|
|
41
|
+
* topicName: 'events-events',
|
|
42
|
+
* enableMessageOrdering: true,
|
|
43
|
+
* partitionStrategy: 'userId',
|
|
44
|
+
* maxBufferSize: 10000,
|
|
45
|
+
* maxBatchSize: 1000,
|
|
46
|
+
* bufferOverflowStrategy: 'block'
|
|
47
|
+
* })
|
|
48
|
+
* ]
|
|
49
|
+
* });
|
|
50
|
+
*
|
|
51
|
+
* // Events ordered by userId
|
|
52
|
+
* await events.trackEvent('order.completed', {
|
|
53
|
+
* userId: 'user_123',
|
|
54
|
+
* amount: 99.99
|
|
55
|
+
* });
|
|
56
|
+
* ```
|
|
57
|
+
*/
|
|
58
|
+
|
|
59
|
+
import {
|
|
60
|
+
StreamingEventSubscriber,
|
|
61
|
+
type BufferOverflowStrategy,
|
|
62
|
+
} from '../src/streaming-event-subscriber';
|
|
63
|
+
import type { EventPayload } from '../src/event-subscriber-base';
|
|
64
|
+
import { PubSub, Topic } from '@google-cloud/pubsub';
|
|
65
|
+
|
|
66
|
+
type PartitionStrategy = 'userId' | 'tenantId' | 'eventType' | 'none';
|
|
67
|
+
|
|
68
|
+
export interface PubSubSubscriberConfig {
|
|
69
|
+
/** GCP Project ID */
|
|
70
|
+
projectId: string;
|
|
71
|
+
|
|
72
|
+
/** Pub/Sub topic name */
|
|
73
|
+
topicName: string;
|
|
74
|
+
|
|
75
|
+
/** Enable message ordering (default: true) */
|
|
76
|
+
enableMessageOrdering?: boolean;
|
|
77
|
+
|
|
78
|
+
/** Partitioning strategy for ordering keys (default: 'userId') */
|
|
79
|
+
partitionStrategy?: PartitionStrategy;
|
|
80
|
+
|
|
81
|
+
/** Enable/disable subscriber */
|
|
82
|
+
enabled?: boolean;
|
|
83
|
+
|
|
84
|
+
/** Maximum buffer size (default: 10000) */
|
|
85
|
+
maxBufferSize?: number;
|
|
86
|
+
|
|
87
|
+
/** Maximum batch size (default: 1000) */
|
|
88
|
+
maxBatchSize?: number;
|
|
89
|
+
|
|
90
|
+
/** Buffer overflow strategy (default: 'block') */
|
|
91
|
+
bufferOverflowStrategy?: BufferOverflowStrategy;
|
|
92
|
+
|
|
93
|
+
/** Flush interval in ms (default: 1000) */
|
|
94
|
+
flushIntervalMs?: number;
|
|
95
|
+
|
|
96
|
+
/** Service account key file path (optional, uses GOOGLE_APPLICATION_CREDENTIALS if not set) */
|
|
97
|
+
keyFilename?: string;
|
|
98
|
+
|
|
99
|
+
/** Enable batching settings (default: true) */
|
|
100
|
+
enableBatching?: boolean;
|
|
101
|
+
|
|
102
|
+
/** Max outstanding messages (flow control) (default: 1000) */
|
|
103
|
+
maxOutstandingMessages?: number;
|
|
104
|
+
|
|
105
|
+
/** Max outstanding bytes (flow control) (default: 100MB) */
|
|
106
|
+
maxOutstandingBytes?: number;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export class PubSubSubscriber extends StreamingEventSubscriber {
|
|
110
|
+
readonly name = 'PubSubSubscriber';
|
|
111
|
+
readonly version = '1.0.0';
|
|
112
|
+
|
|
113
|
+
private client: PubSub;
|
|
114
|
+
private topic: Topic;
|
|
115
|
+
private subscriberConfig: Required<
|
|
116
|
+
Omit<PubSubSubscriberConfig, 'keyFilename'>
|
|
117
|
+
> & {
|
|
118
|
+
keyFilename?: string;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
constructor(config: PubSubSubscriberConfig) {
|
|
122
|
+
super({
|
|
123
|
+
maxBufferSize: config.maxBufferSize ?? 10_000,
|
|
124
|
+
maxBatchSize: config.maxBatchSize ?? 1000,
|
|
125
|
+
bufferOverflowStrategy: config.bufferOverflowStrategy ?? 'block',
|
|
126
|
+
flushIntervalMs: config.flushIntervalMs ?? 1000,
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// Set config defaults
|
|
130
|
+
this.adapterConfig = {
|
|
131
|
+
projectId: config.projectId,
|
|
132
|
+
topicName: config.topicName,
|
|
133
|
+
enableMessageOrdering: config.enableMessageOrdering ?? true,
|
|
134
|
+
partitionStrategy: config.partitionStrategy ?? 'userId',
|
|
135
|
+
enabled: config.enabled ?? true,
|
|
136
|
+
maxBufferSize: config.maxBufferSize ?? 10_000,
|
|
137
|
+
maxBatchSize: config.maxBatchSize ?? 1000,
|
|
138
|
+
bufferOverflowStrategy: config.bufferOverflowStrategy ?? 'block',
|
|
139
|
+
flushIntervalMs: config.flushIntervalMs ?? 1000,
|
|
140
|
+
keyFilename: config.keyFilename,
|
|
141
|
+
enableBatching: config.enableBatching ?? true,
|
|
142
|
+
maxOutstandingMessages: config.maxOutstandingMessages ?? 1000,
|
|
143
|
+
maxOutstandingBytes: config.maxOutstandingBytes ?? 100 * 1024 * 1024, // 100MB
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
this.enabled = this.adapterConfig.enabled;
|
|
147
|
+
|
|
148
|
+
if (this.enabled) {
|
|
149
|
+
this.initializePubSub();
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private initializePubSub(): void {
|
|
154
|
+
try {
|
|
155
|
+
const options: any = {
|
|
156
|
+
projectId: this.adapterConfig.projectId,
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
if (this.adapterConfig.keyFilename) {
|
|
160
|
+
options.keyFilename = this.adapterConfig.keyFilename;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
this.client = new PubSub(options);
|
|
164
|
+
|
|
165
|
+
// Get topic reference
|
|
166
|
+
this.topic = this.client.topic(this.adapterConfig.topicName);
|
|
167
|
+
|
|
168
|
+
// Configure topic settings (combine all options in single call to avoid overwriting)
|
|
169
|
+
const publishOptions: any = {};
|
|
170
|
+
|
|
171
|
+
if (this.adapterConfig.enableMessageOrdering) {
|
|
172
|
+
publishOptions.messageOrdering = true;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (this.adapterConfig.enableBatching) {
|
|
176
|
+
publishOptions.batching = {
|
|
177
|
+
maxMessages: this.adapterConfig.maxBatchSize,
|
|
178
|
+
maxMilliseconds: this.adapterConfig.flushIntervalMs,
|
|
179
|
+
};
|
|
180
|
+
publishOptions.flowControlOptions = {
|
|
181
|
+
maxOutstandingMessages: this.adapterConfig.maxOutstandingMessages,
|
|
182
|
+
maxOutstandingBytes: this.adapterConfig.maxOutstandingBytes,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Set all options at once to avoid overwriting previous settings
|
|
187
|
+
if (Object.keys(publishOptions).length > 0) {
|
|
188
|
+
this.topic.setPublishOptions(publishOptions);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
console.log('[PubSubSubscriber] Initialized successfully');
|
|
192
|
+
} catch (error) {
|
|
193
|
+
console.error('[PubSubSubscriber] Failed to initialize:', error);
|
|
194
|
+
this.enabled = false;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Get ordering key (partition key) based on configured strategy
|
|
200
|
+
*
|
|
201
|
+
* Pub/Sub uses ordering keys to ensure messages with same key
|
|
202
|
+
* are delivered in order to subscribers.
|
|
203
|
+
*/
|
|
204
|
+
protected getPartitionKey(payload: EventPayload): string {
|
|
205
|
+
switch (this.adapterConfig.partitionStrategy) {
|
|
206
|
+
case 'userId': {
|
|
207
|
+
return payload.attributes?.userId?.toString() || '';
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
case 'tenantId': {
|
|
211
|
+
return payload.attributes?.tenantId?.toString() || '';
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
case 'eventType': {
|
|
215
|
+
return payload.type;
|
|
216
|
+
} // 'event', 'funnel', 'outcome', 'value'
|
|
217
|
+
|
|
218
|
+
case 'none': {
|
|
219
|
+
return '';
|
|
220
|
+
} // No ordering
|
|
221
|
+
|
|
222
|
+
default: {
|
|
223
|
+
return '';
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Send batch of events to Pub/Sub
|
|
230
|
+
*/
|
|
231
|
+
protected async sendBatch(events: EventPayload[]): Promise<void> {
|
|
232
|
+
// Publish all messages concurrently
|
|
233
|
+
const publishPromises = events.map((event) => {
|
|
234
|
+
const data = Buffer.from(JSON.stringify(event));
|
|
235
|
+
const orderingKey = this.getPartitionKey(event);
|
|
236
|
+
|
|
237
|
+
const message: any = {
|
|
238
|
+
data,
|
|
239
|
+
attributes: {
|
|
240
|
+
eventType: event.type,
|
|
241
|
+
eventName: event.name,
|
|
242
|
+
timestamp: event.timestamp,
|
|
243
|
+
},
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
// Add ordering key if message ordering is enabled
|
|
247
|
+
if (
|
|
248
|
+
this.adapterConfig.enableMessageOrdering &&
|
|
249
|
+
orderingKey
|
|
250
|
+
) {
|
|
251
|
+
message.orderingKey = orderingKey;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return this.topic.publishMessage(message);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
try {
|
|
258
|
+
// Wait for all publishes to complete
|
|
259
|
+
const messageIds = await Promise.all(publishPromises);
|
|
260
|
+
|
|
261
|
+
// Success - log metrics
|
|
262
|
+
if (process.env.DEBUG) {
|
|
263
|
+
console.log(
|
|
264
|
+
`[PubSubSubscriber] Published ${events.length} messages (IDs: ${messageIds.slice(0, 3).join(', ')}${events.length > 3 ? '...' : ''})`
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
} catch (error: any) {
|
|
268
|
+
console.error(
|
|
269
|
+
`[PubSubSubscriber] Failed to publish ${events.length} messages:`,
|
|
270
|
+
error
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
// Handle specific Pub/Sub errors
|
|
274
|
+
if (error.code === 10) {
|
|
275
|
+
console.error(
|
|
276
|
+
'[PubSubSubscriber] Flow control limits exceeded - reduce rate or increase limits'
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (error.code === 5) {
|
|
281
|
+
console.error(
|
|
282
|
+
`[PubSubSubscriber] Topic not found: ${this.adapterConfig.topicName}`
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
throw error;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Handle errors (override from EventSubscriber)
|
|
292
|
+
*/
|
|
293
|
+
protected handleError(error: Error, payload: EventPayload): void {
|
|
294
|
+
console.error(
|
|
295
|
+
`[PubSubSubscriber] Failed to process ${payload.type} event:`,
|
|
296
|
+
error,
|
|
297
|
+
{
|
|
298
|
+
eventName: payload.name,
|
|
299
|
+
orderingKey: this.getPartitionKey(payload),
|
|
300
|
+
topicName: this.adapterConfig.topicName,
|
|
301
|
+
}
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Graceful shutdown
|
|
307
|
+
*/
|
|
308
|
+
async shutdown(): Promise<void> {
|
|
309
|
+
console.log('[PubSubSubscriber] Starting graceful shutdown...');
|
|
310
|
+
|
|
311
|
+
// Flush buffer and drain pending requests
|
|
312
|
+
await super.shutdown();
|
|
313
|
+
|
|
314
|
+
// Flush any remaining messages in topic's internal buffer
|
|
315
|
+
if (this.topic) {
|
|
316
|
+
try {
|
|
317
|
+
await this.topic.flush();
|
|
318
|
+
console.log('[PubSubSubscriber] Flushed topic buffer');
|
|
319
|
+
} catch (error) {
|
|
320
|
+
console.error('[PubSubSubscriber] Error flushing topic:', error);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Close Pub/Sub client
|
|
325
|
+
if (this.client) {
|
|
326
|
+
try {
|
|
327
|
+
await this.client.close();
|
|
328
|
+
console.log('[PubSubSubscriber] Closed Pub/Sub client');
|
|
329
|
+
} catch (error) {
|
|
330
|
+
console.error('[PubSubSubscriber] Error closing client:', error);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
console.log('[PubSubSubscriber] Shutdown complete');
|
|
335
|
+
}
|
|
336
|
+
}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Snowflake Subscriber Example
|
|
3
|
+
*
|
|
4
|
+
* Sends events events to Snowflake data warehouse.
|
|
5
|
+
* This is a complete, production-ready implementation.
|
|
6
|
+
*
|
|
7
|
+
* Installation:
|
|
8
|
+
* ```bash
|
|
9
|
+
* pnpm add snowflake-sdk
|
|
10
|
+
* ```
|
|
11
|
+
*
|
|
12
|
+
* Setup Snowflake table:
|
|
13
|
+
* ```sql
|
|
14
|
+
* CREATE TABLE events_events (
|
|
15
|
+
* event_id VARCHAR(36) PRIMARY KEY,
|
|
16
|
+
* event_type VARCHAR(50) NOT NULL,
|
|
17
|
+
* event_name VARCHAR(255) NOT NULL,
|
|
18
|
+
* attributes VARIANT,
|
|
19
|
+
* funnel VARCHAR(100),
|
|
20
|
+
* step VARCHAR(50),
|
|
21
|
+
* operation VARCHAR(100),
|
|
22
|
+
* outcome VARCHAR(50),
|
|
23
|
+
* value DECIMAL(18,2),
|
|
24
|
+
* timestamp TIMESTAMP_NTZ NOT NULL,
|
|
25
|
+
* created_at TIMESTAMP_NTZ DEFAULT CURRENT_TIMESTAMP()
|
|
26
|
+
* );
|
|
27
|
+
*
|
|
28
|
+
* CREATE INDEX idx_event_type ON events_events(event_type);
|
|
29
|
+
* CREATE INDEX idx_event_name ON events_events(event_name);
|
|
30
|
+
* CREATE INDEX idx_timestamp ON events_events(timestamp);
|
|
31
|
+
* ```
|
|
32
|
+
*
|
|
33
|
+
* Usage:
|
|
34
|
+
* ```typescript
|
|
35
|
+
* import { Events } from 'autotel/events';
|
|
36
|
+
* import { SnowflakeSubscriber } from './adapter-snowflake';
|
|
37
|
+
*
|
|
38
|
+
* const events = new Events('app', {
|
|
39
|
+
* subscribers: [
|
|
40
|
+
* new SnowflakeSubscriber({
|
|
41
|
+
* account: 'xy12345.us-east-1',
|
|
42
|
+
* username: process.env.SNOWFLAKE_USER!,
|
|
43
|
+
* password: process.env.SNOWFLAKE_PASS!,
|
|
44
|
+
* database: 'ANALYTICS',
|
|
45
|
+
* schema: 'PUBLIC',
|
|
46
|
+
* warehouse: 'COMPUTE_WH',
|
|
47
|
+
* table: 'events_events'
|
|
48
|
+
* })
|
|
49
|
+
* ]
|
|
50
|
+
* });
|
|
51
|
+
*
|
|
52
|
+
* events.trackEvent('order.completed', { orderId: 'ord_123', amount: 99.99 });
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
|
|
56
|
+
import {
|
|
57
|
+
EventSubscriber,
|
|
58
|
+
type EventPayload,
|
|
59
|
+
} from '../src/event-subscriber-base';
|
|
60
|
+
import snowflake from 'snowflake-sdk';
|
|
61
|
+
|
|
62
|
+
export interface SnowflakeSubscriberConfig {
|
|
63
|
+
/** Snowflake account (e.g., 'xy12345.us-east-1') */
|
|
64
|
+
account: string;
|
|
65
|
+
/** Username */
|
|
66
|
+
username: string;
|
|
67
|
+
/** Password */
|
|
68
|
+
password: string;
|
|
69
|
+
/** Database name */
|
|
70
|
+
database: string;
|
|
71
|
+
/** Schema name (default: 'PUBLIC') */
|
|
72
|
+
schema?: string;
|
|
73
|
+
/** Warehouse name (default: 'COMPUTE_WH') */
|
|
74
|
+
warehouse?: string;
|
|
75
|
+
/** Table name (default: 'events_events') */
|
|
76
|
+
table?: string;
|
|
77
|
+
/** Enable/disable subscriber */
|
|
78
|
+
enabled?: boolean;
|
|
79
|
+
/** Batch size (default: 100) */
|
|
80
|
+
batchSize?: number;
|
|
81
|
+
/** Flush interval in ms (default: 10000) */
|
|
82
|
+
flushInterval?: number;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export class SnowflakeSubscriber extends EventSubscriber {
|
|
86
|
+
readonly name = 'SnowflakeSubscriber';
|
|
87
|
+
readonly version = '1.0.0';
|
|
88
|
+
|
|
89
|
+
private connection: snowflake.Connection;
|
|
90
|
+
private config: Required<SnowflakeSubscriberConfig>;
|
|
91
|
+
private buffer: EventPayload[] = [];
|
|
92
|
+
private flushIntervalHandle: NodeJS.Timeout | null = null;
|
|
93
|
+
|
|
94
|
+
constructor(config: SnowflakeSubscriberConfig) {
|
|
95
|
+
super();
|
|
96
|
+
|
|
97
|
+
// Set defaults
|
|
98
|
+
this.config = {
|
|
99
|
+
schema: 'PUBLIC',
|
|
100
|
+
warehouse: 'COMPUTE_WH',
|
|
101
|
+
table: 'events_events',
|
|
102
|
+
enabled: true,
|
|
103
|
+
batchSize: 100,
|
|
104
|
+
flushInterval: 10_000,
|
|
105
|
+
...config,
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
this.enabled = this.config.enabled;
|
|
109
|
+
|
|
110
|
+
if (this.enabled) {
|
|
111
|
+
this.initializeConnection();
|
|
112
|
+
this.startFlushInterval();
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private initializeConnection(): void {
|
|
117
|
+
this.connection = snowflake.createConnection({
|
|
118
|
+
account: this.config.account,
|
|
119
|
+
username: this.config.username,
|
|
120
|
+
password: this.config.password,
|
|
121
|
+
database: this.config.database,
|
|
122
|
+
schema: this.config.schema,
|
|
123
|
+
warehouse: this.config.warehouse,
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// Connect asynchronously
|
|
127
|
+
this.connection.connect((err) => {
|
|
128
|
+
if (err) {
|
|
129
|
+
console.error('[SnowflakeSubscriber] Failed to connect:', err);
|
|
130
|
+
this.enabled = false;
|
|
131
|
+
} else {
|
|
132
|
+
console.log('[SnowflakeSubscriber] Connected successfully');
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private startFlushInterval(): void {
|
|
138
|
+
this.flushIntervalHandle = setInterval(() => {
|
|
139
|
+
void this.flushBuffer();
|
|
140
|
+
}, this.config.flushInterval);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
protected async sendToDestination(payload: EventPayload): Promise<void> {
|
|
144
|
+
this.buffer.push(payload);
|
|
145
|
+
|
|
146
|
+
// Auto-flush at batch size
|
|
147
|
+
if (this.buffer.length >= this.config.batchSize) {
|
|
148
|
+
await this.flushBuffer();
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
private async flushBuffer(): Promise<void> {
|
|
153
|
+
if (this.buffer.length === 0) return;
|
|
154
|
+
|
|
155
|
+
const batch = [...this.buffer];
|
|
156
|
+
this.buffer = [];
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
await this.insertBatch(batch);
|
|
160
|
+
} catch (error) {
|
|
161
|
+
console.error('[SnowflakeSubscriber] Failed to flush batch:', error);
|
|
162
|
+
// Re-add to buffer for retry
|
|
163
|
+
this.buffer.unshift(...batch);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
private async insertBatch(events: EventPayload[]): Promise<void> {
|
|
168
|
+
const sql = `
|
|
169
|
+
INSERT INTO ${this.config.table}
|
|
170
|
+
(event_id, event_type, event_name, attributes, funnel, step, operation, outcome, value, timestamp)
|
|
171
|
+
SELECT
|
|
172
|
+
column1 as event_id,
|
|
173
|
+
column2 as event_type,
|
|
174
|
+
column3 as event_name,
|
|
175
|
+
PARSE_JSON(column4) as attributes,
|
|
176
|
+
column5 as funnel,
|
|
177
|
+
column6 as step,
|
|
178
|
+
column7 as operation,
|
|
179
|
+
column8 as outcome,
|
|
180
|
+
column9 as value,
|
|
181
|
+
TO_TIMESTAMP_NTZ(column10) as timestamp
|
|
182
|
+
FROM VALUES ${events.map(() => '(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)').join(', ')}
|
|
183
|
+
`;
|
|
184
|
+
|
|
185
|
+
const binds = events.flatMap((event) => [
|
|
186
|
+
crypto.randomUUID(),
|
|
187
|
+
event.type,
|
|
188
|
+
event.name,
|
|
189
|
+
JSON.stringify(event.attributes || {}),
|
|
190
|
+
event.funnel || null,
|
|
191
|
+
event.step || null,
|
|
192
|
+
event.operation || null,
|
|
193
|
+
event.outcome || null,
|
|
194
|
+
event.value || null,
|
|
195
|
+
event.timestamp,
|
|
196
|
+
]);
|
|
197
|
+
|
|
198
|
+
return new Promise((resolve, reject) => {
|
|
199
|
+
this.connection.execute({
|
|
200
|
+
sqlText: sql,
|
|
201
|
+
binds,
|
|
202
|
+
complete: (err, _stmt, _rows) => {
|
|
203
|
+
if (err) reject(err);
|
|
204
|
+
else resolve();
|
|
205
|
+
},
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async shutdown(): Promise<void> {
|
|
211
|
+
// Clear flush interval
|
|
212
|
+
if (this.flushIntervalHandle) {
|
|
213
|
+
clearInterval(this.flushIntervalHandle);
|
|
214
|
+
this.flushIntervalHandle = null;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Flush remaining events
|
|
218
|
+
await this.flushBuffer();
|
|
219
|
+
|
|
220
|
+
// Wait for pending requests
|
|
221
|
+
await super.shutdown();
|
|
222
|
+
|
|
223
|
+
// Close connection
|
|
224
|
+
if (this.connection) {
|
|
225
|
+
this.connection.destroy((err) => {
|
|
226
|
+
if (err) {
|
|
227
|
+
console.error('[SnowflakeSubscriber] Error closing connection:', err);
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "autotel-subscribers",
|
|
3
|
+
"version": "4.0.0",
|
|
4
|
+
"description": "Write Once, Observe Anywhere - Event subscribers for autotel (PostHog, Mixpanel, Amplitude, Segment)",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"sideEffects": false,
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js",
|
|
13
|
+
"require": "./dist/index.cjs"
|
|
14
|
+
},
|
|
15
|
+
"./posthog": {
|
|
16
|
+
"types": "./dist/posthog.d.ts",
|
|
17
|
+
"import": "./dist/posthog.js",
|
|
18
|
+
"require": "./dist/posthog.cjs"
|
|
19
|
+
},
|
|
20
|
+
"./mixpanel": {
|
|
21
|
+
"types": "./dist/mixpanel.d.ts",
|
|
22
|
+
"import": "./dist/mixpanel.js",
|
|
23
|
+
"require": "./dist/mixpanel.cjs"
|
|
24
|
+
},
|
|
25
|
+
"./segment": {
|
|
26
|
+
"types": "./dist/segment.d.ts",
|
|
27
|
+
"import": "./dist/segment.js",
|
|
28
|
+
"require": "./dist/segment.cjs"
|
|
29
|
+
},
|
|
30
|
+
"./amplitude": {
|
|
31
|
+
"types": "./dist/amplitude.d.ts",
|
|
32
|
+
"import": "./dist/amplitude.js",
|
|
33
|
+
"require": "./dist/amplitude.cjs"
|
|
34
|
+
},
|
|
35
|
+
"./webhook": {
|
|
36
|
+
"types": "./dist/webhook.d.ts",
|
|
37
|
+
"import": "./dist/webhook.js",
|
|
38
|
+
"require": "./dist/webhook.cjs"
|
|
39
|
+
},
|
|
40
|
+
"./slack": {
|
|
41
|
+
"types": "./dist/slack.d.ts",
|
|
42
|
+
"import": "./dist/slack.js",
|
|
43
|
+
"require": "./dist/slack.cjs"
|
|
44
|
+
},
|
|
45
|
+
"./factories": {
|
|
46
|
+
"types": "./dist/factories.d.ts",
|
|
47
|
+
"import": "./dist/factories.js",
|
|
48
|
+
"require": "./dist/factories.cjs"
|
|
49
|
+
},
|
|
50
|
+
"./middleware": {
|
|
51
|
+
"types": "./dist/middleware.d.ts",
|
|
52
|
+
"import": "./dist/middleware.js",
|
|
53
|
+
"require": "./dist/middleware.cjs"
|
|
54
|
+
},
|
|
55
|
+
"./testing": {
|
|
56
|
+
"types": "./dist/testing/index.d.ts",
|
|
57
|
+
"import": "./dist/testing/index.js",
|
|
58
|
+
"require": "./dist/testing/index.cjs"
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
"files": [
|
|
62
|
+
"dist",
|
|
63
|
+
"src",
|
|
64
|
+
"examples",
|
|
65
|
+
"README.md"
|
|
66
|
+
],
|
|
67
|
+
"keywords": [
|
|
68
|
+
"autotel",
|
|
69
|
+
"opentelemetry",
|
|
70
|
+
"analytics",
|
|
71
|
+
"posthog",
|
|
72
|
+
"mixpanel",
|
|
73
|
+
"amplitude",
|
|
74
|
+
"segment",
|
|
75
|
+
"webhook",
|
|
76
|
+
"adapters",
|
|
77
|
+
"business-metrics",
|
|
78
|
+
"product-analytics",
|
|
79
|
+
"telemetry",
|
|
80
|
+
"observability"
|
|
81
|
+
],
|
|
82
|
+
"author": "Jag Reehal<jag@jagreehal.com> (https://jagreehal.com)",
|
|
83
|
+
"license": "MIT",
|
|
84
|
+
"peerDependencies": {
|
|
85
|
+
"autotel": "2.1.0"
|
|
86
|
+
},
|
|
87
|
+
"peerDependenciesMeta": {
|
|
88
|
+
"posthog-node": {
|
|
89
|
+
"optional": true
|
|
90
|
+
},
|
|
91
|
+
"mixpanel": {
|
|
92
|
+
"optional": true
|
|
93
|
+
},
|
|
94
|
+
"@segment/analytics-node": {
|
|
95
|
+
"optional": true
|
|
96
|
+
},
|
|
97
|
+
"@amplitude/analytics-node": {
|
|
98
|
+
"optional": true
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
"devDependencies": {
|
|
102
|
+
"@amplitude/analytics-node": "^1.5.26",
|
|
103
|
+
"@arethetypeswrong/cli": "^0.18.2",
|
|
104
|
+
"@cloudflare/workers-types": "^4.20251121.0",
|
|
105
|
+
"@prisma/client": "^7.0.0",
|
|
106
|
+
"@segment/analytics-node": "^2.3.0",
|
|
107
|
+
"@types/node": "^24.10.1",
|
|
108
|
+
"@typescript-eslint/eslint-plugin": "^8.47.0",
|
|
109
|
+
"@typescript-eslint/parser": "^8.47.0",
|
|
110
|
+
"drizzle-orm": "^0.44.7",
|
|
111
|
+
"eslint-config-prettier": "^10.1.8",
|
|
112
|
+
"eslint-plugin-unicorn": "^62.0.0",
|
|
113
|
+
"mixpanel": "^0.19.1",
|
|
114
|
+
"mongoose": "^9.0.0",
|
|
115
|
+
"posthog-node": "^5.13.2",
|
|
116
|
+
"prettier": "^3.6.2",
|
|
117
|
+
"rimraf": "^6.1.2",
|
|
118
|
+
"tsup": "^8.5.1",
|
|
119
|
+
"typescript": "^5.9.3",
|
|
120
|
+
"typescript-eslint": "^8.47.0",
|
|
121
|
+
"vitest": "^4.0.13",
|
|
122
|
+
"autotel": "2.1.0"
|
|
123
|
+
},
|
|
124
|
+
"repository": {
|
|
125
|
+
"type": "git",
|
|
126
|
+
"url": "https://github.com/jagreehal/autotel"
|
|
127
|
+
},
|
|
128
|
+
"bugs": {
|
|
129
|
+
"url": "https://github.com/jagreehal/autotel/issues"
|
|
130
|
+
},
|
|
131
|
+
"homepage": "https://github.com/jagreehal/autotel#readme",
|
|
132
|
+
"scripts": {
|
|
133
|
+
"build": "tsup",
|
|
134
|
+
"dev": "tsup --watch",
|
|
135
|
+
"lint": "npx eslint 'src/**/*.ts' 'examples/**/*.ts'",
|
|
136
|
+
"lint:fix": "npx eslint 'src/**/*.ts' 'examples/**/*.ts' --fix",
|
|
137
|
+
"type-check": "tsc --noEmit",
|
|
138
|
+
"test": "vitest run",
|
|
139
|
+
"test:watch": "vitest"
|
|
140
|
+
}
|
|
141
|
+
}
|