autotel-subscribers 33.0.0 → 34.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/dist/file.cjs +262 -0
- package/dist/file.cjs.map +1 -0
- package/dist/file.d.cts +54 -0
- package/dist/file.d.ts +54 -0
- package/dist/file.js +256 -0
- package/dist/file.js.map +1 -0
- package/dist/index.cjs +88 -42
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +89 -44
- package/dist/index.js.map +1 -1
- package/package.json +8 -3
- package/src/file.test.ts +87 -0
- package/src/file.ts +97 -0
- package/src/index.ts +1 -0
package/dist/file.cjs
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var promises = require('fs/promises');
|
|
4
|
+
var path = require('path');
|
|
5
|
+
|
|
6
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
7
|
+
|
|
8
|
+
var path__default = /*#__PURE__*/_interopDefault(path);
|
|
9
|
+
|
|
10
|
+
// src/file.ts
|
|
11
|
+
|
|
12
|
+
// src/event-subscriber-base.ts
|
|
13
|
+
var EventSubscriber = class {
|
|
14
|
+
/**
|
|
15
|
+
* Subscriber version (optional)
|
|
16
|
+
*/
|
|
17
|
+
version;
|
|
18
|
+
/**
|
|
19
|
+
* Enable/disable the subscriber (default: true)
|
|
20
|
+
*/
|
|
21
|
+
enabled = true;
|
|
22
|
+
/**
|
|
23
|
+
* Track pending requests for graceful shutdown
|
|
24
|
+
*/
|
|
25
|
+
pendingRequests = /* @__PURE__ */ new Set();
|
|
26
|
+
/**
|
|
27
|
+
* Optional: Handle errors
|
|
28
|
+
*
|
|
29
|
+
* Override this to customize error handling (logging, retries, etc.).
|
|
30
|
+
* Default behavior: log to console.error
|
|
31
|
+
*
|
|
32
|
+
* @param error - Error that occurred
|
|
33
|
+
* @param payload - Event payload that failed
|
|
34
|
+
*/
|
|
35
|
+
handleError(error, payload) {
|
|
36
|
+
console.error(
|
|
37
|
+
`[${this.name}] Failed to send ${payload.type}:`,
|
|
38
|
+
error,
|
|
39
|
+
payload
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Filter out undefined and null values from attributes
|
|
44
|
+
*
|
|
45
|
+
* This improves DX by allowing callers to pass objects with optional properties
|
|
46
|
+
* without having to manually filter them first.
|
|
47
|
+
*
|
|
48
|
+
* @param attributes - Input attributes (may contain undefined/null)
|
|
49
|
+
* @returns Filtered attributes with only defined values, or undefined if empty
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* ```typescript
|
|
53
|
+
* const filtered = this.filterAttributes({
|
|
54
|
+
* userId: user.id,
|
|
55
|
+
* email: user.email, // might be undefined
|
|
56
|
+
* plan: null, // will be filtered out
|
|
57
|
+
* });
|
|
58
|
+
* // Result: { userId: 'abc', email: 'test@example.com' } or { userId: 'abc' }
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
filterAttributes(attributes) {
|
|
62
|
+
if (!attributes) return void 0;
|
|
63
|
+
const filtered = {};
|
|
64
|
+
for (const [key, value] of Object.entries(attributes)) {
|
|
65
|
+
if (value !== void 0 && value !== null) {
|
|
66
|
+
filtered[key] = value;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return Object.keys(filtered).length > 0 ? filtered : void 0;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Track an event
|
|
73
|
+
*/
|
|
74
|
+
async trackEvent(name, attributes, options) {
|
|
75
|
+
if (!this.enabled) return;
|
|
76
|
+
const payload = {
|
|
77
|
+
type: "event",
|
|
78
|
+
name,
|
|
79
|
+
attributes,
|
|
80
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
81
|
+
autotel: options?.autotel,
|
|
82
|
+
schema: options?.schema
|
|
83
|
+
};
|
|
84
|
+
await this.send(payload);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Track a funnel step
|
|
88
|
+
*/
|
|
89
|
+
async trackFunnelStep(funnelName, step, attributes, options) {
|
|
90
|
+
if (!this.enabled) return;
|
|
91
|
+
const payload = {
|
|
92
|
+
type: "funnel",
|
|
93
|
+
name: `${funnelName}.${step}`,
|
|
94
|
+
funnel: funnelName,
|
|
95
|
+
step,
|
|
96
|
+
attributes,
|
|
97
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
98
|
+
autotel: options?.autotel
|
|
99
|
+
};
|
|
100
|
+
await this.send(payload);
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Track an outcome
|
|
104
|
+
*/
|
|
105
|
+
async trackOutcome(operationName, outcome, attributes, options) {
|
|
106
|
+
if (!this.enabled) return;
|
|
107
|
+
const payload = {
|
|
108
|
+
type: "outcome",
|
|
109
|
+
name: `${operationName}.${outcome}`,
|
|
110
|
+
operation: operationName,
|
|
111
|
+
outcome,
|
|
112
|
+
attributes,
|
|
113
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
114
|
+
autotel: options?.autotel
|
|
115
|
+
};
|
|
116
|
+
await this.send(payload);
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Track a value/metric
|
|
120
|
+
*/
|
|
121
|
+
async trackValue(name, value, attributes, options) {
|
|
122
|
+
if (!this.enabled) return;
|
|
123
|
+
const payload = {
|
|
124
|
+
type: "value",
|
|
125
|
+
name,
|
|
126
|
+
value,
|
|
127
|
+
attributes,
|
|
128
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
129
|
+
autotel: options?.autotel
|
|
130
|
+
};
|
|
131
|
+
await this.send(payload);
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Track funnel progression with custom step names
|
|
135
|
+
*
|
|
136
|
+
* Unlike trackFunnelStep which uses FunnelStatus enum values,
|
|
137
|
+
* this method allows any string as the step name for flexible funnel tracking.
|
|
138
|
+
*
|
|
139
|
+
* @param funnelName - Name of the funnel (e.g., "checkout", "onboarding")
|
|
140
|
+
* @param stepName - Custom step name (e.g., "cart_viewed", "payment_entered")
|
|
141
|
+
* @param stepNumber - Optional numeric position in the funnel
|
|
142
|
+
* @param attributes - Optional event attributes
|
|
143
|
+
* @param options - Optional tracking options including autotel context
|
|
144
|
+
*/
|
|
145
|
+
async trackFunnelProgression(funnelName, stepName, stepNumber, attributes, options) {
|
|
146
|
+
if (!this.enabled) return;
|
|
147
|
+
const payload = {
|
|
148
|
+
type: "funnel",
|
|
149
|
+
name: `${funnelName}.${stepName}`,
|
|
150
|
+
funnel: funnelName,
|
|
151
|
+
step: stepName,
|
|
152
|
+
stepName,
|
|
153
|
+
stepNumber,
|
|
154
|
+
attributes,
|
|
155
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
156
|
+
autotel: options?.autotel
|
|
157
|
+
};
|
|
158
|
+
await this.send(payload);
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Flush pending requests and clean up
|
|
162
|
+
*
|
|
163
|
+
* CRITICAL: Prevents race condition during shutdown
|
|
164
|
+
* 1. Disables subscriber to stop new events
|
|
165
|
+
* 2. Drains all pending requests (with retry logic)
|
|
166
|
+
* 3. Ensures flush guarantee
|
|
167
|
+
*
|
|
168
|
+
* Override this if you need custom cleanup logic (close connections, flush buffers, etc.),
|
|
169
|
+
* but ALWAYS call super.shutdown() first to drain pending requests.
|
|
170
|
+
*/
|
|
171
|
+
async shutdown() {
|
|
172
|
+
this.enabled = false;
|
|
173
|
+
const maxDrainAttempts = 10;
|
|
174
|
+
const drainIntervalMs = 50;
|
|
175
|
+
for (let attempt = 0; attempt < maxDrainAttempts; attempt++) {
|
|
176
|
+
if (this.pendingRequests.size === 0) {
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
await Promise.allSettled(this.pendingRequests);
|
|
180
|
+
if (this.pendingRequests.size > 0 && attempt < maxDrainAttempts - 1) {
|
|
181
|
+
await new Promise((resolve) => setTimeout(resolve, drainIntervalMs));
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
if (this.pendingRequests.size > 0) {
|
|
185
|
+
console.warn(
|
|
186
|
+
`[${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.`
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Internal: Send payload and track request
|
|
192
|
+
*/
|
|
193
|
+
async send(payload) {
|
|
194
|
+
const request = this.sendWithErrorHandling(payload);
|
|
195
|
+
this.pendingRequests.add(request);
|
|
196
|
+
void request.finally(() => {
|
|
197
|
+
this.pendingRequests.delete(request);
|
|
198
|
+
});
|
|
199
|
+
return request;
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Internal: Send with error handling
|
|
203
|
+
*/
|
|
204
|
+
async sendWithErrorHandling(payload) {
|
|
205
|
+
try {
|
|
206
|
+
await this.sendToDestination(payload);
|
|
207
|
+
} catch (error) {
|
|
208
|
+
this.handleError(error, payload);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
// src/file.ts
|
|
214
|
+
var FileSubscriber = class extends EventSubscriber {
|
|
215
|
+
name = "FileSubscriber";
|
|
216
|
+
version = "1.0.0";
|
|
217
|
+
filePath;
|
|
218
|
+
pretty;
|
|
219
|
+
ensureDir;
|
|
220
|
+
transform;
|
|
221
|
+
/** Serializes writes so concurrent events never interleave on disk. */
|
|
222
|
+
writeChain = Promise.resolve();
|
|
223
|
+
dirEnsured = false;
|
|
224
|
+
constructor(config) {
|
|
225
|
+
super();
|
|
226
|
+
this.filePath = config.path;
|
|
227
|
+
this.enabled = config.enabled ?? true;
|
|
228
|
+
this.pretty = config.pretty ?? false;
|
|
229
|
+
this.ensureDir = config.mkdir ?? true;
|
|
230
|
+
this.transform = config.transform;
|
|
231
|
+
}
|
|
232
|
+
async sendToDestination(payload) {
|
|
233
|
+
if (!this.enabled) return;
|
|
234
|
+
const record = this.transform ? this.transform(payload) : payload;
|
|
235
|
+
if (record === null) return;
|
|
236
|
+
const json = this.pretty ? JSON.stringify(record, null, 2) : JSON.stringify(record);
|
|
237
|
+
const line = `${json}
|
|
238
|
+
`;
|
|
239
|
+
const run = this.writeChain.then(() => this.write(line));
|
|
240
|
+
this.writeChain = run.catch(() => {
|
|
241
|
+
});
|
|
242
|
+
await run;
|
|
243
|
+
}
|
|
244
|
+
async write(line) {
|
|
245
|
+
if (this.ensureDir && !this.dirEnsured) {
|
|
246
|
+
const dir = path__default.default.dirname(this.filePath);
|
|
247
|
+
if (dir && dir !== ".") {
|
|
248
|
+
await promises.mkdir(dir, { recursive: true });
|
|
249
|
+
}
|
|
250
|
+
this.dirEnsured = true;
|
|
251
|
+
}
|
|
252
|
+
await promises.appendFile(this.filePath, line, "utf8");
|
|
253
|
+
}
|
|
254
|
+
async shutdown() {
|
|
255
|
+
await this.writeChain;
|
|
256
|
+
await super.shutdown();
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
exports.FileSubscriber = FileSubscriber;
|
|
261
|
+
//# sourceMappingURL=file.cjs.map
|
|
262
|
+
//# sourceMappingURL=file.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/event-subscriber-base.ts","../src/file.ts"],"names":["path","mkdir","appendFile"],"mappings":"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","file":"file.cjs","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 EventAttributesInput,\n FunnelStatus,\n OutcomeStatus,\n AutotelEventContext,\n EventTrackingOptions,\n} from 'autotel/event-subscriber';\n\n// Re-export types for convenience\nexport type { AutotelEventContext, EventTrackingOptions } from 'autotel/event-subscriber';\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 (from FunnelStatus enum) */\n step?: FunnelStatus | string;\n\n /** For funnel events: custom step name (from trackFunnelProgression) */\n stepName?: string;\n\n /** For funnel events: numeric position in funnel */\n stepNumber?: number;\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 * Autotel trace context (present when events.includeTraceContext is enabled)\n *\n * Subscribers should map these to platform-specific field names:\n * - PostHog: autotel.trace_id → $trace_id\n * - Mixpanel: autotel.trace_id → trace_id\n */\n autotel?: AutotelEventContext;\n /** Optional schema metadata for contract-aware subscribers. */\n schema?: EventTrackingOptions['schema'];\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 * Filter out undefined and null values from attributes\n *\n * This improves DX by allowing callers to pass objects with optional properties\n * without having to manually filter them first.\n *\n * @param attributes - Input attributes (may contain undefined/null)\n * @returns Filtered attributes with only defined values, or undefined if empty\n *\n * @example\n * ```typescript\n * const filtered = this.filterAttributes({\n * userId: user.id,\n * email: user.email, // might be undefined\n * plan: null, // will be filtered out\n * });\n * // Result: { userId: 'abc', email: 'test@example.com' } or { userId: 'abc' }\n * ```\n */\n protected filterAttributes(\n attributes?: EventAttributesInput,\n ): EventAttributes | undefined {\n if (!attributes) return undefined;\n\n const filtered: EventAttributes = {};\n for (const [key, value] of Object.entries(attributes)) {\n if (value !== undefined && value !== null) {\n filtered[key] = value;\n }\n }\n\n // Return undefined if no attributes remain after filtering\n return Object.keys(filtered).length > 0 ? filtered : undefined;\n }\n\n /**\n * Track an event\n */\n async trackEvent(\n name: string,\n attributes?: EventAttributes,\n options?: EventTrackingOptions,\n ): 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 autotel: options?.autotel,\n schema: options?.schema,\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 options?: EventTrackingOptions,\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 autotel: options?.autotel,\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 options?: EventTrackingOptions,\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 autotel: options?.autotel,\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 options?: EventTrackingOptions,\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 autotel: options?.autotel,\n };\n\n await this.send(payload);\n }\n\n /**\n * Track funnel progression with custom step names\n *\n * Unlike trackFunnelStep which uses FunnelStatus enum values,\n * this method allows any string as the step name for flexible funnel tracking.\n *\n * @param funnelName - Name of the funnel (e.g., \"checkout\", \"onboarding\")\n * @param stepName - Custom step name (e.g., \"cart_viewed\", \"payment_entered\")\n * @param stepNumber - Optional numeric position in the funnel\n * @param attributes - Optional event attributes\n * @param options - Optional tracking options including autotel context\n */\n async trackFunnelProgression(\n funnelName: string,\n stepName: string,\n stepNumber?: number,\n attributes?: EventAttributes,\n options?: EventTrackingOptions,\n ): Promise<void> {\n if (!this.enabled) return;\n\n const payload: EventPayload = {\n type: 'funnel',\n name: `${funnelName}.${stepName}`,\n funnel: funnelName,\n step: stepName,\n stepName,\n stepNumber,\n attributes,\n timestamp: new Date().toISOString(),\n autotel: options?.autotel,\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 {\n type EventAttributes,\n type EventAttributesInput,\n type FunnelStatus,\n type OutcomeStatus,\n} from 'autotel/event-subscriber';\n","/**\n * File subscriber for autotel.\n *\n * Appends each tracked event to a file as newline-delimited JSON (NDJSON).\n * Useful for AI agents, scripts, evals, and local debugging that want\n * structured events on disk without a hosted backend. Query the file with\n * `jq`, load it into a notebook, or feed it to an agent.\n *\n * @example\n * ```typescript\n * import { Event } from 'autotel/events';\n * import { FileSubscriber } from 'autotel-subscribers/file';\n *\n * const events = new Event('worker', {\n * subscribers: [new FileSubscriber({ path: './telemetry/events.ndjson' })],\n * });\n * ```\n */\n\nimport { appendFile, mkdir } from 'node:fs/promises';\nimport path from 'node:path';\nimport { EventSubscriber, type EventPayload } from './event-subscriber-base';\n\nexport interface FileSubscriberConfig {\n /** File path to append newline-delimited JSON events to. */\n path: string;\n /** Enable or disable the subscriber. Default `true`. */\n enabled?: boolean;\n /** Pretty-print each event as indented JSON instead of one line. Default `false`. */\n pretty?: boolean;\n /** Create parent directories if they do not exist. Default `true`. */\n mkdir?: boolean;\n /**\n * Transform a payload before writing. Return `null` to skip the event.\n * Defaults to writing the normalized payload unchanged.\n */\n transform?: (payload: EventPayload) => Record<string, unknown> | null;\n}\n\nexport class FileSubscriber extends EventSubscriber {\n readonly name = 'FileSubscriber';\n readonly version = '1.0.0';\n\n private readonly filePath: string;\n private readonly pretty: boolean;\n private readonly ensureDir: boolean;\n private readonly transform?: (\n payload: EventPayload,\n ) => Record<string, unknown> | null;\n\n /** Serializes writes so concurrent events never interleave on disk. */\n private writeChain: Promise<void> = Promise.resolve();\n private dirEnsured = false;\n\n constructor(config: FileSubscriberConfig) {\n super();\n this.filePath = config.path;\n this.enabled = config.enabled ?? true;\n this.pretty = config.pretty ?? false;\n this.ensureDir = config.mkdir ?? true;\n this.transform = config.transform;\n }\n\n protected async sendToDestination(payload: EventPayload): Promise<void> {\n if (!this.enabled) return;\n\n const record = this.transform ? this.transform(payload) : payload;\n if (record === null) return;\n\n const json = this.pretty\n ? JSON.stringify(record, null, 2)\n : JSON.stringify(record);\n const line = `${json}\\n`;\n\n const run = this.writeChain.then(() => this.write(line));\n // Keep the chain ordered and alive even if one write rejects; the failed\n // write still rejects `run` so the base class can report it.\n this.writeChain = run.catch(() => {});\n await run;\n }\n\n private async write(line: string): Promise<void> {\n if (this.ensureDir && !this.dirEnsured) {\n const dir = path.dirname(this.filePath);\n if (dir && dir !== '.') {\n await mkdir(dir, { recursive: true });\n }\n this.dirEnsured = true;\n }\n await appendFile(this.filePath, line, 'utf8');\n }\n\n override async shutdown(): Promise<void> {\n await this.writeChain;\n await super.shutdown();\n }\n}\n"]}
|
package/dist/file.d.cts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { E as EventSubscriber, a as EventPayload } from './event-subscriber-base-C5NlyV_O.cjs';
|
|
2
|
+
import 'autotel/event-subscriber';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* File subscriber for autotel.
|
|
6
|
+
*
|
|
7
|
+
* Appends each tracked event to a file as newline-delimited JSON (NDJSON).
|
|
8
|
+
* Useful for AI agents, scripts, evals, and local debugging that want
|
|
9
|
+
* structured events on disk without a hosted backend. Query the file with
|
|
10
|
+
* `jq`, load it into a notebook, or feed it to an agent.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```typescript
|
|
14
|
+
* import { Event } from 'autotel/events';
|
|
15
|
+
* import { FileSubscriber } from 'autotel-subscribers/file';
|
|
16
|
+
*
|
|
17
|
+
* const events = new Event('worker', {
|
|
18
|
+
* subscribers: [new FileSubscriber({ path: './telemetry/events.ndjson' })],
|
|
19
|
+
* });
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
interface FileSubscriberConfig {
|
|
24
|
+
/** File path to append newline-delimited JSON events to. */
|
|
25
|
+
path: string;
|
|
26
|
+
/** Enable or disable the subscriber. Default `true`. */
|
|
27
|
+
enabled?: boolean;
|
|
28
|
+
/** Pretty-print each event as indented JSON instead of one line. Default `false`. */
|
|
29
|
+
pretty?: boolean;
|
|
30
|
+
/** Create parent directories if they do not exist. Default `true`. */
|
|
31
|
+
mkdir?: boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Transform a payload before writing. Return `null` to skip the event.
|
|
34
|
+
* Defaults to writing the normalized payload unchanged.
|
|
35
|
+
*/
|
|
36
|
+
transform?: (payload: EventPayload) => Record<string, unknown> | null;
|
|
37
|
+
}
|
|
38
|
+
declare class FileSubscriber extends EventSubscriber {
|
|
39
|
+
readonly name = "FileSubscriber";
|
|
40
|
+
readonly version = "1.0.0";
|
|
41
|
+
private readonly filePath;
|
|
42
|
+
private readonly pretty;
|
|
43
|
+
private readonly ensureDir;
|
|
44
|
+
private readonly transform?;
|
|
45
|
+
/** Serializes writes so concurrent events never interleave on disk. */
|
|
46
|
+
private writeChain;
|
|
47
|
+
private dirEnsured;
|
|
48
|
+
constructor(config: FileSubscriberConfig);
|
|
49
|
+
protected sendToDestination(payload: EventPayload): Promise<void>;
|
|
50
|
+
private write;
|
|
51
|
+
shutdown(): Promise<void>;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export { FileSubscriber, type FileSubscriberConfig };
|
package/dist/file.d.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { E as EventSubscriber, a as EventPayload } from './event-subscriber-base-C5NlyV_O.js';
|
|
2
|
+
import 'autotel/event-subscriber';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* File subscriber for autotel.
|
|
6
|
+
*
|
|
7
|
+
* Appends each tracked event to a file as newline-delimited JSON (NDJSON).
|
|
8
|
+
* Useful for AI agents, scripts, evals, and local debugging that want
|
|
9
|
+
* structured events on disk without a hosted backend. Query the file with
|
|
10
|
+
* `jq`, load it into a notebook, or feed it to an agent.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```typescript
|
|
14
|
+
* import { Event } from 'autotel/events';
|
|
15
|
+
* import { FileSubscriber } from 'autotel-subscribers/file';
|
|
16
|
+
*
|
|
17
|
+
* const events = new Event('worker', {
|
|
18
|
+
* subscribers: [new FileSubscriber({ path: './telemetry/events.ndjson' })],
|
|
19
|
+
* });
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
interface FileSubscriberConfig {
|
|
24
|
+
/** File path to append newline-delimited JSON events to. */
|
|
25
|
+
path: string;
|
|
26
|
+
/** Enable or disable the subscriber. Default `true`. */
|
|
27
|
+
enabled?: boolean;
|
|
28
|
+
/** Pretty-print each event as indented JSON instead of one line. Default `false`. */
|
|
29
|
+
pretty?: boolean;
|
|
30
|
+
/** Create parent directories if they do not exist. Default `true`. */
|
|
31
|
+
mkdir?: boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Transform a payload before writing. Return `null` to skip the event.
|
|
34
|
+
* Defaults to writing the normalized payload unchanged.
|
|
35
|
+
*/
|
|
36
|
+
transform?: (payload: EventPayload) => Record<string, unknown> | null;
|
|
37
|
+
}
|
|
38
|
+
declare class FileSubscriber extends EventSubscriber {
|
|
39
|
+
readonly name = "FileSubscriber";
|
|
40
|
+
readonly version = "1.0.0";
|
|
41
|
+
private readonly filePath;
|
|
42
|
+
private readonly pretty;
|
|
43
|
+
private readonly ensureDir;
|
|
44
|
+
private readonly transform?;
|
|
45
|
+
/** Serializes writes so concurrent events never interleave on disk. */
|
|
46
|
+
private writeChain;
|
|
47
|
+
private dirEnsured;
|
|
48
|
+
constructor(config: FileSubscriberConfig);
|
|
49
|
+
protected sendToDestination(payload: EventPayload): Promise<void>;
|
|
50
|
+
private write;
|
|
51
|
+
shutdown(): Promise<void>;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export { FileSubscriber, type FileSubscriberConfig };
|
package/dist/file.js
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { mkdir, appendFile } from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
// src/file.ts
|
|
5
|
+
|
|
6
|
+
// src/event-subscriber-base.ts
|
|
7
|
+
var EventSubscriber = class {
|
|
8
|
+
/**
|
|
9
|
+
* Subscriber version (optional)
|
|
10
|
+
*/
|
|
11
|
+
version;
|
|
12
|
+
/**
|
|
13
|
+
* Enable/disable the subscriber (default: true)
|
|
14
|
+
*/
|
|
15
|
+
enabled = true;
|
|
16
|
+
/**
|
|
17
|
+
* Track pending requests for graceful shutdown
|
|
18
|
+
*/
|
|
19
|
+
pendingRequests = /* @__PURE__ */ new Set();
|
|
20
|
+
/**
|
|
21
|
+
* Optional: Handle errors
|
|
22
|
+
*
|
|
23
|
+
* Override this to customize error handling (logging, retries, etc.).
|
|
24
|
+
* Default behavior: log to console.error
|
|
25
|
+
*
|
|
26
|
+
* @param error - Error that occurred
|
|
27
|
+
* @param payload - Event payload that failed
|
|
28
|
+
*/
|
|
29
|
+
handleError(error, payload) {
|
|
30
|
+
console.error(
|
|
31
|
+
`[${this.name}] Failed to send ${payload.type}:`,
|
|
32
|
+
error,
|
|
33
|
+
payload
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Filter out undefined and null values from attributes
|
|
38
|
+
*
|
|
39
|
+
* This improves DX by allowing callers to pass objects with optional properties
|
|
40
|
+
* without having to manually filter them first.
|
|
41
|
+
*
|
|
42
|
+
* @param attributes - Input attributes (may contain undefined/null)
|
|
43
|
+
* @returns Filtered attributes with only defined values, or undefined if empty
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* ```typescript
|
|
47
|
+
* const filtered = this.filterAttributes({
|
|
48
|
+
* userId: user.id,
|
|
49
|
+
* email: user.email, // might be undefined
|
|
50
|
+
* plan: null, // will be filtered out
|
|
51
|
+
* });
|
|
52
|
+
* // Result: { userId: 'abc', email: 'test@example.com' } or { userId: 'abc' }
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
filterAttributes(attributes) {
|
|
56
|
+
if (!attributes) return void 0;
|
|
57
|
+
const filtered = {};
|
|
58
|
+
for (const [key, value] of Object.entries(attributes)) {
|
|
59
|
+
if (value !== void 0 && value !== null) {
|
|
60
|
+
filtered[key] = value;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return Object.keys(filtered).length > 0 ? filtered : void 0;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Track an event
|
|
67
|
+
*/
|
|
68
|
+
async trackEvent(name, attributes, options) {
|
|
69
|
+
if (!this.enabled) return;
|
|
70
|
+
const payload = {
|
|
71
|
+
type: "event",
|
|
72
|
+
name,
|
|
73
|
+
attributes,
|
|
74
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
75
|
+
autotel: options?.autotel,
|
|
76
|
+
schema: options?.schema
|
|
77
|
+
};
|
|
78
|
+
await this.send(payload);
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Track a funnel step
|
|
82
|
+
*/
|
|
83
|
+
async trackFunnelStep(funnelName, step, attributes, options) {
|
|
84
|
+
if (!this.enabled) return;
|
|
85
|
+
const payload = {
|
|
86
|
+
type: "funnel",
|
|
87
|
+
name: `${funnelName}.${step}`,
|
|
88
|
+
funnel: funnelName,
|
|
89
|
+
step,
|
|
90
|
+
attributes,
|
|
91
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
92
|
+
autotel: options?.autotel
|
|
93
|
+
};
|
|
94
|
+
await this.send(payload);
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Track an outcome
|
|
98
|
+
*/
|
|
99
|
+
async trackOutcome(operationName, outcome, attributes, options) {
|
|
100
|
+
if (!this.enabled) return;
|
|
101
|
+
const payload = {
|
|
102
|
+
type: "outcome",
|
|
103
|
+
name: `${operationName}.${outcome}`,
|
|
104
|
+
operation: operationName,
|
|
105
|
+
outcome,
|
|
106
|
+
attributes,
|
|
107
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
108
|
+
autotel: options?.autotel
|
|
109
|
+
};
|
|
110
|
+
await this.send(payload);
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Track a value/metric
|
|
114
|
+
*/
|
|
115
|
+
async trackValue(name, value, attributes, options) {
|
|
116
|
+
if (!this.enabled) return;
|
|
117
|
+
const payload = {
|
|
118
|
+
type: "value",
|
|
119
|
+
name,
|
|
120
|
+
value,
|
|
121
|
+
attributes,
|
|
122
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
123
|
+
autotel: options?.autotel
|
|
124
|
+
};
|
|
125
|
+
await this.send(payload);
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Track funnel progression with custom step names
|
|
129
|
+
*
|
|
130
|
+
* Unlike trackFunnelStep which uses FunnelStatus enum values,
|
|
131
|
+
* this method allows any string as the step name for flexible funnel tracking.
|
|
132
|
+
*
|
|
133
|
+
* @param funnelName - Name of the funnel (e.g., "checkout", "onboarding")
|
|
134
|
+
* @param stepName - Custom step name (e.g., "cart_viewed", "payment_entered")
|
|
135
|
+
* @param stepNumber - Optional numeric position in the funnel
|
|
136
|
+
* @param attributes - Optional event attributes
|
|
137
|
+
* @param options - Optional tracking options including autotel context
|
|
138
|
+
*/
|
|
139
|
+
async trackFunnelProgression(funnelName, stepName, stepNumber, attributes, options) {
|
|
140
|
+
if (!this.enabled) return;
|
|
141
|
+
const payload = {
|
|
142
|
+
type: "funnel",
|
|
143
|
+
name: `${funnelName}.${stepName}`,
|
|
144
|
+
funnel: funnelName,
|
|
145
|
+
step: stepName,
|
|
146
|
+
stepName,
|
|
147
|
+
stepNumber,
|
|
148
|
+
attributes,
|
|
149
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
150
|
+
autotel: options?.autotel
|
|
151
|
+
};
|
|
152
|
+
await this.send(payload);
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Flush pending requests and clean up
|
|
156
|
+
*
|
|
157
|
+
* CRITICAL: Prevents race condition during shutdown
|
|
158
|
+
* 1. Disables subscriber to stop new events
|
|
159
|
+
* 2. Drains all pending requests (with retry logic)
|
|
160
|
+
* 3. Ensures flush guarantee
|
|
161
|
+
*
|
|
162
|
+
* Override this if you need custom cleanup logic (close connections, flush buffers, etc.),
|
|
163
|
+
* but ALWAYS call super.shutdown() first to drain pending requests.
|
|
164
|
+
*/
|
|
165
|
+
async shutdown() {
|
|
166
|
+
this.enabled = false;
|
|
167
|
+
const maxDrainAttempts = 10;
|
|
168
|
+
const drainIntervalMs = 50;
|
|
169
|
+
for (let attempt = 0; attempt < maxDrainAttempts; attempt++) {
|
|
170
|
+
if (this.pendingRequests.size === 0) {
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
173
|
+
await Promise.allSettled(this.pendingRequests);
|
|
174
|
+
if (this.pendingRequests.size > 0 && attempt < maxDrainAttempts - 1) {
|
|
175
|
+
await new Promise((resolve) => setTimeout(resolve, drainIntervalMs));
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if (this.pendingRequests.size > 0) {
|
|
179
|
+
console.warn(
|
|
180
|
+
`[${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.`
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Internal: Send payload and track request
|
|
186
|
+
*/
|
|
187
|
+
async send(payload) {
|
|
188
|
+
const request = this.sendWithErrorHandling(payload);
|
|
189
|
+
this.pendingRequests.add(request);
|
|
190
|
+
void request.finally(() => {
|
|
191
|
+
this.pendingRequests.delete(request);
|
|
192
|
+
});
|
|
193
|
+
return request;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Internal: Send with error handling
|
|
197
|
+
*/
|
|
198
|
+
async sendWithErrorHandling(payload) {
|
|
199
|
+
try {
|
|
200
|
+
await this.sendToDestination(payload);
|
|
201
|
+
} catch (error) {
|
|
202
|
+
this.handleError(error, payload);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
// src/file.ts
|
|
208
|
+
var FileSubscriber = class extends EventSubscriber {
|
|
209
|
+
name = "FileSubscriber";
|
|
210
|
+
version = "1.0.0";
|
|
211
|
+
filePath;
|
|
212
|
+
pretty;
|
|
213
|
+
ensureDir;
|
|
214
|
+
transform;
|
|
215
|
+
/** Serializes writes so concurrent events never interleave on disk. */
|
|
216
|
+
writeChain = Promise.resolve();
|
|
217
|
+
dirEnsured = false;
|
|
218
|
+
constructor(config) {
|
|
219
|
+
super();
|
|
220
|
+
this.filePath = config.path;
|
|
221
|
+
this.enabled = config.enabled ?? true;
|
|
222
|
+
this.pretty = config.pretty ?? false;
|
|
223
|
+
this.ensureDir = config.mkdir ?? true;
|
|
224
|
+
this.transform = config.transform;
|
|
225
|
+
}
|
|
226
|
+
async sendToDestination(payload) {
|
|
227
|
+
if (!this.enabled) return;
|
|
228
|
+
const record = this.transform ? this.transform(payload) : payload;
|
|
229
|
+
if (record === null) return;
|
|
230
|
+
const json = this.pretty ? JSON.stringify(record, null, 2) : JSON.stringify(record);
|
|
231
|
+
const line = `${json}
|
|
232
|
+
`;
|
|
233
|
+
const run = this.writeChain.then(() => this.write(line));
|
|
234
|
+
this.writeChain = run.catch(() => {
|
|
235
|
+
});
|
|
236
|
+
await run;
|
|
237
|
+
}
|
|
238
|
+
async write(line) {
|
|
239
|
+
if (this.ensureDir && !this.dirEnsured) {
|
|
240
|
+
const dir = path.dirname(this.filePath);
|
|
241
|
+
if (dir && dir !== ".") {
|
|
242
|
+
await mkdir(dir, { recursive: true });
|
|
243
|
+
}
|
|
244
|
+
this.dirEnsured = true;
|
|
245
|
+
}
|
|
246
|
+
await appendFile(this.filePath, line, "utf8");
|
|
247
|
+
}
|
|
248
|
+
async shutdown() {
|
|
249
|
+
await this.writeChain;
|
|
250
|
+
await super.shutdown();
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
export { FileSubscriber };
|
|
255
|
+
//# sourceMappingURL=file.js.map
|
|
256
|
+
//# sourceMappingURL=file.js.map
|