autotel-subscribers 10.0.0 → 11.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/README.md CHANGED
@@ -191,12 +191,56 @@ const events = new Event('checkout', {
191
191
  });
192
192
 
193
193
  // Sent to: OpenTelemetry + PostHog
194
- events.trackEvent('order.completed', {
195
- userId: '123',
196
- amount: 99.99
194
+ events.trackEvent('order.completed', {
195
+ userId: '123',
196
+ amount: 99.99
197
+ });
198
+ ```
199
+
200
+ **Serverless Configuration (AWS Lambda, Vercel, Next.js):**
201
+
202
+ ```typescript
203
+ const subscriber = new PostHogSubscriber({
204
+ apiKey: 'phc_...',
205
+ serverless: true, // Auto-configures for serverless (flushAt: 1, flushInterval: 0)
206
+ });
207
+ ```
208
+
209
+ **Browser Usage (with global PostHog client):**
210
+
211
+ ```typescript
212
+ // When PostHog is already loaded via script tag
213
+ const subscriber = new PostHogSubscriber({
214
+ useGlobalClient: true, // Uses window.posthog
215
+ });
216
+ ```
217
+
218
+ **Advanced Options:**
219
+
220
+ ```typescript
221
+ const subscriber = new PostHogSubscriber({
222
+ apiKey: 'phc_...',
223
+
224
+ // Automatic filtering (enabled by default)
225
+ filterUndefinedValues: true, // Removes undefined/null from attributes
226
+
227
+ // Enhanced error handling
228
+ onErrorWithContext: (ctx) => {
229
+ console.error(`${ctx.eventType} failed: ${ctx.eventName}`, ctx.error);
230
+ Sentry.captureException(ctx.error, { extra: ctx });
231
+ },
197
232
  });
198
233
  ```
199
234
 
235
+ **Custom Funnel Tracking:**
236
+
237
+ ```typescript
238
+ // Track custom step names (not limited to 'started'/'completed')
239
+ event.trackFunnelProgression('checkout', 'cart_viewed', 1);
240
+ event.trackFunnelProgression('checkout', 'shipping_selected', 2);
241
+ event.trackFunnelProgression('checkout', 'payment_entered', 3);
242
+ ```
243
+
200
244
  ### Mixpanel
201
245
 
202
246
  ```typescript
@@ -357,28 +401,39 @@ All subscribers implement these methods:
357
401
  ```typescript
358
402
  interface EventSubscriber {
359
403
  // Track events
360
- trackEvent(name: string, attributes?: Record<string, any>): void;
361
-
362
- // Track conversion funnels
404
+ trackEvent(name: string, attributes?: Record<string, any>): Promise<void>;
405
+
406
+ // Track conversion funnels (enum-based steps)
363
407
  trackFunnelStep(
364
- funnelName: string,
408
+ funnelName: string,
365
409
  step: 'started' | 'completed' | 'abandoned' | 'failed',
366
410
  attributes?: Record<string, any>
367
- ): void;
368
-
411
+ ): Promise<void>;
412
+
413
+ // Track funnel progression (custom step names)
414
+ trackFunnelProgression?(
415
+ funnelName: string,
416
+ stepName: string, // Any string, not limited to enum
417
+ stepNumber?: number, // Optional numeric position
418
+ attributes?: Record<string, any>
419
+ ): Promise<void>;
420
+
369
421
  // Track business outcomes
370
422
  trackOutcome(
371
423
  operationName: string,
372
424
  outcome: 'success' | 'failure' | 'partial',
373
425
  attributes?: Record<string, any>
374
- ): void;
375
-
426
+ ): Promise<void>;
427
+
376
428
  // Track business values (revenue, counts, etc.)
377
429
  trackValue(
378
- name: string,
430
+ name: string,
379
431
  value: number,
380
432
  attributes?: Record<string, any>
381
- ): void;
433
+ ): Promise<void>;
434
+
435
+ // Flush and clean up resources
436
+ shutdown?(): Promise<void>;
382
437
  }
383
438
  ```
384
439
 
@@ -1,4 +1,4 @@
1
- import { EventSubscriber as EventSubscriber$1, EventAttributes, FunnelStatus, OutcomeStatus } from 'autotel/event-subscriber';
1
+ import { EventSubscriber as EventSubscriber$1, EventAttributes, FunnelStatus, OutcomeStatus, EventAttributesInput } from 'autotel/event-subscriber';
2
2
 
3
3
  /**
4
4
  * EventSubscriber - Standard base class for building custom subscribers
@@ -78,8 +78,12 @@ interface EventPayload {
78
78
  attributes?: EventAttributes;
79
79
  /** For funnel events: funnel name */
80
80
  funnel?: string;
81
- /** For funnel events: step status */
82
- step?: FunnelStatus;
81
+ /** For funnel events: step status (from FunnelStatus enum) */
82
+ step?: FunnelStatus | string;
83
+ /** For funnel events: custom step name (from trackFunnelProgression) */
84
+ stepName?: string;
85
+ /** For funnel events: numeric position in funnel */
86
+ stepNumber?: number;
83
87
  /** For outcome events: operation name */
84
88
  operation?: string;
85
89
  /** For outcome events: outcome status */
@@ -141,6 +145,26 @@ declare abstract class EventSubscriber implements EventSubscriber$1 {
141
145
  * @param payload - Event payload that failed
142
146
  */
143
147
  protected handleError(error: Error, payload: EventPayload): void;
148
+ /**
149
+ * Filter out undefined and null values from attributes
150
+ *
151
+ * This improves DX by allowing callers to pass objects with optional properties
152
+ * without having to manually filter them first.
153
+ *
154
+ * @param attributes - Input attributes (may contain undefined/null)
155
+ * @returns Filtered attributes with only defined values, or undefined if empty
156
+ *
157
+ * @example
158
+ * ```typescript
159
+ * const filtered = this.filterAttributes({
160
+ * userId: user.id,
161
+ * email: user.email, // might be undefined
162
+ * plan: null, // will be filtered out
163
+ * });
164
+ * // Result: { userId: 'abc', email: 'test@example.com' } or { userId: 'abc' }
165
+ * ```
166
+ */
167
+ protected filterAttributes(attributes?: EventAttributesInput): EventAttributes | undefined;
144
168
  /**
145
169
  * Track an event
146
170
  */
@@ -157,6 +181,18 @@ declare abstract class EventSubscriber implements EventSubscriber$1 {
157
181
  * Track a value/metric
158
182
  */
159
183
  trackValue(name: string, value: number, attributes?: EventAttributes): Promise<void>;
184
+ /**
185
+ * Track funnel progression with custom step names
186
+ *
187
+ * Unlike trackFunnelStep which uses FunnelStatus enum values,
188
+ * this method allows any string as the step name for flexible funnel tracking.
189
+ *
190
+ * @param funnelName - Name of the funnel (e.g., "checkout", "onboarding")
191
+ * @param stepName - Custom step name (e.g., "cart_viewed", "payment_entered")
192
+ * @param stepNumber - Optional numeric position in the funnel
193
+ * @param attributes - Optional event attributes
194
+ */
195
+ trackFunnelProgression(funnelName: string, stepName: string, stepNumber?: number, attributes?: EventAttributes): Promise<void>;
160
196
  /**
161
197
  * Flush pending requests and clean up
162
198
  *
@@ -1,4 +1,4 @@
1
- import { EventSubscriber as EventSubscriber$1, EventAttributes, FunnelStatus, OutcomeStatus } from 'autotel/event-subscriber';
1
+ import { EventSubscriber as EventSubscriber$1, EventAttributes, FunnelStatus, OutcomeStatus, EventAttributesInput } from 'autotel/event-subscriber';
2
2
 
3
3
  /**
4
4
  * EventSubscriber - Standard base class for building custom subscribers
@@ -78,8 +78,12 @@ interface EventPayload {
78
78
  attributes?: EventAttributes;
79
79
  /** For funnel events: funnel name */
80
80
  funnel?: string;
81
- /** For funnel events: step status */
82
- step?: FunnelStatus;
81
+ /** For funnel events: step status (from FunnelStatus enum) */
82
+ step?: FunnelStatus | string;
83
+ /** For funnel events: custom step name (from trackFunnelProgression) */
84
+ stepName?: string;
85
+ /** For funnel events: numeric position in funnel */
86
+ stepNumber?: number;
83
87
  /** For outcome events: operation name */
84
88
  operation?: string;
85
89
  /** For outcome events: outcome status */
@@ -141,6 +145,26 @@ declare abstract class EventSubscriber implements EventSubscriber$1 {
141
145
  * @param payload - Event payload that failed
142
146
  */
143
147
  protected handleError(error: Error, payload: EventPayload): void;
148
+ /**
149
+ * Filter out undefined and null values from attributes
150
+ *
151
+ * This improves DX by allowing callers to pass objects with optional properties
152
+ * without having to manually filter them first.
153
+ *
154
+ * @param attributes - Input attributes (may contain undefined/null)
155
+ * @returns Filtered attributes with only defined values, or undefined if empty
156
+ *
157
+ * @example
158
+ * ```typescript
159
+ * const filtered = this.filterAttributes({
160
+ * userId: user.id,
161
+ * email: user.email, // might be undefined
162
+ * plan: null, // will be filtered out
163
+ * });
164
+ * // Result: { userId: 'abc', email: 'test@example.com' } or { userId: 'abc' }
165
+ * ```
166
+ */
167
+ protected filterAttributes(attributes?: EventAttributesInput): EventAttributes | undefined;
144
168
  /**
145
169
  * Track an event
146
170
  */
@@ -157,6 +181,18 @@ declare abstract class EventSubscriber implements EventSubscriber$1 {
157
181
  * Track a value/metric
158
182
  */
159
183
  trackValue(name: string, value: number, attributes?: EventAttributes): Promise<void>;
184
+ /**
185
+ * Track funnel progression with custom step names
186
+ *
187
+ * Unlike trackFunnelStep which uses FunnelStatus enum values,
188
+ * this method allows any string as the step name for flexible funnel tracking.
189
+ *
190
+ * @param funnelName - Name of the funnel (e.g., "checkout", "onboarding")
191
+ * @param stepName - Custom step name (e.g., "cart_viewed", "payment_entered")
192
+ * @param stepNumber - Optional numeric position in the funnel
193
+ * @param attributes - Optional event attributes
194
+ */
195
+ trackFunnelProgression(funnelName: string, stepName: string, stepNumber?: number, attributes?: EventAttributes): Promise<void>;
160
196
  /**
161
197
  * Flush pending requests and clean up
162
198
  *
@@ -15425,6 +15425,35 @@ var EventSubscriber = class {
15425
15425
  payload
15426
15426
  );
15427
15427
  }
15428
+ /**
15429
+ * Filter out undefined and null values from attributes
15430
+ *
15431
+ * This improves DX by allowing callers to pass objects with optional properties
15432
+ * without having to manually filter them first.
15433
+ *
15434
+ * @param attributes - Input attributes (may contain undefined/null)
15435
+ * @returns Filtered attributes with only defined values, or undefined if empty
15436
+ *
15437
+ * @example
15438
+ * ```typescript
15439
+ * const filtered = this.filterAttributes({
15440
+ * userId: user.id,
15441
+ * email: user.email, // might be undefined
15442
+ * plan: null, // will be filtered out
15443
+ * });
15444
+ * // Result: { userId: 'abc', email: 'test@example.com' } or { userId: 'abc' }
15445
+ * ```
15446
+ */
15447
+ filterAttributes(attributes) {
15448
+ if (!attributes) return void 0;
15449
+ const filtered = {};
15450
+ for (const [key, value] of Object.entries(attributes)) {
15451
+ if (value !== void 0 && value !== null) {
15452
+ filtered[key] = value;
15453
+ }
15454
+ }
15455
+ return Object.keys(filtered).length > 0 ? filtered : void 0;
15456
+ }
15428
15457
  /**
15429
15458
  * Track an event
15430
15459
  */
@@ -15482,6 +15511,31 @@ var EventSubscriber = class {
15482
15511
  };
15483
15512
  await this.send(payload);
15484
15513
  }
15514
+ /**
15515
+ * Track funnel progression with custom step names
15516
+ *
15517
+ * Unlike trackFunnelStep which uses FunnelStatus enum values,
15518
+ * this method allows any string as the step name for flexible funnel tracking.
15519
+ *
15520
+ * @param funnelName - Name of the funnel (e.g., "checkout", "onboarding")
15521
+ * @param stepName - Custom step name (e.g., "cart_viewed", "payment_entered")
15522
+ * @param stepNumber - Optional numeric position in the funnel
15523
+ * @param attributes - Optional event attributes
15524
+ */
15525
+ async trackFunnelProgression(funnelName, stepName, stepNumber, attributes) {
15526
+ if (!this.enabled) return;
15527
+ const payload = {
15528
+ type: "funnel",
15529
+ name: `${funnelName}.${stepName}`,
15530
+ funnel: funnelName,
15531
+ step: stepName,
15532
+ stepName,
15533
+ stepNumber,
15534
+ attributes,
15535
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
15536
+ };
15537
+ await this.send(payload);
15538
+ }
15485
15539
  /**
15486
15540
  * Flush pending requests and clean up
15487
15541
  *
@@ -15542,19 +15596,47 @@ var PostHogSubscriber = class extends EventSubscriber {
15542
15596
  posthog = null;
15543
15597
  config;
15544
15598
  initPromise = null;
15599
+ /** True when using browser's window.posthog (different API signature) */
15600
+ isBrowserClient = false;
15545
15601
  constructor(config) {
15546
15602
  super();
15547
- if (!config.apiKey && !config.client) {
15548
- throw new Error("PostHogSubscriber requires either apiKey or client to be provided");
15603
+ if (config.serverless) {
15604
+ config = {
15605
+ flushAt: 1,
15606
+ flushInterval: 0,
15607
+ requestTimeout: 3e3,
15608
+ ...config
15609
+ // User config overrides serverless defaults
15610
+ };
15611
+ }
15612
+ if (!config.apiKey && !config.client && !config.useGlobalClient) {
15613
+ throw new Error(
15614
+ "PostHogSubscriber requires either apiKey, client, or useGlobalClient to be provided"
15615
+ );
15549
15616
  }
15550
15617
  this.enabled = config.enabled ?? true;
15551
- this.config = config;
15618
+ this.config = {
15619
+ filterUndefinedValues: true,
15620
+ ...config
15621
+ };
15552
15622
  if (this.enabled) {
15553
15623
  this.initPromise = this.initialize();
15554
15624
  }
15555
15625
  }
15556
15626
  async initialize() {
15557
15627
  try {
15628
+ if (this.config.useGlobalClient) {
15629
+ const globalWindow = typeof globalThis === "undefined" ? void 0 : globalThis;
15630
+ if (globalWindow?.posthog) {
15631
+ this.posthog = globalWindow.posthog;
15632
+ this.isBrowserClient = true;
15633
+ this.setupErrorHandling();
15634
+ return;
15635
+ }
15636
+ throw new Error(
15637
+ "useGlobalClient enabled but window.posthog not found. Ensure PostHog script is loaded before initializing the subscriber."
15638
+ );
15639
+ }
15558
15640
  if (this.config.client) {
15559
15641
  this.posthog = this.config.client;
15560
15642
  this.setupErrorHandling();
@@ -15601,19 +15683,31 @@ var PostHogSubscriber = class extends EventSubscriber {
15601
15683
  */
15602
15684
  async sendToDestination(payload) {
15603
15685
  await this.ensureInitialized();
15604
- let properties = payload.attributes;
15686
+ const filteredAttributes = this.config.filterUndefinedValues === false ? payload.attributes : this.filterAttributes(payload.attributes);
15687
+ const properties = { ...filteredAttributes };
15605
15688
  if (payload.value !== void 0) {
15606
- properties = { ...payload.attributes, value: payload.value };
15689
+ properties.value = payload.value;
15607
15690
  }
15608
- const capturePayload = {
15609
- distinctId: this.extractDistinctId(payload.attributes),
15610
- event: payload.name,
15611
- properties
15612
- };
15613
- if (payload.attributes?.groups) {
15614
- capturePayload.groups = payload.attributes.groups;
15691
+ if (payload.stepNumber !== void 0) {
15692
+ properties.step_number = payload.stepNumber;
15693
+ }
15694
+ if (payload.stepName !== void 0) {
15695
+ properties.step_name = payload.stepName;
15696
+ }
15697
+ const distinctId = this.extractDistinctId(filteredAttributes);
15698
+ if (this.isBrowserClient) {
15699
+ this.posthog?.capture(payload.name, properties);
15700
+ } else {
15701
+ const capturePayload = {
15702
+ distinctId,
15703
+ event: payload.name,
15704
+ properties
15705
+ };
15706
+ if (filteredAttributes?.groups) {
15707
+ capturePayload.groups = filteredAttributes.groups;
15708
+ }
15709
+ this.posthog?.capture(capturePayload);
15615
15710
  }
15616
- this.posthog?.capture(capturePayload);
15617
15711
  }
15618
15712
  // Feature Flag Methods
15619
15713
  /**
@@ -15835,6 +15929,15 @@ var PostHogSubscriber = class extends EventSubscriber {
15835
15929
  */
15836
15930
  handleError(error, payload) {
15837
15931
  this.config.onError?.(error);
15932
+ if (this.config.onErrorWithContext) {
15933
+ this.config.onErrorWithContext({
15934
+ error,
15935
+ eventName: payload.name,
15936
+ eventType: payload.type,
15937
+ attributes: payload.attributes,
15938
+ subscriberName: this.name
15939
+ });
15940
+ }
15838
15941
  super.handleError(error, payload);
15839
15942
  }
15840
15943
  };