autotel-subscribers 10.0.0 → 12.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
  *
@@ -4628,7 +4628,7 @@ var require_dist = __commonJS({
4628
4628
  var http3 = __importStar(__require("http"));
4629
4629
  var https_1 = __require("https");
4630
4630
  __exportStar(require_helpers(), exports$1);
4631
- var INTERNAL2 = Symbol("AgentBaseInternalState");
4631
+ var INTERNAL2 = /* @__PURE__ */ Symbol("AgentBaseInternalState");
4632
4632
  var Agent = class extends http3.Agent {
4633
4633
  constructor(opts) {
4634
4634
  super(opts);
@@ -12287,8 +12287,8 @@ var require_utils3 = __commonJS({
12287
12287
  Object.defineProperty(target, keys[i], Object.getOwnPropertyDescriptor(source, keys[i]));
12288
12288
  }
12289
12289
  };
12290
- module.exports.wrapperSymbol = Symbol("wrapper");
12291
- module.exports.implSymbol = Symbol("impl");
12290
+ module.exports.wrapperSymbol = /* @__PURE__ */ Symbol("wrapper");
12291
+ module.exports.implSymbol = /* @__PURE__ */ Symbol("impl");
12292
12292
  module.exports.wrapperForImpl = function(impl) {
12293
12293
  return impl[module.exports.wrapperSymbol];
12294
12294
  };
@@ -12479,7 +12479,7 @@ var require_url_state_machine = __commonJS({
12479
12479
  ws: 80,
12480
12480
  wss: 443
12481
12481
  };
12482
- var failure = Symbol("failure");
12482
+ var failure = /* @__PURE__ */ Symbol("failure");
12483
12483
  function countSymbols(str) {
12484
12484
  return punycode.ucs2.decode(str).length;
12485
12485
  }
@@ -14492,8 +14492,8 @@ var init_lib = __esm({
14492
14492
  "../../node_modules/.pnpm/node-fetch@2.7.0/node_modules/node-fetch/lib/index.mjs"() {
14493
14493
  import_whatwg_url = __toESM(require_public_api());
14494
14494
  Readable = Stream__default.default.Readable;
14495
- BUFFER2 = Symbol("buffer");
14496
- TYPE = Symbol("type");
14495
+ BUFFER2 = /* @__PURE__ */ Symbol("buffer");
14496
+ TYPE = /* @__PURE__ */ Symbol("type");
14497
14497
  Blob2 = class _Blob {
14498
14498
  constructor() {
14499
14499
  this[TYPE] = "";
@@ -14598,7 +14598,7 @@ var init_lib = __esm({
14598
14598
  convert = __require("encoding").convert;
14599
14599
  } catch (e) {
14600
14600
  }
14601
- INTERNALS = Symbol("Body internals");
14601
+ INTERNALS = /* @__PURE__ */ Symbol("Body internals");
14602
14602
  PassThrough = Stream__default.default.PassThrough;
14603
14603
  Body.prototype = {
14604
14604
  get body() {
@@ -14701,7 +14701,7 @@ var init_lib = __esm({
14701
14701
  Body.Promise = global.Promise;
14702
14702
  invalidTokenRegex = /[^\^_`a-zA-Z\-0-9!#$%&'*+.|~]/;
14703
14703
  invalidHeaderCharRegex = /[^\t\x20-\x7e\x80-\xff]/;
14704
- MAP = Symbol("map");
14704
+ MAP = /* @__PURE__ */ Symbol("map");
14705
14705
  Headers = class _Headers {
14706
14706
  /**
14707
14707
  * Headers class
@@ -14898,7 +14898,7 @@ var init_lib = __esm({
14898
14898
  values: { enumerable: true },
14899
14899
  entries: { enumerable: true }
14900
14900
  });
14901
- INTERNAL = Symbol("internal");
14901
+ INTERNAL = /* @__PURE__ */ Symbol("internal");
14902
14902
  HeadersIteratorPrototype = Object.setPrototypeOf({
14903
14903
  next() {
14904
14904
  if (!this || Object.getPrototypeOf(this) !== HeadersIteratorPrototype) {
@@ -14927,7 +14927,7 @@ var init_lib = __esm({
14927
14927
  enumerable: false,
14928
14928
  configurable: true
14929
14929
  });
14930
- INTERNALS$1 = Symbol("Response internals");
14930
+ INTERNALS$1 = /* @__PURE__ */ Symbol("Response internals");
14931
14931
  STATUS_CODES = http2__namespace.default.STATUS_CODES;
14932
14932
  Response2 = class _Response {
14933
14933
  constructor() {
@@ -15003,7 +15003,7 @@ var init_lib = __esm({
15003
15003
  enumerable: false,
15004
15004
  configurable: true
15005
15005
  });
15006
- INTERNALS$2 = Symbol("Request internals");
15006
+ INTERNALS$2 = /* @__PURE__ */ Symbol("Request internals");
15007
15007
  URL2 = Url__default.default.URL || import_whatwg_url.default.URL;
15008
15008
  parse_url = Url__default.default.parse;
15009
15009
  format_url = Url__default.default.format;
@@ -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
  };