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/src/posthog.ts CHANGED
@@ -86,12 +86,34 @@
86
86
  * ```
87
87
  */
88
88
 
89
- import type { EventAttributes } from 'autotel/event-subscriber';
89
+ import type { EventAttributes, EventAttributesInput } from 'autotel/event-subscriber';
90
90
  import { EventSubscriber, type EventPayload } from './event-subscriber-base';
91
91
 
92
92
  // Type-only import to avoid runtime dependency
93
93
  import type { PostHog } from 'posthog-node';
94
94
 
95
+ /**
96
+ * Error context for enhanced error handling
97
+ *
98
+ * Provides detailed context about the event that caused an error.
99
+ */
100
+ export interface ErrorContext {
101
+ /** The error that occurred */
102
+ error: Error;
103
+
104
+ /** Event name (if applicable) */
105
+ eventName?: string;
106
+
107
+ /** Event type (event, funnel, outcome, value) */
108
+ eventType?: 'event' | 'funnel' | 'outcome' | 'value';
109
+
110
+ /** Event attributes (filtered) */
111
+ attributes?: EventAttributes;
112
+
113
+ /** Subscriber name */
114
+ subscriberName: string;
115
+ }
116
+
95
117
  export interface PostHogConfig {
96
118
  /** PostHog API key (starts with phc_) - required if not providing custom client */
97
119
  apiKey?: string;
@@ -105,6 +127,41 @@ export interface PostHogConfig {
105
127
  /** Custom PostHog client instance (bypasses apiKey/host) */
106
128
  client?: PostHog;
107
129
 
130
+ /**
131
+ * Use global browser client (window.posthog)
132
+ *
133
+ * When true, uses the PostHog client already loaded on the page via script tag.
134
+ * This is useful for Next.js apps that initialize PostHog in _app.tsx.
135
+ *
136
+ * @example
137
+ * ```typescript
138
+ * // Browser - uses window.posthog
139
+ * const subscriber = new PostHogSubscriber({
140
+ * useGlobalClient: true,
141
+ * });
142
+ * ```
143
+ */
144
+ useGlobalClient?: boolean;
145
+
146
+ /**
147
+ * Serverless mode preset (AWS Lambda, Vercel Functions, Next.js API routes)
148
+ *
149
+ * When true, auto-configures for serverless environments:
150
+ * - flushAt: 1 (send immediately, don't batch)
151
+ * - flushInterval: 0 (disable interval-based flushing)
152
+ * - requestTimeout: 3000 (shorter timeout for fast responses)
153
+ *
154
+ * @example
155
+ * ```typescript
156
+ * // Vercel / Next.js API route
157
+ * const subscriber = new PostHogSubscriber({
158
+ * apiKey: 'phc_...',
159
+ * serverless: true,
160
+ * });
161
+ * ```
162
+ */
163
+ serverless?: boolean;
164
+
108
165
  // Serverless optimizations
109
166
  /** Flush batch when it reaches this size (default: 20, set to 1 for immediate send) */
110
167
  flushAt?: number;
@@ -122,10 +179,49 @@ export interface PostHogConfig {
122
179
  /** Send feature flag evaluation events (default: true) */
123
180
  sendFeatureFlags?: boolean;
124
181
 
182
+ /**
183
+ * Automatically filter out undefined and null values from attributes
184
+ *
185
+ * When true (default), undefined and null values are removed before sending.
186
+ * This improves DX when passing objects with optional properties.
187
+ *
188
+ * @default true
189
+ *
190
+ * @example
191
+ * ```typescript
192
+ * // With filterUndefinedValues: true (default)
193
+ * subscriber.trackEvent('user.action', {
194
+ * userId: user.id,
195
+ * email: user.email, // might be undefined - will be filtered
196
+ * plan: user.subscription, // might be null - will be filtered
197
+ * });
198
+ * ```
199
+ */
200
+ filterUndefinedValues?: boolean;
201
+
125
202
  // Error handling
126
203
  /** Error callback for debugging and monitoring */
127
204
  onError?: (error: Error) => void;
128
205
 
206
+ /**
207
+ * Enhanced error callback with event context
208
+ *
209
+ * Provides detailed context about the event that caused the error.
210
+ * If both onError and onErrorWithContext are provided, both are called.
211
+ *
212
+ * @example
213
+ * ```typescript
214
+ * const subscriber = new PostHogSubscriber({
215
+ * apiKey: 'phc_...',
216
+ * onErrorWithContext: (ctx) => {
217
+ * console.error(`Failed to track ${ctx.eventType}: ${ctx.eventName}`, ctx.error);
218
+ * Sentry.captureException(ctx.error, { extra: ctx });
219
+ * }
220
+ * });
221
+ * ```
222
+ */
223
+ onErrorWithContext?: (context: ErrorContext) => void;
224
+
129
225
  /** Enable debug logging (default: false) */
130
226
  debug?: boolean;
131
227
  }
@@ -171,16 +267,35 @@ export class PostHogSubscriber extends EventSubscriber {
171
267
  private posthog: PostHog | null = null;
172
268
  private config: PostHogConfig;
173
269
  private initPromise: Promise<void> | null = null;
270
+ /** True when using browser's window.posthog (different API signature) */
271
+ private isBrowserClient = false;
174
272
 
175
273
  constructor(config: PostHogConfig) {
176
274
  super();
177
275
 
178
- if (!config.apiKey && !config.client) {
179
- throw new Error('PostHogSubscriber requires either apiKey or client to be provided');
276
+ // Apply serverless preset first (can be overridden by explicit config)
277
+ if (config.serverless) {
278
+ config = {
279
+ flushAt: 1,
280
+ flushInterval: 0,
281
+ requestTimeout: 3000,
282
+ ...config, // User config overrides serverless defaults
283
+ };
284
+ }
285
+
286
+ // Validate: need either apiKey, client, or useGlobalClient
287
+ if (!config.apiKey && !config.client && !config.useGlobalClient) {
288
+ throw new Error(
289
+ 'PostHogSubscriber requires either apiKey, client, or useGlobalClient to be provided',
290
+ );
180
291
  }
181
292
 
182
293
  this.enabled = config.enabled ?? true;
183
- this.config = config;
294
+ // Default filterUndefinedValues to true
295
+ this.config = {
296
+ filterUndefinedValues: true,
297
+ ...config,
298
+ };
184
299
 
185
300
  if (this.enabled) {
186
301
  // Start initialization immediately but don't block constructor
@@ -190,14 +305,29 @@ export class PostHogSubscriber extends EventSubscriber {
190
305
 
191
306
  private async initialize(): Promise<void> {
192
307
  try {
193
- // Use custom client if provided
308
+ // Option 1: Use global browser client (window.posthog)
309
+ if (this.config.useGlobalClient) {
310
+ const globalWindow = typeof globalThis === 'undefined' ? undefined : (globalThis as Record<string, unknown>);
311
+ if (globalWindow?.posthog) {
312
+ this.posthog = globalWindow.posthog as PostHog;
313
+ this.isBrowserClient = true;
314
+ this.setupErrorHandling();
315
+ return;
316
+ }
317
+ throw new Error(
318
+ 'useGlobalClient enabled but window.posthog not found. ' +
319
+ 'Ensure PostHog script is loaded before initializing the subscriber.',
320
+ );
321
+ }
322
+
323
+ // Option 2: Use custom client if provided
194
324
  if (this.config.client) {
195
325
  this.posthog = this.config.client;
196
326
  this.setupErrorHandling();
197
327
  return;
198
328
  }
199
329
 
200
- // Dynamic import to avoid adding posthog-node as a hard dependency
330
+ // Option 3: Create new PostHog client with dynamic import
201
331
  const { PostHog } = await import('posthog-node');
202
332
 
203
333
  this.posthog = new PostHog(this.config.apiKey!, {
@@ -247,25 +377,46 @@ export class PostHogSubscriber extends EventSubscriber {
247
377
  protected async sendToDestination(payload: EventPayload): Promise<void> {
248
378
  await this.ensureInitialized();
249
379
 
250
- // Build properties object, including value if present
251
- let properties: any = payload.attributes;
380
+ // Filter attributes if enabled (default: true)
381
+ const filteredAttributes =
382
+ this.config.filterUndefinedValues === false
383
+ ? payload.attributes
384
+ : this.filterAttributes(payload.attributes as EventAttributesInput);
385
+
386
+ // Build properties object, including value and funnel metadata if present
387
+ const properties: Record<string, unknown> = { ...filteredAttributes };
252
388
  if (payload.value !== undefined) {
253
- properties = { ...payload.attributes, value: payload.value };
389
+ properties.value = payload.value;
390
+ }
391
+ // Add funnel progression metadata
392
+ if (payload.stepNumber !== undefined) {
393
+ properties.step_number = payload.stepNumber;
394
+ }
395
+ if (payload.stepName !== undefined) {
396
+ properties.step_name = payload.stepName;
254
397
  }
255
398
 
256
- // Build PostHog capture payload
257
- const capturePayload: any = {
258
- distinctId: this.extractDistinctId(payload.attributes),
259
- event: payload.name,
260
- properties,
261
- };
399
+ const distinctId = this.extractDistinctId(filteredAttributes);
262
400
 
263
- // Add groups if present in attributes
264
- if (payload.attributes?.groups) {
265
- capturePayload.groups = payload.attributes.groups;
266
- }
401
+ // Browser client has different API signature
402
+ if (this.isBrowserClient) {
403
+ // Browser API: capture(eventName, properties)
404
+ (this.posthog as any)?.capture(payload.name, properties);
405
+ } else {
406
+ // Server API: capture({ distinctId, event, properties, groups })
407
+ const capturePayload: any = {
408
+ distinctId,
409
+ event: payload.name,
410
+ properties,
411
+ };
267
412
 
268
- this.posthog?.capture(capturePayload);
413
+ // Add groups if present in attributes
414
+ if (filteredAttributes?.groups) {
415
+ capturePayload.groups = filteredAttributes.groups;
416
+ }
417
+
418
+ this.posthog?.capture(capturePayload);
419
+ }
269
420
  }
270
421
 
271
422
  // Feature Flag Methods
@@ -524,7 +675,20 @@ export class PostHogSubscriber extends EventSubscriber {
524
675
  * Handle errors with custom error handler
525
676
  */
526
677
  protected handleError(error: Error, payload: EventPayload): void {
678
+ // Call basic onError if provided
527
679
  this.config.onError?.(error);
680
+
681
+ // Call enhanced onErrorWithContext if provided
682
+ if (this.config.onErrorWithContext) {
683
+ this.config.onErrorWithContext({
684
+ error,
685
+ eventName: payload.name,
686
+ eventType: payload.type,
687
+ attributes: payload.attributes,
688
+ subscriberName: this.name,
689
+ });
690
+ }
691
+
528
692
  super.handleError(error, payload);
529
693
  }
530
694
  }