autotel-posthog 0.1.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.
@@ -0,0 +1,543 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ //#region \0rolldown/runtime.js
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
11
+ key = keys[i];
12
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
13
+ get: ((k) => from[k]).bind(null, key),
14
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
15
+ });
16
+ }
17
+ return to;
18
+ };
19
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
20
+ value: mod,
21
+ enumerable: true
22
+ }) : target, mod));
23
+ //#endregion
24
+ let autotel_subscribers = require("autotel-subscribers");
25
+ let slow_redact = require("slow-redact");
26
+ slow_redact = __toESM(slow_redact, 1);
27
+ //#region src/subscriber/error-formatter.ts
28
+ const MAX_CAUSE_DEPTH = 5;
29
+ function formatExceptionForPostHog(exceptionList, platform = "web:javascript", redactor) {
30
+ return { $exception_list: exceptionList.map((ex) => ({
31
+ type: ex.type,
32
+ value: redactor ? redactor(ex.value) : ex.value,
33
+ mechanism: ex.mechanism,
34
+ stacktrace: { frames: (ex.stacktrace?.frames || []).map((frame) => ({
35
+ ...frame,
36
+ abs_path: frame.abs_path && redactor ? redactor(frame.abs_path) : frame.abs_path,
37
+ platform
38
+ })) }
39
+ })) };
40
+ }
41
+ function errorToExceptionList(input, redactor) {
42
+ const error = input instanceof Error ? input : new Error(input === null || input === void 0 ? "Unknown error" : String(input));
43
+ const records = [];
44
+ let current = error;
45
+ let depth = 0;
46
+ while (current && depth < MAX_CAUSE_DEPTH) {
47
+ const value = current.message || "Unknown error";
48
+ const frames = current.stack ? parseStackBasic(current.stack) : void 0;
49
+ records.push({
50
+ type: current.name || "Error",
51
+ value: redactor ? redactor(value) : value,
52
+ mechanism: {
53
+ type: "manual",
54
+ handled: true
55
+ },
56
+ stacktrace: frames ? { frames: redactor ? frames.map((f) => ({
57
+ ...f,
58
+ abs_path: f.abs_path ? redactor(f.abs_path) : f.abs_path
59
+ })) : frames } : void 0
60
+ });
61
+ current = current.cause instanceof Error ? current.cause : void 0;
62
+ depth++;
63
+ }
64
+ return records.toReversed();
65
+ }
66
+ function parseStackBasic(stack) {
67
+ const lines = stack.split("\n");
68
+ const frames = [];
69
+ const re = /^\s*at\s+(?:(.+?)\s+\()?(.+?):(\d+):(\d+)\)?$/;
70
+ for (const line of lines) {
71
+ const match = line.trim().match(re);
72
+ if (match) {
73
+ const [, fn, absPath, lineStr, colStr] = match;
74
+ if (!absPath) continue;
75
+ frames.push({
76
+ function: fn || void 0,
77
+ abs_path: absPath,
78
+ filename: absPath.split("/").pop() || absPath,
79
+ lineno: Number.parseInt(lineStr ?? "0", 10),
80
+ colno: Number.parseInt(colStr ?? "0", 10),
81
+ in_app: !absPath.includes("node_modules")
82
+ });
83
+ }
84
+ }
85
+ return frames;
86
+ }
87
+ //#endregion
88
+ //#region src/values.ts
89
+ /** The value as an object, or undefined when it is anything else. */
90
+ function asRecord(value) {
91
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
92
+ }
93
+ /** The string the value carries, or undefined when it carries anything else. */
94
+ function asString(value) {
95
+ return typeof value === "string" ? value : void 0;
96
+ }
97
+ /** One field of a value, when the value is an object at all. */
98
+ function readProperty(source, key) {
99
+ return asRecord(source)?.[key];
100
+ }
101
+ /**
102
+ * The Error a caller's callback expects, wrapping whatever was actually
103
+ * thrown - a string, a number, an object with no stack.
104
+ */
105
+ function toError(cause) {
106
+ return cause instanceof Error ? cause : new Error(String(cause));
107
+ }
108
+ //#endregion
109
+ //#region src/subscriber/index.ts
110
+ var PostHogSubscriber = class extends autotel_subscribers.EventSubscriber {
111
+ name = "PostHogSubscriber";
112
+ version = "2.0.0";
113
+ posthog = null;
114
+ config;
115
+ initPromise = null;
116
+ /** True when using browser's window.posthog (different API signature) */
117
+ isBrowserClient = false;
118
+ /**
119
+ * The same client seen through the browser SDK's shape.
120
+ *
121
+ * The two SDKs disagree about `capture`: Node takes one object, the browser
122
+ * takes a name and properties. Rather than cast at each call site, the cast
123
+ * happens once here and the calls are typed against `posthog-js` itself — so
124
+ * a change to the browser signature is a compile error rather than a runtime
125
+ * one in a page.
126
+ */
127
+ get browserClient() {
128
+ return this.isBrowserClient && this.posthog ? this.posthog : void 0;
129
+ }
130
+ pathRedactor = null;
131
+ stringRedactor = null;
132
+ constructor(config) {
133
+ super();
134
+ if (config.serverless) config = {
135
+ flushAt: 1,
136
+ flushInterval: 0,
137
+ requestTimeout: 3e3,
138
+ ...config
139
+ };
140
+ if (!config.apiKey && !config.client && !config.useGlobalClient) throw new Error("PostHogSubscriber requires either apiKey, client, or useGlobalClient to be provided");
141
+ this.enabled = config.enabled ?? true;
142
+ this.config = {
143
+ filterUndefinedValues: true,
144
+ ...config
145
+ };
146
+ if (this.config.redactPaths && this.config.redactPaths.length > 0) this.pathRedactor = (0, slow_redact.default)({
147
+ paths: this.config.redactPaths,
148
+ serialize: false
149
+ });
150
+ if (this.config.stringRedactor) this.stringRedactor = this.config.stringRedactor;
151
+ if (this.enabled) this.initPromise = this.initialize();
152
+ }
153
+ async initialize() {
154
+ try {
155
+ if (this.config.useGlobalClient) {
156
+ const globalPostHog = readProperty(globalThis, "posthog");
157
+ if (globalPostHog) {
158
+ this.posthog = globalPostHog;
159
+ this.isBrowserClient = true;
160
+ this.setupErrorHandling();
161
+ return;
162
+ }
163
+ throw new Error("useGlobalClient enabled but window.posthog not found. Ensure PostHog script is loaded before initializing the subscriber.");
164
+ }
165
+ if (this.config.client) {
166
+ this.posthog = this.config.client;
167
+ this.setupErrorHandling();
168
+ return;
169
+ }
170
+ const { PostHog } = await import("posthog-node");
171
+ this.posthog = new PostHog(this.config.apiKey, {
172
+ host: this.config.host || "https://us.i.posthog.com",
173
+ flushAt: this.config.flushAt,
174
+ flushInterval: this.config.flushInterval,
175
+ requestTimeout: this.config.requestTimeout,
176
+ disableGeoip: this.config.disableGeoip,
177
+ sendFeatureFlagEvent: this.config.sendFeatureFlags
178
+ });
179
+ this.setupErrorHandling();
180
+ } catch (error) {
181
+ console.error("PostHog subscriber failed to initialize. Install posthog-node: pnpm add posthog-node", error);
182
+ this.enabled = false;
183
+ this.config.onError?.(toError(error));
184
+ }
185
+ }
186
+ setupErrorHandling() {
187
+ if (this.config.debug) this.posthog?.debug();
188
+ if (this.config.onError && this.posthog?.on) this.posthog.on("error", this.config.onError);
189
+ }
190
+ async ensureInitialized() {
191
+ if (this.initPromise) {
192
+ await this.initPromise;
193
+ this.initPromise = null;
194
+ }
195
+ }
196
+ /**
197
+ * Feature-flag options as posthog-node's own parameter type.
198
+ *
199
+ * SAFETY: FeatureFlagOptions above mirrors the fields posthog-node accepts -
200
+ * groups, person and group properties, and the two evaluation switches. The
201
+ * SDK declares them inline on each method rather than as a named type, so
202
+ * the correspondence is stated here once instead of at each call.
203
+ */
204
+ flagOptions(options) {
205
+ return options;
206
+ }
207
+ extractDistinctId(attributes) {
208
+ return asString(attributes?.userId) ?? asString(attributes?.user_id) ?? "anonymous";
209
+ }
210
+ redactProperties(properties) {
211
+ let result = properties;
212
+ if (this.pathRedactor) result = this.pathRedactor(properties);
213
+ if (this.stringRedactor) result = this.redactStringValues(result);
214
+ return result;
215
+ }
216
+ redactStringValues(obj) {
217
+ const result = {};
218
+ for (const [key, value] of Object.entries(obj)) result[key] = this.redactValue(value);
219
+ return result;
220
+ }
221
+ redactArray(arr) {
222
+ return arr.map((item) => this.redactValue(item));
223
+ }
224
+ /** One value, redacted: a string masked, a container walked. */
225
+ redactValue(value) {
226
+ const text = asString(value);
227
+ if (text !== void 0) return this.stringRedactor(text);
228
+ if (Array.isArray(value)) return this.redactArray(value);
229
+ const nested = asRecord(value);
230
+ return nested ? this.redactStringValues(nested) : value;
231
+ }
232
+ /**
233
+ * Set the string redactor. Called by autotel init() when attributeRedactor is configured.
234
+ * Can also be called manually.
235
+ */
236
+ setStringRedactor(redactor) {
237
+ this.stringRedactor = redactor;
238
+ }
239
+ /**
240
+ * Send payload to PostHog
241
+ *
242
+ * Maps autotel context to PostHog-specific field names:
243
+ * - autotel.trace_id → $trace_id
244
+ * - autotel.span_id → $span_id
245
+ * - autotel.correlation_id → $correlation_id
246
+ * - autotel.trace_url → $trace_url
247
+ */
248
+ async sendToDestination(payload) {
249
+ await this.ensureInitialized();
250
+ const filteredAttributes = this.config.filterUndefinedValues === false ? payload.attributes : this.filterAttributes(payload.attributes);
251
+ const properties = { ...filteredAttributes };
252
+ if (payload.value !== void 0) properties.value = payload.value;
253
+ if (payload.stepNumber !== void 0) properties.step_number = payload.stepNumber;
254
+ if (payload.stepName !== void 0) properties.step_name = payload.stepName;
255
+ if (payload.autotel) {
256
+ if (payload.autotel.trace_id) properties.$trace_id = payload.autotel.trace_id;
257
+ if (payload.autotel.span_id) properties.$span_id = payload.autotel.span_id;
258
+ if (payload.autotel.correlation_id) properties.$correlation_id = payload.autotel.correlation_id;
259
+ if (payload.autotel.trace_flags) properties.$trace_flags = payload.autotel.trace_flags;
260
+ if (payload.autotel.trace_state) properties.$trace_state = payload.autotel.trace_state;
261
+ if (payload.autotel.trace_url) properties.$trace_url = payload.autotel.trace_url;
262
+ if (payload.autotel.linked_trace_id_count !== void 0) properties.$linked_trace_id_count = payload.autotel.linked_trace_id_count;
263
+ if (payload.autotel.linked_trace_id_hash) properties.$linked_trace_id_hash = payload.autotel.linked_trace_id_hash;
264
+ if (payload.autotel.linked_trace_ids) properties.$linked_trace_ids = payload.autotel.linked_trace_ids;
265
+ }
266
+ const redactedProperties = this.redactProperties(properties);
267
+ if (payload.attributes?.["exception.list"]) try {
268
+ const formatted = formatExceptionForPostHog(JSON.parse(asString(payload.attributes["exception.list"]) ?? "[]"), void 0, this.stringRedactor ?? void 0);
269
+ const exceptionProperties = {
270
+ ...redactedProperties,
271
+ ...formatted
272
+ };
273
+ if (this.isBrowserClient) this.browserClient?.capture("$exception", exceptionProperties);
274
+ else this.posthog?.capture({
275
+ distinctId: this.extractDistinctId(filteredAttributes),
276
+ event: "$exception",
277
+ properties: exceptionProperties
278
+ });
279
+ } catch {}
280
+ const distinctId = this.extractDistinctId(filteredAttributes);
281
+ if (this.isBrowserClient) this.browserClient?.capture(payload.name, redactedProperties);
282
+ else {
283
+ const capturePayload = {
284
+ distinctId,
285
+ event: payload.name,
286
+ properties: redactedProperties
287
+ };
288
+ if (filteredAttributes?.groups) capturePayload.groups = filteredAttributes.groups;
289
+ this.posthog?.capture(capturePayload);
290
+ }
291
+ }
292
+ /**
293
+ * Check if a feature flag is enabled for a user
294
+ *
295
+ * @param flagKey - Feature flag key
296
+ * @param distinctId - User ID or anonymous ID
297
+ * @param options - Feature flag evaluation options
298
+ * @returns true if enabled, false otherwise
299
+ *
300
+ * @example
301
+ * ```typescript
302
+ * const isEnabled = await subscriber.isFeatureEnabled('new-checkout', 'user-123');
303
+ *
304
+ * // With groups
305
+ * const isEnabled = await subscriber.isFeatureEnabled('beta-features', 'user-123', {
306
+ * groups: { company: 'acme-corp' }
307
+ * });
308
+ * ```
309
+ */
310
+ async isFeatureEnabled(flagKey, distinctId, options) {
311
+ if (!this.enabled) return false;
312
+ await this.ensureInitialized();
313
+ try {
314
+ return await this.posthog?.isFeatureEnabled(flagKey, distinctId, this.flagOptions(options)) ?? false;
315
+ } catch (error) {
316
+ this.config.onError?.(toError(error));
317
+ return false;
318
+ }
319
+ }
320
+ /**
321
+ * Get feature flag value for a user
322
+ *
323
+ * @param flagKey - Feature flag key
324
+ * @param distinctId - User ID or anonymous ID
325
+ * @param options - Feature flag evaluation options
326
+ * @returns Flag value (string, boolean, or undefined)
327
+ *
328
+ * @example
329
+ * ```typescript
330
+ * const variant = await subscriber.getFeatureFlag('experiment-variant', 'user-123');
331
+ * // Returns: 'control' | 'test' | 'test-2' | undefined
332
+ *
333
+ * // With person properties
334
+ * const variant = await subscriber.getFeatureFlag('premium-feature', 'user-123', {
335
+ * personProperties: { plan: 'premium' }
336
+ * });
337
+ * ```
338
+ */
339
+ async getFeatureFlag(flagKey, distinctId, options) {
340
+ if (!this.enabled) return void 0;
341
+ await this.ensureInitialized();
342
+ try {
343
+ return await this.posthog?.getFeatureFlag(flagKey, distinctId, this.flagOptions(options));
344
+ } catch (error) {
345
+ this.config.onError?.(toError(error));
346
+ return;
347
+ }
348
+ }
349
+ /**
350
+ * Get all feature flags for a user
351
+ *
352
+ * @param distinctId - User ID or anonymous ID
353
+ * @param options - Feature flag evaluation options
354
+ * @returns Object mapping flag keys to their values
355
+ *
356
+ * @example
357
+ * ```typescript
358
+ * const flags = await subscriber.getAllFlags('user-123');
359
+ * // Returns: { 'new-checkout': true, 'experiment-variant': 'test', ... }
360
+ * ```
361
+ */
362
+ async getAllFlags(distinctId, options) {
363
+ if (!this.enabled) return {};
364
+ await this.ensureInitialized();
365
+ try {
366
+ return await this.posthog?.getAllFlags(distinctId, this.flagOptions(options)) ?? {};
367
+ } catch (error) {
368
+ this.config.onError?.(toError(error));
369
+ return {};
370
+ }
371
+ }
372
+ /**
373
+ * Reload feature flags from PostHog server
374
+ *
375
+ * Call this to refresh feature flag definitions without restarting.
376
+ *
377
+ * @example
378
+ * ```typescript
379
+ * await subscriber.reloadFeatureFlags();
380
+ * ```
381
+ */
382
+ async reloadFeatureFlags() {
383
+ if (!this.enabled) return;
384
+ await this.ensureInitialized();
385
+ try {
386
+ await this.posthog?.reloadFeatureFlags();
387
+ } catch (error) {
388
+ this.config.onError?.(toError(error));
389
+ }
390
+ }
391
+ /**
392
+ * Identify a user and set their properties
393
+ *
394
+ * @param distinctId - User ID
395
+ * @param properties - Person properties ($set, $set_once, or custom properties)
396
+ *
397
+ * @example
398
+ * ```typescript
399
+ * // Set properties (will update existing values)
400
+ * await subscriber.identify('user-123', {
401
+ * $set: {
402
+ * email: 'user@example.com',
403
+ * plan: 'premium'
404
+ * }
405
+ * });
406
+ *
407
+ * // Set properties only once (won't update if already exists)
408
+ * await subscriber.identify('user-123', {
409
+ * $set_once: {
410
+ * signup_date: '2025-01-17'
411
+ * }
412
+ * });
413
+ * ```
414
+ */
415
+ async identify(distinctId, properties) {
416
+ if (!this.enabled) return;
417
+ await this.ensureInitialized();
418
+ try {
419
+ this.posthog?.identify({
420
+ distinctId,
421
+ properties
422
+ });
423
+ } catch (error) {
424
+ this.config.onError?.(toError(error));
425
+ }
426
+ }
427
+ /**
428
+ * Identify a group and set its properties
429
+ *
430
+ * Groups are useful for B2B SaaS to track organizations, teams, or accounts.
431
+ *
432
+ * @param groupType - Type of group (e.g., 'company', 'organization', 'team')
433
+ * @param groupKey - Unique identifier for the group
434
+ * @param properties - Group properties
435
+ *
436
+ * @example
437
+ * ```typescript
438
+ * await subscriber.groupIdentify('company', 'acme-corp', {
439
+ * $set: {
440
+ * name: 'Acme Corporation',
441
+ * industry: 'saas',
442
+ * employees: 500,
443
+ * plan: 'enterprise'
444
+ * }
445
+ * });
446
+ * ```
447
+ */
448
+ async groupIdentify(groupType, groupKey, properties) {
449
+ if (!this.enabled) return;
450
+ await this.ensureInitialized();
451
+ try {
452
+ this.posthog?.groupIdentify({
453
+ groupType,
454
+ groupKey: String(groupKey),
455
+ properties
456
+ });
457
+ } catch (error) {
458
+ this.config.onError?.(toError(error));
459
+ }
460
+ }
461
+ /**
462
+ * Track an event with group context
463
+ *
464
+ * Use this to associate events with groups (e.g., organizations).
465
+ *
466
+ * @param name - Event name
467
+ * @param attributes - Event attributes
468
+ * @param groups - Group context (e.g., { company: 'acme-corp' })
469
+ *
470
+ * @example
471
+ * ```typescript
472
+ * await subscriber.trackEventWithGroups('feature.used', {
473
+ * userId: 'user-123',
474
+ * feature: 'advanced-events'
475
+ * }, {
476
+ * company: 'acme-corp'
477
+ * });
478
+ * ```
479
+ */
480
+ async trackEventWithGroups(name, attributes, groups) {
481
+ if (!this.enabled) return;
482
+ await this.ensureInitialized();
483
+ const eventAttributes = { ...attributes };
484
+ if (groups) eventAttributes.groups = groups;
485
+ await this.trackEvent(name, eventAttributes);
486
+ }
487
+ /**
488
+ * Capture an exception and send to PostHog error tracking.
489
+ *
490
+ * If using browser client (window.posthog), delegates to its captureException.
491
+ * Otherwise, formats and sends via posthog-node capture API.
492
+ */
493
+ async captureException(error, options) {
494
+ if (!this.enabled) return;
495
+ await this.ensureInitialized();
496
+ try {
497
+ if (this.isBrowserClient) {
498
+ const browserProps = options?.additionalProperties ? this.redactProperties(options.additionalProperties) : void 0;
499
+ this.browserClient?.captureException?.(error, browserProps);
500
+ return;
501
+ }
502
+ const properties = {
503
+ ...formatExceptionForPostHog(errorToExceptionList(error, this.stringRedactor ?? void 0), "node:javascript", this.stringRedactor ?? void 0),
504
+ ...options?.additionalProperties
505
+ };
506
+ this.posthog?.capture({
507
+ distinctId: options?.distinctId || "anonymous",
508
+ event: "$exception",
509
+ properties: this.redactProperties(properties)
510
+ });
511
+ } catch (error_) {
512
+ this.config.onError?.(toError(error_));
513
+ }
514
+ }
515
+ /**
516
+ * Flush pending events and clean up resources
517
+ */
518
+ async shutdown() {
519
+ await super.shutdown();
520
+ await this.ensureInitialized();
521
+ if (this.posthog) try {
522
+ await this.posthog.shutdown();
523
+ } catch (error) {
524
+ this.config.onError?.(toError(error));
525
+ }
526
+ }
527
+ /**
528
+ * Handle errors with custom error handler
529
+ */
530
+ handleError(error, payload) {
531
+ this.config.onError?.(error);
532
+ if (this.config.onErrorWithContext) this.config.onErrorWithContext({
533
+ error,
534
+ eventName: payload.name,
535
+ eventType: payload.type,
536
+ attributes: payload.attributes,
537
+ subscriberName: this.name
538
+ });
539
+ super.handleError(error, payload);
540
+ }
541
+ };
542
+ //#endregion
543
+ exports.PostHogSubscriber = PostHogSubscriber;