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
package/dist/webhook.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// src/webhook.ts
|
|
2
|
+
var WebhookSubscriber = class {
|
|
3
|
+
name = "WebhookSubscriber";
|
|
4
|
+
version = "1.0.0";
|
|
5
|
+
config;
|
|
6
|
+
enabled;
|
|
7
|
+
pendingRequests = /* @__PURE__ */ new Set();
|
|
8
|
+
constructor(config) {
|
|
9
|
+
this.config = config;
|
|
10
|
+
this.enabled = config.enabled ?? true;
|
|
11
|
+
}
|
|
12
|
+
async send(payload) {
|
|
13
|
+
if (!this.enabled) return;
|
|
14
|
+
const maxRetries = this.config.maxRetries ?? 3;
|
|
15
|
+
let lastError;
|
|
16
|
+
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
17
|
+
try {
|
|
18
|
+
const response = await fetch(this.config.url, {
|
|
19
|
+
method: "POST",
|
|
20
|
+
headers: {
|
|
21
|
+
"Content-Type": "application/json",
|
|
22
|
+
...this.config.headers
|
|
23
|
+
},
|
|
24
|
+
body: JSON.stringify(payload)
|
|
25
|
+
});
|
|
26
|
+
if (!response.ok) {
|
|
27
|
+
throw new Error(`Webhook returned ${response.status}: ${response.statusText}`);
|
|
28
|
+
}
|
|
29
|
+
return;
|
|
30
|
+
} catch (error) {
|
|
31
|
+
lastError = error;
|
|
32
|
+
if (attempt < maxRetries - 1) {
|
|
33
|
+
await new Promise((resolve) => setTimeout(resolve, Math.pow(2, attempt) * 1e3));
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
console.error(`Webhook subscriber failed after ${maxRetries} attempts:`, lastError);
|
|
38
|
+
}
|
|
39
|
+
async trackEvent(name, attributes) {
|
|
40
|
+
const request = this.send({
|
|
41
|
+
type: "event",
|
|
42
|
+
name,
|
|
43
|
+
attributes,
|
|
44
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
45
|
+
});
|
|
46
|
+
this.trackRequest(request);
|
|
47
|
+
await request;
|
|
48
|
+
}
|
|
49
|
+
async trackFunnelStep(funnelName, step, attributes) {
|
|
50
|
+
const request = this.send({
|
|
51
|
+
type: "funnel",
|
|
52
|
+
funnel: funnelName,
|
|
53
|
+
step,
|
|
54
|
+
attributes,
|
|
55
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
56
|
+
});
|
|
57
|
+
this.trackRequest(request);
|
|
58
|
+
await request;
|
|
59
|
+
}
|
|
60
|
+
async trackOutcome(operationName, outcome, attributes) {
|
|
61
|
+
const request = this.send({
|
|
62
|
+
type: "outcome",
|
|
63
|
+
operation: operationName,
|
|
64
|
+
outcome,
|
|
65
|
+
attributes,
|
|
66
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
67
|
+
});
|
|
68
|
+
this.trackRequest(request);
|
|
69
|
+
await request;
|
|
70
|
+
}
|
|
71
|
+
async trackValue(name, value, attributes) {
|
|
72
|
+
const request = this.send({
|
|
73
|
+
type: "value",
|
|
74
|
+
name,
|
|
75
|
+
value,
|
|
76
|
+
attributes,
|
|
77
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
78
|
+
});
|
|
79
|
+
this.trackRequest(request);
|
|
80
|
+
await request;
|
|
81
|
+
}
|
|
82
|
+
trackRequest(request) {
|
|
83
|
+
this.pendingRequests.add(request);
|
|
84
|
+
void request.finally(() => {
|
|
85
|
+
this.pendingRequests.delete(request);
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
/** Wait for all pending webhook requests to complete */
|
|
89
|
+
async shutdown() {
|
|
90
|
+
if (this.pendingRequests.size > 0) {
|
|
91
|
+
await Promise.allSettled(this.pendingRequests);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
export { WebhookSubscriber };
|
|
97
|
+
//# sourceMappingURL=webhook.js.map
|
|
98
|
+
//# sourceMappingURL=webhook.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/webhook.ts"],"names":[],"mappings":"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","file":"webhook.js","sourcesContent":["/**\n * Webhook Subscriber for autotel\n *\n * Send events to any webhook endpoint (custom integrations, Zapier, Make.com, etc.).\n *\n * @example\n * ```typescript\n * import { Events } from 'autotel/events';\n * import { WebhookSubscriber } from 'autotel-subscribers/webhook';\n *\n * const events = new Events('checkout', {\n * subscribers: [\n * new WebhookSubscriber({\n * url: 'https://hooks.zapier.com/hooks/catch/...',\n * headers: { 'X-API-Key': 'secret' }\n * })\n * ]\n * });\n *\n * events.trackEvent('order.completed', { userId: '123', amount: 99.99 });\n * ```\n */\n\nimport type {\n EventSubscriber,\n EventAttributes,\n FunnelStatus,\n OutcomeStatus,\n} from 'autotel/event-subscriber';\n\nexport interface WebhookConfig {\n /** Webhook URL */\n url: string;\n /** Optional headers (e.g., API keys) */\n headers?: Record<string, string>;\n /** Enable/disable the subscriber */\n enabled?: boolean;\n /** Retry failed requests (default: 3) */\n maxRetries?: number;\n}\n\nexport class WebhookSubscriber implements EventSubscriber {\n readonly name = 'WebhookSubscriber';\n readonly version = '1.0.0';\n\n private config: WebhookConfig;\n private enabled: boolean;\n private pendingRequests: Set<Promise<void>> = new Set();\n\n constructor(config: WebhookConfig) {\n this.config = config;\n this.enabled = config.enabled ?? true;\n }\n\n private async send(payload: any): Promise<void> {\n if (!this.enabled) return;\n\n const maxRetries = this.config.maxRetries ?? 3;\n let lastError: Error | undefined;\n\n for (let attempt = 0; attempt < maxRetries; attempt++) {\n try {\n const response = await fetch(this.config.url, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n ...this.config.headers,\n },\n body: JSON.stringify(payload),\n });\n\n if (!response.ok) {\n throw new Error(`Webhook returned ${response.status}: ${response.statusText}`);\n }\n\n return; // Success\n } catch (error) {\n lastError = error as Error;\n if (attempt < maxRetries - 1) {\n // Exponential backoff\n await new Promise((resolve) => setTimeout(resolve, Math.pow(2, attempt) * 1000));\n }\n }\n }\n\n console.error(`Webhook subscriber failed after ${maxRetries} attempts:`, lastError);\n }\n\n async trackEvent(name: string, attributes?: EventAttributes): Promise<void> {\n const request = this.send({\n type: 'event',\n name,\n attributes,\n timestamp: new Date().toISOString(),\n });\n this.trackRequest(request);\n await request;\n }\n\n async trackFunnelStep(\n funnelName: string,\n step: FunnelStatus,\n attributes?: EventAttributes,\n ): Promise<void> {\n const request = this.send({\n type: 'funnel',\n funnel: funnelName,\n step,\n attributes,\n timestamp: new Date().toISOString(),\n });\n this.trackRequest(request);\n await request;\n }\n\n async trackOutcome(\n operationName: string,\n outcome: OutcomeStatus,\n attributes?: EventAttributes,\n ): Promise<void> {\n const request = this.send({\n type: 'outcome',\n operation: operationName,\n outcome,\n attributes,\n timestamp: new Date().toISOString(),\n });\n this.trackRequest(request);\n await request;\n }\n\n async trackValue(name: string, value: number, attributes?: EventAttributes): Promise<void> {\n const request = this.send({\n type: 'value',\n name,\n value,\n attributes,\n timestamp: new Date().toISOString(),\n });\n this.trackRequest(request);\n await request;\n }\n\n private trackRequest(request: Promise<void>): void {\n this.pendingRequests.add(request);\n void request.finally(() => {\n this.pendingRequests.delete(request);\n });\n }\n\n /** Wait for all pending webhook requests to complete */\n async shutdown(): Promise<void> {\n if (this.pendingRequests.size > 0) {\n await Promise.allSettled(this.pendingRequests);\n }\n }\n}\n\n"]}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QUICKSTART: Your First Custom Subscriber in 5 Minutes
|
|
3
|
+
*
|
|
4
|
+
* This template shows you EVERYTHING you need to write a custom events subscriber.
|
|
5
|
+
* Just copy-paste this code and replace the console.log statements with your API calls.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* import { Events } from 'autotel/events';
|
|
10
|
+
* import { MyFirstSubscriber } from './my-first-subscriber';
|
|
11
|
+
*
|
|
12
|
+
* const event =new Event('my-app', {
|
|
13
|
+
* subscribers: [new MyFirstSubscriber()]
|
|
14
|
+
* });
|
|
15
|
+
*
|
|
16
|
+
* events.trackEvent('user.signup', { userId: '123', email: 'user@example.com' });
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import type { EventSubscriber, EventAttributes, FunnelStatus, OutcomeStatus } from '../src/event-subscriber-base';
|
|
21
|
+
|
|
22
|
+
export class MyFirstSubscriber implements EventSubscriber {
|
|
23
|
+
// Required: Subscriber name (shows up in logs)
|
|
24
|
+
readonly name = 'MyFirstSubscriber';
|
|
25
|
+
|
|
26
|
+
// Optional: Version for debugging
|
|
27
|
+
readonly version = '1.0.0';
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Track a business event (e.g., "user.signup", "order.completed")
|
|
31
|
+
*/
|
|
32
|
+
async trackEvent(name: string, attributes?: EventAttributes): Promise<void> {
|
|
33
|
+
console.log('📊 EVENT:', name, attributes);
|
|
34
|
+
|
|
35
|
+
// TODO: Replace with your API call
|
|
36
|
+
// Example:
|
|
37
|
+
// await fetch('https://your-api.com/events', {
|
|
38
|
+
// method: 'POST',
|
|
39
|
+
// headers: { 'Content-Type': 'application/json' },
|
|
40
|
+
// body: JSON.stringify({ event: name, properties: attributes })
|
|
41
|
+
// });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Track a funnel step (e.g., checkout flow: "started" → "completed")
|
|
46
|
+
*/
|
|
47
|
+
async trackFunnelStep(
|
|
48
|
+
funnel: string,
|
|
49
|
+
step: FunnelStatus,
|
|
50
|
+
attributes?: EventAttributes
|
|
51
|
+
): Promise<void> {
|
|
52
|
+
console.log('🔄 FUNNEL:', funnel, step, attributes);
|
|
53
|
+
|
|
54
|
+
// TODO: Replace with your API call
|
|
55
|
+
// Most platforms just treat this as a regular event:
|
|
56
|
+
// await this.trackEvent(`${funnel}.${step}`, { ...attributes, funnel, step });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Track an operation outcome (e.g., payment: "success" or "failure")
|
|
61
|
+
*/
|
|
62
|
+
async trackOutcome(
|
|
63
|
+
operation: string,
|
|
64
|
+
outcome: OutcomeStatus,
|
|
65
|
+
attributes?: EventAttributes
|
|
66
|
+
): Promise<void> {
|
|
67
|
+
console.log('✅ OUTCOME:', operation, outcome, attributes);
|
|
68
|
+
|
|
69
|
+
// TODO: Replace with your API call
|
|
70
|
+
// await this.trackEvent(`${operation}.${outcome}`, { ...attributes, operation, outcome });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Track a numeric value (e.g., revenue, response time)
|
|
75
|
+
*/
|
|
76
|
+
async trackValue(
|
|
77
|
+
name: string,
|
|
78
|
+
value: number,
|
|
79
|
+
attributes?: EventAttributes
|
|
80
|
+
): Promise<void> {
|
|
81
|
+
console.log('💰 VALUE:', name, value, attributes);
|
|
82
|
+
|
|
83
|
+
// TODO: Replace with your API call
|
|
84
|
+
// await this.trackEvent(name, { ...attributes, value });
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Optional: Cleanup on shutdown
|
|
89
|
+
* Use this to flush buffered events, close connections, etc.
|
|
90
|
+
*/
|
|
91
|
+
async shutdown(): Promise<void> {
|
|
92
|
+
console.log('👋 SHUTDOWN');
|
|
93
|
+
|
|
94
|
+
// TODO: Add cleanup logic
|
|
95
|
+
// Example:
|
|
96
|
+
// await this.flushBuffer();
|
|
97
|
+
// await this.httpClient.close();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ============================================================================
|
|
102
|
+
// USAGE EXAMPLE
|
|
103
|
+
// ============================================================================
|
|
104
|
+
|
|
105
|
+
// Uncomment to test:
|
|
106
|
+
// import { Events } from 'autotel/events';
|
|
107
|
+
//
|
|
108
|
+
// const events = new Events('my-app', {
|
|
109
|
+
// subscribers: [new MyFirstSubscriber()]
|
|
110
|
+
// });
|
|
111
|
+
//
|
|
112
|
+
// // Track events
|
|
113
|
+
// await events.trackEvent('user.signup', {
|
|
114
|
+
// userId: 'user-123',
|
|
115
|
+
// email: 'user@example.com',
|
|
116
|
+
// plan: 'pro'
|
|
117
|
+
// });
|
|
118
|
+
//
|
|
119
|
+
// await events.trackFunnelStep('checkout', 'started', {
|
|
120
|
+
// cartValue: 99.99
|
|
121
|
+
// });
|
|
122
|
+
//
|
|
123
|
+
// await events.trackOutcome('payment', 'success', {
|
|
124
|
+
// amount: 99.99,
|
|
125
|
+
// method: 'credit_card'
|
|
126
|
+
// });
|
|
127
|
+
//
|
|
128
|
+
// await events.trackValue('revenue', 99.99, {
|
|
129
|
+
// currency: 'USD',
|
|
130
|
+
// orderId: 'ord-456'
|
|
131
|
+
// });
|
|
132
|
+
//
|
|
133
|
+
// // Cleanup
|
|
134
|
+
// await events.shutdown();
|
|
135
|
+
|
|
136
|
+
// ============================================================================
|
|
137
|
+
// NEXT STEPS
|
|
138
|
+
// ============================================================================
|
|
139
|
+
|
|
140
|
+
// 1. Copy this file to your project
|
|
141
|
+
// 2. Rename "MyFirstSubscriber" to your service name (e.g., "AmplitudeSubscriber")
|
|
142
|
+
// 3. Replace console.log with your API calls
|
|
143
|
+
// 4. Test it! (see examples/testing-custom-adapter.ts)
|
|
144
|
+
// 5. Add error handling and retry logic (see docs/adapter-guide.md)
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BigQuery Subscriber Example
|
|
3
|
+
*
|
|
4
|
+
* Sends events events to Google BigQuery data warehouse.
|
|
5
|
+
* This is a complete, production-ready implementation.
|
|
6
|
+
*
|
|
7
|
+
* Installation:
|
|
8
|
+
* ```bash
|
|
9
|
+
* pnpm add @google-cloud/bigquery
|
|
10
|
+
* ```
|
|
11
|
+
*
|
|
12
|
+
* Setup BigQuery table:
|
|
13
|
+
* ```sql
|
|
14
|
+
* CREATE TABLE `project.dataset.events_events` (
|
|
15
|
+
* event_id STRING NOT NULL,
|
|
16
|
+
* event_type STRING NOT NULL,
|
|
17
|
+
* event_name STRING NOT NULL,
|
|
18
|
+
* attributes JSON,
|
|
19
|
+
* funnel STRING,
|
|
20
|
+
* step STRING,
|
|
21
|
+
* operation STRING,
|
|
22
|
+
* outcome STRING,
|
|
23
|
+
* value NUMERIC,
|
|
24
|
+
* timestamp TIMESTAMP NOT NULL,
|
|
25
|
+
* created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP()
|
|
26
|
+
* )
|
|
27
|
+
* PARTITION BY DATE(timestamp)
|
|
28
|
+
* CLUSTER BY event_type, event_name;
|
|
29
|
+
* ```
|
|
30
|
+
*
|
|
31
|
+
* Setup Authentication:
|
|
32
|
+
* ```bash
|
|
33
|
+
* # Set environment variable
|
|
34
|
+
* export GOOGLE_APPLICATION_CREDENTIALS="/path/to/service-account-key.json"
|
|
35
|
+
* ```
|
|
36
|
+
*
|
|
37
|
+
* Usage:
|
|
38
|
+
* ```typescript
|
|
39
|
+
* import { Events } from 'autotel/events';
|
|
40
|
+
* import { BigQuerySubscriber } from './adapter-bigquery';
|
|
41
|
+
*
|
|
42
|
+
* const events = new Events('app', {
|
|
43
|
+
* subscribers: [
|
|
44
|
+
* new BigQuerySubscriber({
|
|
45
|
+
* projectId: 'my-gcp-project',
|
|
46
|
+
* dataset: 'events',
|
|
47
|
+
* table: '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 { BigQuery } from '@google-cloud/bigquery';
|
|
61
|
+
|
|
62
|
+
export interface BigQuerySubscriberConfig {
|
|
63
|
+
/** GCP Project ID */
|
|
64
|
+
projectId: string;
|
|
65
|
+
/** BigQuery dataset name */
|
|
66
|
+
dataset: string;
|
|
67
|
+
/** BigQuery table name */
|
|
68
|
+
table: string;
|
|
69
|
+
/** Service account key file path (optional, uses GOOGLE_APPLICATION_CREDENTIALS if not set) */
|
|
70
|
+
keyFilename?: string;
|
|
71
|
+
/** Enable/disable subscriber */
|
|
72
|
+
enabled?: boolean;
|
|
73
|
+
/** Batch size (default: 500) */
|
|
74
|
+
batchSize?: number;
|
|
75
|
+
/** Flush interval in ms (default: 10000) */
|
|
76
|
+
flushInterval?: number;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export class BigQuerySubscriber extends EventSubscriber {
|
|
80
|
+
readonly name = 'BigQuerySubscriber';
|
|
81
|
+
readonly version = '1.0.0';
|
|
82
|
+
|
|
83
|
+
private client: BigQuery;
|
|
84
|
+
private tableRef: any;
|
|
85
|
+
private config: Required<BigQuerySubscriberConfig>;
|
|
86
|
+
private buffer: EventPayload[] = [];
|
|
87
|
+
private flushIntervalHandle: NodeJS.Timeout | null = null;
|
|
88
|
+
|
|
89
|
+
constructor(config: BigQuerySubscriberConfig) {
|
|
90
|
+
super();
|
|
91
|
+
|
|
92
|
+
// Set defaults
|
|
93
|
+
this.config = {
|
|
94
|
+
keyFilename: '',
|
|
95
|
+
enabled: true,
|
|
96
|
+
batchSize: 500,
|
|
97
|
+
flushInterval: 10_000,
|
|
98
|
+
...config,
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
this.enabled = this.config.enabled;
|
|
102
|
+
|
|
103
|
+
if (this.enabled) {
|
|
104
|
+
this.initializeClient();
|
|
105
|
+
this.startFlushInterval();
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private initializeClient(): void {
|
|
110
|
+
try {
|
|
111
|
+
const options: any = {
|
|
112
|
+
projectId: this.config.projectId,
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
if (this.config.keyFilename) {
|
|
116
|
+
options.keyFilename = this.config.keyFilename;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
this.client = new BigQuery(options);
|
|
120
|
+
|
|
121
|
+
const dataset = this.client.dataset(this.config.dataset);
|
|
122
|
+
this.tableRef = dataset.table(this.config.table);
|
|
123
|
+
|
|
124
|
+
console.log('[BigQuerySubscriber] Initialized successfully');
|
|
125
|
+
} catch (error) {
|
|
126
|
+
console.error('[BigQuerySubscriber] Failed to initialize:', error);
|
|
127
|
+
this.enabled = false;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private startFlushInterval(): void {
|
|
132
|
+
this.flushIntervalHandle = setInterval(() => {
|
|
133
|
+
void this.flushBuffer();
|
|
134
|
+
}, this.config.flushInterval);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
protected async sendToDestination(payload: EventPayload): Promise<void> {
|
|
138
|
+
this.buffer.push(payload);
|
|
139
|
+
|
|
140
|
+
// Auto-flush at batch size
|
|
141
|
+
if (this.buffer.length >= this.config.batchSize) {
|
|
142
|
+
await this.flushBuffer();
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
private async flushBuffer(): Promise<void> {
|
|
147
|
+
if (this.buffer.length === 0) return;
|
|
148
|
+
|
|
149
|
+
const batch = [...this.buffer];
|
|
150
|
+
this.buffer = [];
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
await this.insertBatch(batch);
|
|
154
|
+
} catch (error) {
|
|
155
|
+
console.error('[BigQuerySubscriber] Failed to flush batch:', error);
|
|
156
|
+
// Re-add to buffer for retry
|
|
157
|
+
this.buffer.unshift(...batch);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
private async insertBatch(events: EventPayload[]): Promise<void> {
|
|
162
|
+
const rows = events.map((event) => ({
|
|
163
|
+
event_id: crypto.randomUUID(),
|
|
164
|
+
event_type: event.type,
|
|
165
|
+
event_name: event.name,
|
|
166
|
+
attributes: event.attributes || {},
|
|
167
|
+
funnel: event.funnel || null,
|
|
168
|
+
step: event.step || null,
|
|
169
|
+
operation: event.operation || null,
|
|
170
|
+
outcome: event.outcome || null,
|
|
171
|
+
value: event.value || null,
|
|
172
|
+
timestamp: event.timestamp,
|
|
173
|
+
}));
|
|
174
|
+
|
|
175
|
+
// Insert rows
|
|
176
|
+
await this.tableRef.insert(rows, {
|
|
177
|
+
// Skip invalid rows (don't fail entire batch)
|
|
178
|
+
skipInvalidRows: false,
|
|
179
|
+
// Don't ignore unknown values
|
|
180
|
+
ignoreUnknownValues: false,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
protected handleError(error: Error, payload: EventPayload): void {
|
|
185
|
+
console.error(
|
|
186
|
+
`[BigQuerySubscriber] Failed to send ${payload.type}:`,
|
|
187
|
+
error,
|
|
188
|
+
{
|
|
189
|
+
eventName: payload.name,
|
|
190
|
+
attributes: payload.attributes,
|
|
191
|
+
}
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
// BigQuery-specific error handling
|
|
195
|
+
if (error.message.includes('quota')) {
|
|
196
|
+
console.error('[BigQuerySubscriber] Quota exceeded - consider increasing batchSize or flushInterval');
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (error.message.includes('schema')) {
|
|
200
|
+
console.error('[BigQuerySubscriber] Schema mismatch - check table schema');
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async shutdown(): Promise<void> {
|
|
205
|
+
// Clear flush interval
|
|
206
|
+
if (this.flushIntervalHandle) {
|
|
207
|
+
clearInterval(this.flushIntervalHandle);
|
|
208
|
+
this.flushIntervalHandle = null;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Flush remaining events
|
|
212
|
+
await this.flushBuffer();
|
|
213
|
+
|
|
214
|
+
// Wait for pending requests
|
|
215
|
+
await super.shutdown();
|
|
216
|
+
|
|
217
|
+
console.log('[BigQuerySubscriber] Shutdown complete');
|
|
218
|
+
}
|
|
219
|
+
}
|