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