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/slack.js
ADDED
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
// src/event-subscriber-base.ts
|
|
2
|
+
var EventSubscriber = class {
|
|
3
|
+
/**
|
|
4
|
+
* Subscriber version (optional)
|
|
5
|
+
*/
|
|
6
|
+
version;
|
|
7
|
+
/**
|
|
8
|
+
* Enable/disable the subscriber (default: true)
|
|
9
|
+
*/
|
|
10
|
+
enabled = true;
|
|
11
|
+
/**
|
|
12
|
+
* Track pending requests for graceful shutdown
|
|
13
|
+
*/
|
|
14
|
+
pendingRequests = /* @__PURE__ */ new Set();
|
|
15
|
+
/**
|
|
16
|
+
* Optional: Handle errors
|
|
17
|
+
*
|
|
18
|
+
* Override this to customize error handling (logging, retries, etc.).
|
|
19
|
+
* Default behavior: log to console.error
|
|
20
|
+
*
|
|
21
|
+
* @param error - Error that occurred
|
|
22
|
+
* @param payload - Event payload that failed
|
|
23
|
+
*/
|
|
24
|
+
handleError(error, payload) {
|
|
25
|
+
console.error(
|
|
26
|
+
`[${this.name}] Failed to send ${payload.type}:`,
|
|
27
|
+
error,
|
|
28
|
+
payload
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Track an event
|
|
33
|
+
*/
|
|
34
|
+
async trackEvent(name, attributes) {
|
|
35
|
+
if (!this.enabled) return;
|
|
36
|
+
const payload = {
|
|
37
|
+
type: "event",
|
|
38
|
+
name,
|
|
39
|
+
attributes,
|
|
40
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
41
|
+
};
|
|
42
|
+
await this.send(payload);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Track a funnel step
|
|
46
|
+
*/
|
|
47
|
+
async trackFunnelStep(funnelName, step, attributes) {
|
|
48
|
+
if (!this.enabled) return;
|
|
49
|
+
const payload = {
|
|
50
|
+
type: "funnel",
|
|
51
|
+
name: `${funnelName}.${step}`,
|
|
52
|
+
funnel: funnelName,
|
|
53
|
+
step,
|
|
54
|
+
attributes,
|
|
55
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
56
|
+
};
|
|
57
|
+
await this.send(payload);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Track an outcome
|
|
61
|
+
*/
|
|
62
|
+
async trackOutcome(operationName, outcome, attributes) {
|
|
63
|
+
if (!this.enabled) return;
|
|
64
|
+
const payload = {
|
|
65
|
+
type: "outcome",
|
|
66
|
+
name: `${operationName}.${outcome}`,
|
|
67
|
+
operation: operationName,
|
|
68
|
+
outcome,
|
|
69
|
+
attributes,
|
|
70
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
71
|
+
};
|
|
72
|
+
await this.send(payload);
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Track a value/metric
|
|
76
|
+
*/
|
|
77
|
+
async trackValue(name, value, attributes) {
|
|
78
|
+
if (!this.enabled) return;
|
|
79
|
+
const payload = {
|
|
80
|
+
type: "value",
|
|
81
|
+
name,
|
|
82
|
+
value,
|
|
83
|
+
attributes,
|
|
84
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
85
|
+
};
|
|
86
|
+
await this.send(payload);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Flush pending requests and clean up
|
|
90
|
+
*
|
|
91
|
+
* CRITICAL: Prevents race condition during shutdown
|
|
92
|
+
* 1. Disables subscriber to stop new events
|
|
93
|
+
* 2. Drains all pending requests (with retry logic)
|
|
94
|
+
* 3. Ensures flush guarantee
|
|
95
|
+
*
|
|
96
|
+
* Override this if you need custom cleanup logic (close connections, flush buffers, etc.),
|
|
97
|
+
* but ALWAYS call super.shutdown() first to drain pending requests.
|
|
98
|
+
*/
|
|
99
|
+
async shutdown() {
|
|
100
|
+
this.enabled = false;
|
|
101
|
+
const maxDrainAttempts = 10;
|
|
102
|
+
const drainIntervalMs = 50;
|
|
103
|
+
for (let attempt = 0; attempt < maxDrainAttempts; attempt++) {
|
|
104
|
+
if (this.pendingRequests.size === 0) {
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
await Promise.allSettled(this.pendingRequests);
|
|
108
|
+
if (this.pendingRequests.size > 0 && attempt < maxDrainAttempts - 1) {
|
|
109
|
+
await new Promise((resolve) => setTimeout(resolve, drainIntervalMs));
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (this.pendingRequests.size > 0) {
|
|
113
|
+
console.warn(
|
|
114
|
+
`[${this.name}] Shutdown completed with ${this.pendingRequests.size} pending requests still in-flight. This may indicate a bug in the subscriber or extremely slow destination.`
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Internal: Send payload and track request
|
|
120
|
+
*/
|
|
121
|
+
async send(payload) {
|
|
122
|
+
const request = this.sendWithErrorHandling(payload);
|
|
123
|
+
this.pendingRequests.add(request);
|
|
124
|
+
void request.finally(() => {
|
|
125
|
+
this.pendingRequests.delete(request);
|
|
126
|
+
});
|
|
127
|
+
return request;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Internal: Send with error handling
|
|
131
|
+
*/
|
|
132
|
+
async sendWithErrorHandling(payload) {
|
|
133
|
+
try {
|
|
134
|
+
await this.sendToDestination(payload);
|
|
135
|
+
} catch (error) {
|
|
136
|
+
this.handleError(error, payload);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
// src/slack.ts
|
|
142
|
+
var SlackSubscriber = class extends EventSubscriber {
|
|
143
|
+
name = "SlackSubscriber";
|
|
144
|
+
version = "1.0.0";
|
|
145
|
+
config;
|
|
146
|
+
constructor(config) {
|
|
147
|
+
super();
|
|
148
|
+
this.config = {
|
|
149
|
+
webhookUrl: config.webhookUrl,
|
|
150
|
+
channel: config.channel,
|
|
151
|
+
username: config.username ?? "Events Bot",
|
|
152
|
+
iconEmoji: config.iconEmoji ?? ":chart_with_upwards_trend:",
|
|
153
|
+
includeTimestamp: config.includeTimestamp ?? true,
|
|
154
|
+
includeAttributes: config.includeAttributes ?? true,
|
|
155
|
+
maxAttributeFields: config.maxAttributeFields ?? 10,
|
|
156
|
+
filter: config.filter,
|
|
157
|
+
enabled: config.enabled ?? true
|
|
158
|
+
};
|
|
159
|
+
this.enabled = this.config.enabled;
|
|
160
|
+
if (!this.config.webhookUrl) {
|
|
161
|
+
console.error(
|
|
162
|
+
"[SlackSubscriber] No webhook URL provided - subscriber disabled"
|
|
163
|
+
);
|
|
164
|
+
this.enabled = false;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
async sendToDestination(payload) {
|
|
168
|
+
if (this.config.filter) {
|
|
169
|
+
const filterFn = this.config.filter;
|
|
170
|
+
const shouldInclude = filterFn(payload);
|
|
171
|
+
if (!shouldInclude) {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
const message = this.formatSlackMessage(payload);
|
|
176
|
+
const response = await fetch(this.config.webhookUrl, {
|
|
177
|
+
method: "POST",
|
|
178
|
+
headers: { "Content-Type": "application/json" },
|
|
179
|
+
body: JSON.stringify(message)
|
|
180
|
+
});
|
|
181
|
+
if (!response.ok) {
|
|
182
|
+
const errorText = await response.text();
|
|
183
|
+
throw new Error(
|
|
184
|
+
`Slack webhook failed (${response.status}): ${errorText}`
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Format events payload as Slack message
|
|
190
|
+
*/
|
|
191
|
+
formatSlackMessage(payload) {
|
|
192
|
+
const emoji = this.getEventEmoji(payload);
|
|
193
|
+
const color = this.getEventColor(payload);
|
|
194
|
+
const title = `${emoji} ${payload.name}`;
|
|
195
|
+
const fields = [
|
|
196
|
+
{
|
|
197
|
+
title: "Event Type",
|
|
198
|
+
value: this.formatEventType(payload),
|
|
199
|
+
short: true
|
|
200
|
+
}
|
|
201
|
+
];
|
|
202
|
+
if (this.config.includeTimestamp) {
|
|
203
|
+
fields.push({
|
|
204
|
+
title: "Timestamp",
|
|
205
|
+
value: new Date(payload.timestamp).toLocaleString("en-US", {
|
|
206
|
+
month: "short",
|
|
207
|
+
day: "numeric",
|
|
208
|
+
hour: "2-digit",
|
|
209
|
+
minute: "2-digit",
|
|
210
|
+
second: "2-digit"
|
|
211
|
+
}),
|
|
212
|
+
short: true
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
if (this.config.includeAttributes && payload.attributes) {
|
|
216
|
+
const attributeFields = this.formatAttributes(payload.attributes);
|
|
217
|
+
fields.push(...attributeFields);
|
|
218
|
+
}
|
|
219
|
+
const attachment = {
|
|
220
|
+
color,
|
|
221
|
+
title,
|
|
222
|
+
fields,
|
|
223
|
+
footer: "Events Events",
|
|
224
|
+
footer_icon: "https://i.imgur.com/QpCKbNL.png"
|
|
225
|
+
};
|
|
226
|
+
if (this.config.includeTimestamp) {
|
|
227
|
+
attachment.ts = Math.floor(
|
|
228
|
+
new Date(payload.timestamp).getTime() / 1e3
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
return {
|
|
232
|
+
channel: this.config.channel,
|
|
233
|
+
username: this.config.username,
|
|
234
|
+
icon_emoji: this.config.iconEmoji,
|
|
235
|
+
attachments: [attachment]
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Get emoji for event type
|
|
240
|
+
*/
|
|
241
|
+
getEventEmoji(payload) {
|
|
242
|
+
switch (payload.type) {
|
|
243
|
+
case "outcome": {
|
|
244
|
+
return payload.outcome === "success" ? "\u2705" : "\u274C";
|
|
245
|
+
}
|
|
246
|
+
case "funnel": {
|
|
247
|
+
return "\u{1F504}";
|
|
248
|
+
}
|
|
249
|
+
case "value": {
|
|
250
|
+
return "\u{1F4CA}";
|
|
251
|
+
}
|
|
252
|
+
default: {
|
|
253
|
+
if (payload.name.includes("order") || payload.name.includes("payment"))
|
|
254
|
+
return "\u{1F4B0}";
|
|
255
|
+
if (payload.name.includes("signup") || payload.name.includes("user"))
|
|
256
|
+
return "\u{1F464}";
|
|
257
|
+
if (payload.name.includes("error") || payload.name.includes("fail"))
|
|
258
|
+
return "\u26A0\uFE0F";
|
|
259
|
+
return "\u{1F4CC}";
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Get Slack attachment color for event type
|
|
265
|
+
*/
|
|
266
|
+
getEventColor(payload) {
|
|
267
|
+
switch (payload.type) {
|
|
268
|
+
case "outcome": {
|
|
269
|
+
return payload.outcome === "success" ? "good" : "danger";
|
|
270
|
+
}
|
|
271
|
+
// Green or red
|
|
272
|
+
case "funnel": {
|
|
273
|
+
return "#3AA3E3";
|
|
274
|
+
}
|
|
275
|
+
// Blue
|
|
276
|
+
case "value": {
|
|
277
|
+
return "#764FA5";
|
|
278
|
+
}
|
|
279
|
+
// Purple
|
|
280
|
+
default: {
|
|
281
|
+
if (payload.name.includes("error") || payload.name.includes("fail"))
|
|
282
|
+
return "danger";
|
|
283
|
+
if (payload.name.includes("warning")) return "warning";
|
|
284
|
+
return "good";
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Format event type for display
|
|
290
|
+
*/
|
|
291
|
+
formatEventType(payload) {
|
|
292
|
+
switch (payload.type) {
|
|
293
|
+
case "event": {
|
|
294
|
+
return "Event";
|
|
295
|
+
}
|
|
296
|
+
case "funnel": {
|
|
297
|
+
return `Funnel: ${payload.step || "unknown"}`;
|
|
298
|
+
}
|
|
299
|
+
case "outcome": {
|
|
300
|
+
return `Outcome: ${payload.outcome || "unknown"}`;
|
|
301
|
+
}
|
|
302
|
+
case "value": {
|
|
303
|
+
return `Value: ${payload.value ?? "N/A"}`;
|
|
304
|
+
}
|
|
305
|
+
default: {
|
|
306
|
+
return payload.type;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Format attributes as Slack fields
|
|
312
|
+
*/
|
|
313
|
+
formatAttributes(attributes) {
|
|
314
|
+
const fields = [];
|
|
315
|
+
const entries = Object.entries(attributes);
|
|
316
|
+
const limit = Math.min(entries.length, this.config.maxAttributeFields);
|
|
317
|
+
for (let i = 0; i < limit; i++) {
|
|
318
|
+
const [key, value] = entries[i];
|
|
319
|
+
if (key.startsWith("_") || key === "timestamp") continue;
|
|
320
|
+
fields.push({
|
|
321
|
+
title: this.formatFieldName(key),
|
|
322
|
+
value: this.formatFieldValue(value),
|
|
323
|
+
short: true
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
if (entries.length > this.config.maxAttributeFields) {
|
|
327
|
+
fields.push({
|
|
328
|
+
title: "Note",
|
|
329
|
+
value: `... and ${entries.length - this.config.maxAttributeFields} more fields`,
|
|
330
|
+
short: false
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
return fields;
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Format field name (convert camelCase to Title Case)
|
|
337
|
+
*/
|
|
338
|
+
formatFieldName(name) {
|
|
339
|
+
return name.replaceAll(/([A-Z])/g, " $1").replace(/^./, (str) => str.toUpperCase()).trim();
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Format field value
|
|
343
|
+
*/
|
|
344
|
+
formatFieldValue(value) {
|
|
345
|
+
if (value === null || value === void 0) return "N/A";
|
|
346
|
+
if (typeof value === "boolean") return value ? "Yes" : "No";
|
|
347
|
+
if (typeof value === "object") return JSON.stringify(value);
|
|
348
|
+
if (typeof value === "number" && !Number.isInteger(value)) {
|
|
349
|
+
return value.toFixed(2);
|
|
350
|
+
}
|
|
351
|
+
return String(value);
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Handle errors (override from EventSubscriber)
|
|
355
|
+
*/
|
|
356
|
+
handleError(error, payload) {
|
|
357
|
+
console.error(
|
|
358
|
+
`[SlackSubscriber] Failed to send ${payload.type} event "${payload.name}":`,
|
|
359
|
+
error
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
export { SlackSubscriber };
|
|
365
|
+
//# sourceMappingURL=slack.js.map
|
|
366
|
+
//# sourceMappingURL=slack.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/event-subscriber-base.ts","../src/slack.ts"],"names":[],"mappings":"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","file":"slack.js","sourcesContent":["/**\n * EventSubscriber - Standard base class for building custom subscribers\n *\n * This is the recommended base class for creating custom events subscribers.\n * It provides production-ready features out of the box:\n *\n * **Built-in Features:**\n * - **Error Handling**: Automatic error catching with customizable handlers\n * - **Pending Request Tracking**: Ensures all requests complete during shutdown\n * - **Graceful Shutdown**: Drains pending requests before closing\n * - **Enable/Disable**: Runtime control to turn subscriber on/off\n * - **Normalized Payload**: Consistent event structure across all event types\n *\n * **When to use:**\n * - Building custom subscribers for any platform\n * - Production deployments requiring reliability\n * - Need graceful shutdown and error handling\n *\n * @example Basic usage\n * ```typescript\n * import { EventSubscriber, EventPayload } from 'autotel-subscribers';\n *\n * class SnowflakeSubscriber extends EventSubscriber {\n * name = 'SnowflakeSubscriber';\n * version = '1.0.0';\n *\n * protected async sendToDestination(payload: EventPayload): Promise<void> {\n * await snowflakeClient.execute(\n * `INSERT INTO events VALUES (?, ?, ?)`,\n * [payload.type, payload.name, JSON.stringify(payload.attributes)]\n * );\n * }\n * }\n * ```\n *\n * @example With buffering\n * ```typescript\n * class BufferedSubscriber extends EventSubscriber {\n * name = 'BufferedSubscriber';\n * private buffer: EventPayload[] = [];\n *\n * protected async sendToDestination(payload: EventPayload): Promise<void> {\n * this.buffer.push(payload);\n *\n * if (this.buffer.length >= 100) {\n * await this.flush();\n * }\n * }\n *\n * async shutdown(): Promise<void> {\n * await super.shutdown(); // Drain pending requests first\n * await this.flush(); // Then flush buffer\n * }\n *\n * private async flush(): Promise<void> {\n * if (this.buffer.length === 0) return;\n *\n * const batch = [...this.buffer];\n * this.buffer = [];\n *\n * await apiClient.sendBatch(batch);\n * }\n * }\n * ```\n */\n\nimport type {\n EventSubscriber as IEventSubscriber,\n EventAttributes,\n FunnelStatus,\n OutcomeStatus,\n} from 'autotel/event-subscriber';\n\n// Re-export types for convenience\n\n\n/**\n * Payload sent to destination\n */\nexport interface EventPayload {\n /** Event type: 'event', 'funnel', 'outcome', or 'value' */\n type: 'event' | 'funnel' | 'outcome' | 'value';\n\n /** Event name or metric name */\n name: string;\n\n /** Optional attributes */\n attributes?: EventAttributes;\n\n /** For funnel events: funnel name */\n funnel?: string;\n\n /** For funnel events: step status */\n step?: FunnelStatus;\n\n /** For outcome events: operation name */\n operation?: string;\n\n /** For outcome events: outcome status */\n outcome?: OutcomeStatus;\n\n /** For value events: numeric value */\n value?: number;\n\n /** Timestamp (ISO 8601) */\n timestamp: string;\n}\n\n/**\n * Standard base class for building custom events subscribers\n *\n * **What it provides:**\n * - Consistent payload structure (normalized across all event types)\n * - Enable/disable flag (runtime control)\n * - Automatic error handling (with customizable error handlers)\n * - Pending requests tracking (ensures no lost events during shutdown)\n * - Graceful shutdown (drains pending requests before closing)\n *\n * **Usage:**\n * Extend this class and implement `sendToDestination()`. All other methods\n * (trackEvent, trackFunnelStep, trackOutcome, trackValue, shutdown) are handled automatically.\n *\n * For high-throughput streaming platforms (Kafka, Kinesis, Pub/Sub), use `StreamingEventSubscriber` instead.\n */\nexport abstract class EventSubscriber implements IEventSubscriber {\n /**\n * Subscriber name (required for debugging)\n */\n abstract readonly name: string;\n\n /**\n * Subscriber version (optional)\n */\n readonly version?: string;\n\n /**\n * Enable/disable the subscriber (default: true)\n */\n protected enabled: boolean = true;\n\n /**\n * Track pending requests for graceful shutdown\n */\n private pendingRequests: Set<Promise<void>> = new Set();\n\n /**\n * Send payload to destination\n *\n * Override this method to implement your destination-specific logic.\n * This is called for all event types (event, funnel, outcome, value).\n *\n * @param payload - Normalized event payload\n */\n protected abstract sendToDestination(payload: EventPayload): Promise<void>;\n\n /**\n * Optional: Handle errors\n *\n * Override this to customize error handling (logging, retries, etc.).\n * Default behavior: log to console.error\n *\n * @param error - Error that occurred\n * @param payload - Event payload that failed\n */\n protected handleError(error: Error, payload: EventPayload): void {\n console.error(\n `[${this.name}] Failed to send ${payload.type}:`,\n error,\n payload,\n );\n }\n\n /**\n * Track an event\n */\n async trackEvent(name: string, attributes?: EventAttributes): Promise<void> {\n if (!this.enabled) return;\n\n const payload: EventPayload = {\n type: 'event',\n name,\n attributes,\n timestamp: new Date().toISOString(),\n };\n\n await this.send(payload);\n }\n\n /**\n * Track a funnel step\n */\n async trackFunnelStep(\n funnelName: string,\n step: FunnelStatus,\n attributes?: EventAttributes,\n ): Promise<void> {\n if (!this.enabled) return;\n\n const payload: EventPayload = {\n type: 'funnel',\n name: `${funnelName}.${step}`,\n funnel: funnelName,\n step,\n attributes,\n timestamp: new Date().toISOString(),\n };\n\n await this.send(payload);\n }\n\n /**\n * Track an outcome\n */\n async trackOutcome(\n operationName: string,\n outcome: OutcomeStatus,\n attributes?: EventAttributes,\n ): Promise<void> {\n if (!this.enabled) return;\n\n const payload: EventPayload = {\n type: 'outcome',\n name: `${operationName}.${outcome}`,\n operation: operationName,\n outcome,\n attributes,\n timestamp: new Date().toISOString(),\n };\n\n await this.send(payload);\n }\n\n /**\n * Track a value/metric\n */\n async trackValue(\n name: string,\n value: number,\n attributes?: EventAttributes,\n ): Promise<void> {\n if (!this.enabled) return;\n\n const payload: EventPayload = {\n type: 'value',\n name,\n value,\n attributes,\n timestamp: new Date().toISOString(),\n };\n\n await this.send(payload);\n }\n\n /**\n * Flush pending requests and clean up\n *\n * CRITICAL: Prevents race condition during shutdown\n * 1. Disables subscriber to stop new events\n * 2. Drains all pending requests (with retry logic)\n * 3. Ensures flush guarantee\n *\n * Override this if you need custom cleanup logic (close connections, flush buffers, etc.),\n * but ALWAYS call super.shutdown() first to drain pending requests.\n */\n async shutdown(): Promise<void> {\n // 1. Stop accepting new events (prevents race condition)\n this.enabled = false;\n\n // 2. Drain pending requests with retry logic\n // Loop until empty to handle race where new requests added during Promise.allSettled\n const maxDrainAttempts = 10;\n const drainIntervalMs = 50;\n\n for (let attempt = 0; attempt < maxDrainAttempts; attempt++) {\n if (this.pendingRequests.size === 0) {\n break;\n }\n\n // Wait for current batch\n await Promise.allSettled(this.pendingRequests);\n\n // Small delay to catch any stragglers added during allSettled\n if (this.pendingRequests.size > 0 && attempt < maxDrainAttempts - 1) {\n await new Promise((resolve) => setTimeout(resolve, drainIntervalMs));\n }\n }\n\n // 3. Warn if we still have pending requests (shouldn't happen, but be defensive)\n if (this.pendingRequests.size > 0) {\n console.warn(\n `[${this.name}] Shutdown completed with ${this.pendingRequests.size} pending requests still in-flight. ` +\n `This may indicate a bug in the subscriber or extremely slow destination.`\n );\n }\n }\n\n /**\n * Internal: Send payload and track request\n */\n private async send(payload: EventPayload): Promise<void> {\n const request = this.sendWithErrorHandling(payload);\n this.pendingRequests.add(request);\n\n void request.finally(() => {\n this.pendingRequests.delete(request);\n });\n\n return request;\n }\n\n /**\n * Internal: Send with error handling\n */\n private async sendWithErrorHandling(\n payload: EventPayload,\n ): Promise<void> {\n try {\n await this.sendToDestination(payload);\n } catch (error) {\n this.handleError(error as Error, payload);\n }\n }\n}\n\nexport {type EventAttributes, type FunnelStatus, type OutcomeStatus} from 'autotel/event-subscriber';","/**\n * Slack Subscriber for autotel\n *\n * Send events events as notifications to Slack channels via webhooks.\n *\n * Perfect for:\n * - Critical business events (orders, payments, signups)\n * - Real-time alerts for failures\n * - Team notifications for important milestones\n * - Monitoring funnel completions\n *\n * @example Basic usage\n * ```typescript\n * import { Events } from 'autotel/events';\n * import { SlackSubscriber } from 'autotel-subscribers/slack';\n *\n * const events = new Events('app', {\n * subscribers: [\n * new SlackSubscriber({\n * webhookUrl: process.env.SLACK_WEBHOOK_URL!,\n * channel: '#order-events'\n * })\n * ]\n * });\n *\n * // Sends to Slack\n * events.trackEvent('order.completed', {\n * orderId: 'ord_123',\n * userId: 'user_456',\n * amount: 99.99\n * });\n * ```\n *\n * @example Filter critical events only\n * ```typescript\n * const events = new Events('app', {\n * subscribers: [\n * new SlackSubscriber({\n * webhookUrl: process.env.SLACK_WEBHOOK_URL!,\n * channel: '#alerts',\n * filter: (payload) => {\n * // Only send failures and high-value orders\n * if (payload.type === 'outcome' && payload.outcome === 'failure') {\n * return true;\n * }\n * if (payload.name === 'order.completed' && payload.attributes?.amount > 1000) {\n * return true;\n * }\n * return false;\n * }\n * })\n * ]\n * });\n * ```\n *\n * Setup:\n * 1. Create Slack App: https://api.slack.com/apps\n * 2. Enable Incoming Webhooks\n * 3. Add webhook to workspace\n * 4. Copy webhook URL (https://hooks.slack.com/services/...)\n */\n\nimport {\n EventSubscriber,\n type EventPayload,\n} from './event-subscriber-base';\n\nexport interface SlackSubscriberConfig {\n /** Slack webhook URL (https://hooks.slack.com/services/...) */\n webhookUrl: string;\n\n /** Default channel to post to (optional, overrides webhook default) */\n channel?: string;\n\n /** Custom username for bot (default: 'Events Bot') */\n username?: string;\n\n /** Custom emoji icon (default: ':chart_with_upwards_trend:') */\n iconEmoji?: string;\n\n /** Include timestamp in messages (default: true) */\n includeTimestamp?: boolean;\n\n /** Include event attributes as fields (default: true) */\n includeAttributes?: boolean;\n\n /** Maximum attributes to show (default: 10) */\n maxAttributeFields?: number;\n\n /** Filter function - return true to send, false to skip */\n filter?: (payload: EventPayload) => boolean;\n\n /** Enable/disable subscriber */\n enabled?: boolean;\n}\n\ninterface SlackMessage {\n channel?: string;\n username?: string;\n icon_emoji?: string;\n text?: string;\n attachments: SlackAttachment[];\n}\n\ninterface SlackAttachment {\n color?: string;\n title?: string;\n text?: string;\n fields?: SlackField[];\n footer?: string;\n footer_icon?: string;\n ts?: number;\n}\n\ninterface SlackField {\n title: string;\n value: string;\n short: boolean;\n}\n\nexport class SlackSubscriber extends EventSubscriber {\n readonly name = 'SlackSubscriber';\n readonly version = '1.0.0';\n\n private config: Required<Omit<SlackSubscriberConfig, 'channel' | 'filter'>> & {\n channel?: string;\n filter?: (payload: EventPayload) => boolean;\n };\n\n constructor(config: SlackSubscriberConfig) {\n super();\n\n this.config = {\n webhookUrl: config.webhookUrl,\n channel: config.channel,\n username: config.username ?? 'Events Bot',\n iconEmoji: config.iconEmoji ?? ':chart_with_upwards_trend:',\n includeTimestamp: config.includeTimestamp ?? true,\n includeAttributes: config.includeAttributes ?? true,\n maxAttributeFields: config.maxAttributeFields ?? 10,\n filter: config.filter,\n enabled: config.enabled ?? true,\n };\n\n this.enabled = this.config.enabled;\n\n if (!this.config.webhookUrl) {\n console.error(\n '[SlackSubscriber] No webhook URL provided - subscriber disabled'\n );\n this.enabled = false;\n }\n }\n\n protected async sendToDestination(payload: EventPayload): Promise<void> {\n // Apply filter if provided\n if (this.config.filter) {\n const filterFn = this.config.filter;\n const shouldInclude = filterFn(payload);\n if (!shouldInclude) {\n return; // Skip this event\n }\n }\n\n const message = this.formatSlackMessage(payload);\n\n const response = await fetch(this.config.webhookUrl, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(message),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(\n `Slack webhook failed (${response.status}): ${errorText}`\n );\n }\n }\n\n /**\n * Format events payload as Slack message\n */\n private formatSlackMessage(payload: EventPayload): SlackMessage {\n const emoji = this.getEventEmoji(payload);\n const color = this.getEventColor(payload);\n const title = `${emoji} ${payload.name}`;\n\n // Add event type\n const fields: SlackField[] = [\n {\n title: 'Event Type',\n value: this.formatEventType(payload),\n short: true,\n },\n ];\n\n // Add timestamp if enabled\n if (this.config.includeTimestamp) {\n fields.push({\n title: 'Timestamp',\n value: new Date(payload.timestamp).toLocaleString('en-US', {\n month: 'short',\n day: 'numeric',\n hour: '2-digit',\n minute: '2-digit',\n second: '2-digit',\n }),\n short: true,\n });\n }\n\n // Add attributes as fields\n if (this.config.includeAttributes && payload.attributes) {\n const attributeFields = this.formatAttributes(payload.attributes);\n fields.push(...attributeFields);\n }\n\n const attachment: SlackAttachment = {\n color,\n title,\n fields,\n footer: 'Events Events',\n footer_icon: 'https://i.imgur.com/QpCKbNL.png',\n };\n\n // Add Unix timestamp for Slack\n if (this.config.includeTimestamp) {\n attachment.ts = Math.floor(\n new Date(payload.timestamp).getTime() / 1000\n );\n }\n\n return {\n channel: this.config.channel,\n username: this.config.username,\n icon_emoji: this.config.iconEmoji,\n attachments: [attachment],\n };\n }\n\n /**\n * Get emoji for event type\n */\n private getEventEmoji(payload: EventPayload): string {\n switch (payload.type) {\n case 'outcome': {\n return payload.outcome === 'success' ? '✅' : '❌';\n }\n case 'funnel': {\n return '🔄';\n }\n case 'value': {\n return '📊';\n }\n default: {\n // Use custom emoji for common event patterns\n if (payload.name.includes('order') || payload.name.includes('payment'))\n return '💰';\n if (payload.name.includes('signup') || payload.name.includes('user'))\n return '👤';\n if (payload.name.includes('error') || payload.name.includes('fail'))\n return '⚠️';\n return '📌';\n }\n }\n }\n\n /**\n * Get Slack attachment color for event type\n */\n private getEventColor(payload: EventPayload): string {\n switch (payload.type) {\n case 'outcome': {\n return payload.outcome === 'success' ? 'good' : 'danger';\n } // Green or red\n case 'funnel': {\n return '#3AA3E3';\n } // Blue\n case 'value': {\n return '#764FA5';\n } // Purple\n default: {\n // Custom colors for patterns\n if (payload.name.includes('error') || payload.name.includes('fail'))\n return 'danger';\n if (payload.name.includes('warning')) return 'warning';\n return 'good';\n } // Default green\n }\n }\n\n /**\n * Format event type for display\n */\n private formatEventType(payload: EventPayload): string {\n switch (payload.type) {\n case 'event': {\n return 'Event';\n }\n case 'funnel': {\n return `Funnel: ${payload.step || 'unknown'}`;\n }\n case 'outcome': {\n return `Outcome: ${payload.outcome || 'unknown'}`;\n }\n case 'value': {\n return `Value: ${payload.value ?? 'N/A'}`;\n }\n default: {\n return payload.type;\n }\n }\n }\n\n /**\n * Format attributes as Slack fields\n */\n private formatAttributes(attributes: Record<string, any>): SlackField[] {\n const fields: SlackField[] = [];\n const entries = Object.entries(attributes);\n\n // Limit number of fields\n const limit = Math.min(entries.length, this.config.maxAttributeFields);\n\n for (let i = 0; i < limit; i++) {\n const [key, value] = entries[i];\n\n // Skip internal/system fields\n if (key.startsWith('_') || key === 'timestamp') continue;\n\n fields.push({\n title: this.formatFieldName(key),\n value: this.formatFieldValue(value),\n short: true,\n });\n }\n\n // Add truncation notice if needed\n if (entries.length > this.config.maxAttributeFields) {\n fields.push({\n title: 'Note',\n value: `... and ${entries.length - this.config.maxAttributeFields} more fields`,\n short: false,\n });\n }\n\n return fields;\n }\n\n /**\n * Format field name (convert camelCase to Title Case)\n */\n private formatFieldName(name: string): string {\n return name\n .replaceAll(/([A-Z])/g, ' $1') // Add space before capitals\n .replace(/^./, (str) => str.toUpperCase()) // Capitalize first letter\n .trim();\n }\n\n /**\n * Format field value\n */\n private formatFieldValue(value: any): string {\n if (value === null || value === undefined) return 'N/A';\n if (typeof value === 'boolean') return value ? 'Yes' : 'No';\n if (typeof value === 'object') return JSON.stringify(value);\n if (typeof value === 'number' && !Number.isInteger(value)) {\n return value.toFixed(2);\n }\n return String(value);\n }\n\n /**\n * Handle errors (override from EventSubscriber)\n */\n protected handleError(error: Error, payload: EventPayload): void {\n console.error(\n `[SlackSubscriber] Failed to send ${payload.type} event \"${payload.name}\":`,\n error\n );\n }\n}\n"]}
|
package/dist/webhook.cjs
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/webhook.ts
|
|
4
|
+
var WebhookSubscriber = class {
|
|
5
|
+
name = "WebhookSubscriber";
|
|
6
|
+
version = "1.0.0";
|
|
7
|
+
config;
|
|
8
|
+
enabled;
|
|
9
|
+
pendingRequests = /* @__PURE__ */ new Set();
|
|
10
|
+
constructor(config) {
|
|
11
|
+
this.config = config;
|
|
12
|
+
this.enabled = config.enabled ?? true;
|
|
13
|
+
}
|
|
14
|
+
async send(payload) {
|
|
15
|
+
if (!this.enabled) return;
|
|
16
|
+
const maxRetries = this.config.maxRetries ?? 3;
|
|
17
|
+
let lastError;
|
|
18
|
+
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
19
|
+
try {
|
|
20
|
+
const response = await fetch(this.config.url, {
|
|
21
|
+
method: "POST",
|
|
22
|
+
headers: {
|
|
23
|
+
"Content-Type": "application/json",
|
|
24
|
+
...this.config.headers
|
|
25
|
+
},
|
|
26
|
+
body: JSON.stringify(payload)
|
|
27
|
+
});
|
|
28
|
+
if (!response.ok) {
|
|
29
|
+
throw new Error(`Webhook returned ${response.status}: ${response.statusText}`);
|
|
30
|
+
}
|
|
31
|
+
return;
|
|
32
|
+
} catch (error) {
|
|
33
|
+
lastError = error;
|
|
34
|
+
if (attempt < maxRetries - 1) {
|
|
35
|
+
await new Promise((resolve) => setTimeout(resolve, Math.pow(2, attempt) * 1e3));
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
console.error(`Webhook subscriber failed after ${maxRetries} attempts:`, lastError);
|
|
40
|
+
}
|
|
41
|
+
async trackEvent(name, attributes) {
|
|
42
|
+
const request = this.send({
|
|
43
|
+
type: "event",
|
|
44
|
+
name,
|
|
45
|
+
attributes,
|
|
46
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
47
|
+
});
|
|
48
|
+
this.trackRequest(request);
|
|
49
|
+
await request;
|
|
50
|
+
}
|
|
51
|
+
async trackFunnelStep(funnelName, step, attributes) {
|
|
52
|
+
const request = this.send({
|
|
53
|
+
type: "funnel",
|
|
54
|
+
funnel: funnelName,
|
|
55
|
+
step,
|
|
56
|
+
attributes,
|
|
57
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
58
|
+
});
|
|
59
|
+
this.trackRequest(request);
|
|
60
|
+
await request;
|
|
61
|
+
}
|
|
62
|
+
async trackOutcome(operationName, outcome, attributes) {
|
|
63
|
+
const request = this.send({
|
|
64
|
+
type: "outcome",
|
|
65
|
+
operation: operationName,
|
|
66
|
+
outcome,
|
|
67
|
+
attributes,
|
|
68
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
69
|
+
});
|
|
70
|
+
this.trackRequest(request);
|
|
71
|
+
await request;
|
|
72
|
+
}
|
|
73
|
+
async trackValue(name, value, attributes) {
|
|
74
|
+
const request = this.send({
|
|
75
|
+
type: "value",
|
|
76
|
+
name,
|
|
77
|
+
value,
|
|
78
|
+
attributes,
|
|
79
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
80
|
+
});
|
|
81
|
+
this.trackRequest(request);
|
|
82
|
+
await request;
|
|
83
|
+
}
|
|
84
|
+
trackRequest(request) {
|
|
85
|
+
this.pendingRequests.add(request);
|
|
86
|
+
void request.finally(() => {
|
|
87
|
+
this.pendingRequests.delete(request);
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
/** Wait for all pending webhook requests to complete */
|
|
91
|
+
async shutdown() {
|
|
92
|
+
if (this.pendingRequests.size > 0) {
|
|
93
|
+
await Promise.allSettled(this.pendingRequests);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
exports.WebhookSubscriber = WebhookSubscriber;
|
|
99
|
+
//# sourceMappingURL=webhook.cjs.map
|
|
100
|
+
//# sourceMappingURL=webhook.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/webhook.ts"],"names":[],"mappings":"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","file":"webhook.cjs","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,53 @@
|
|
|
1
|
+
import { EventSubscriber, EventAttributes, FunnelStatus, OutcomeStatus } from 'autotel/event-subscriber';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Webhook Subscriber for autotel
|
|
5
|
+
*
|
|
6
|
+
* Send events to any webhook endpoint (custom integrations, Zapier, Make.com, etc.).
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```typescript
|
|
10
|
+
* import { Events } from 'autotel/events';
|
|
11
|
+
* import { WebhookSubscriber } from 'autotel-subscribers/webhook';
|
|
12
|
+
*
|
|
13
|
+
* const events = new Events('checkout', {
|
|
14
|
+
* subscribers: [
|
|
15
|
+
* new WebhookSubscriber({
|
|
16
|
+
* url: 'https://hooks.zapier.com/hooks/catch/...',
|
|
17
|
+
* headers: { 'X-API-Key': 'secret' }
|
|
18
|
+
* })
|
|
19
|
+
* ]
|
|
20
|
+
* });
|
|
21
|
+
*
|
|
22
|
+
* events.trackEvent('order.completed', { userId: '123', amount: 99.99 });
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
interface WebhookConfig {
|
|
27
|
+
/** Webhook URL */
|
|
28
|
+
url: string;
|
|
29
|
+
/** Optional headers (e.g., API keys) */
|
|
30
|
+
headers?: Record<string, string>;
|
|
31
|
+
/** Enable/disable the subscriber */
|
|
32
|
+
enabled?: boolean;
|
|
33
|
+
/** Retry failed requests (default: 3) */
|
|
34
|
+
maxRetries?: number;
|
|
35
|
+
}
|
|
36
|
+
declare class WebhookSubscriber implements EventSubscriber {
|
|
37
|
+
readonly name = "WebhookSubscriber";
|
|
38
|
+
readonly version = "1.0.0";
|
|
39
|
+
private config;
|
|
40
|
+
private enabled;
|
|
41
|
+
private pendingRequests;
|
|
42
|
+
constructor(config: WebhookConfig);
|
|
43
|
+
private send;
|
|
44
|
+
trackEvent(name: string, attributes?: EventAttributes): Promise<void>;
|
|
45
|
+
trackFunnelStep(funnelName: string, step: FunnelStatus, attributes?: EventAttributes): Promise<void>;
|
|
46
|
+
trackOutcome(operationName: string, outcome: OutcomeStatus, attributes?: EventAttributes): Promise<void>;
|
|
47
|
+
trackValue(name: string, value: number, attributes?: EventAttributes): Promise<void>;
|
|
48
|
+
private trackRequest;
|
|
49
|
+
/** Wait for all pending webhook requests to complete */
|
|
50
|
+
shutdown(): Promise<void>;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export { type WebhookConfig, WebhookSubscriber };
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { EventSubscriber, EventAttributes, FunnelStatus, OutcomeStatus } from 'autotel/event-subscriber';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Webhook Subscriber for autotel
|
|
5
|
+
*
|
|
6
|
+
* Send events to any webhook endpoint (custom integrations, Zapier, Make.com, etc.).
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```typescript
|
|
10
|
+
* import { Events } from 'autotel/events';
|
|
11
|
+
* import { WebhookSubscriber } from 'autotel-subscribers/webhook';
|
|
12
|
+
*
|
|
13
|
+
* const events = new Events('checkout', {
|
|
14
|
+
* subscribers: [
|
|
15
|
+
* new WebhookSubscriber({
|
|
16
|
+
* url: 'https://hooks.zapier.com/hooks/catch/...',
|
|
17
|
+
* headers: { 'X-API-Key': 'secret' }
|
|
18
|
+
* })
|
|
19
|
+
* ]
|
|
20
|
+
* });
|
|
21
|
+
*
|
|
22
|
+
* events.trackEvent('order.completed', { userId: '123', amount: 99.99 });
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
interface WebhookConfig {
|
|
27
|
+
/** Webhook URL */
|
|
28
|
+
url: string;
|
|
29
|
+
/** Optional headers (e.g., API keys) */
|
|
30
|
+
headers?: Record<string, string>;
|
|
31
|
+
/** Enable/disable the subscriber */
|
|
32
|
+
enabled?: boolean;
|
|
33
|
+
/** Retry failed requests (default: 3) */
|
|
34
|
+
maxRetries?: number;
|
|
35
|
+
}
|
|
36
|
+
declare class WebhookSubscriber implements EventSubscriber {
|
|
37
|
+
readonly name = "WebhookSubscriber";
|
|
38
|
+
readonly version = "1.0.0";
|
|
39
|
+
private config;
|
|
40
|
+
private enabled;
|
|
41
|
+
private pendingRequests;
|
|
42
|
+
constructor(config: WebhookConfig);
|
|
43
|
+
private send;
|
|
44
|
+
trackEvent(name: string, attributes?: EventAttributes): Promise<void>;
|
|
45
|
+
trackFunnelStep(funnelName: string, step: FunnelStatus, attributes?: EventAttributes): Promise<void>;
|
|
46
|
+
trackOutcome(operationName: string, outcome: OutcomeStatus, attributes?: EventAttributes): Promise<void>;
|
|
47
|
+
trackValue(name: string, value: number, attributes?: EventAttributes): Promise<void>;
|
|
48
|
+
private trackRequest;
|
|
49
|
+
/** Wait for all pending webhook requests to complete */
|
|
50
|
+
shutdown(): Promise<void>;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export { type WebhookConfig, WebhookSubscriber };
|