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.
Files changed (87) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +669 -0
  3. package/dist/amplitude.cjs +2486 -0
  4. package/dist/amplitude.cjs.map +1 -0
  5. package/dist/amplitude.d.cts +49 -0
  6. package/dist/amplitude.d.ts +49 -0
  7. package/dist/amplitude.js +2463 -0
  8. package/dist/amplitude.js.map +1 -0
  9. package/dist/event-subscriber-base-CnF3V56W.d.cts +182 -0
  10. package/dist/event-subscriber-base-CnF3V56W.d.ts +182 -0
  11. package/dist/factories.cjs +16660 -0
  12. package/dist/factories.cjs.map +1 -0
  13. package/dist/factories.d.cts +304 -0
  14. package/dist/factories.d.ts +304 -0
  15. package/dist/factories.js +16624 -0
  16. package/dist/factories.js.map +1 -0
  17. package/dist/index.cjs +16575 -0
  18. package/dist/index.cjs.map +1 -0
  19. package/dist/index.d.cts +179 -0
  20. package/dist/index.d.ts +179 -0
  21. package/dist/index.js +16539 -0
  22. package/dist/index.js.map +1 -0
  23. package/dist/middleware.cjs +220 -0
  24. package/dist/middleware.cjs.map +1 -0
  25. package/dist/middleware.d.cts +227 -0
  26. package/dist/middleware.d.ts +227 -0
  27. package/dist/middleware.js +208 -0
  28. package/dist/middleware.js.map +1 -0
  29. package/dist/mixpanel.cjs +2940 -0
  30. package/dist/mixpanel.cjs.map +1 -0
  31. package/dist/mixpanel.d.cts +47 -0
  32. package/dist/mixpanel.d.ts +47 -0
  33. package/dist/mixpanel.js +2932 -0
  34. package/dist/mixpanel.js.map +1 -0
  35. package/dist/posthog.cjs +4115 -0
  36. package/dist/posthog.cjs.map +1 -0
  37. package/dist/posthog.d.cts +299 -0
  38. package/dist/posthog.d.ts +299 -0
  39. package/dist/posthog.js +4113 -0
  40. package/dist/posthog.js.map +1 -0
  41. package/dist/segment.cjs +6822 -0
  42. package/dist/segment.cjs.map +1 -0
  43. package/dist/segment.d.cts +49 -0
  44. package/dist/segment.d.ts +49 -0
  45. package/dist/segment.js +6794 -0
  46. package/dist/segment.js.map +1 -0
  47. package/dist/slack.cjs +368 -0
  48. package/dist/slack.cjs.map +1 -0
  49. package/dist/slack.d.cts +126 -0
  50. package/dist/slack.d.ts +126 -0
  51. package/dist/slack.js +366 -0
  52. package/dist/slack.js.map +1 -0
  53. package/dist/webhook.cjs +100 -0
  54. package/dist/webhook.cjs.map +1 -0
  55. package/dist/webhook.d.cts +53 -0
  56. package/dist/webhook.d.ts +53 -0
  57. package/dist/webhook.js +98 -0
  58. package/dist/webhook.js.map +1 -0
  59. package/examples/quickstart-custom-subscriber.ts +144 -0
  60. package/examples/subscriber-bigquery.ts +219 -0
  61. package/examples/subscriber-databricks.ts +280 -0
  62. package/examples/subscriber-kafka.ts +326 -0
  63. package/examples/subscriber-kinesis.ts +307 -0
  64. package/examples/subscriber-posthog.ts +421 -0
  65. package/examples/subscriber-pubsub.ts +336 -0
  66. package/examples/subscriber-snowflake.ts +232 -0
  67. package/package.json +141 -0
  68. package/src/amplitude.test.ts +231 -0
  69. package/src/amplitude.ts +148 -0
  70. package/src/event-subscriber-base.ts +325 -0
  71. package/src/factories.ts +197 -0
  72. package/src/index.ts +50 -0
  73. package/src/middleware.ts +489 -0
  74. package/src/mixpanel.test.ts +194 -0
  75. package/src/mixpanel.ts +134 -0
  76. package/src/mock-event-subscriber.ts +333 -0
  77. package/src/posthog.test.ts +629 -0
  78. package/src/posthog.ts +530 -0
  79. package/src/segment.test.ts +228 -0
  80. package/src/segment.ts +148 -0
  81. package/src/slack.ts +383 -0
  82. package/src/streaming-event-subscriber.ts +323 -0
  83. package/src/testing/index.ts +37 -0
  84. package/src/testing/mock-webhook-server.ts +242 -0
  85. package/src/testing/subscriber-test-harness.ts +365 -0
  86. package/src/webhook.test.ts +264 -0
  87. package/src/webhook.ts +158 -0
package/src/posthog.ts ADDED
@@ -0,0 +1,530 @@
1
+ /**
2
+ * PostHog Subscriber for autotel
3
+ *
4
+ * Send events to PostHog for product events, feature flags, and A/B testing.
5
+ *
6
+ * @example Basic usage
7
+ * ```typescript
8
+ * import { Events } from 'autotel/events';
9
+ * import { PostHogSubscriber } from 'autotel-subscribers/posthog';
10
+ *
11
+ * const events = new Events('checkout', {
12
+ * subscribers: [
13
+ * new PostHogSubscriber({
14
+ * apiKey: process.env.POSTHOG_API_KEY!,
15
+ * host: 'https://us.i.posthog.com' // optional, defaults to US cloud
16
+ * })
17
+ * ]
18
+ * });
19
+ *
20
+ * // Events go to both OpenTelemetry AND PostHog
21
+ * events.trackEvent('order.completed', { userId: '123', amount: 99.99 });
22
+ * ```
23
+ *
24
+ * @example Feature flags
25
+ * ```typescript
26
+ * const subscriber = new PostHogSubscriber({ apiKey: 'phc_...' });
27
+ *
28
+ * // Check if feature is enabled
29
+ * const isEnabled = await subscriber.isFeatureEnabled('new-checkout', 'user-123');
30
+ *
31
+ * // Get feature flag value (string, boolean, number)
32
+ * const variant = await subscriber.getFeatureFlag('experiment-variant', 'user-123');
33
+ *
34
+ * // Get all flags for a user
35
+ * const allFlags = await subscriber.getAllFlags('user-123');
36
+ * ```
37
+ *
38
+ * @example Person and group events
39
+ * ```typescript
40
+ * // Identify user and set properties
41
+ * await subscriber.identify('user-123', {
42
+ * email: 'user@example.com',
43
+ * plan: 'premium'
44
+ * });
45
+ *
46
+ * // Identify a group (e.g., organization)
47
+ * await subscriber.groupIdentify('company', 'acme-corp', {
48
+ * industry: 'saas',
49
+ * employees: 500
50
+ * });
51
+ * ```
52
+ *
53
+ * @example Serverless configuration
54
+ * ```typescript
55
+ * // Optimized for AWS Lambda / Vercel Functions
56
+ * const subscriber = new PostHogSubscriber({
57
+ * apiKey: 'phc_...',
58
+ * flushAt: 1, // Send immediately (don't batch)
59
+ * flushInterval: 0, // Disable interval-based flushing
60
+ * });
61
+ * ```
62
+ *
63
+ * @example Custom PostHog client
64
+ * ```typescript
65
+ * import { PostHog } from 'posthog-node';
66
+ *
67
+ * const customClient = new PostHog('phc_...', {
68
+ * host: 'https://eu.i.posthog.com',
69
+ * // ... other PostHog options
70
+ * });
71
+ *
72
+ * const subscriber = new PostHogSubscriber({
73
+ * client: customClient
74
+ * });
75
+ * ```
76
+ *
77
+ * @example Error handling
78
+ * ```typescript
79
+ * const subscriber = new PostHogSubscriber({
80
+ * apiKey: 'phc_...',
81
+ * onError: (error) => {
82
+ * console.error('PostHog error:', error);
83
+ * // Send to error tracking service
84
+ * }
85
+ * });
86
+ * ```
87
+ */
88
+
89
+ import type { EventAttributes } from 'autotel/event-subscriber';
90
+ import { EventSubscriber, type EventPayload } from './event-subscriber-base';
91
+
92
+ // Type-only import to avoid runtime dependency
93
+ import type { PostHog } from 'posthog-node';
94
+
95
+ export interface PostHogConfig {
96
+ /** PostHog API key (starts with phc_) - required if not providing custom client */
97
+ apiKey?: string;
98
+
99
+ /** PostHog host (defaults to US cloud) */
100
+ host?: string;
101
+
102
+ /** Enable/disable the subscriber */
103
+ enabled?: boolean;
104
+
105
+ /** Custom PostHog client instance (bypasses apiKey/host) */
106
+ client?: PostHog;
107
+
108
+ // Serverless optimizations
109
+ /** Flush batch when it reaches this size (default: 20, set to 1 for immediate send) */
110
+ flushAt?: number;
111
+
112
+ /** Flush interval in milliseconds (default: 10000, set to 0 to disable) */
113
+ flushInterval?: number;
114
+
115
+ // Performance tuning
116
+ /** Disable geoip lookup to reduce request size (default: false) */
117
+ disableGeoip?: boolean;
118
+
119
+ /** Request timeout in milliseconds (default: 10000) */
120
+ requestTimeout?: number;
121
+
122
+ /** Send feature flag evaluation events (default: true) */
123
+ sendFeatureFlags?: boolean;
124
+
125
+ // Error handling
126
+ /** Error callback for debugging and monitoring */
127
+ onError?: (error: Error) => void;
128
+
129
+ /** Enable debug logging (default: false) */
130
+ debug?: boolean;
131
+ }
132
+
133
+ /**
134
+ * PostHog feature flag options
135
+ */
136
+ export interface FeatureFlagOptions {
137
+ /** Group context for group-based feature flags */
138
+ groups?: Record<string, string | number>;
139
+
140
+ /** Group properties for feature flag evaluation */
141
+ groupProperties?: Record<string, Record<string, any>>;
142
+
143
+ /** Person properties for feature flag evaluation */
144
+ personProperties?: Record<string, any>;
145
+
146
+ /** Only evaluate locally, don't send $feature_flag_called event */
147
+ onlyEvaluateLocally?: boolean;
148
+
149
+ /** Send feature flag events even if disabled globally */
150
+ sendFeatureFlagEvents?: boolean;
151
+ }
152
+
153
+ /**
154
+ * Person properties for identify calls
155
+ */
156
+ export interface PersonProperties {
157
+ /** Set properties (will update existing values) */
158
+ $set?: Record<string, any>;
159
+
160
+ /** Set properties only if they don't exist */
161
+ $set_once?: Record<string, any>;
162
+
163
+ /** Any custom properties */
164
+ [key: string]: any;
165
+ }
166
+
167
+ export class PostHogSubscriber extends EventSubscriber {
168
+ readonly name = 'PostHogSubscriber';
169
+ readonly version = '2.0.0';
170
+
171
+ private posthog: PostHog | null = null;
172
+ private config: PostHogConfig;
173
+ private initPromise: Promise<void> | null = null;
174
+
175
+ constructor(config: PostHogConfig) {
176
+ super();
177
+
178
+ if (!config.apiKey && !config.client) {
179
+ throw new Error('PostHogSubscriber requires either apiKey or client to be provided');
180
+ }
181
+
182
+ this.enabled = config.enabled ?? true;
183
+ this.config = config;
184
+
185
+ if (this.enabled) {
186
+ // Start initialization immediately but don't block constructor
187
+ this.initPromise = this.initialize();
188
+ }
189
+ }
190
+
191
+ private async initialize(): Promise<void> {
192
+ try {
193
+ // Use custom client if provided
194
+ if (this.config.client) {
195
+ this.posthog = this.config.client;
196
+ this.setupErrorHandling();
197
+ return;
198
+ }
199
+
200
+ // Dynamic import to avoid adding posthog-node as a hard dependency
201
+ const { PostHog } = await import('posthog-node');
202
+
203
+ this.posthog = new PostHog(this.config.apiKey!, {
204
+ host: this.config.host || 'https://us.i.posthog.com',
205
+ flushAt: this.config.flushAt,
206
+ flushInterval: this.config.flushInterval,
207
+ requestTimeout: this.config.requestTimeout,
208
+ disableGeoip: this.config.disableGeoip,
209
+ sendFeatureFlagEvent: this.config.sendFeatureFlags,
210
+ });
211
+
212
+ this.setupErrorHandling();
213
+ } catch (error) {
214
+ console.error(
215
+ 'PostHog subscriber failed to initialize. Install posthog-node: pnpm add posthog-node',
216
+ error,
217
+ );
218
+ this.enabled = false;
219
+ this.config.onError?.(error as Error);
220
+ }
221
+ }
222
+
223
+ private setupErrorHandling(): void {
224
+ if (this.config.debug) {
225
+ this.posthog?.debug();
226
+ }
227
+
228
+ if (this.config.onError && this.posthog?.on) {
229
+ this.posthog.on('error', this.config.onError);
230
+ }
231
+ }
232
+
233
+ private async ensureInitialized(): Promise<void> {
234
+ if (this.initPromise) {
235
+ await this.initPromise;
236
+ this.initPromise = null;
237
+ }
238
+ }
239
+
240
+ private extractDistinctId(attributes?: EventAttributes): string {
241
+ return (attributes?.userId || attributes?.user_id || 'anonymous') as string;
242
+ }
243
+
244
+ /**
245
+ * Send payload to PostHog
246
+ */
247
+ protected async sendToDestination(payload: EventPayload): Promise<void> {
248
+ await this.ensureInitialized();
249
+
250
+ // Build properties object, including value if present
251
+ let properties: any = payload.attributes;
252
+ if (payload.value !== undefined) {
253
+ properties = { ...payload.attributes, value: payload.value };
254
+ }
255
+
256
+ // Build PostHog capture payload
257
+ const capturePayload: any = {
258
+ distinctId: this.extractDistinctId(payload.attributes),
259
+ event: payload.name,
260
+ properties,
261
+ };
262
+
263
+ // Add groups if present in attributes
264
+ if (payload.attributes?.groups) {
265
+ capturePayload.groups = payload.attributes.groups;
266
+ }
267
+
268
+ this.posthog?.capture(capturePayload);
269
+ }
270
+
271
+ // Feature Flag Methods
272
+
273
+ /**
274
+ * Check if a feature flag is enabled for a user
275
+ *
276
+ * @param flagKey - Feature flag key
277
+ * @param distinctId - User ID or anonymous ID
278
+ * @param options - Feature flag evaluation options
279
+ * @returns true if enabled, false otherwise
280
+ *
281
+ * @example
282
+ * ```typescript
283
+ * const isEnabled = await subscriber.isFeatureEnabled('new-checkout', 'user-123');
284
+ *
285
+ * // With groups
286
+ * const isEnabled = await subscriber.isFeatureEnabled('beta-features', 'user-123', {
287
+ * groups: { company: 'acme-corp' }
288
+ * });
289
+ * ```
290
+ */
291
+ async isFeatureEnabled(
292
+ flagKey: string,
293
+ distinctId: string,
294
+ options?: FeatureFlagOptions,
295
+ ): Promise<boolean> {
296
+ if (!this.enabled) return false;
297
+ await this.ensureInitialized();
298
+
299
+ try {
300
+ return await this.posthog?.isFeatureEnabled(flagKey, distinctId, options as any) ?? false;
301
+ } catch (error) {
302
+ this.config.onError?.(error as Error);
303
+ return false;
304
+ }
305
+ }
306
+
307
+ /**
308
+ * Get feature flag value for a user
309
+ *
310
+ * @param flagKey - Feature flag key
311
+ * @param distinctId - User ID or anonymous ID
312
+ * @param options - Feature flag evaluation options
313
+ * @returns Flag value (string, boolean, or undefined)
314
+ *
315
+ * @example
316
+ * ```typescript
317
+ * const variant = await subscriber.getFeatureFlag('experiment-variant', 'user-123');
318
+ * // Returns: 'control' | 'test' | 'test-2' | undefined
319
+ *
320
+ * // With person properties
321
+ * const variant = await subscriber.getFeatureFlag('premium-feature', 'user-123', {
322
+ * personProperties: { plan: 'premium' }
323
+ * });
324
+ * ```
325
+ */
326
+ async getFeatureFlag(
327
+ flagKey: string,
328
+ distinctId: string,
329
+ options?: FeatureFlagOptions,
330
+ ): Promise<string | boolean | undefined> {
331
+ if (!this.enabled) return undefined;
332
+ await this.ensureInitialized();
333
+
334
+ try {
335
+ return await this.posthog?.getFeatureFlag(flagKey, distinctId, options as any);
336
+ } catch (error) {
337
+ this.config.onError?.(error as Error);
338
+ return undefined;
339
+ }
340
+ }
341
+
342
+ /**
343
+ * Get all feature flags for a user
344
+ *
345
+ * @param distinctId - User ID or anonymous ID
346
+ * @param options - Feature flag evaluation options
347
+ * @returns Object mapping flag keys to their values
348
+ *
349
+ * @example
350
+ * ```typescript
351
+ * const flags = await subscriber.getAllFlags('user-123');
352
+ * // Returns: { 'new-checkout': true, 'experiment-variant': 'test', ... }
353
+ * ```
354
+ */
355
+ async getAllFlags(
356
+ distinctId: string,
357
+ options?: FeatureFlagOptions,
358
+ ): Promise<Record<string, string | number | boolean>> {
359
+ if (!this.enabled) return {};
360
+ await this.ensureInitialized();
361
+
362
+ try {
363
+ const flags = await this.posthog?.getAllFlags(distinctId, options as any);
364
+ return flags ?? {};
365
+ } catch (error) {
366
+ this.config.onError?.(error as Error);
367
+ return {};
368
+ }
369
+ }
370
+
371
+ /**
372
+ * Reload feature flags from PostHog server
373
+ *
374
+ * Call this to refresh feature flag definitions without restarting.
375
+ *
376
+ * @example
377
+ * ```typescript
378
+ * await subscriber.reloadFeatureFlags();
379
+ * ```
380
+ */
381
+ async reloadFeatureFlags(): Promise<void> {
382
+ if (!this.enabled) return;
383
+ await this.ensureInitialized();
384
+
385
+ try {
386
+ await this.posthog?.reloadFeatureFlags();
387
+ } catch (error) {
388
+ this.config.onError?.(error as Error);
389
+ }
390
+ }
391
+
392
+ // Person and Group Events
393
+
394
+ /**
395
+ * Identify a user and set their properties
396
+ *
397
+ * @param distinctId - User ID
398
+ * @param properties - Person properties ($set, $set_once, or custom properties)
399
+ *
400
+ * @example
401
+ * ```typescript
402
+ * // Set properties (will update existing values)
403
+ * await subscriber.identify('user-123', {
404
+ * $set: {
405
+ * email: 'user@example.com',
406
+ * plan: 'premium'
407
+ * }
408
+ * });
409
+ *
410
+ * // Set properties only once (won't update if already exists)
411
+ * await subscriber.identify('user-123', {
412
+ * $set_once: {
413
+ * signup_date: '2025-01-17'
414
+ * }
415
+ * });
416
+ * ```
417
+ */
418
+ async identify(distinctId: string, properties?: PersonProperties): Promise<void> {
419
+ if (!this.enabled) return;
420
+ await this.ensureInitialized();
421
+
422
+ try {
423
+ this.posthog?.identify({
424
+ distinctId,
425
+ properties,
426
+ });
427
+ } catch (error) {
428
+ this.config.onError?.(error as Error);
429
+ }
430
+ }
431
+
432
+ /**
433
+ * Identify a group and set its properties
434
+ *
435
+ * Groups are useful for B2B SaaS to track organizations, teams, or accounts.
436
+ *
437
+ * @param groupType - Type of group (e.g., 'company', 'organization', 'team')
438
+ * @param groupKey - Unique identifier for the group
439
+ * @param properties - Group properties
440
+ *
441
+ * @example
442
+ * ```typescript
443
+ * await subscriber.groupIdentify('company', 'acme-corp', {
444
+ * $set: {
445
+ * name: 'Acme Corporation',
446
+ * industry: 'saas',
447
+ * employees: 500,
448
+ * plan: 'enterprise'
449
+ * }
450
+ * });
451
+ * ```
452
+ */
453
+ async groupIdentify(
454
+ groupType: string,
455
+ groupKey: string | number,
456
+ properties?: Record<string, any>,
457
+ ): Promise<void> {
458
+ if (!this.enabled) return;
459
+ await this.ensureInitialized();
460
+
461
+ try {
462
+ this.posthog?.groupIdentify({
463
+ groupType,
464
+ groupKey: String(groupKey), // Convert to string for PostHog SDK
465
+ properties,
466
+ });
467
+ } catch (error) {
468
+ this.config.onError?.(error as Error);
469
+ }
470
+ }
471
+
472
+ /**
473
+ * Track an event with group context
474
+ *
475
+ * Use this to associate events with groups (e.g., organizations).
476
+ *
477
+ * @param name - Event name
478
+ * @param attributes - Event attributes
479
+ * @param groups - Group context (e.g., { company: 'acme-corp' })
480
+ *
481
+ * @example
482
+ * ```typescript
483
+ * await subscriber.trackEventWithGroups('feature.used', {
484
+ * userId: 'user-123',
485
+ * feature: 'advanced-events'
486
+ * }, {
487
+ * company: 'acme-corp'
488
+ * });
489
+ * ```
490
+ */
491
+ async trackEventWithGroups(
492
+ name: string,
493
+ attributes?: EventAttributes,
494
+ groups?: Record<string, string | number>,
495
+ ): Promise<void> {
496
+ if (!this.enabled) return;
497
+ await this.ensureInitialized();
498
+
499
+ const eventAttributes: EventAttributes = { ...attributes } as EventAttributes;
500
+ if (groups) {
501
+ (eventAttributes as any).groups = groups;
502
+ }
503
+
504
+ await this.trackEvent(name, eventAttributes);
505
+ }
506
+
507
+ /**
508
+ * Flush pending events and clean up resources
509
+ */
510
+ async shutdown(): Promise<void> {
511
+ await super.shutdown(); // Drain pending requests first
512
+ await this.ensureInitialized();
513
+
514
+ if (this.posthog) {
515
+ try {
516
+ await this.posthog.shutdown();
517
+ } catch (error) {
518
+ this.config.onError?.(error as Error);
519
+ }
520
+ }
521
+ }
522
+
523
+ /**
524
+ * Handle errors with custom error handler
525
+ */
526
+ protected handleError(error: Error, payload: EventPayload): void {
527
+ this.config.onError?.(error);
528
+ super.handleError(error, payload);
529
+ }
530
+ }