posthog-node 5.8.8 → 5.9.1

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.
Files changed (99) hide show
  1. package/dist/{index.d.ts → client.d.ts} +7 -378
  2. package/dist/client.d.ts.map +1 -0
  3. package/dist/client.js +480 -0
  4. package/dist/client.mjs +436 -0
  5. package/dist/entrypoints/index.edge.d.ts +6 -0
  6. package/dist/entrypoints/index.edge.d.ts.map +1 -0
  7. package/dist/entrypoints/index.edge.js +96 -0
  8. package/dist/entrypoints/index.edge.mjs +19 -0
  9. package/dist/entrypoints/index.node.d.ts +6 -0
  10. package/dist/entrypoints/index.node.d.ts.map +1 -0
  11. package/dist/entrypoints/index.node.js +107 -0
  12. package/dist/entrypoints/index.node.mjs +24 -0
  13. package/dist/exports.d.ts +4 -0
  14. package/dist/exports.d.ts.map +1 -0
  15. package/dist/exports.js +78 -0
  16. package/dist/exports.mjs +3 -0
  17. package/dist/extensions/error-tracking/autocapture.d.ts +4 -0
  18. package/dist/extensions/error-tracking/autocapture.d.ts.map +1 -0
  19. package/dist/extensions/error-tracking/autocapture.js +68 -0
  20. package/dist/extensions/error-tracking/autocapture.mjs +31 -0
  21. package/dist/extensions/error-tracking/index.d.ts +19 -0
  22. package/dist/extensions/error-tracking/index.d.ts.map +1 -0
  23. package/dist/extensions/error-tracking/index.js +97 -0
  24. package/dist/extensions/error-tracking/index.mjs +63 -0
  25. package/dist/extensions/error-tracking/modifiers/context-lines.node.d.ts +5 -0
  26. package/dist/extensions/error-tracking/modifiers/context-lines.node.d.ts.map +1 -0
  27. package/dist/extensions/error-tracking/modifiers/context-lines.node.js +227 -0
  28. package/dist/extensions/error-tracking/modifiers/context-lines.node.mjs +187 -0
  29. package/dist/extensions/error-tracking/modifiers/module.node.d.ts +3 -0
  30. package/dist/extensions/error-tracking/modifiers/module.node.d.ts.map +1 -0
  31. package/dist/extensions/error-tracking/modifiers/module.node.js +64 -0
  32. package/dist/extensions/error-tracking/modifiers/module.node.mjs +30 -0
  33. package/dist/extensions/express.d.ts +17 -0
  34. package/dist/extensions/express.d.ts.map +1 -0
  35. package/dist/extensions/express.js +61 -0
  36. package/dist/extensions/express.mjs +17 -0
  37. package/dist/extensions/feature-flags/crypto-helpers.d.ts +3 -0
  38. package/dist/extensions/feature-flags/crypto-helpers.d.ts.map +1 -0
  39. package/dist/extensions/feature-flags/crypto-helpers.js +77 -0
  40. package/dist/extensions/feature-flags/crypto-helpers.mjs +22 -0
  41. package/dist/extensions/feature-flags/crypto.d.ts +2 -0
  42. package/dist/extensions/feature-flags/crypto.d.ts.map +1 -0
  43. package/dist/extensions/feature-flags/crypto.js +47 -0
  44. package/dist/extensions/feature-flags/crypto.mjs +13 -0
  45. package/dist/extensions/feature-flags/feature-flags.d.ts +89 -0
  46. package/dist/extensions/feature-flags/feature-flags.d.ts.map +1 -0
  47. package/dist/extensions/feature-flags/feature-flags.js +529 -0
  48. package/dist/extensions/feature-flags/feature-flags.mjs +483 -0
  49. package/dist/extensions/feature-flags/lazy.d.ts +24 -0
  50. package/dist/extensions/feature-flags/lazy.d.ts.map +1 -0
  51. package/dist/extensions/feature-flags/lazy.js +60 -0
  52. package/dist/extensions/feature-flags/lazy.mjs +26 -0
  53. package/dist/extensions/sentry-integration.d.ts +54 -0
  54. package/dist/extensions/sentry-integration.d.ts.map +1 -0
  55. package/dist/extensions/sentry-integration.js +113 -0
  56. package/dist/extensions/sentry-integration.mjs +73 -0
  57. package/dist/storage-memory.d.ts +7 -0
  58. package/dist/storage-memory.d.ts.map +1 -0
  59. package/dist/storage-memory.js +46 -0
  60. package/dist/storage-memory.mjs +12 -0
  61. package/dist/types.d.ts +253 -0
  62. package/dist/types.d.ts.map +1 -0
  63. package/dist/types.js +18 -0
  64. package/dist/types.mjs +0 -0
  65. package/dist/utils/logger.d.ts +3 -0
  66. package/dist/utils/logger.d.ts.map +1 -0
  67. package/dist/utils/logger.js +63 -0
  68. package/dist/utils/logger.mjs +29 -0
  69. package/dist/version.d.ts +2 -0
  70. package/dist/version.d.ts.map +1 -0
  71. package/dist/version.js +36 -0
  72. package/dist/version.mjs +2 -0
  73. package/package.json +32 -31
  74. package/src/client.ts +1532 -0
  75. package/src/entrypoints/index.edge.ts +22 -0
  76. package/src/entrypoints/index.node.ts +26 -0
  77. package/src/exports.ts +3 -0
  78. package/src/extensions/error-tracking/autocapture.ts +67 -0
  79. package/src/extensions/error-tracking/index.ts +104 -0
  80. package/src/extensions/error-tracking/modifiers/context-lines.node.ts +404 -0
  81. package/src/extensions/error-tracking/modifiers/module.node.ts +68 -0
  82. package/src/extensions/express.ts +40 -0
  83. package/src/extensions/feature-flags/crypto-helpers.ts +36 -0
  84. package/src/extensions/feature-flags/crypto.ts +22 -0
  85. package/src/extensions/feature-flags/feature-flags.ts +1003 -0
  86. package/src/extensions/feature-flags/lazy.ts +55 -0
  87. package/src/extensions/sentry-integration.ts +216 -0
  88. package/src/storage-memory.ts +13 -0
  89. package/src/types.ts +294 -0
  90. package/src/utils/logger.ts +39 -0
  91. package/src/version.ts +1 -0
  92. package/dist/edge/index.cjs +0 -3150
  93. package/dist/edge/index.cjs.map +0 -1
  94. package/dist/edge/index.mjs +0 -3144
  95. package/dist/edge/index.mjs.map +0 -1
  96. package/dist/node/index.cjs +0 -3556
  97. package/dist/node/index.cjs.map +0 -1
  98. package/dist/node/index.mjs +0 -3550
  99. package/dist/node/index.mjs.map +0 -1
@@ -1,3550 +0,0 @@
1
- import { dirname, posix, sep } from 'path';
2
- import { createReadStream } from 'node:fs';
3
- import { createInterface } from 'node:readline';
4
- import { PostHogCoreStateless, getFeatureFlagValue, safeSetTimeout as safeSetTimeout$1 } from '@posthog/core';
5
-
6
- /**
7
- * @file Adapted from [posthog-js](https://github.com/PostHog/posthog-js/blob/8157df935a4d0e71d2fefef7127aa85ee51c82d1/src/extensions/sentry-integration.ts) with modifications for the Node SDK.
8
- */
9
- /**
10
- * Integrate Sentry with PostHog. This will add a direct link to the person in Sentry, and an $exception event in PostHog.
11
- *
12
- * ### Usage
13
- *
14
- * Sentry.init({
15
- * dsn: 'https://example',
16
- * integrations: [
17
- * new PostHogSentryIntegration(posthog)
18
- * ]
19
- * })
20
- *
21
- * Sentry.setTag(PostHogSentryIntegration.POSTHOG_ID_TAG, 'some distinct id');
22
- *
23
- * @param {Object} [posthog] The posthog object
24
- * @param {string} [organization] Optional: The Sentry organization, used to send a direct link from PostHog to Sentry
25
- * @param {Number} [projectId] Optional: The Sentry project id, used to send a direct link from PostHog to Sentry
26
- * @param {string} [prefix] Optional: Url of a self-hosted sentry instance (default: https://sentry.io/organizations/)
27
- * @param {SeverityLevel[] | '*'} [severityAllowList] Optional: send events matching the provided levels. Use '*' to send all events (default: ['error'])
28
- * @param {boolean} [sendExceptionsToPostHog] Optional: capture exceptions as events in PostHog (default: true)
29
- */
30
- const NAME = 'posthog-node';
31
- function createEventProcessor(_posthog, {
32
- organization,
33
- projectId,
34
- prefix,
35
- severityAllowList = ['error'],
36
- sendExceptionsToPostHog = true
37
- } = {}) {
38
- return event => {
39
- const shouldProcessLevel = severityAllowList === '*' || severityAllowList.includes(event.level);
40
- if (!shouldProcessLevel) {
41
- return event;
42
- }
43
- if (!event.tags) {
44
- event.tags = {};
45
- }
46
- // Get the PostHog user ID from a specific tag, which users can set on their Sentry scope as they need.
47
- const userId = event.tags[PostHogSentryIntegration.POSTHOG_ID_TAG];
48
- if (userId === undefined) {
49
- // If we can't find a user ID, don't bother linking the event. We won't be able to send anything meaningful to PostHog without it.
50
- return event;
51
- }
52
- const uiHost = _posthog.options.host ?? 'https://us.i.posthog.com';
53
- const personUrl = new URL(`/project/${_posthog.apiKey}/person/${userId}`, uiHost).toString();
54
- event.tags['PostHog Person URL'] = personUrl;
55
- const exceptions = event.exception?.values || [];
56
- const exceptionList = exceptions.map(exception => ({
57
- ...exception,
58
- stacktrace: exception.stacktrace ? {
59
- ...exception.stacktrace,
60
- type: 'raw',
61
- frames: (exception.stacktrace.frames || []).map(frame => {
62
- return {
63
- ...frame,
64
- platform: 'node:javascript'
65
- };
66
- })
67
- } : undefined
68
- }));
69
- const properties = {
70
- // PostHog Exception Properties,
71
- $exception_message: exceptions[0]?.value || event.message,
72
- $exception_type: exceptions[0]?.type,
73
- $exception_personURL: personUrl,
74
- $exception_level: event.level,
75
- $exception_list: exceptionList,
76
- // Sentry Exception Properties
77
- $sentry_event_id: event.event_id,
78
- $sentry_exception: event.exception,
79
- $sentry_exception_message: exceptions[0]?.value || event.message,
80
- $sentry_exception_type: exceptions[0]?.type,
81
- $sentry_tags: event.tags
82
- };
83
- if (organization && projectId) {
84
- properties['$sentry_url'] = (prefix || 'https://sentry.io/organizations/') + organization + '/issues/?project=' + projectId + '&query=' + event.event_id;
85
- }
86
- if (sendExceptionsToPostHog) {
87
- _posthog.capture({
88
- event: '$exception',
89
- distinctId: userId,
90
- properties
91
- });
92
- }
93
- return event;
94
- };
95
- }
96
- // V8 integration - function based
97
- function sentryIntegration(_posthog, options) {
98
- const processor = createEventProcessor(_posthog, options);
99
- return {
100
- name: NAME,
101
- processEvent(event) {
102
- return processor(event);
103
- }
104
- };
105
- }
106
- // V7 integration - class based
107
- class PostHogSentryIntegration {
108
- constructor(_posthog, organization, prefix, severityAllowList, sendExceptionsToPostHog) {
109
- this.name = NAME;
110
- // setupOnce gets called by Sentry when it intializes the plugin
111
- this.name = NAME;
112
- this.setupOnce = function (addGlobalEventProcessor, getCurrentHub) {
113
- const projectId = getCurrentHub()?.getClient()?.getDsn()?.projectId;
114
- addGlobalEventProcessor(createEventProcessor(_posthog, {
115
- organization,
116
- projectId,
117
- prefix,
118
- severityAllowList,
119
- sendExceptionsToPostHog: sendExceptionsToPostHog ?? true
120
- }));
121
- };
122
- }
123
- }
124
- PostHogSentryIntegration.POSTHOG_ID_TAG = 'posthog_distinct_id';
125
-
126
- /*! For license information please see uuidv7.mjs.LICENSE.txt */
127
- /**
128
- * uuidv7: An experimental implementation of the proposed UUID Version 7
129
- *
130
- * @license Apache-2.0
131
- * @copyright 2021-2023 LiosK
132
- * @packageDocumentation
133
- */ const DIGITS = "0123456789abcdef";
134
- class UUID {
135
- static ofInner(bytes) {
136
- if (16 === bytes.length) return new UUID(bytes);
137
- throw new TypeError("not 128-bit length");
138
- }
139
- static fromFieldsV7(unixTsMs, randA, randBHi, randBLo) {
140
- if (!Number.isInteger(unixTsMs) || !Number.isInteger(randA) || !Number.isInteger(randBHi) || !Number.isInteger(randBLo) || unixTsMs < 0 || randA < 0 || randBHi < 0 || randBLo < 0 || unixTsMs > 0xffffffffffff || randA > 0xfff || randBHi > 0x3fffffff || randBLo > 0xffffffff) throw new RangeError("invalid field value");
141
- const bytes = new Uint8Array(16);
142
- bytes[0] = unixTsMs / 2 ** 40;
143
- bytes[1] = unixTsMs / 2 ** 32;
144
- bytes[2] = unixTsMs / 2 ** 24;
145
- bytes[3] = unixTsMs / 2 ** 16;
146
- bytes[4] = unixTsMs / 256;
147
- bytes[5] = unixTsMs;
148
- bytes[6] = 0x70 | randA >>> 8;
149
- bytes[7] = randA;
150
- bytes[8] = 0x80 | randBHi >>> 24;
151
- bytes[9] = randBHi >>> 16;
152
- bytes[10] = randBHi >>> 8;
153
- bytes[11] = randBHi;
154
- bytes[12] = randBLo >>> 24;
155
- bytes[13] = randBLo >>> 16;
156
- bytes[14] = randBLo >>> 8;
157
- bytes[15] = randBLo;
158
- return new UUID(bytes);
159
- }
160
- static parse(uuid) {
161
- let hex;
162
- switch(uuid.length){
163
- case 32:
164
- var _exec;
165
- hex = null == (_exec = /^[0-9a-f]{32}$/i.exec(uuid)) ? void 0 : _exec[0];
166
- break;
167
- case 36:
168
- var _exec1;
169
- hex = null == (_exec1 = /^([0-9a-f]{8})-([0-9a-f]{4})-([0-9a-f]{4})-([0-9a-f]{4})-([0-9a-f]{12})$/i.exec(uuid)) ? void 0 : _exec1.slice(1, 6).join("");
170
- break;
171
- case 38:
172
- var _exec2;
173
- hex = null == (_exec2 = /^\{([0-9a-f]{8})-([0-9a-f]{4})-([0-9a-f]{4})-([0-9a-f]{4})-([0-9a-f]{12})\}$/i.exec(uuid)) ? void 0 : _exec2.slice(1, 6).join("");
174
- break;
175
- case 45:
176
- var _exec3;
177
- hex = null == (_exec3 = /^urn:uuid:([0-9a-f]{8})-([0-9a-f]{4})-([0-9a-f]{4})-([0-9a-f]{4})-([0-9a-f]{12})$/i.exec(uuid)) ? void 0 : _exec3.slice(1, 6).join("");
178
- break;
179
- }
180
- if (hex) {
181
- const inner = new Uint8Array(16);
182
- for(let i = 0; i < 16; i += 4){
183
- const n = parseInt(hex.substring(2 * i, 2 * i + 8), 16);
184
- inner[i + 0] = n >>> 24;
185
- inner[i + 1] = n >>> 16;
186
- inner[i + 2] = n >>> 8;
187
- inner[i + 3] = n;
188
- }
189
- return new UUID(inner);
190
- }
191
- throw new SyntaxError("could not parse UUID string");
192
- }
193
- toString() {
194
- let text = "";
195
- for(let i = 0; i < this.bytes.length; i++){
196
- text += DIGITS.charAt(this.bytes[i] >>> 4);
197
- text += DIGITS.charAt(0xf & this.bytes[i]);
198
- if (3 === i || 5 === i || 7 === i || 9 === i) text += "-";
199
- }
200
- return text;
201
- }
202
- toHex() {
203
- let text = "";
204
- for(let i = 0; i < this.bytes.length; i++){
205
- text += DIGITS.charAt(this.bytes[i] >>> 4);
206
- text += DIGITS.charAt(0xf & this.bytes[i]);
207
- }
208
- return text;
209
- }
210
- toJSON() {
211
- return this.toString();
212
- }
213
- getVariant() {
214
- const n = this.bytes[8] >>> 4;
215
- if (n < 0) throw new Error("unreachable");
216
- if (n <= 7) return this.bytes.every((e)=>0 === e) ? "NIL" : "VAR_0";
217
- if (n <= 11) return "VAR_10";
218
- if (n <= 13) return "VAR_110";
219
- if (n <= 15) return this.bytes.every((e)=>0xff === e) ? "MAX" : "VAR_RESERVED";
220
- else throw new Error("unreachable");
221
- }
222
- getVersion() {
223
- return "VAR_10" === this.getVariant() ? this.bytes[6] >>> 4 : void 0;
224
- }
225
- clone() {
226
- return new UUID(this.bytes.slice(0));
227
- }
228
- equals(other) {
229
- return 0 === this.compareTo(other);
230
- }
231
- compareTo(other) {
232
- for(let i = 0; i < 16; i++){
233
- const diff = this.bytes[i] - other.bytes[i];
234
- if (0 !== diff) return Math.sign(diff);
235
- }
236
- return 0;
237
- }
238
- constructor(bytes){
239
- this.bytes = bytes;
240
- }
241
- }
242
- class V7Generator {
243
- generate() {
244
- return this.generateOrResetCore(Date.now(), 10000);
245
- }
246
- generateOrAbort() {
247
- return this.generateOrAbortCore(Date.now(), 10000);
248
- }
249
- generateOrResetCore(unixTsMs, rollbackAllowance) {
250
- let value = this.generateOrAbortCore(unixTsMs, rollbackAllowance);
251
- if (void 0 === value) {
252
- this.timestamp = 0;
253
- value = this.generateOrAbortCore(unixTsMs, rollbackAllowance);
254
- }
255
- return value;
256
- }
257
- generateOrAbortCore(unixTsMs, rollbackAllowance) {
258
- const MAX_COUNTER = 0x3ffffffffff;
259
- if (!Number.isInteger(unixTsMs) || unixTsMs < 1 || unixTsMs > 0xffffffffffff) throw new RangeError("`unixTsMs` must be a 48-bit positive integer");
260
- if (rollbackAllowance < 0 || rollbackAllowance > 0xffffffffffff) throw new RangeError("`rollbackAllowance` out of reasonable range");
261
- if (unixTsMs > this.timestamp) {
262
- this.timestamp = unixTsMs;
263
- this.resetCounter();
264
- } else {
265
- if (!(unixTsMs + rollbackAllowance >= this.timestamp)) return;
266
- this.counter++;
267
- if (this.counter > MAX_COUNTER) {
268
- this.timestamp++;
269
- this.resetCounter();
270
- }
271
- }
272
- return UUID.fromFieldsV7(this.timestamp, Math.trunc(this.counter / 2 ** 30), this.counter & 2 ** 30 - 1, this.random.nextUint32());
273
- }
274
- resetCounter() {
275
- this.counter = 0x400 * this.random.nextUint32() + (0x3ff & this.random.nextUint32());
276
- }
277
- generateV4() {
278
- const bytes = new Uint8Array(Uint32Array.of(this.random.nextUint32(), this.random.nextUint32(), this.random.nextUint32(), this.random.nextUint32()).buffer);
279
- bytes[6] = 0x40 | bytes[6] >>> 4;
280
- bytes[8] = 0x80 | bytes[8] >>> 2;
281
- return UUID.ofInner(bytes);
282
- }
283
- constructor(randomNumberGenerator){
284
- this.timestamp = 0;
285
- this.counter = 0;
286
- this.random = null != randomNumberGenerator ? randomNumberGenerator : getDefaultRandom();
287
- }
288
- }
289
- const getDefaultRandom = ()=>({
290
- nextUint32: ()=>0x10000 * Math.trunc(0x10000 * Math.random()) + Math.trunc(0x10000 * Math.random())
291
- });
292
- let defaultGenerator;
293
- const uuidv7 = ()=>uuidv7obj().toString();
294
- const uuidv7obj = ()=>(defaultGenerator || (defaultGenerator = new V7Generator())).generate();
295
-
296
- // Portions of this file are derived from getsentry/sentry-javascript by Software, Inc. dba Sentry
297
- // Licensed under the MIT License
298
- function makeUncaughtExceptionHandler(captureFn, onFatalFn) {
299
- let calledFatalError = false;
300
- return Object.assign(error => {
301
- // Attaching a listener to `uncaughtException` will prevent the node process from exiting. We generally do not
302
- // want to alter this behaviour so we check for other listeners that users may have attached themselves and adjust
303
- // exit behaviour of the SDK accordingly:
304
- // - If other listeners are attached, do not exit.
305
- // - If the only listener attached is ours, exit.
306
- const userProvidedListenersCount = global.process.listeners('uncaughtException').filter(listener => {
307
- // There are 2 listeners we ignore:
308
- return (
309
- // as soon as we're using domains this listener is attached by node itself
310
- listener.name !== 'domainUncaughtExceptionClear' &&
311
- // the handler we register in this integration
312
- listener._posthogErrorHandler !== true
313
- );
314
- }).length;
315
- const processWouldExit = userProvidedListenersCount === 0;
316
- captureFn(error, {
317
- mechanism: {
318
- type: 'onuncaughtexception',
319
- handled: false
320
- }
321
- });
322
- if (!calledFatalError && processWouldExit) {
323
- calledFatalError = true;
324
- onFatalFn(error);
325
- }
326
- }, {
327
- _posthogErrorHandler: true
328
- });
329
- }
330
- function addUncaughtExceptionListener(captureFn, onFatalFn) {
331
- global.process.on('uncaughtException', makeUncaughtExceptionHandler(captureFn, onFatalFn));
332
- }
333
- function addUnhandledRejectionListener(captureFn) {
334
- global.process.on('unhandledRejection', reason => {
335
- return captureFn(reason, {
336
- mechanism: {
337
- type: 'onunhandledrejection',
338
- handled: false
339
- }
340
- });
341
- });
342
- }
343
-
344
- // Portions of this file are derived from getsentry/sentry-javascript by Software, Inc. dba Sentry
345
- // Licensed under the MIT License
346
- let parsedStackResults;
347
- let lastKeysCount;
348
- let cachedFilenameChunkIds;
349
- function getFilenameToChunkIdMap(stackParser) {
350
- const chunkIdMap = globalThis._posthogChunkIds;
351
- if (!chunkIdMap) {
352
- return null;
353
- }
354
- const chunkIdKeys = Object.keys(chunkIdMap);
355
- if (cachedFilenameChunkIds && chunkIdKeys.length === lastKeysCount) {
356
- return cachedFilenameChunkIds;
357
- }
358
- lastKeysCount = chunkIdKeys.length;
359
- cachedFilenameChunkIds = chunkIdKeys.reduce((acc, stackKey) => {
360
- if (!parsedStackResults) {
361
- parsedStackResults = {};
362
- }
363
- const result = parsedStackResults[stackKey];
364
- if (result) {
365
- acc[result[0]] = result[1];
366
- } else {
367
- const parsedStack = stackParser(stackKey);
368
- for (let i = parsedStack.length - 1; i >= 0; i--) {
369
- const stackFrame = parsedStack[i];
370
- const filename = stackFrame?.filename;
371
- const chunkId = chunkIdMap[stackKey];
372
- if (filename && chunkId) {
373
- acc[filename] = chunkId;
374
- parsedStackResults[stackKey] = [filename, chunkId];
375
- break;
376
- }
377
- }
378
- }
379
- return acc;
380
- }, {});
381
- return cachedFilenameChunkIds;
382
- }
383
-
384
- // Portions of this file are derived from getsentry/sentry-javascript by Software, Inc. dba Sentry
385
- // Licensed under the MIT License
386
- function isEvent(candidate) {
387
- return typeof Event !== 'undefined' && isInstanceOf(candidate, Event);
388
- }
389
- function isPlainObject(candidate) {
390
- return isBuiltin(candidate, 'Object');
391
- }
392
- function isError(candidate) {
393
- switch (Object.prototype.toString.call(candidate)) {
394
- case '[object Error]':
395
- case '[object Exception]':
396
- case '[object DOMException]':
397
- case '[object WebAssembly.Exception]':
398
- return true;
399
- default:
400
- return isInstanceOf(candidate, Error);
401
- }
402
- }
403
- function isInstanceOf(candidate, base) {
404
- try {
405
- return candidate instanceof base;
406
- } catch {
407
- return false;
408
- }
409
- }
410
- function isErrorEvent(event) {
411
- return isBuiltin(event, 'ErrorEvent');
412
- }
413
- function isBuiltin(candidate, className) {
414
- return Object.prototype.toString.call(candidate) === `[object ${className}]`;
415
- }
416
-
417
- // Portions of this file are derived from getsentry/sentry-javascript by Software, Inc. dba Sentry
418
- // Licensed under the MIT License
419
- async function propertiesFromUnknownInput(stackParser, frameModifiers, input, hint) {
420
- const providedMechanism = hint && hint.mechanism;
421
- const mechanism = providedMechanism || {
422
- handled: true,
423
- type: 'generic'
424
- };
425
- const errorList = getErrorList(mechanism, input, hint);
426
- const exceptionList = await Promise.all(errorList.map(async error => {
427
- const exception = await exceptionFromError(stackParser, frameModifiers, error);
428
- exception.value = exception.value || '';
429
- exception.type = exception.type || 'Error';
430
- exception.mechanism = mechanism;
431
- return exception;
432
- }));
433
- const properties = {
434
- $exception_list: exceptionList
435
- };
436
- return properties;
437
- }
438
- // Flatten error causes into a list of errors
439
- // See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/cause
440
- function getErrorList(mechanism, input, hint) {
441
- const error = getError(mechanism, input, hint);
442
- if (error.cause) {
443
- return [error, ...getErrorList(mechanism, error.cause, hint)];
444
- }
445
- return [error];
446
- }
447
- function getError(mechanism, exception, hint) {
448
- if (isError(exception)) {
449
- return exception;
450
- }
451
- mechanism.synthetic = true;
452
- if (isPlainObject(exception)) {
453
- const errorFromProp = getErrorPropertyFromObject(exception);
454
- if (errorFromProp) {
455
- return errorFromProp;
456
- }
457
- const message = getMessageForObject(exception);
458
- const ex = hint?.syntheticException || new Error(message);
459
- ex.message = message;
460
- return ex;
461
- }
462
- // This handles when someone does: `throw "something awesome";`
463
- // We use synthesized Error here so we can extract a (rough) stack trace.
464
- const ex = hint?.syntheticException || new Error(exception);
465
- ex.message = `${exception}`;
466
- return ex;
467
- }
468
- /** If a plain object has a property that is an `Error`, return this error. */
469
- function getErrorPropertyFromObject(obj) {
470
- for (const prop in obj) {
471
- if (Object.prototype.hasOwnProperty.call(obj, prop)) {
472
- const value = obj[prop];
473
- if (isError(value)) {
474
- return value;
475
- }
476
- }
477
- }
478
- return undefined;
479
- }
480
- function getMessageForObject(exception) {
481
- if ('name' in exception && typeof exception.name === 'string') {
482
- let message = `'${exception.name}' captured as exception`;
483
- if ('message' in exception && typeof exception.message === 'string') {
484
- message += ` with message '${exception.message}'`;
485
- }
486
- return message;
487
- } else if ('message' in exception && typeof exception.message === 'string') {
488
- return exception.message;
489
- }
490
- const keys = extractExceptionKeysForMessage(exception);
491
- // Some ErrorEvent instances do not have an `error` property, which is why they are not handled before
492
- // We still want to try to get a decent message for these cases
493
- if (isErrorEvent(exception)) {
494
- return `Event \`ErrorEvent\` captured as exception with message \`${exception.message}\``;
495
- }
496
- const className = getObjectClassName(exception);
497
- return `${className && className !== 'Object' ? `'${className}'` : 'Object'} captured as exception with keys: ${keys}`;
498
- }
499
- function getObjectClassName(obj) {
500
- try {
501
- const prototype = Object.getPrototypeOf(obj);
502
- return prototype ? prototype.constructor.name : undefined;
503
- } catch (e) {
504
- // ignore errors here
505
- }
506
- }
507
- /**
508
- * Given any captured exception, extract its keys and create a sorted
509
- * and truncated list that will be used inside the event message.
510
- * eg. `Non-error exception captured with keys: foo, bar, baz`
511
- */
512
- function extractExceptionKeysForMessage(exception, maxLength = 40) {
513
- const keys = Object.keys(convertToPlainObject(exception));
514
- keys.sort();
515
- const firstKey = keys[0];
516
- if (!firstKey) {
517
- return '[object has no keys]';
518
- }
519
- if (firstKey.length >= maxLength) {
520
- return truncate(firstKey, maxLength);
521
- }
522
- for (let includedKeys = keys.length; includedKeys > 0; includedKeys--) {
523
- const serialized = keys.slice(0, includedKeys).join(', ');
524
- if (serialized.length > maxLength) {
525
- continue;
526
- }
527
- if (includedKeys === keys.length) {
528
- return serialized;
529
- }
530
- return truncate(serialized, maxLength);
531
- }
532
- return '';
533
- }
534
- function truncate(str, max = 0) {
535
- if (typeof str !== 'string' || max === 0) {
536
- return str;
537
- }
538
- return str.length <= max ? str : `${str.slice(0, max)}...`;
539
- }
540
- /**
541
- * Transforms any `Error` or `Event` into a plain object with all of their enumerable properties, and some of their
542
- * non-enumerable properties attached.
543
- *
544
- * @param value Initial source that we have to transform in order for it to be usable by the serializer
545
- * @returns An Event or Error turned into an object - or the value argument itself, when value is neither an Event nor
546
- * an Error.
547
- */
548
- function convertToPlainObject(value) {
549
- if (isError(value)) {
550
- return {
551
- message: value.message,
552
- name: value.name,
553
- stack: value.stack,
554
- ...getOwnProperties(value)
555
- };
556
- } else if (isEvent(value)) {
557
- const newObj = {
558
- type: value.type,
559
- target: serializeEventTarget(value.target),
560
- currentTarget: serializeEventTarget(value.currentTarget),
561
- ...getOwnProperties(value)
562
- };
563
- // TODO: figure out why this fails typing (I think CustomEvent is only supported in Node 19 onwards)
564
- // if (typeof CustomEvent !== 'undefined' && isInstanceOf(value, CustomEvent)) {
565
- // newObj.detail = (value as unknown as CustomEvent).detail
566
- // }
567
- return newObj;
568
- } else {
569
- return value;
570
- }
571
- }
572
- /** Filters out all but an object's own properties */
573
- function getOwnProperties(obj) {
574
- if (typeof obj === 'object' && obj !== null) {
575
- const extractedProps = {};
576
- for (const property in obj) {
577
- if (Object.prototype.hasOwnProperty.call(obj, property)) {
578
- extractedProps[property] = obj[property];
579
- }
580
- }
581
- return extractedProps;
582
- } else {
583
- return {};
584
- }
585
- }
586
- /** Creates a string representation of the target of an `Event` object */
587
- function serializeEventTarget(target) {
588
- try {
589
- return Object.prototype.toString.call(target);
590
- } catch (_oO) {
591
- return '<unknown>';
592
- }
593
- }
594
- /**
595
- * Extracts stack frames from the error and builds an Exception
596
- */
597
- async function exceptionFromError(stackParser, frameModifiers, error) {
598
- const exception = {
599
- type: error.name || error.constructor.name,
600
- value: error.message
601
- };
602
- let frames = parseStackFrames(stackParser, error);
603
- for (const modifier of frameModifiers) {
604
- frames = await modifier(frames);
605
- }
606
- if (frames.length) {
607
- exception.stacktrace = {
608
- frames,
609
- type: 'raw'
610
- };
611
- }
612
- return exception;
613
- }
614
- /**
615
- * Extracts stack frames from the error.stack string
616
- */
617
- function parseStackFrames(stackParser, error) {
618
- return applyChunkIds(stackParser(error.stack || '', 1), stackParser);
619
- }
620
- function applyChunkIds(frames, parser) {
621
- const filenameChunkIdMap = getFilenameToChunkIdMap(parser);
622
- frames.forEach(frame => {
623
- if (frame.filename && filenameChunkIdMap) {
624
- frame.chunk_id = filenameChunkIdMap[frame.filename];
625
- }
626
- });
627
- return frames;
628
- }
629
-
630
- const ObjProto = Object.prototype;
631
- const type_utils_toString = ObjProto.toString;
632
- const isNumber = (x)=>'[object Number]' == type_utils_toString.call(x);
633
-
634
- function clampToRange(value, min, max, logger, fallbackValue) {
635
- if (min > max) {
636
- logger.warn('min cannot be greater than max.');
637
- min = max;
638
- }
639
- if (isNumber(value)) if (value > max) {
640
- logger.warn(' cannot be greater than max: ' + max + '. Using max value instead.');
641
- return max;
642
- } else {
643
- if (!(value < min)) return value;
644
- logger.warn(' cannot be less than min: ' + min + '. Using min value instead.');
645
- return min;
646
- }
647
- logger.warn(' must be a number. using max or fallback. max: ' + max + ', fallback: ' + fallbackValue);
648
- return clampToRange(max, min, max, logger);
649
- }
650
-
651
- class BucketedRateLimiter {
652
- stop() {
653
- if (this._removeInterval) {
654
- clearInterval(this._removeInterval);
655
- this._removeInterval = void 0;
656
- }
657
- }
658
- constructor(_options){
659
- this._options = _options;
660
- this._buckets = {};
661
- this._refillBuckets = ()=>{
662
- Object.keys(this._buckets).forEach((key)=>{
663
- const newTokens = this._getBucket(key) + this._refillRate;
664
- if (newTokens >= this._bucketSize) delete this._buckets[key];
665
- else this._setBucket(key, newTokens);
666
- });
667
- };
668
- this._getBucket = (key)=>this._buckets[String(key)];
669
- this._setBucket = (key, value)=>{
670
- this._buckets[String(key)] = value;
671
- };
672
- this.consumeRateLimit = (key)=>{
673
- var _this__getBucket;
674
- let tokens = null != (_this__getBucket = this._getBucket(key)) ? _this__getBucket : this._bucketSize;
675
- tokens = Math.max(tokens - 1, 0);
676
- if (0 === tokens) return true;
677
- this._setBucket(key, tokens);
678
- const hasReachedZero = 0 === tokens;
679
- if (hasReachedZero) {
680
- var _this__onBucketRateLimited, _this;
681
- null == (_this__onBucketRateLimited = (_this = this)._onBucketRateLimited) || _this__onBucketRateLimited.call(_this, key);
682
- }
683
- return hasReachedZero;
684
- };
685
- this._onBucketRateLimited = this._options._onBucketRateLimited;
686
- this._bucketSize = clampToRange(this._options.bucketSize, 0, 100, this._options._logger);
687
- this._refillRate = clampToRange(this._options.refillRate, 0, this._bucketSize, this._options._logger);
688
- this._refillInterval = clampToRange(this._options.refillInterval, 0, 86400000, this._options._logger);
689
- this._removeInterval = setInterval(()=>{
690
- this._refillBuckets();
691
- }, this._refillInterval);
692
- }
693
- }
694
-
695
- function safeSetTimeout(fn, timeout) {
696
- const t = setTimeout(fn, timeout);
697
- (null == t ? void 0 : t.unref) && (null == t || t.unref());
698
- return t;
699
- }
700
-
701
- const SHUTDOWN_TIMEOUT = 2000;
702
- class ErrorTracking {
703
- constructor(client, options, _logger) {
704
- this.client = client;
705
- this._exceptionAutocaptureEnabled = options.enableExceptionAutocapture || false;
706
- this._logger = _logger;
707
- // by default captures ten exceptions before rate limiting by exception type
708
- // refills at a rate of one token / 10 second period
709
- // e.g. will capture 1 exception rate limited exception every 10 seconds until burst ends
710
- this._rateLimiter = new BucketedRateLimiter({
711
- refillRate: 1,
712
- bucketSize: 10,
713
- refillInterval: 10000,
714
- // ten seconds in milliseconds
715
- _logger: this._logger
716
- });
717
- this.startAutocaptureIfEnabled();
718
- }
719
- static async buildEventMessage(error, hint, distinctId, additionalProperties) {
720
- const properties = {
721
- ...additionalProperties
722
- };
723
- // Given stateless nature of Node SDK we capture exceptions using personless processing when no
724
- // user can be determined because a distinct_id is not provided e.g. exception autocapture
725
- if (!distinctId) {
726
- properties.$process_person_profile = false;
727
- }
728
- const exceptionProperties = await propertiesFromUnknownInput(this.stackParser, this.frameModifiers, error, hint);
729
- return {
730
- event: '$exception',
731
- distinctId: distinctId || uuidv7(),
732
- properties: {
733
- ...exceptionProperties,
734
- ...properties
735
- }
736
- };
737
- }
738
- startAutocaptureIfEnabled() {
739
- if (this.isEnabled()) {
740
- addUncaughtExceptionListener(this.onException.bind(this), this.onFatalError.bind(this));
741
- addUnhandledRejectionListener(this.onException.bind(this));
742
- }
743
- }
744
- async onException(exception, hint) {
745
- this.client.addPendingPromise((async () => {
746
- const eventMessage = await ErrorTracking.buildEventMessage(exception, hint);
747
- const exceptionProperties = eventMessage.properties;
748
- const exceptionType = exceptionProperties?.$exception_list[0]?.type ?? 'Exception';
749
- const isRateLimited = this._rateLimiter.consumeRateLimit(exceptionType);
750
- if (isRateLimited) {
751
- this._logger.info('Skipping exception capture because of client rate limiting.', {
752
- exception: exceptionType
753
- });
754
- return;
755
- }
756
- return this.client.capture(eventMessage);
757
- })());
758
- }
759
- async onFatalError(exception) {
760
- console.error(exception);
761
- await this.client.shutdown(SHUTDOWN_TIMEOUT);
762
- process.exit(1);
763
- }
764
- isEnabled() {
765
- return !this.client.isDisabled && this._exceptionAutocaptureEnabled;
766
- }
767
- shutdown() {
768
- this._rateLimiter.stop();
769
- }
770
- }
771
-
772
- function setupExpressErrorHandler(_posthog, app) {
773
- app.use((error, _, __, next) => {
774
- const hint = {
775
- mechanism: {
776
- type: 'middleware',
777
- handled: false
778
- }
779
- };
780
- // Given stateless nature of Node SDK we capture exceptions using personless processing
781
- // when no user can be determined e.g. in the case of exception autocapture
782
- ErrorTracking.buildEventMessage(error, hint, uuidv7(), {
783
- $process_person_profile: false
784
- }).then(msg => _posthog.capture(msg));
785
- next(error);
786
- });
787
- }
788
-
789
- // Portions of this file are derived from getsentry/sentry-javascript by Software, Inc. dba Sentry
790
- // Licensed under the MIT License
791
- /** Creates a function that gets the module name from a filename */
792
- function createGetModuleFromFilename(basePath = process.argv[1] ? dirname(process.argv[1]) : process.cwd(), isWindows = sep === '\\') {
793
- const normalizedBase = isWindows ? normalizeWindowsPath(basePath) : basePath;
794
- return filename => {
795
- if (!filename) {
796
- return;
797
- }
798
- const normalizedFilename = isWindows ? normalizeWindowsPath(filename) : filename;
799
- // eslint-disable-next-line prefer-const
800
- let {
801
- dir,
802
- base: file,
803
- ext
804
- } = posix.parse(normalizedFilename);
805
- if (ext === '.js' || ext === '.mjs' || ext === '.cjs') {
806
- file = file.slice(0, ext.length * -1);
807
- }
808
- // The file name might be URI-encoded which we want to decode to
809
- // the original file name.
810
- const decodedFile = decodeURIComponent(file);
811
- if (!dir) {
812
- // No dirname whatsoever
813
- dir = '.';
814
- }
815
- const n = dir.lastIndexOf('/node_modules');
816
- if (n > -1) {
817
- return `${dir.slice(n + 14).replace(/\//g, '.')}:${decodedFile}`;
818
- }
819
- // Let's see if it's a part of the main module
820
- // To be a part of main module, it has to share the same base
821
- if (dir.startsWith(normalizedBase)) {
822
- const moduleName = dir.slice(normalizedBase.length + 1).replace(/\//g, '.');
823
- return moduleName ? `${moduleName}:${decodedFile}` : decodedFile;
824
- }
825
- return decodedFile;
826
- };
827
- }
828
- /** normalizes Windows paths */
829
- function normalizeWindowsPath(path) {
830
- return path.replace(/^[A-Z]:/, '') // remove Windows-style prefix
831
- .replace(/\\/g, '/'); // replace all `\` instances with `/`
832
- }
833
-
834
- // Portions of this file are derived from getsentry/sentry-javascript by Software, Inc. dba Sentry
835
- // Licensed under the MIT License
836
- /** A simple Least Recently Used map */
837
- class ReduceableCache {
838
- constructor(_maxSize) {
839
- this._maxSize = _maxSize;
840
- this._cache = new Map();
841
- }
842
- /** Get an entry or undefined if it was not in the cache. Re-inserts to update the recently used order */
843
- get(key) {
844
- const value = this._cache.get(key);
845
- if (value === undefined) {
846
- return undefined;
847
- }
848
- // Remove and re-insert to update the order
849
- this._cache.delete(key);
850
- this._cache.set(key, value);
851
- return value;
852
- }
853
- /** Insert an entry and evict an older entry if we've reached maxSize */
854
- set(key, value) {
855
- this._cache.set(key, value);
856
- }
857
- /** Remove an entry and return the entry if it was in the cache */
858
- reduce() {
859
- while (this._cache.size >= this._maxSize) {
860
- const value = this._cache.keys().next().value;
861
- if (value) {
862
- // keys() returns an iterator in insertion order so keys().next() gives us the oldest key
863
- this._cache.delete(value);
864
- }
865
- }
866
- }
867
- }
868
-
869
- // Portions of this file are derived from getsentry/sentry-javascript by Software, Inc. dba Sentry
870
- // Licensed under the MIT License
871
- const LRU_FILE_CONTENTS_CACHE = new ReduceableCache(25);
872
- const LRU_FILE_CONTENTS_FS_READ_FAILED = new ReduceableCache(20);
873
- const DEFAULT_LINES_OF_CONTEXT = 7;
874
- // Determines the upper bound of lineno/colno that we will attempt to read. Large colno values are likely to be
875
- // minified code while large lineno values are likely to be bundled code.
876
- // Exported for testing purposes.
877
- const MAX_CONTEXTLINES_COLNO = 1000;
878
- const MAX_CONTEXTLINES_LINENO = 10000;
879
- async function addSourceContext(frames) {
880
- // keep a lookup map of which files we've already enqueued to read,
881
- // so we don't enqueue the same file multiple times which would cause multiple i/o reads
882
- const filesToLines = {};
883
- // Maps preserve insertion order, so we iterate in reverse, starting at the
884
- // outermost frame and closer to where the exception has occurred (poor mans priority)
885
- for (let i = frames.length - 1; i >= 0; i--) {
886
- const frame = frames[i];
887
- const filename = frame?.filename;
888
- if (!frame || typeof filename !== 'string' || typeof frame.lineno !== 'number' || shouldSkipContextLinesForFile(filename) || shouldSkipContextLinesForFrame(frame)) {
889
- continue;
890
- }
891
- const filesToLinesOutput = filesToLines[filename];
892
- if (!filesToLinesOutput) {
893
- filesToLines[filename] = [];
894
- }
895
- filesToLines[filename].push(frame.lineno);
896
- }
897
- const files = Object.keys(filesToLines);
898
- if (files.length == 0) {
899
- return frames;
900
- }
901
- const readlinePromises = [];
902
- for (const file of files) {
903
- // If we failed to read this before, dont try reading it again.
904
- if (LRU_FILE_CONTENTS_FS_READ_FAILED.get(file)) {
905
- continue;
906
- }
907
- const filesToLineRanges = filesToLines[file];
908
- if (!filesToLineRanges) {
909
- continue;
910
- }
911
- // Sort ranges so that they are sorted by line increasing order and match how the file is read.
912
- filesToLineRanges.sort((a, b) => a - b);
913
- // Check if the contents are already in the cache and if we can avoid reading the file again.
914
- const ranges = makeLineReaderRanges(filesToLineRanges);
915
- if (ranges.every(r => rangeExistsInContentCache(file, r))) {
916
- continue;
917
- }
918
- const cache = emplace(LRU_FILE_CONTENTS_CACHE, file, {});
919
- readlinePromises.push(getContextLinesFromFile(file, ranges, cache));
920
- }
921
- // The promise rejections are caught in order to prevent them from short circuiting Promise.all
922
- await Promise.all(readlinePromises).catch(() => {});
923
- // Perform the same loop as above, but this time we can assume all files are in the cache
924
- // and attempt to add source context to frames.
925
- if (frames && frames.length > 0) {
926
- addSourceContextToFrames(frames, LRU_FILE_CONTENTS_CACHE);
927
- }
928
- // Once we're finished processing an exception reduce the files held in the cache
929
- // so that we don't indefinetly increase the size of this map
930
- LRU_FILE_CONTENTS_CACHE.reduce();
931
- return frames;
932
- }
933
- /**
934
- * Extracts lines from a file and stores them in a cache.
935
- */
936
- function getContextLinesFromFile(path, ranges, output) {
937
- return new Promise(resolve => {
938
- // It is important *not* to have any async code between createInterface and the 'line' event listener
939
- // as it will cause the 'line' event to
940
- // be emitted before the listener is attached.
941
- const stream = createReadStream(path);
942
- const lineReaded = createInterface({
943
- input: stream
944
- });
945
- // We need to explicitly destroy the stream to prevent memory leaks,
946
- // removing the listeners on the readline interface is not enough.
947
- // See: https://github.com/nodejs/node/issues/9002 and https://github.com/getsentry/sentry-javascript/issues/14892
948
- function destroyStreamAndResolve() {
949
- stream.destroy();
950
- resolve();
951
- }
952
- // Init at zero and increment at the start of the loop because lines are 1 indexed.
953
- let lineNumber = 0;
954
- let currentRangeIndex = 0;
955
- const range = ranges[currentRangeIndex];
956
- if (range === undefined) {
957
- // We should never reach this point, but if we do, we should resolve the promise to prevent it from hanging.
958
- destroyStreamAndResolve();
959
- return;
960
- }
961
- let rangeStart = range[0];
962
- let rangeEnd = range[1];
963
- // We use this inside Promise.all, so we need to resolve the promise even if there is an error
964
- // to prevent Promise.all from short circuiting the rest.
965
- function onStreamError() {
966
- // Mark file path as failed to read and prevent multiple read attempts.
967
- LRU_FILE_CONTENTS_FS_READ_FAILED.set(path, 1);
968
- lineReaded.close();
969
- lineReaded.removeAllListeners();
970
- destroyStreamAndResolve();
971
- }
972
- // We need to handle the error event to prevent the process from crashing in < Node 16
973
- // https://github.com/nodejs/node/pull/31603
974
- stream.on('error', onStreamError);
975
- lineReaded.on('error', onStreamError);
976
- lineReaded.on('close', destroyStreamAndResolve);
977
- lineReaded.on('line', line => {
978
- lineNumber++;
979
- if (lineNumber < rangeStart) {
980
- return;
981
- }
982
- // !Warning: This mutates the cache by storing the snipped line into the cache.
983
- output[lineNumber] = snipLine(line, 0);
984
- if (lineNumber >= rangeEnd) {
985
- if (currentRangeIndex === ranges.length - 1) {
986
- // We need to close the file stream and remove listeners, else the reader will continue to run our listener;
987
- lineReaded.close();
988
- lineReaded.removeAllListeners();
989
- return;
990
- }
991
- currentRangeIndex++;
992
- const range = ranges[currentRangeIndex];
993
- if (range === undefined) {
994
- // This should never happen as it means we have a bug in the context.
995
- lineReaded.close();
996
- lineReaded.removeAllListeners();
997
- return;
998
- }
999
- rangeStart = range[0];
1000
- rangeEnd = range[1];
1001
- }
1002
- });
1003
- });
1004
- }
1005
- /** Adds context lines to frames */
1006
- function addSourceContextToFrames(frames, cache) {
1007
- for (const frame of frames) {
1008
- // Only add context if we have a filename and it hasn't already been added
1009
- if (frame.filename && frame.context_line === undefined && typeof frame.lineno === 'number') {
1010
- const contents = cache.get(frame.filename);
1011
- if (contents === undefined) {
1012
- continue;
1013
- }
1014
- addContextToFrame(frame.lineno, frame, contents);
1015
- }
1016
- }
1017
- }
1018
- /**
1019
- * Resolves context lines before and after the given line number and appends them to the frame;
1020
- */
1021
- function addContextToFrame(lineno, frame, contents) {
1022
- // When there is no line number in the frame, attaching context is nonsensical and will even break grouping.
1023
- // We already check for lineno before calling this, but since StackFrame lineno is optional, we check it again.
1024
- if (frame.lineno === undefined || contents === undefined) {
1025
- return;
1026
- }
1027
- frame.pre_context = [];
1028
- for (let i = makeRangeStart(lineno); i < lineno; i++) {
1029
- // We always expect the start context as line numbers cannot be negative. If we dont find a line, then
1030
- // something went wrong somewhere. Clear the context and return without adding any linecontext.
1031
- const line = contents[i];
1032
- if (line === undefined) {
1033
- clearLineContext(frame);
1034
- return;
1035
- }
1036
- frame.pre_context.push(line);
1037
- }
1038
- // We should always have the context line. If we dont, something went wrong, so we clear the context and return
1039
- // without adding any linecontext.
1040
- if (contents[lineno] === undefined) {
1041
- clearLineContext(frame);
1042
- return;
1043
- }
1044
- frame.context_line = contents[lineno];
1045
- const end = makeRangeEnd(lineno);
1046
- frame.post_context = [];
1047
- for (let i = lineno + 1; i <= end; i++) {
1048
- // Since we dont track when the file ends, we cant clear the context if we dont find a line as it could
1049
- // just be that we reached the end of the file.
1050
- const line = contents[i];
1051
- if (line === undefined) {
1052
- break;
1053
- }
1054
- frame.post_context.push(line);
1055
- }
1056
- }
1057
- /**
1058
- * Clears the context lines from a frame, used to reset a frame to its original state
1059
- * if we fail to resolve all context lines for it.
1060
- */
1061
- function clearLineContext(frame) {
1062
- delete frame.pre_context;
1063
- delete frame.context_line;
1064
- delete frame.post_context;
1065
- }
1066
- /**
1067
- * Determines if context lines should be skipped for a file.
1068
- * - .min.(mjs|cjs|js) files are and not useful since they dont point to the original source
1069
- * - node: prefixed modules are part of the runtime and cannot be resolved to a file
1070
- * - data: skip json, wasm and inline js https://nodejs.org/api/esm.html#data-imports
1071
- */
1072
- function shouldSkipContextLinesForFile(path) {
1073
- // Test the most common prefix and extension first. These are the ones we
1074
- // are most likely to see in user applications and are the ones we can break out of first.
1075
- return path.startsWith('node:') || path.endsWith('.min.js') || path.endsWith('.min.cjs') || path.endsWith('.min.mjs') || path.startsWith('data:');
1076
- }
1077
- /**
1078
- * Determines if we should skip contextlines based off the max lineno and colno values.
1079
- */
1080
- function shouldSkipContextLinesForFrame(frame) {
1081
- if (frame.lineno !== undefined && frame.lineno > MAX_CONTEXTLINES_LINENO) {
1082
- return true;
1083
- }
1084
- if (frame.colno !== undefined && frame.colno > MAX_CONTEXTLINES_COLNO) {
1085
- return true;
1086
- }
1087
- return false;
1088
- }
1089
- /**
1090
- * Checks if we have all the contents that we need in the cache.
1091
- */
1092
- function rangeExistsInContentCache(file, range) {
1093
- const contents = LRU_FILE_CONTENTS_CACHE.get(file);
1094
- if (contents === undefined) {
1095
- return false;
1096
- }
1097
- for (let i = range[0]; i <= range[1]; i++) {
1098
- if (contents[i] === undefined) {
1099
- return false;
1100
- }
1101
- }
1102
- return true;
1103
- }
1104
- /**
1105
- * Creates contiguous ranges of lines to read from a file. In the case where context lines overlap,
1106
- * the ranges are merged to create a single range.
1107
- */
1108
- function makeLineReaderRanges(lines) {
1109
- if (!lines.length) {
1110
- return [];
1111
- }
1112
- let i = 0;
1113
- const line = lines[0];
1114
- if (typeof line !== 'number') {
1115
- return [];
1116
- }
1117
- let current = makeContextRange(line);
1118
- const out = [];
1119
- while (true) {
1120
- if (i === lines.length - 1) {
1121
- out.push(current);
1122
- break;
1123
- }
1124
- // If the next line falls into the current range, extend the current range to lineno + linecontext.
1125
- const next = lines[i + 1];
1126
- if (typeof next !== 'number') {
1127
- break;
1128
- }
1129
- if (next <= current[1]) {
1130
- current[1] = next + DEFAULT_LINES_OF_CONTEXT;
1131
- } else {
1132
- out.push(current);
1133
- current = makeContextRange(next);
1134
- }
1135
- i++;
1136
- }
1137
- return out;
1138
- }
1139
- // Determine start and end indices for context range (inclusive);
1140
- function makeContextRange(line) {
1141
- return [makeRangeStart(line), makeRangeEnd(line)];
1142
- }
1143
- // Compute inclusive end context range
1144
- function makeRangeStart(line) {
1145
- return Math.max(1, line - DEFAULT_LINES_OF_CONTEXT);
1146
- }
1147
- // Compute inclusive start context range
1148
- function makeRangeEnd(line) {
1149
- return line + DEFAULT_LINES_OF_CONTEXT;
1150
- }
1151
- /**
1152
- * Get or init map value
1153
- */
1154
- function emplace(map, key, contents) {
1155
- const value = map.get(key);
1156
- if (value === undefined) {
1157
- map.set(key, contents);
1158
- return contents;
1159
- }
1160
- return value;
1161
- }
1162
- function snipLine(line, colno) {
1163
- let newLine = line;
1164
- const lineLength = newLine.length;
1165
- if (lineLength <= 150) {
1166
- return newLine;
1167
- }
1168
- if (colno > lineLength) {
1169
- colno = lineLength;
1170
- }
1171
- let start = Math.max(colno - 60, 0);
1172
- if (start < 5) {
1173
- start = 0;
1174
- }
1175
- let end = Math.min(start + 140, lineLength);
1176
- if (end > lineLength - 5) {
1177
- end = lineLength;
1178
- }
1179
- if (end === lineLength) {
1180
- start = Math.max(end - 140, 0);
1181
- }
1182
- newLine = newLine.slice(start, end);
1183
- if (start > 0) {
1184
- newLine = `...${newLine}`;
1185
- }
1186
- if (end < lineLength) {
1187
- newLine += '...';
1188
- }
1189
- return newLine;
1190
- }
1191
-
1192
- var version = "5.8.8";
1193
-
1194
- /**
1195
- * A lazy value that is only computed when needed. Inspired by C#'s Lazy<T> class.
1196
- */
1197
- class Lazy {
1198
- constructor(factory) {
1199
- this.factory = factory;
1200
- }
1201
- /**
1202
- * Gets the value, initializing it if necessary.
1203
- * Multiple concurrent calls will share the same initialization promise.
1204
- */
1205
- async getValue() {
1206
- if (this.value !== undefined) {
1207
- return this.value;
1208
- }
1209
- if (this.initializationPromise === undefined) {
1210
- this.initializationPromise = (async () => {
1211
- try {
1212
- const result = await this.factory();
1213
- this.value = result;
1214
- return result;
1215
- } finally {
1216
- // Clear the promise so we can retry if needed
1217
- this.initializationPromise = undefined;
1218
- }
1219
- })();
1220
- }
1221
- return this.initializationPromise;
1222
- }
1223
- /**
1224
- * Returns true if the value has been initialized.
1225
- */
1226
- isInitialized() {
1227
- return this.value !== undefined;
1228
- }
1229
- /**
1230
- * Returns a promise that resolves when the value is initialized.
1231
- * If already initialized, resolves immediately.
1232
- */
1233
- async waitForInitialization() {
1234
- if (this.isInitialized()) {
1235
- return;
1236
- }
1237
- await this.getValue();
1238
- }
1239
- }
1240
-
1241
- /// <reference lib="dom" />
1242
- const nodeCrypto = new Lazy(async () => {
1243
- try {
1244
- return await import('crypto');
1245
- } catch {
1246
- return undefined;
1247
- }
1248
- });
1249
- async function getNodeCrypto() {
1250
- return await nodeCrypto.getValue();
1251
- }
1252
- const webCrypto = new Lazy(async () => {
1253
- if (typeof globalThis.crypto?.subtle !== 'undefined') {
1254
- return globalThis.crypto.subtle;
1255
- }
1256
- try {
1257
- // Node.js: use built-in webcrypto and assign it if needed
1258
- const crypto = await nodeCrypto.getValue();
1259
- if (crypto?.webcrypto?.subtle) {
1260
- return crypto.webcrypto.subtle;
1261
- }
1262
- } catch {
1263
- // Ignore if not available
1264
- }
1265
- return undefined;
1266
- });
1267
- async function getWebCrypto() {
1268
- return await webCrypto.getValue();
1269
- }
1270
-
1271
- /// <reference lib="dom" />
1272
- async function hashSHA1(text) {
1273
- // Try Node.js crypto first
1274
- const nodeCrypto = await getNodeCrypto();
1275
- if (nodeCrypto) {
1276
- return nodeCrypto.createHash('sha1').update(text).digest('hex');
1277
- }
1278
- const webCrypto = await getWebCrypto();
1279
- // Fall back to Web Crypto API
1280
- if (webCrypto) {
1281
- const hashBuffer = await webCrypto.digest('SHA-1', new TextEncoder().encode(text));
1282
- const hashArray = Array.from(new Uint8Array(hashBuffer));
1283
- return hashArray.map(byte => byte.toString(16).padStart(2, '0')).join('');
1284
- }
1285
- throw new Error('No crypto implementation available. Tried Node Crypto API and Web SubtleCrypto API');
1286
- }
1287
-
1288
- const SIXTY_SECONDS = 60 * 1000;
1289
- // eslint-disable-next-line
1290
- const LONG_SCALE = 0xfffffffffffffff;
1291
- const NULL_VALUES_ALLOWED_OPERATORS = ['is_not'];
1292
- class ClientError extends Error {
1293
- constructor(message) {
1294
- super();
1295
- Error.captureStackTrace(this, this.constructor);
1296
- this.name = 'ClientError';
1297
- this.message = message;
1298
- Object.setPrototypeOf(this, ClientError.prototype);
1299
- }
1300
- }
1301
- class InconclusiveMatchError extends Error {
1302
- constructor(message) {
1303
- super(message);
1304
- this.name = this.constructor.name;
1305
- Error.captureStackTrace(this, this.constructor);
1306
- // instanceof doesn't work in ES3 or ES5
1307
- // https://www.dannyguo.com/blog/how-to-fix-instanceof-not-working-for-custom-errors-in-typescript/
1308
- // this is the workaround
1309
- Object.setPrototypeOf(this, InconclusiveMatchError.prototype);
1310
- }
1311
- }
1312
- class FeatureFlagsPoller {
1313
- constructor({
1314
- pollingInterval,
1315
- personalApiKey,
1316
- projectApiKey,
1317
- timeout,
1318
- host,
1319
- customHeaders,
1320
- ...options
1321
- }) {
1322
- this.debugMode = false;
1323
- this.shouldBeginExponentialBackoff = false;
1324
- this.backOffCount = 0;
1325
- this.pollingInterval = pollingInterval;
1326
- this.personalApiKey = personalApiKey;
1327
- this.featureFlags = [];
1328
- this.featureFlagsByKey = {};
1329
- this.groupTypeMapping = {};
1330
- this.cohorts = {};
1331
- this.loadedSuccessfullyOnce = false;
1332
- this.timeout = timeout;
1333
- this.projectApiKey = projectApiKey;
1334
- this.host = host;
1335
- this.poller = undefined;
1336
- this.fetch = options.fetch || fetch;
1337
- this.onError = options.onError;
1338
- this.customHeaders = customHeaders;
1339
- this.onLoad = options.onLoad;
1340
- void this.loadFeatureFlags();
1341
- }
1342
- debug(enabled = true) {
1343
- this.debugMode = enabled;
1344
- }
1345
- logMsgIfDebug(fn) {
1346
- if (this.debugMode) {
1347
- fn();
1348
- }
1349
- }
1350
- async getFeatureFlag(key, distinctId, groups = {}, personProperties = {}, groupProperties = {}) {
1351
- await this.loadFeatureFlags();
1352
- let response = undefined;
1353
- let featureFlag = undefined;
1354
- if (!this.loadedSuccessfullyOnce) {
1355
- return response;
1356
- }
1357
- featureFlag = this.featureFlagsByKey[key];
1358
- if (featureFlag !== undefined) {
1359
- try {
1360
- const result = await this.computeFlagAndPayloadLocally(featureFlag, distinctId, groups, personProperties, groupProperties);
1361
- response = result.value;
1362
- this.logMsgIfDebug(() => console.debug(`Successfully computed flag locally: ${key} -> ${response}`));
1363
- } catch (e) {
1364
- if (e instanceof InconclusiveMatchError) {
1365
- this.logMsgIfDebug(() => console.debug(`InconclusiveMatchError when computing flag locally: ${key}: ${e}`));
1366
- } else if (e instanceof Error) {
1367
- this.onError?.(new Error(`Error computing flag locally: ${key}: ${e}`));
1368
- }
1369
- }
1370
- }
1371
- return response;
1372
- }
1373
- async getAllFlagsAndPayloads(distinctId, groups = {}, personProperties = {}, groupProperties = {}, flagKeysToExplicitlyEvaluate) {
1374
- await this.loadFeatureFlags();
1375
- const response = {};
1376
- const payloads = {};
1377
- let fallbackToFlags = this.featureFlags.length == 0;
1378
- const flagsToEvaluate = flagKeysToExplicitlyEvaluate ? flagKeysToExplicitlyEvaluate.map(key => this.featureFlagsByKey[key]).filter(Boolean) : this.featureFlags;
1379
- // Create a shared evaluation cache to prevent memory leaks when processing many flags
1380
- const sharedEvaluationCache = {};
1381
- await Promise.all(flagsToEvaluate.map(async flag => {
1382
- try {
1383
- const {
1384
- value: matchValue,
1385
- payload: matchPayload
1386
- } = await this.computeFlagAndPayloadLocally(flag, distinctId, groups, personProperties, groupProperties, undefined /* matchValue */, sharedEvaluationCache);
1387
- response[flag.key] = matchValue;
1388
- if (matchPayload) {
1389
- payloads[flag.key] = matchPayload;
1390
- }
1391
- } catch (e) {
1392
- if (e instanceof InconclusiveMatchError) {
1393
- this.logMsgIfDebug(() => console.debug(`InconclusiveMatchError when computing flag locally: ${flag.key}: ${e}`));
1394
- } else if (e instanceof Error) {
1395
- this.onError?.(new Error(`Error computing flag locally: ${flag.key}: ${e}`));
1396
- }
1397
- fallbackToFlags = true;
1398
- }
1399
- }));
1400
- return {
1401
- response,
1402
- payloads,
1403
- fallbackToFlags
1404
- };
1405
- }
1406
- async computeFlagAndPayloadLocally(flag, distinctId, groups = {}, personProperties = {}, groupProperties = {}, matchValue, evaluationCache, skipLoadCheck = false) {
1407
- // Only load flags if not already loaded and not skipping the check
1408
- if (!skipLoadCheck) {
1409
- await this.loadFeatureFlags();
1410
- }
1411
- if (!this.loadedSuccessfullyOnce) {
1412
- return {
1413
- value: false,
1414
- payload: null
1415
- };
1416
- }
1417
- let flagValue;
1418
- // If matchValue is provided, use it directly; otherwise evaluate the flag
1419
- if (matchValue !== undefined) {
1420
- flagValue = matchValue;
1421
- } else {
1422
- flagValue = await this.computeFlagValueLocally(flag, distinctId, groups, personProperties, groupProperties, evaluationCache);
1423
- }
1424
- // Always compute payload based on the final flagValue (whether provided or computed)
1425
- const payload = this.getFeatureFlagPayload(flag.key, flagValue);
1426
- return {
1427
- value: flagValue,
1428
- payload
1429
- };
1430
- }
1431
- async computeFlagValueLocally(flag, distinctId, groups = {}, personProperties = {}, groupProperties = {}, evaluationCache = {}) {
1432
- if (flag.ensure_experience_continuity) {
1433
- throw new InconclusiveMatchError('Flag has experience continuity enabled');
1434
- }
1435
- if (!flag.active) {
1436
- return false;
1437
- }
1438
- const flagFilters = flag.filters || {};
1439
- const aggregation_group_type_index = flagFilters.aggregation_group_type_index;
1440
- if (aggregation_group_type_index != undefined) {
1441
- const groupName = this.groupTypeMapping[String(aggregation_group_type_index)];
1442
- if (!groupName) {
1443
- this.logMsgIfDebug(() => console.warn(`[FEATURE FLAGS] Unknown group type index ${aggregation_group_type_index} for feature flag ${flag.key}`));
1444
- throw new InconclusiveMatchError('Flag has unknown group type index');
1445
- }
1446
- if (!(groupName in groups)) {
1447
- this.logMsgIfDebug(() => console.warn(`[FEATURE FLAGS] Can't compute group feature flag: ${flag.key} without group names passed in`));
1448
- return false;
1449
- }
1450
- const focusedGroupProperties = groupProperties[groupName];
1451
- return await this.matchFeatureFlagProperties(flag, groups[groupName], focusedGroupProperties, evaluationCache);
1452
- } else {
1453
- return await this.matchFeatureFlagProperties(flag, distinctId, personProperties, evaluationCache);
1454
- }
1455
- }
1456
- getFeatureFlagPayload(key, flagValue) {
1457
- let payload = null;
1458
- if (flagValue !== false && flagValue !== null && flagValue !== undefined) {
1459
- if (typeof flagValue == 'boolean') {
1460
- payload = this.featureFlagsByKey?.[key]?.filters?.payloads?.[flagValue.toString()] || null;
1461
- } else if (typeof flagValue == 'string') {
1462
- payload = this.featureFlagsByKey?.[key]?.filters?.payloads?.[flagValue] || null;
1463
- }
1464
- if (payload !== null && payload !== undefined) {
1465
- // If payload is already an object, return it directly
1466
- if (typeof payload === 'object') {
1467
- return payload;
1468
- }
1469
- // If payload is a string, try to parse it as JSON
1470
- if (typeof payload === 'string') {
1471
- try {
1472
- return JSON.parse(payload);
1473
- } catch {
1474
- // If parsing fails, return the string as is
1475
- return payload;
1476
- }
1477
- }
1478
- // For other types, return as is
1479
- return payload;
1480
- }
1481
- }
1482
- return null;
1483
- }
1484
- async evaluateFlagDependency(property, distinctId, properties, evaluationCache) {
1485
- const targetFlagKey = property.key;
1486
- if (!this.featureFlagsByKey) {
1487
- throw new InconclusiveMatchError('Feature flags not available for dependency evaluation');
1488
- }
1489
- // Check if dependency_chain is present - it should always be provided for flag dependencies
1490
- if (!('dependency_chain' in property)) {
1491
- throw new InconclusiveMatchError(`Flag dependency property for '${targetFlagKey}' is missing required 'dependency_chain' field`);
1492
- }
1493
- const dependencyChain = property.dependency_chain;
1494
- // Check for missing or invalid dependency chain (This should never happen, but being defensive)
1495
- if (!Array.isArray(dependencyChain)) {
1496
- throw new InconclusiveMatchError(`Flag dependency property for '${targetFlagKey}' has an invalid 'dependency_chain' (expected array, got ${typeof dependencyChain})`);
1497
- }
1498
- // Handle circular dependency (empty chain means circular) (This should never happen, but being defensive)
1499
- if (dependencyChain.length === 0) {
1500
- throw new InconclusiveMatchError(`Circular dependency detected for flag '${targetFlagKey}' (empty dependency chain)`);
1501
- }
1502
- // Evaluate all dependencies in the chain order
1503
- for (const depFlagKey of dependencyChain) {
1504
- if (!(depFlagKey in evaluationCache)) {
1505
- // Need to evaluate this dependency first
1506
- const depFlag = this.featureFlagsByKey[depFlagKey];
1507
- if (!depFlag) {
1508
- // Missing flag dependency - cannot evaluate locally
1509
- throw new InconclusiveMatchError(`Missing flag dependency '${depFlagKey}' for flag '${targetFlagKey}'`);
1510
- } else if (!depFlag.active) {
1511
- // Inactive flag evaluates to false
1512
- evaluationCache[depFlagKey] = false;
1513
- } else {
1514
- // Recursively evaluate the dependency
1515
- try {
1516
- const depResult = await this.matchFeatureFlagProperties(depFlag, distinctId, properties, evaluationCache);
1517
- evaluationCache[depFlagKey] = depResult;
1518
- } catch (error) {
1519
- // If we can't evaluate a dependency, store throw InconclusiveMatchError(`Missing flag dependency '${depFlagKey}' for flag '${targetFlagKey}'`)
1520
- throw new InconclusiveMatchError(`Error evaluating flag dependency '${depFlagKey}' for flag '${targetFlagKey}': ${error}`);
1521
- }
1522
- }
1523
- }
1524
- // Check if dependency evaluation was inconclusive
1525
- const cachedResult = evaluationCache[depFlagKey];
1526
- if (cachedResult === null || cachedResult === undefined) {
1527
- throw new InconclusiveMatchError(`Dependency '${depFlagKey}' could not be evaluated`);
1528
- }
1529
- }
1530
- // The target flag is specified in property.key (This should match the last element in the dependency chain)
1531
- const targetFlagValue = evaluationCache[targetFlagKey];
1532
- return this.flagEvaluatesToExpectedValue(property.value, targetFlagValue);
1533
- }
1534
- flagEvaluatesToExpectedValue(expectedValue, flagValue) {
1535
- // If the expected value is a boolean, then return true if the flag evaluated to true (or any string variant)
1536
- // If the expected value is false, then only return true if the flag evaluated to false.
1537
- if (typeof expectedValue === 'boolean') {
1538
- return expectedValue === flagValue || typeof flagValue === 'string' && flagValue !== '' && expectedValue === true;
1539
- }
1540
- // If the expected value is a string, then return true if and only if the flag evaluated to the expected value.
1541
- if (typeof expectedValue === 'string') {
1542
- return flagValue === expectedValue;
1543
- }
1544
- // The `flag_evaluates_to` operator is not supported for numbers and arrays.
1545
- return false;
1546
- }
1547
- async matchFeatureFlagProperties(flag, distinctId, properties, evaluationCache = {}) {
1548
- const flagFilters = flag.filters || {};
1549
- const flagConditions = flagFilters.groups || [];
1550
- let isInconclusive = false;
1551
- let result = undefined;
1552
- for (const condition of flagConditions) {
1553
- try {
1554
- if (await this.isConditionMatch(flag, distinctId, condition, properties, evaluationCache)) {
1555
- const variantOverride = condition.variant;
1556
- const flagVariants = flagFilters.multivariate?.variants || [];
1557
- if (variantOverride && flagVariants.some(variant => variant.key === variantOverride)) {
1558
- result = variantOverride;
1559
- } else {
1560
- result = (await this.getMatchingVariant(flag, distinctId)) || true;
1561
- }
1562
- break;
1563
- }
1564
- } catch (e) {
1565
- if (e instanceof InconclusiveMatchError) {
1566
- isInconclusive = true;
1567
- } else {
1568
- throw e;
1569
- }
1570
- }
1571
- }
1572
- if (result !== undefined) {
1573
- return result;
1574
- } else if (isInconclusive) {
1575
- throw new InconclusiveMatchError("Can't determine if feature flag is enabled or not with given properties");
1576
- }
1577
- // We can only return False when all conditions are False
1578
- return false;
1579
- }
1580
- async isConditionMatch(flag, distinctId, condition, properties, evaluationCache = {}) {
1581
- const rolloutPercentage = condition.rollout_percentage;
1582
- const warnFunction = msg => {
1583
- this.logMsgIfDebug(() => console.warn(msg));
1584
- };
1585
- if ((condition.properties || []).length > 0) {
1586
- for (const prop of condition.properties) {
1587
- const propertyType = prop.type;
1588
- let matches = false;
1589
- if (propertyType === 'cohort') {
1590
- matches = matchCohort(prop, properties, this.cohorts, this.debugMode);
1591
- } else if (propertyType === 'flag') {
1592
- matches = await this.evaluateFlagDependency(prop, distinctId, properties, evaluationCache);
1593
- } else {
1594
- matches = matchProperty(prop, properties, warnFunction);
1595
- }
1596
- if (!matches) {
1597
- return false;
1598
- }
1599
- }
1600
- if (rolloutPercentage == undefined) {
1601
- return true;
1602
- }
1603
- }
1604
- if (rolloutPercentage != undefined && (await _hash(flag.key, distinctId)) > rolloutPercentage / 100.0) {
1605
- return false;
1606
- }
1607
- return true;
1608
- }
1609
- async getMatchingVariant(flag, distinctId) {
1610
- const hashValue = await _hash(flag.key, distinctId, 'variant');
1611
- const matchingVariant = this.variantLookupTable(flag).find(variant => {
1612
- return hashValue >= variant.valueMin && hashValue < variant.valueMax;
1613
- });
1614
- if (matchingVariant) {
1615
- return matchingVariant.key;
1616
- }
1617
- return undefined;
1618
- }
1619
- variantLookupTable(flag) {
1620
- const lookupTable = [];
1621
- let valueMin = 0;
1622
- let valueMax = 0;
1623
- const flagFilters = flag.filters || {};
1624
- const multivariates = flagFilters.multivariate?.variants || [];
1625
- multivariates.forEach(variant => {
1626
- valueMax = valueMin + variant.rollout_percentage / 100.0;
1627
- lookupTable.push({
1628
- valueMin,
1629
- valueMax,
1630
- key: variant.key
1631
- });
1632
- valueMin = valueMax;
1633
- });
1634
- return lookupTable;
1635
- }
1636
- async loadFeatureFlags(forceReload = false) {
1637
- if (!this.loadedSuccessfullyOnce || forceReload) {
1638
- await this._loadFeatureFlags();
1639
- }
1640
- }
1641
- /**
1642
- * Returns true if the feature flags poller has loaded successfully at least once and has more than 0 feature flags.
1643
- * This is useful to check if local evaluation is ready before calling getFeatureFlag.
1644
- */
1645
- isLocalEvaluationReady() {
1646
- return (this.loadedSuccessfullyOnce ?? false) && (this.featureFlags?.length ?? 0) > 0;
1647
- }
1648
- /**
1649
- * If a client is misconfigured with an invalid or improper API key, the polling interval is doubled each time
1650
- * until a successful request is made, up to a maximum of 60 seconds.
1651
- *
1652
- * @returns The polling interval to use for the next request.
1653
- */
1654
- getPollingInterval() {
1655
- if (!this.shouldBeginExponentialBackoff) {
1656
- return this.pollingInterval;
1657
- }
1658
- return Math.min(SIXTY_SECONDS, this.pollingInterval * 2 ** this.backOffCount);
1659
- }
1660
- async _loadFeatureFlags() {
1661
- if (this.poller) {
1662
- clearTimeout(this.poller);
1663
- this.poller = undefined;
1664
- }
1665
- this.poller = setTimeout(() => this._loadFeatureFlags(), this.getPollingInterval());
1666
- try {
1667
- const res = await this._requestFeatureFlagDefinitions();
1668
- // Handle undefined res case, this shouldn't happen, but it doesn't hurt to handle it anyway
1669
- if (!res) {
1670
- // Don't override existing flags when something goes wrong
1671
- return;
1672
- }
1673
- // NB ON ERROR HANDLING & `loadedSuccessfullyOnce`:
1674
- //
1675
- // `loadedSuccessfullyOnce` indicates we've successfully loaded a valid set of flags at least once.
1676
- // If we set it to `true` in an error scenario (e.g. 402 Over Quota, 401 Invalid Key, etc.),
1677
- // any manual call to `loadFeatureFlags()` (without forceReload) will skip refetching entirely,
1678
- // leaving us stuck with zero or outdated flags. The poller does keep running, but we also want
1679
- // manual reloads to be possible as soon as the error condition is resolved.
1680
- //
1681
- // Therefore, on error statuses, we do *not* set `loadedSuccessfullyOnce = true`, ensuring that
1682
- // both the background poller and any subsequent manual calls can keep trying to load flags
1683
- // once the issue (quota, permission, rate limit, etc.) is resolved.
1684
- switch (res.status) {
1685
- case 401:
1686
- // Invalid API key
1687
- this.shouldBeginExponentialBackoff = true;
1688
- this.backOffCount += 1;
1689
- throw new ClientError(`Your project key or personal API key is invalid. Setting next polling interval to ${this.getPollingInterval()}ms. More information: https://posthog.com/docs/api#rate-limiting`);
1690
- case 402:
1691
- // Quota exceeded - clear all flags
1692
- console.warn('[FEATURE FLAGS] Feature flags quota limit exceeded - unsetting all local flags. Learn more about billing limits at https://posthog.com/docs/billing/limits-alerts');
1693
- this.featureFlags = [];
1694
- this.featureFlagsByKey = {};
1695
- this.groupTypeMapping = {};
1696
- this.cohorts = {};
1697
- return;
1698
- case 403:
1699
- // Permissions issue
1700
- this.shouldBeginExponentialBackoff = true;
1701
- this.backOffCount += 1;
1702
- throw new ClientError(`Your personal API key does not have permission to fetch feature flag definitions for local evaluation. Setting next polling interval to ${this.getPollingInterval()}ms. Are you sure you're using the correct personal and Project API key pair? More information: https://posthog.com/docs/api/overview`);
1703
- case 429:
1704
- // Rate limited
1705
- this.shouldBeginExponentialBackoff = true;
1706
- this.backOffCount += 1;
1707
- throw new ClientError(`You are being rate limited. Setting next polling interval to ${this.getPollingInterval()}ms. More information: https://posthog.com/docs/api#rate-limiting`);
1708
- case 200:
1709
- {
1710
- // Process successful response
1711
- const responseJson = (await res.json()) ?? {};
1712
- if (!('flags' in responseJson)) {
1713
- this.onError?.(new Error(`Invalid response when getting feature flags: ${JSON.stringify(responseJson)}`));
1714
- return;
1715
- }
1716
- this.featureFlags = responseJson.flags ?? [];
1717
- this.featureFlagsByKey = this.featureFlags.reduce((acc, curr) => (acc[curr.key] = curr, acc), {});
1718
- this.groupTypeMapping = responseJson.group_type_mapping || {};
1719
- this.cohorts = responseJson.cohorts || {};
1720
- this.loadedSuccessfullyOnce = true;
1721
- this.shouldBeginExponentialBackoff = false;
1722
- this.backOffCount = 0;
1723
- this.onLoad?.(this.featureFlags.length);
1724
- break;
1725
- }
1726
- default:
1727
- // Something else went wrong, or the server is down.
1728
- // In this case, don't override existing flags
1729
- return;
1730
- }
1731
- } catch (err) {
1732
- if (err instanceof ClientError) {
1733
- this.onError?.(err);
1734
- }
1735
- }
1736
- }
1737
- getPersonalApiKeyRequestOptions(method = 'GET') {
1738
- return {
1739
- method,
1740
- headers: {
1741
- ...this.customHeaders,
1742
- 'Content-Type': 'application/json',
1743
- Authorization: `Bearer ${this.personalApiKey}`
1744
- }
1745
- };
1746
- }
1747
- async _requestFeatureFlagDefinitions() {
1748
- const url = `${this.host}/api/feature_flag/local_evaluation?token=${this.projectApiKey}&send_cohorts`;
1749
- const options = this.getPersonalApiKeyRequestOptions();
1750
- let abortTimeout = null;
1751
- if (this.timeout && typeof this.timeout === 'number') {
1752
- const controller = new AbortController();
1753
- abortTimeout = safeSetTimeout(() => {
1754
- controller.abort();
1755
- }, this.timeout);
1756
- options.signal = controller.signal;
1757
- }
1758
- try {
1759
- return await this.fetch(url, options);
1760
- } finally {
1761
- clearTimeout(abortTimeout);
1762
- }
1763
- }
1764
- stopPoller() {
1765
- clearTimeout(this.poller);
1766
- }
1767
- }
1768
- // # This function takes a distinct_id and a feature flag key and returns a float between 0 and 1.
1769
- // # Given the same distinct_id and key, it'll always return the same float. These floats are
1770
- // # uniformly distributed between 0 and 1, so if we want to show this feature to 20% of traffic
1771
- // # we can do _hash(key, distinct_id) < 0.2
1772
- async function _hash(key, distinctId, salt = '') {
1773
- const hashString = await hashSHA1(`${key}.${distinctId}${salt}`);
1774
- return parseInt(hashString.slice(0, 15), 16) / LONG_SCALE;
1775
- }
1776
- function matchProperty(property, propertyValues, warnFunction) {
1777
- const key = property.key;
1778
- const value = property.value;
1779
- const operator = property.operator || 'exact';
1780
- if (!(key in propertyValues)) {
1781
- throw new InconclusiveMatchError(`Property ${key} not found in propertyValues`);
1782
- } else if (operator === 'is_not_set') {
1783
- throw new InconclusiveMatchError(`Operator is_not_set is not supported`);
1784
- }
1785
- const overrideValue = propertyValues[key];
1786
- if (overrideValue == null && !NULL_VALUES_ALLOWED_OPERATORS.includes(operator)) {
1787
- // if the value is null, just fail the feature flag comparison
1788
- // this isn't an InconclusiveMatchError because the property value was provided.
1789
- if (warnFunction) {
1790
- warnFunction(`Property ${key} cannot have a value of null/undefined with the ${operator} operator`);
1791
- }
1792
- return false;
1793
- }
1794
- function computeExactMatch(value, overrideValue) {
1795
- if (Array.isArray(value)) {
1796
- return value.map(val => String(val).toLowerCase()).includes(String(overrideValue).toLowerCase());
1797
- }
1798
- return String(value).toLowerCase() === String(overrideValue).toLowerCase();
1799
- }
1800
- function compare(lhs, rhs, operator) {
1801
- if (operator === 'gt') {
1802
- return lhs > rhs;
1803
- } else if (operator === 'gte') {
1804
- return lhs >= rhs;
1805
- } else if (operator === 'lt') {
1806
- return lhs < rhs;
1807
- } else if (operator === 'lte') {
1808
- return lhs <= rhs;
1809
- } else {
1810
- throw new Error(`Invalid operator: ${operator}`);
1811
- }
1812
- }
1813
- switch (operator) {
1814
- case 'exact':
1815
- return computeExactMatch(value, overrideValue);
1816
- case 'is_not':
1817
- return !computeExactMatch(value, overrideValue);
1818
- case 'is_set':
1819
- return key in propertyValues;
1820
- case 'icontains':
1821
- return String(overrideValue).toLowerCase().includes(String(value).toLowerCase());
1822
- case 'not_icontains':
1823
- return !String(overrideValue).toLowerCase().includes(String(value).toLowerCase());
1824
- case 'regex':
1825
- return isValidRegex(String(value)) && String(overrideValue).match(String(value)) !== null;
1826
- case 'not_regex':
1827
- return isValidRegex(String(value)) && String(overrideValue).match(String(value)) === null;
1828
- case 'gt':
1829
- case 'gte':
1830
- case 'lt':
1831
- case 'lte':
1832
- {
1833
- // :TRICKY: We adjust comparison based on the override value passed in,
1834
- // to make sure we handle both numeric and string comparisons appropriately.
1835
- let parsedValue = typeof value === 'number' ? value : null;
1836
- if (typeof value === 'string') {
1837
- try {
1838
- parsedValue = parseFloat(value);
1839
- } catch (err) {
1840
- // pass
1841
- }
1842
- }
1843
- if (parsedValue != null && overrideValue != null) {
1844
- // check both null and undefined
1845
- if (typeof overrideValue === 'string') {
1846
- return compare(overrideValue, String(value), operator);
1847
- } else {
1848
- return compare(overrideValue, parsedValue, operator);
1849
- }
1850
- } else {
1851
- return compare(String(overrideValue), String(value), operator);
1852
- }
1853
- }
1854
- case 'is_date_after':
1855
- case 'is_date_before':
1856
- {
1857
- // Boolean values should never be used with date operations
1858
- if (typeof value === 'boolean') {
1859
- throw new InconclusiveMatchError(`Date operations cannot be performed on boolean values`);
1860
- }
1861
- let parsedDate = relativeDateParseForFeatureFlagMatching(String(value));
1862
- if (parsedDate == null) {
1863
- parsedDate = convertToDateTime(value);
1864
- }
1865
- if (parsedDate == null) {
1866
- throw new InconclusiveMatchError(`Invalid date: ${value}`);
1867
- }
1868
- const overrideDate = convertToDateTime(overrideValue);
1869
- if (['is_date_before'].includes(operator)) {
1870
- return overrideDate < parsedDate;
1871
- }
1872
- return overrideDate > parsedDate;
1873
- }
1874
- default:
1875
- throw new InconclusiveMatchError(`Unknown operator: ${operator}`);
1876
- }
1877
- }
1878
- function matchCohort(property, propertyValues, cohortProperties, debugMode = false) {
1879
- const cohortId = String(property.value);
1880
- if (!(cohortId in cohortProperties)) {
1881
- throw new InconclusiveMatchError("can't match cohort without a given cohort property value");
1882
- }
1883
- const propertyGroup = cohortProperties[cohortId];
1884
- return matchPropertyGroup(propertyGroup, propertyValues, cohortProperties, debugMode);
1885
- }
1886
- function matchPropertyGroup(propertyGroup, propertyValues, cohortProperties, debugMode = false) {
1887
- if (!propertyGroup) {
1888
- return true;
1889
- }
1890
- const propertyGroupType = propertyGroup.type;
1891
- const properties = propertyGroup.values;
1892
- if (!properties || properties.length === 0) {
1893
- // empty groups are no-ops, always match
1894
- return true;
1895
- }
1896
- let errorMatchingLocally = false;
1897
- if ('values' in properties[0]) {
1898
- // a nested property group
1899
- for (const prop of properties) {
1900
- try {
1901
- const matches = matchPropertyGroup(prop, propertyValues, cohortProperties, debugMode);
1902
- if (propertyGroupType === 'AND') {
1903
- if (!matches) {
1904
- return false;
1905
- }
1906
- } else {
1907
- // OR group
1908
- if (matches) {
1909
- return true;
1910
- }
1911
- }
1912
- } catch (err) {
1913
- if (err instanceof InconclusiveMatchError) {
1914
- if (debugMode) {
1915
- console.debug(`Failed to compute property ${prop} locally: ${err}`);
1916
- }
1917
- errorMatchingLocally = true;
1918
- } else {
1919
- throw err;
1920
- }
1921
- }
1922
- }
1923
- if (errorMatchingLocally) {
1924
- throw new InconclusiveMatchError("Can't match cohort without a given cohort property value");
1925
- }
1926
- // if we get here, all matched in AND case, or none matched in OR case
1927
- return propertyGroupType === 'AND';
1928
- } else {
1929
- for (const prop of properties) {
1930
- try {
1931
- let matches;
1932
- if (prop.type === 'cohort') {
1933
- matches = matchCohort(prop, propertyValues, cohortProperties, debugMode);
1934
- } else if (prop.type === 'flag') {
1935
- if (debugMode) {
1936
- console.warn(`[FEATURE FLAGS] Flag dependency filters are not supported in local evaluation. ` + `Skipping condition with dependency on flag '${prop.key || 'unknown'}'`);
1937
- }
1938
- continue;
1939
- } else {
1940
- matches = matchProperty(prop, propertyValues);
1941
- }
1942
- const negation = prop.negation || false;
1943
- if (propertyGroupType === 'AND') {
1944
- // if negated property, do the inverse
1945
- if (!matches && !negation) {
1946
- return false;
1947
- }
1948
- if (matches && negation) {
1949
- return false;
1950
- }
1951
- } else {
1952
- // OR group
1953
- if (matches && !negation) {
1954
- return true;
1955
- }
1956
- if (!matches && negation) {
1957
- return true;
1958
- }
1959
- }
1960
- } catch (err) {
1961
- if (err instanceof InconclusiveMatchError) {
1962
- if (debugMode) {
1963
- console.debug(`Failed to compute property ${prop} locally: ${err}`);
1964
- }
1965
- errorMatchingLocally = true;
1966
- } else {
1967
- throw err;
1968
- }
1969
- }
1970
- }
1971
- if (errorMatchingLocally) {
1972
- throw new InconclusiveMatchError("can't match cohort without a given cohort property value");
1973
- }
1974
- // if we get here, all matched in AND case, or none matched in OR case
1975
- return propertyGroupType === 'AND';
1976
- }
1977
- }
1978
- function isValidRegex(regex) {
1979
- try {
1980
- new RegExp(regex);
1981
- return true;
1982
- } catch (err) {
1983
- return false;
1984
- }
1985
- }
1986
- function convertToDateTime(value) {
1987
- if (value instanceof Date) {
1988
- return value;
1989
- } else if (typeof value === 'string' || typeof value === 'number') {
1990
- const date = new Date(value);
1991
- if (!isNaN(date.valueOf())) {
1992
- return date;
1993
- }
1994
- throw new InconclusiveMatchError(`${value} is in an invalid date format`);
1995
- } else {
1996
- throw new InconclusiveMatchError(`The date provided ${value} must be a string, number, or date object`);
1997
- }
1998
- }
1999
- function relativeDateParseForFeatureFlagMatching(value) {
2000
- const regex = /^-?(?<number>[0-9]+)(?<interval>[a-z])$/;
2001
- const match = value.match(regex);
2002
- const parsedDt = new Date(new Date().toISOString());
2003
- if (match) {
2004
- if (!match.groups) {
2005
- return null;
2006
- }
2007
- const number = parseInt(match.groups['number']);
2008
- if (number >= 10000) {
2009
- // Guard against overflow, disallow numbers greater than 10_000
2010
- return null;
2011
- }
2012
- const interval = match.groups['interval'];
2013
- if (interval == 'h') {
2014
- parsedDt.setUTCHours(parsedDt.getUTCHours() - number);
2015
- } else if (interval == 'd') {
2016
- parsedDt.setUTCDate(parsedDt.getUTCDate() - number);
2017
- } else if (interval == 'w') {
2018
- parsedDt.setUTCDate(parsedDt.getUTCDate() - number * 7);
2019
- } else if (interval == 'm') {
2020
- parsedDt.setUTCMonth(parsedDt.getUTCMonth() - number);
2021
- } else if (interval == 'y') {
2022
- parsedDt.setUTCFullYear(parsedDt.getUTCFullYear() - number);
2023
- } else {
2024
- return null;
2025
- }
2026
- return parsedDt;
2027
- } else {
2028
- return null;
2029
- }
2030
- }
2031
-
2032
- class PostHogMemoryStorage {
2033
- constructor() {
2034
- this._memoryStorage = {};
2035
- }
2036
- getProperty(key) {
2037
- return this._memoryStorage[key];
2038
- }
2039
- setProperty(key, value) {
2040
- this._memoryStorage[key] = value !== null ? value : undefined;
2041
- }
2042
- }
2043
-
2044
- const _createLogger = (prefix, logMsgIfDebug) => {
2045
- const logger = {
2046
- _log: (level, ...args) => {
2047
- logMsgIfDebug(() => {
2048
- const consoleLog = console[level];
2049
- consoleLog(prefix, ...args);
2050
- });
2051
- },
2052
- info: (...args) => {
2053
- logger._log('log', ...args);
2054
- },
2055
- warn: (...args) => {
2056
- logger._log('warn', ...args);
2057
- },
2058
- error: (...args) => {
2059
- logger._log('error', ...args);
2060
- },
2061
- critical: (...args) => {
2062
- // Critical errors are always logged to the console
2063
- // eslint-disable-next-line no-console
2064
- console.error(prefix, ...args);
2065
- },
2066
- uninitializedWarning: methodName => {
2067
- logger.error(`You must initialize PostHog before calling ${methodName}`);
2068
- },
2069
- createLogger: additionalPrefix => _createLogger(`${prefix} ${additionalPrefix}`, logMsgIfDebug)
2070
- };
2071
- return logger;
2072
- };
2073
- const createLogger = logMsgIfDebug => _createLogger('[PostHog.js]', logMsgIfDebug);
2074
-
2075
- // Standard local evaluation rate limit is 600 per minute (10 per second),
2076
- // so the fastest a poller should ever be set is 100ms.
2077
- const MINIMUM_POLLING_INTERVAL = 100;
2078
- const THIRTY_SECONDS = 30 * 1000;
2079
- const MAX_CACHE_SIZE = 50 * 1000;
2080
- // The actual exported Nodejs API.
2081
- class PostHogBackendClient extends PostHogCoreStateless {
2082
- /**
2083
- * Initialize a new PostHog client instance.
2084
- *
2085
- * @example
2086
- * ```ts
2087
- * // Basic initialization
2088
- * const client = new PostHogBackendClient(
2089
- * 'your-api-key',
2090
- * { host: 'https://app.posthog.com' }
2091
- * )
2092
- * ```
2093
- *
2094
- * @example
2095
- * ```ts
2096
- * // With personal API key
2097
- * const client = new PostHogBackendClient(
2098
- * 'your-api-key',
2099
- * {
2100
- * host: 'https://app.posthog.com',
2101
- * personalApiKey: 'your-personal-api-key'
2102
- * }
2103
- * )
2104
- * ```
2105
- *
2106
- * {@label Initialization}
2107
- *
2108
- * @param apiKey - Your PostHog project API key
2109
- * @param options - Configuration options for the client
2110
- */
2111
- constructor(apiKey, options = {}) {
2112
- super(apiKey, options);
2113
- this._memoryStorage = new PostHogMemoryStorage();
2114
- this.options = options;
2115
- this.logger = createLogger(this.logMsgIfDebug.bind(this));
2116
- this.options.featureFlagsPollingInterval = typeof options.featureFlagsPollingInterval === 'number' ? Math.max(options.featureFlagsPollingInterval, MINIMUM_POLLING_INTERVAL) : THIRTY_SECONDS;
2117
- if (options.personalApiKey) {
2118
- if (options.personalApiKey.includes('phc_')) {
2119
- throw new Error('Your Personal API key is invalid. These keys are prefixed with "phx_" and can be created in PostHog project settings.');
2120
- }
2121
- // Only start the poller if local evaluation is enabled (defaults to true for backward compatibility)
2122
- const shouldEnableLocalEvaluation = options.enableLocalEvaluation !== false;
2123
- if (shouldEnableLocalEvaluation) {
2124
- this.featureFlagsPoller = new FeatureFlagsPoller({
2125
- pollingInterval: this.options.featureFlagsPollingInterval,
2126
- personalApiKey: options.personalApiKey,
2127
- projectApiKey: apiKey,
2128
- timeout: options.requestTimeout ?? 10000,
2129
- // 10 seconds
2130
- host: this.host,
2131
- fetch: options.fetch,
2132
- onError: err => {
2133
- this._events.emit('error', err);
2134
- },
2135
- onLoad: count => {
2136
- this._events.emit('localEvaluationFlagsLoaded', count);
2137
- },
2138
- customHeaders: this.getCustomHeaders()
2139
- });
2140
- }
2141
- }
2142
- this.errorTracking = new ErrorTracking(this, options, this.logger);
2143
- this.distinctIdHasSentFlagCalls = {};
2144
- this.maxCacheSize = options.maxCacheSize || MAX_CACHE_SIZE;
2145
- }
2146
- /**
2147
- * Get a persisted property value from memory storage.
2148
- *
2149
- * @example
2150
- * ```ts
2151
- * // Get user ID
2152
- * const userId = client.getPersistedProperty('userId')
2153
- * ```
2154
- *
2155
- * @example
2156
- * ```ts
2157
- * // Get session ID
2158
- * const sessionId = client.getPersistedProperty('sessionId')
2159
- * ```
2160
- *
2161
- * {@label Initialization}
2162
- *
2163
- * @param key - The property key to retrieve
2164
- * @returns The stored property value or undefined if not found
2165
- */
2166
- getPersistedProperty(key) {
2167
- return this._memoryStorage.getProperty(key);
2168
- }
2169
- /**
2170
- * Set a persisted property value in memory storage.
2171
- *
2172
- * @example
2173
- * ```ts
2174
- * // Set user ID
2175
- * client.setPersistedProperty('userId', 'user_123')
2176
- * ```
2177
- *
2178
- * @example
2179
- * ```ts
2180
- * // Set session ID
2181
- * client.setPersistedProperty('sessionId', 'session_456')
2182
- * ```
2183
- *
2184
- * {@label Initialization}
2185
- *
2186
- * @param key - The property key to set
2187
- * @param value - The value to store (null to remove)
2188
- */
2189
- setPersistedProperty(key, value) {
2190
- return this._memoryStorage.setProperty(key, value);
2191
- }
2192
- /**
2193
- * Make an HTTP request using the configured fetch function or default fetch.
2194
- *
2195
- * @example
2196
- * ```ts
2197
- * // POST request
2198
- * const response = await client.fetch('/api/endpoint', {
2199
- * method: 'POST',
2200
- * headers: { 'Content-Type': 'application/json' },
2201
- * body: JSON.stringify(data)
2202
- * })
2203
- * ```
2204
- *
2205
- * @internal
2206
- *
2207
- * {@label Initialization}
2208
- *
2209
- * @param url - The URL to fetch
2210
- * @param options - Fetch options
2211
- * @returns Promise resolving to the fetch response
2212
- */
2213
- fetch(url, options) {
2214
- return this.options.fetch ? this.options.fetch(url, options) : fetch(url, options);
2215
- }
2216
- /**
2217
- * Get the library version from package.json.
2218
- *
2219
- * @example
2220
- * ```ts
2221
- * // Get version
2222
- * const version = client.getLibraryVersion()
2223
- * console.log(`Using PostHog SDK version: ${version}`)
2224
- * ```
2225
- *
2226
- * {@label Initialization}
2227
- *
2228
- * @returns The current library version string
2229
- */
2230
- getLibraryVersion() {
2231
- return version;
2232
- }
2233
- /**
2234
- * Get the custom user agent string for this client.
2235
- *
2236
- * @example
2237
- * ```ts
2238
- * // Get user agent
2239
- * const userAgent = client.getCustomUserAgent()
2240
- * // Returns: "posthog-node/5.7.0"
2241
- * ```
2242
- *
2243
- * {@label Identification}
2244
- *
2245
- * @returns The formatted user agent string
2246
- */
2247
- getCustomUserAgent() {
2248
- return `${this.getLibraryId()}/${this.getLibraryVersion()}`;
2249
- }
2250
- /**
2251
- * Enable the PostHog client (opt-in).
2252
- *
2253
- * @example
2254
- * ```ts
2255
- * // Enable client
2256
- * await client.enable()
2257
- * // Client is now enabled and will capture events
2258
- * ```
2259
- *
2260
- * {@label Privacy}
2261
- *
2262
- * @returns Promise that resolves when the client is enabled
2263
- */
2264
- enable() {
2265
- return super.optIn();
2266
- }
2267
- /**
2268
- * Disable the PostHog client (opt-out).
2269
- *
2270
- * @example
2271
- * ```ts
2272
- * // Disable client
2273
- * await client.disable()
2274
- * // Client is now disabled and will not capture events
2275
- * ```
2276
- *
2277
- * {@label Privacy}
2278
- *
2279
- * @returns Promise that resolves when the client is disabled
2280
- */
2281
- disable() {
2282
- return super.optOut();
2283
- }
2284
- /**
2285
- * Enable or disable debug logging.
2286
- *
2287
- * @example
2288
- * ```ts
2289
- * // Enable debug logging
2290
- * client.debug(true)
2291
- * ```
2292
- *
2293
- * @example
2294
- * ```ts
2295
- * // Disable debug logging
2296
- * client.debug(false)
2297
- * ```
2298
- *
2299
- * {@label Initialization}
2300
- *
2301
- * @param enabled - Whether to enable debug logging
2302
- */
2303
- debug(enabled = true) {
2304
- super.debug(enabled);
2305
- this.featureFlagsPoller?.debug(enabled);
2306
- }
2307
- /**
2308
- * Capture an event manually.
2309
- *
2310
- * @example
2311
- * ```ts
2312
- * // Basic capture
2313
- * client.capture({
2314
- * distinctId: 'user_123',
2315
- * event: 'button_clicked',
2316
- * properties: { button_color: 'red' }
2317
- * })
2318
- * ```
2319
- *
2320
- * {@label Capture}
2321
- *
2322
- * @param props - The event properties
2323
- * @returns void
2324
- */
2325
- capture(props) {
2326
- if (typeof props === 'string') {
2327
- this.logMsgIfDebug(() => console.warn('Called capture() with a string as the first argument when an object was expected.'));
2328
- }
2329
- this.addPendingPromise(this.prepareEventMessage(props).then(({
2330
- distinctId,
2331
- event,
2332
- properties,
2333
- options
2334
- }) => {
2335
- return super.captureStateless(distinctId, event, properties, {
2336
- timestamp: options.timestamp,
2337
- disableGeoip: options.disableGeoip,
2338
- uuid: options.uuid
2339
- });
2340
- }).catch(err => {
2341
- if (err) {
2342
- console.error(err);
2343
- }
2344
- }));
2345
- }
2346
- /**
2347
- * Capture an event immediately (synchronously).
2348
- *
2349
- * @example
2350
- * ```ts
2351
- * // Basic immediate capture
2352
- * await client.captureImmediate({
2353
- * distinctId: 'user_123',
2354
- * event: 'button_clicked',
2355
- * properties: { button_color: 'red' }
2356
- * })
2357
- * ```
2358
- *
2359
- * @example
2360
- * ```ts
2361
- * // With feature flags
2362
- * await client.captureImmediate({
2363
- * distinctId: 'user_123',
2364
- * event: 'user_action',
2365
- * sendFeatureFlags: true
2366
- * })
2367
- * ```
2368
- *
2369
- * @example
2370
- * ```ts
2371
- * // With custom feature flags options
2372
- * await client.captureImmediate({
2373
- * distinctId: 'user_123',
2374
- * event: 'user_action',
2375
- * sendFeatureFlags: {
2376
- * onlyEvaluateLocally: true,
2377
- * personProperties: { plan: 'premium' },
2378
- * groupProperties: { org: { tier: 'enterprise' } }
2379
- * flagKeys: ['flag1', 'flag2']
2380
- * }
2381
- * })
2382
- * ```
2383
- *
2384
- * {@label Capture}
2385
- *
2386
- * @param props - The event properties
2387
- * @returns Promise that resolves when the event is captured
2388
- */
2389
- async captureImmediate(props) {
2390
- if (typeof props === 'string') {
2391
- this.logMsgIfDebug(() => console.warn('Called captureImmediate() with a string as the first argument when an object was expected.'));
2392
- }
2393
- return this.addPendingPromise(this.prepareEventMessage(props).then(({
2394
- distinctId,
2395
- event,
2396
- properties,
2397
- options
2398
- }) => {
2399
- return super.captureStatelessImmediate(distinctId, event, properties, {
2400
- timestamp: options.timestamp,
2401
- disableGeoip: options.disableGeoip,
2402
- uuid: options.uuid
2403
- });
2404
- }).catch(err => {
2405
- if (err) {
2406
- console.error(err);
2407
- }
2408
- }));
2409
- }
2410
- /**
2411
- * Identify a user and set their properties.
2412
- *
2413
- * @example
2414
- * ```ts
2415
- * // Basic identify with properties
2416
- * client.identify({
2417
- * distinctId: 'user_123',
2418
- * properties: {
2419
- * name: 'John Doe',
2420
- * email: 'john@example.com',
2421
- * plan: 'premium'
2422
- * }
2423
- * })
2424
- * ```
2425
- *
2426
- * @example
2427
- * ```ts
2428
- * // Using $set and $set_once
2429
- * client.identify({
2430
- * distinctId: 'user_123',
2431
- * properties: {
2432
- * $set: { name: 'John Doe', email: 'john@example.com' },
2433
- * $set_once: { first_login: new Date().toISOString() }
2434
- * }
2435
- * })
2436
- * ```
2437
- *
2438
- * {@label Identification}
2439
- *
2440
- * @param data - The identify data containing distinctId and properties
2441
- */
2442
- identify({
2443
- distinctId,
2444
- properties,
2445
- disableGeoip
2446
- }) {
2447
- // Catch properties passed as $set and move them to the top level
2448
- // promote $set and $set_once to top level
2449
- const userPropsOnce = properties?.$set_once;
2450
- delete properties?.$set_once;
2451
- // if no $set is provided we assume all properties are $set
2452
- const userProps = properties?.$set || properties;
2453
- super.identifyStateless(distinctId, {
2454
- $set: userProps,
2455
- $set_once: userPropsOnce
2456
- }, {
2457
- disableGeoip
2458
- });
2459
- }
2460
- /**
2461
- * Identify a user and set their properties immediately (synchronously).
2462
- *
2463
- * @example
2464
- * ```ts
2465
- * // Basic immediate identify
2466
- * await client.identifyImmediate({
2467
- * distinctId: 'user_123',
2468
- * properties: {
2469
- * name: 'John Doe',
2470
- * email: 'john@example.com'
2471
- * }
2472
- * })
2473
- * ```
2474
- *
2475
- * {@label Identification}
2476
- *
2477
- * @param data - The identify data containing distinctId and properties
2478
- * @returns Promise that resolves when the identify is processed
2479
- */
2480
- async identifyImmediate({
2481
- distinctId,
2482
- properties,
2483
- disableGeoip
2484
- }) {
2485
- // promote $set and $set_once to top level
2486
- const userPropsOnce = properties?.$set_once;
2487
- delete properties?.$set_once;
2488
- // if no $set is provided we assume all properties are $set
2489
- const userProps = properties?.$set || properties;
2490
- await super.identifyStatelessImmediate(distinctId, {
2491
- $set: userProps,
2492
- $set_once: userPropsOnce
2493
- }, {
2494
- disableGeoip
2495
- });
2496
- }
2497
- /**
2498
- * Create an alias to link two distinct IDs together.
2499
- *
2500
- * @example
2501
- * ```ts
2502
- * // Link an anonymous user to an identified user
2503
- * client.alias({
2504
- * distinctId: 'anonymous_123',
2505
- * alias: 'user_456'
2506
- * })
2507
- * ```
2508
- *
2509
- * {@label Identification}
2510
- *
2511
- * @param data - The alias data containing distinctId and alias
2512
- */
2513
- alias(data) {
2514
- super.aliasStateless(data.alias, data.distinctId, undefined, {
2515
- disableGeoip: data.disableGeoip
2516
- });
2517
- }
2518
- /**
2519
- * Create an alias to link two distinct IDs together immediately (synchronously).
2520
- *
2521
- * @example
2522
- * ```ts
2523
- * // Link an anonymous user to an identified user immediately
2524
- * await client.aliasImmediate({
2525
- * distinctId: 'anonymous_123',
2526
- * alias: 'user_456'
2527
- * })
2528
- * ```
2529
- *
2530
- * {@label Identification}
2531
- *
2532
- * @param data - The alias data containing distinctId and alias
2533
- * @returns Promise that resolves when the alias is processed
2534
- */
2535
- async aliasImmediate(data) {
2536
- await super.aliasStatelessImmediate(data.alias, data.distinctId, undefined, {
2537
- disableGeoip: data.disableGeoip
2538
- });
2539
- }
2540
- /**
2541
- * Check if local evaluation of feature flags is ready.
2542
- *
2543
- * @example
2544
- * ```ts
2545
- * // Check if ready
2546
- * if (client.isLocalEvaluationReady()) {
2547
- * // Local evaluation is ready, can evaluate flags locally
2548
- * const flag = await client.getFeatureFlag('flag-key', 'user_123')
2549
- * } else {
2550
- * // Local evaluation not ready, will use remote evaluation
2551
- * const flag = await client.getFeatureFlag('flag-key', 'user_123')
2552
- * }
2553
- * ```
2554
- *
2555
- * {@label Feature flags}
2556
- *
2557
- * @returns true if local evaluation is ready, false otherwise
2558
- */
2559
- isLocalEvaluationReady() {
2560
- return this.featureFlagsPoller?.isLocalEvaluationReady() ?? false;
2561
- }
2562
- /**
2563
- * Wait for local evaluation of feature flags to be ready.
2564
- *
2565
- * @example
2566
- * ```ts
2567
- * // Wait for local evaluation
2568
- * const isReady = await client.waitForLocalEvaluationReady()
2569
- * if (isReady) {
2570
- * console.log('Local evaluation is ready')
2571
- * } else {
2572
- * console.log('Local evaluation timed out')
2573
- * }
2574
- * ```
2575
- *
2576
- * @example
2577
- * ```ts
2578
- * // Wait with custom timeout
2579
- * const isReady = await client.waitForLocalEvaluationReady(10000) // 10 seconds
2580
- * ```
2581
- *
2582
- * {@label Feature flags}
2583
- *
2584
- * @param timeoutMs - Timeout in milliseconds (default: 30000)
2585
- * @returns Promise that resolves to true if ready, false if timed out
2586
- */
2587
- async waitForLocalEvaluationReady(timeoutMs = THIRTY_SECONDS) {
2588
- if (this.isLocalEvaluationReady()) {
2589
- return true;
2590
- }
2591
- if (this.featureFlagsPoller === undefined) {
2592
- return false;
2593
- }
2594
- return new Promise(resolve => {
2595
- const timeout = setTimeout(() => {
2596
- cleanup();
2597
- resolve(false);
2598
- }, timeoutMs);
2599
- const cleanup = this._events.on('localEvaluationFlagsLoaded', count => {
2600
- clearTimeout(timeout);
2601
- cleanup();
2602
- resolve(count > 0);
2603
- });
2604
- });
2605
- }
2606
- /**
2607
- * Get the value of a feature flag for a specific user.
2608
- *
2609
- * @example
2610
- * ```ts
2611
- * // Basic feature flag check
2612
- * const flagValue = await client.getFeatureFlag('new-feature', 'user_123')
2613
- * if (flagValue === 'variant-a') {
2614
- * // Show variant A
2615
- * } else if (flagValue === 'variant-b') {
2616
- * // Show variant B
2617
- * } else {
2618
- * // Flag is disabled or not found
2619
- * }
2620
- * ```
2621
- *
2622
- * @example
2623
- * ```ts
2624
- * // With groups and properties
2625
- * const flagValue = await client.getFeatureFlag('org-feature', 'user_123', {
2626
- * groups: { organization: 'acme-corp' },
2627
- * personProperties: { plan: 'enterprise' },
2628
- * groupProperties: { organization: { tier: 'premium' } }
2629
- * })
2630
- * ```
2631
- *
2632
- * @example
2633
- * ```ts
2634
- * // Only evaluate locally
2635
- * const flagValue = await client.getFeatureFlag('local-flag', 'user_123', {
2636
- * onlyEvaluateLocally: true
2637
- * })
2638
- * ```
2639
- *
2640
- * {@label Feature flags}
2641
- *
2642
- * @param key - The feature flag key
2643
- * @param distinctId - The user's distinct ID
2644
- * @param options - Optional configuration for flag evaluation
2645
- * @returns Promise that resolves to the flag value or undefined
2646
- */
2647
- async getFeatureFlag(key, distinctId, options) {
2648
- const {
2649
- groups,
2650
- disableGeoip
2651
- } = options || {};
2652
- let {
2653
- onlyEvaluateLocally,
2654
- sendFeatureFlagEvents,
2655
- personProperties,
2656
- groupProperties
2657
- } = options || {};
2658
- const adjustedProperties = this.addLocalPersonAndGroupProperties(distinctId, groups, personProperties, groupProperties);
2659
- personProperties = adjustedProperties.allPersonProperties;
2660
- groupProperties = adjustedProperties.allGroupProperties;
2661
- // set defaults
2662
- if (onlyEvaluateLocally == undefined) {
2663
- onlyEvaluateLocally = false;
2664
- }
2665
- if (sendFeatureFlagEvents == undefined) {
2666
- sendFeatureFlagEvents = this.options.sendFeatureFlagEvent ?? true;
2667
- }
2668
- let response = await this.featureFlagsPoller?.getFeatureFlag(key, distinctId, groups, personProperties, groupProperties);
2669
- const flagWasLocallyEvaluated = response !== undefined;
2670
- let requestId = undefined;
2671
- let flagDetail = undefined;
2672
- if (!flagWasLocallyEvaluated && !onlyEvaluateLocally) {
2673
- const remoteResponse = await super.getFeatureFlagDetailStateless(key, distinctId, groups, personProperties, groupProperties, disableGeoip);
2674
- if (remoteResponse === undefined) {
2675
- return undefined;
2676
- }
2677
- flagDetail = remoteResponse.response;
2678
- response = getFeatureFlagValue(flagDetail);
2679
- requestId = remoteResponse?.requestId;
2680
- }
2681
- const featureFlagReportedKey = `${key}_${response}`;
2682
- if (sendFeatureFlagEvents && (!(distinctId in this.distinctIdHasSentFlagCalls) || !this.distinctIdHasSentFlagCalls[distinctId].includes(featureFlagReportedKey))) {
2683
- if (Object.keys(this.distinctIdHasSentFlagCalls).length >= this.maxCacheSize) {
2684
- this.distinctIdHasSentFlagCalls = {};
2685
- }
2686
- if (Array.isArray(this.distinctIdHasSentFlagCalls[distinctId])) {
2687
- this.distinctIdHasSentFlagCalls[distinctId].push(featureFlagReportedKey);
2688
- } else {
2689
- this.distinctIdHasSentFlagCalls[distinctId] = [featureFlagReportedKey];
2690
- }
2691
- this.capture({
2692
- distinctId,
2693
- event: '$feature_flag_called',
2694
- properties: {
2695
- $feature_flag: key,
2696
- $feature_flag_response: response,
2697
- $feature_flag_id: flagDetail?.metadata?.id,
2698
- $feature_flag_version: flagDetail?.metadata?.version,
2699
- $feature_flag_reason: flagDetail?.reason?.description ?? flagDetail?.reason?.code,
2700
- locally_evaluated: flagWasLocallyEvaluated,
2701
- [`$feature/${key}`]: response,
2702
- $feature_flag_request_id: requestId
2703
- },
2704
- groups,
2705
- disableGeoip
2706
- });
2707
- }
2708
- return response;
2709
- }
2710
- /**
2711
- * Get the payload for a feature flag.
2712
- *
2713
- * @example
2714
- * ```ts
2715
- * // Get payload for a feature flag
2716
- * const payload = await client.getFeatureFlagPayload('flag-key', 'user_123')
2717
- * if (payload) {
2718
- * console.log('Flag payload:', payload)
2719
- * }
2720
- * ```
2721
- *
2722
- * @example
2723
- * ```ts
2724
- * // Get payload with specific match value
2725
- * const payload = await client.getFeatureFlagPayload('flag-key', 'user_123', 'variant-a')
2726
- * ```
2727
- *
2728
- * @example
2729
- * ```ts
2730
- * // With groups and properties
2731
- * const payload = await client.getFeatureFlagPayload('org-flag', 'user_123', undefined, {
2732
- * groups: { organization: 'acme-corp' },
2733
- * personProperties: { plan: 'enterprise' }
2734
- * })
2735
- * ```
2736
- *
2737
- * {@label Feature flags}
2738
- *
2739
- * @param key - The feature flag key
2740
- * @param distinctId - The user's distinct ID
2741
- * @param matchValue - Optional match value to get payload for
2742
- * @param options - Optional configuration for flag evaluation
2743
- * @returns Promise that resolves to the flag payload or undefined
2744
- */
2745
- async getFeatureFlagPayload(key, distinctId, matchValue, options) {
2746
- const {
2747
- groups,
2748
- disableGeoip
2749
- } = options || {};
2750
- let {
2751
- onlyEvaluateLocally,
2752
- personProperties,
2753
- groupProperties
2754
- } = options || {};
2755
- const adjustedProperties = this.addLocalPersonAndGroupProperties(distinctId, groups, personProperties, groupProperties);
2756
- personProperties = adjustedProperties.allPersonProperties;
2757
- groupProperties = adjustedProperties.allGroupProperties;
2758
- let response = undefined;
2759
- const localEvaluationEnabled = this.featureFlagsPoller !== undefined;
2760
- if (localEvaluationEnabled) {
2761
- // Ensure flags are loaded before checking for the specific flag
2762
- await this.featureFlagsPoller?.loadFeatureFlags();
2763
- const flag = this.featureFlagsPoller?.featureFlagsByKey[key];
2764
- if (flag) {
2765
- const result = await this.featureFlagsPoller?.computeFlagAndPayloadLocally(flag, distinctId, groups, personProperties, groupProperties, matchValue);
2766
- if (result) {
2767
- matchValue = result.value;
2768
- response = result.payload;
2769
- }
2770
- }
2771
- }
2772
- // set defaults
2773
- if (onlyEvaluateLocally == undefined) {
2774
- onlyEvaluateLocally = false;
2775
- }
2776
- const payloadWasLocallyEvaluated = response !== undefined;
2777
- if (!payloadWasLocallyEvaluated && !onlyEvaluateLocally) {
2778
- response = await super.getFeatureFlagPayloadStateless(key, distinctId, groups, personProperties, groupProperties, disableGeoip);
2779
- }
2780
- return response;
2781
- }
2782
- /**
2783
- * Get the remote config payload for a feature flag.
2784
- *
2785
- * @example
2786
- * ```ts
2787
- * // Get remote config payload
2788
- * const payload = await client.getRemoteConfigPayload('flag-key')
2789
- * if (payload) {
2790
- * console.log('Remote config payload:', payload)
2791
- * }
2792
- * ```
2793
- *
2794
- * {@label Feature flags}
2795
- *
2796
- * @param flagKey - The feature flag key
2797
- * @returns Promise that resolves to the remote config payload or undefined
2798
- * @throws Error if personal API key is not provided
2799
- */
2800
- async getRemoteConfigPayload(flagKey) {
2801
- if (!this.options.personalApiKey) {
2802
- throw new Error('Personal API key is required for remote config payload decryption');
2803
- }
2804
- const response = await this._requestRemoteConfigPayload(flagKey);
2805
- if (!response) {
2806
- return undefined;
2807
- }
2808
- const parsed = await response.json();
2809
- // The payload from the endpoint is stored as a JSON encoded string. So when we return
2810
- // it, it's effectively double encoded. As far as we know, we should never get single-encoded
2811
- // JSON, but we'll be defensive here just in case.
2812
- if (typeof parsed === 'string') {
2813
- try {
2814
- // If the parsed value is a string, try parsing it again to handle double-encoded JSON
2815
- return JSON.parse(parsed);
2816
- } catch (e) {
2817
- // If second parse fails, return the string as is
2818
- return parsed;
2819
- }
2820
- }
2821
- return parsed;
2822
- }
2823
- /**
2824
- * Check if a feature flag is enabled for a specific user.
2825
- *
2826
- * @example
2827
- * ```ts
2828
- * // Basic feature flag check
2829
- * const isEnabled = await client.isFeatureEnabled('new-feature', 'user_123')
2830
- * if (isEnabled) {
2831
- * // Feature is enabled
2832
- * console.log('New feature is active')
2833
- * } else {
2834
- * // Feature is disabled
2835
- * console.log('New feature is not active')
2836
- * }
2837
- * ```
2838
- *
2839
- * @example
2840
- * ```ts
2841
- * // With groups and properties
2842
- * const isEnabled = await client.isFeatureEnabled('org-feature', 'user_123', {
2843
- * groups: { organization: 'acme-corp' },
2844
- * personProperties: { plan: 'enterprise' }
2845
- * })
2846
- * ```
2847
- *
2848
- * {@label Feature flags}
2849
- *
2850
- * @param key - The feature flag key
2851
- * @param distinctId - The user's distinct ID
2852
- * @param options - Optional configuration for flag evaluation
2853
- * @returns Promise that resolves to true if enabled, false if disabled, undefined if not found
2854
- */
2855
- async isFeatureEnabled(key, distinctId, options) {
2856
- const feat = await this.getFeatureFlag(key, distinctId, options);
2857
- if (feat === undefined) {
2858
- return undefined;
2859
- }
2860
- return !!feat || false;
2861
- }
2862
- /**
2863
- * Get all feature flag values for a specific user.
2864
- *
2865
- * @example
2866
- * ```ts
2867
- * // Get all flags for a user
2868
- * const allFlags = await client.getAllFlags('user_123')
2869
- * console.log('User flags:', allFlags)
2870
- * // Output: { 'flag-1': 'variant-a', 'flag-2': false, 'flag-3': 'variant-b' }
2871
- * ```
2872
- *
2873
- * @example
2874
- * ```ts
2875
- * // With specific flag keys
2876
- * const specificFlags = await client.getAllFlags('user_123', {
2877
- * flagKeys: ['flag-1', 'flag-2']
2878
- * })
2879
- * ```
2880
- *
2881
- * @example
2882
- * ```ts
2883
- * // With groups and properties
2884
- * const orgFlags = await client.getAllFlags('user_123', {
2885
- * groups: { organization: 'acme-corp' },
2886
- * personProperties: { plan: 'enterprise' }
2887
- * })
2888
- * ```
2889
- *
2890
- * {@label Feature flags}
2891
- *
2892
- * @param distinctId - The user's distinct ID
2893
- * @param options - Optional configuration for flag evaluation
2894
- * @returns Promise that resolves to a record of flag keys and their values
2895
- */
2896
- async getAllFlags(distinctId, options) {
2897
- const response = await this.getAllFlagsAndPayloads(distinctId, options);
2898
- return response.featureFlags || {};
2899
- }
2900
- /**
2901
- * Get all feature flag values and payloads for a specific user.
2902
- *
2903
- * @example
2904
- * ```ts
2905
- * // Get all flags and payloads for a user
2906
- * const result = await client.getAllFlagsAndPayloads('user_123')
2907
- * console.log('Flags:', result.featureFlags)
2908
- * console.log('Payloads:', result.featureFlagPayloads)
2909
- * ```
2910
- *
2911
- * @example
2912
- * ```ts
2913
- * // With specific flag keys
2914
- * const result = await client.getAllFlagsAndPayloads('user_123', {
2915
- * flagKeys: ['flag-1', 'flag-2']
2916
- * })
2917
- * ```
2918
- *
2919
- * @example
2920
- * ```ts
2921
- * // Only evaluate locally
2922
- * const result = await client.getAllFlagsAndPayloads('user_123', {
2923
- * onlyEvaluateLocally: true
2924
- * })
2925
- * ```
2926
- *
2927
- * {@label Feature flags}
2928
- *
2929
- * @param distinctId - The user's distinct ID
2930
- * @param options - Optional configuration for flag evaluation
2931
- * @returns Promise that resolves to flags and payloads
2932
- */
2933
- async getAllFlagsAndPayloads(distinctId, options) {
2934
- const {
2935
- groups,
2936
- disableGeoip,
2937
- flagKeys
2938
- } = options || {};
2939
- let {
2940
- onlyEvaluateLocally,
2941
- personProperties,
2942
- groupProperties
2943
- } = options || {};
2944
- const adjustedProperties = this.addLocalPersonAndGroupProperties(distinctId, groups, personProperties, groupProperties);
2945
- personProperties = adjustedProperties.allPersonProperties;
2946
- groupProperties = adjustedProperties.allGroupProperties;
2947
- // set defaults
2948
- if (onlyEvaluateLocally == undefined) {
2949
- onlyEvaluateLocally = false;
2950
- }
2951
- const localEvaluationResult = await this.featureFlagsPoller?.getAllFlagsAndPayloads(distinctId, groups, personProperties, groupProperties, flagKeys);
2952
- let featureFlags = {};
2953
- let featureFlagPayloads = {};
2954
- let fallbackToFlags = true;
2955
- if (localEvaluationResult) {
2956
- featureFlags = localEvaluationResult.response;
2957
- featureFlagPayloads = localEvaluationResult.payloads;
2958
- fallbackToFlags = localEvaluationResult.fallbackToFlags;
2959
- }
2960
- if (fallbackToFlags && !onlyEvaluateLocally) {
2961
- const remoteEvaluationResult = await super.getFeatureFlagsAndPayloadsStateless(distinctId, groups, personProperties, groupProperties, disableGeoip, flagKeys);
2962
- featureFlags = {
2963
- ...featureFlags,
2964
- ...(remoteEvaluationResult.flags || {})
2965
- };
2966
- featureFlagPayloads = {
2967
- ...featureFlagPayloads,
2968
- ...(remoteEvaluationResult.payloads || {})
2969
- };
2970
- }
2971
- return {
2972
- featureFlags,
2973
- featureFlagPayloads
2974
- };
2975
- }
2976
- /**
2977
- * Create or update a group and its properties.
2978
- *
2979
- * @example
2980
- * ```ts
2981
- * // Create a company group
2982
- * client.groupIdentify({
2983
- * groupType: 'company',
2984
- * groupKey: 'acme-corp',
2985
- * properties: {
2986
- * name: 'Acme Corporation',
2987
- * industry: 'Technology',
2988
- * employee_count: 500
2989
- * },
2990
- * distinctId: 'user_123'
2991
- * })
2992
- * ```
2993
- *
2994
- * @example
2995
- * ```ts
2996
- * // Update organization properties
2997
- * client.groupIdentify({
2998
- * groupType: 'organization',
2999
- * groupKey: 'org-456',
3000
- * properties: {
3001
- * plan: 'enterprise',
3002
- * region: 'US-West'
3003
- * }
3004
- * })
3005
- * ```
3006
- *
3007
- * {@label Identification}
3008
- *
3009
- * @param data - The group identify data
3010
- */
3011
- groupIdentify({
3012
- groupType,
3013
- groupKey,
3014
- properties,
3015
- distinctId,
3016
- disableGeoip
3017
- }) {
3018
- super.groupIdentifyStateless(groupType, groupKey, properties, {
3019
- disableGeoip
3020
- }, distinctId);
3021
- }
3022
- /**
3023
- * Reload feature flag definitions from the server for local evaluation.
3024
- *
3025
- * @example
3026
- * ```ts
3027
- * // Force reload of feature flags
3028
- * await client.reloadFeatureFlags()
3029
- * console.log('Feature flags reloaded')
3030
- * ```
3031
- *
3032
- * @example
3033
- * ```ts
3034
- * // Reload before checking a specific flag
3035
- * await client.reloadFeatureFlags()
3036
- * const flag = await client.getFeatureFlag('flag-key', 'user_123')
3037
- * ```
3038
- *
3039
- * {@label Feature flags}
3040
- *
3041
- * @returns Promise that resolves when flags are reloaded
3042
- */
3043
- async reloadFeatureFlags() {
3044
- await this.featureFlagsPoller?.loadFeatureFlags(true);
3045
- }
3046
- /**
3047
- * Shutdown the PostHog client gracefully.
3048
- *
3049
- * @example
3050
- * ```ts
3051
- * // Shutdown with default timeout
3052
- * await client._shutdown()
3053
- * ```
3054
- *
3055
- * @example
3056
- * ```ts
3057
- * // Shutdown with custom timeout
3058
- * await client._shutdown(5000) // 5 seconds
3059
- * ```
3060
- *
3061
- * {@label Shutdown}
3062
- *
3063
- * @param shutdownTimeoutMs - Timeout in milliseconds for shutdown
3064
- * @returns Promise that resolves when shutdown is complete
3065
- */
3066
- async _shutdown(shutdownTimeoutMs) {
3067
- this.featureFlagsPoller?.stopPoller();
3068
- this.errorTracking.shutdown();
3069
- return super._shutdown(shutdownTimeoutMs);
3070
- }
3071
- async _requestRemoteConfigPayload(flagKey) {
3072
- if (!this.options.personalApiKey) {
3073
- return undefined;
3074
- }
3075
- const url = `${this.host}/api/projects/@current/feature_flags/${flagKey}/remote_config?token=${encodeURIComponent(this.apiKey)}`;
3076
- const options = {
3077
- method: 'GET',
3078
- headers: {
3079
- ...this.getCustomHeaders(),
3080
- 'Content-Type': 'application/json',
3081
- Authorization: `Bearer ${this.options.personalApiKey}`
3082
- }
3083
- };
3084
- let abortTimeout = null;
3085
- if (this.options.requestTimeout && typeof this.options.requestTimeout === 'number') {
3086
- const controller = new AbortController();
3087
- abortTimeout = safeSetTimeout$1(() => {
3088
- controller.abort();
3089
- }, this.options.requestTimeout);
3090
- options.signal = controller.signal;
3091
- }
3092
- try {
3093
- return await this.fetch(url, options);
3094
- } catch (error) {
3095
- this._events.emit('error', error);
3096
- return undefined;
3097
- } finally {
3098
- if (abortTimeout) {
3099
- clearTimeout(abortTimeout);
3100
- }
3101
- }
3102
- }
3103
- extractPropertiesFromEvent(eventProperties, groups) {
3104
- if (!eventProperties) {
3105
- return {
3106
- personProperties: {},
3107
- groupProperties: {}
3108
- };
3109
- }
3110
- const personProperties = {};
3111
- const groupProperties = {};
3112
- for (const [key, value] of Object.entries(eventProperties)) {
3113
- // If the value is a plain object and the key exists in groups, treat it as group properties
3114
- if (isPlainObject(value) && groups && key in groups) {
3115
- const groupProps = {};
3116
- for (const [groupKey, groupValue] of Object.entries(value)) {
3117
- groupProps[String(groupKey)] = String(groupValue);
3118
- }
3119
- groupProperties[String(key)] = groupProps;
3120
- } else {
3121
- // Otherwise treat as person property
3122
- personProperties[String(key)] = String(value);
3123
- }
3124
- }
3125
- return {
3126
- personProperties,
3127
- groupProperties
3128
- };
3129
- }
3130
- async getFeatureFlagsForEvent(distinctId, groups, disableGeoip, sendFeatureFlagsOptions) {
3131
- // Use properties directly from options if they exist
3132
- const finalPersonProperties = sendFeatureFlagsOptions?.personProperties || {};
3133
- const finalGroupProperties = sendFeatureFlagsOptions?.groupProperties || {};
3134
- const flagKeys = sendFeatureFlagsOptions?.flagKeys;
3135
- // Check if we should only evaluate locally
3136
- const onlyEvaluateLocally = sendFeatureFlagsOptions?.onlyEvaluateLocally ?? false;
3137
- // If onlyEvaluateLocally is true, only use local evaluation
3138
- if (onlyEvaluateLocally) {
3139
- if ((this.featureFlagsPoller?.featureFlags?.length || 0) > 0) {
3140
- const groupsWithStringValues = {};
3141
- for (const [key, value] of Object.entries(groups || {})) {
3142
- groupsWithStringValues[key] = String(value);
3143
- }
3144
- return await this.getAllFlags(distinctId, {
3145
- groups: groupsWithStringValues,
3146
- personProperties: finalPersonProperties,
3147
- groupProperties: finalGroupProperties,
3148
- disableGeoip,
3149
- onlyEvaluateLocally: true,
3150
- flagKeys
3151
- });
3152
- } else {
3153
- // If onlyEvaluateLocally is true but we don't have local flags, return empty
3154
- return {};
3155
- }
3156
- }
3157
- // Prefer local evaluation if available (default behavior; I'd rather not penalize users who haven't updated to the new API but still want to use local evaluation)
3158
- if ((this.featureFlagsPoller?.featureFlags?.length || 0) > 0) {
3159
- const groupsWithStringValues = {};
3160
- for (const [key, value] of Object.entries(groups || {})) {
3161
- groupsWithStringValues[key] = String(value);
3162
- }
3163
- return await this.getAllFlags(distinctId, {
3164
- groups: groupsWithStringValues,
3165
- personProperties: finalPersonProperties,
3166
- groupProperties: finalGroupProperties,
3167
- disableGeoip,
3168
- onlyEvaluateLocally: true,
3169
- flagKeys
3170
- });
3171
- }
3172
- // Fall back to remote evaluation if local evaluation is not available
3173
- return (await super.getFeatureFlagsStateless(distinctId, groups, finalPersonProperties, finalGroupProperties, disableGeoip)).flags;
3174
- }
3175
- addLocalPersonAndGroupProperties(distinctId, groups, personProperties, groupProperties) {
3176
- const allPersonProperties = {
3177
- distinct_id: distinctId,
3178
- ...(personProperties || {})
3179
- };
3180
- const allGroupProperties = {};
3181
- if (groups) {
3182
- for (const groupName of Object.keys(groups)) {
3183
- allGroupProperties[groupName] = {
3184
- $group_key: groups[groupName],
3185
- ...(groupProperties?.[groupName] || {})
3186
- };
3187
- }
3188
- }
3189
- return {
3190
- allPersonProperties,
3191
- allGroupProperties
3192
- };
3193
- }
3194
- /**
3195
- * Capture an error exception as an event.
3196
- *
3197
- * @example
3198
- * ```ts
3199
- * // Capture an error with user ID
3200
- * try {
3201
- * // Some risky operation
3202
- * riskyOperation()
3203
- * } catch (error) {
3204
- * client.captureException(error, 'user_123')
3205
- * }
3206
- * ```
3207
- *
3208
- * @example
3209
- * ```ts
3210
- * // Capture with additional properties
3211
- * try {
3212
- * apiCall()
3213
- * } catch (error) {
3214
- * client.captureException(error, 'user_123', {
3215
- * endpoint: '/api/users',
3216
- * method: 'POST',
3217
- * status_code: 500
3218
- * })
3219
- * }
3220
- * ```
3221
- *
3222
- * {@label Error tracking}
3223
- *
3224
- * @param error - The error to capture
3225
- * @param distinctId - Optional user distinct ID
3226
- * @param additionalProperties - Optional additional properties to include
3227
- */
3228
- captureException(error, distinctId, additionalProperties) {
3229
- const syntheticException = new Error('PostHog syntheticException');
3230
- this.addPendingPromise(ErrorTracking.buildEventMessage(error, {
3231
- syntheticException
3232
- }, distinctId, additionalProperties).then(msg => this.capture(msg)));
3233
- }
3234
- /**
3235
- * Capture an error exception as an event immediately (synchronously).
3236
- *
3237
- * @example
3238
- * ```ts
3239
- * // Capture an error immediately with user ID
3240
- * try {
3241
- * // Some risky operation
3242
- * riskyOperation()
3243
- * } catch (error) {
3244
- * await client.captureExceptionImmediate(error, 'user_123')
3245
- * }
3246
- * ```
3247
- *
3248
- * @example
3249
- * ```ts
3250
- * // Capture with additional properties
3251
- * try {
3252
- * apiCall()
3253
- * } catch (error) {
3254
- * await client.captureExceptionImmediate(error, 'user_123', {
3255
- * endpoint: '/api/users',
3256
- * method: 'POST',
3257
- * status_code: 500
3258
- * })
3259
- * }
3260
- * ```
3261
- *
3262
- * {@label Error tracking}
3263
- *
3264
- * @param error - The error to capture
3265
- * @param distinctId - Optional user distinct ID
3266
- * @param additionalProperties - Optional additional properties to include
3267
- * @returns Promise that resolves when the error is captured
3268
- */
3269
- async captureExceptionImmediate(error, distinctId, additionalProperties) {
3270
- const syntheticException = new Error('PostHog syntheticException');
3271
- this.addPendingPromise(ErrorTracking.buildEventMessage(error, {
3272
- syntheticException
3273
- }, distinctId, additionalProperties).then(msg => this.captureImmediate(msg)));
3274
- }
3275
- async prepareEventMessage(props) {
3276
- const {
3277
- distinctId,
3278
- event,
3279
- properties,
3280
- groups,
3281
- sendFeatureFlags,
3282
- timestamp,
3283
- disableGeoip,
3284
- uuid
3285
- } = props;
3286
- // Run before_send if configured
3287
- const eventMessage = this._runBeforeSend({
3288
- distinctId,
3289
- event,
3290
- properties,
3291
- groups,
3292
- sendFeatureFlags,
3293
- timestamp,
3294
- disableGeoip,
3295
- uuid
3296
- });
3297
- if (!eventMessage) {
3298
- return Promise.reject(null);
3299
- }
3300
- // :TRICKY: If we flush, or need to shut down, to not lose events we want this promise to resolve before we flush
3301
- const eventProperties = await Promise.resolve().then(async () => {
3302
- if (sendFeatureFlags) {
3303
- // If we are sending feature flags, we evaluate them locally if the user prefers it, otherwise we fall back to remote evaluation
3304
- const sendFeatureFlagsOptions = typeof sendFeatureFlags === 'object' ? sendFeatureFlags : undefined;
3305
- return await this.getFeatureFlagsForEvent(distinctId, groups, disableGeoip, sendFeatureFlagsOptions);
3306
- }
3307
- if (event === '$feature_flag_called') {
3308
- // If we're capturing a $feature_flag_called event, we don't want to enrich the event with cached flags that may be out of date.
3309
- return {};
3310
- }
3311
- return {};
3312
- }).then(flags => {
3313
- // Derive the relevant flag properties to add
3314
- const additionalProperties = {};
3315
- if (flags) {
3316
- for (const [feature, variant] of Object.entries(flags)) {
3317
- additionalProperties[`$feature/${feature}`] = variant;
3318
- }
3319
- }
3320
- const activeFlags = Object.keys(flags || {}).filter(flag => flags?.[flag] !== false).sort();
3321
- if (activeFlags.length > 0) {
3322
- additionalProperties['$active_feature_flags'] = activeFlags;
3323
- }
3324
- return additionalProperties;
3325
- }).catch(() => {
3326
- // Something went wrong getting the flag info - we should capture the event anyways
3327
- return {};
3328
- }).then(additionalProperties => {
3329
- // No matter what - capture the event
3330
- const props = {
3331
- ...additionalProperties,
3332
- ...(eventMessage.properties || {}),
3333
- $groups: eventMessage.groups || groups
3334
- };
3335
- return props;
3336
- });
3337
- return {
3338
- distinctId: eventMessage.distinctId,
3339
- event: eventMessage.event,
3340
- properties: eventProperties,
3341
- options: {
3342
- timestamp: eventMessage.timestamp,
3343
- disableGeoip: eventMessage.disableGeoip,
3344
- uuid: eventMessage.uuid
3345
- }
3346
- };
3347
- }
3348
- _runBeforeSend(eventMessage) {
3349
- const beforeSend = this.options.before_send;
3350
- if (!beforeSend) {
3351
- return eventMessage;
3352
- }
3353
- const fns = Array.isArray(beforeSend) ? beforeSend : [beforeSend];
3354
- let result = eventMessage;
3355
- for (const fn of fns) {
3356
- result = fn(result);
3357
- if (!result) {
3358
- this.logMsgIfDebug(() => console.info(`Event '${eventMessage.event}' was rejected in beforeSend function`));
3359
- return null;
3360
- }
3361
- if (!result.properties || Object.keys(result.properties).length === 0) {
3362
- const message = `Event '${result.event}' has no properties after beforeSend function, this is likely an error.`;
3363
- this.logMsgIfDebug(() => console.warn(message));
3364
- }
3365
- }
3366
- return result;
3367
- }
3368
- }
3369
-
3370
- // Portions of this file are derived from getsentry/sentry-javascript by Software, Inc. dba Sentry
3371
- // Licensed under the MIT License
3372
- // This was originally forked from https://github.com/csnover/TraceKit, and was largely
3373
- // re-written as part of raven - js.
3374
- //
3375
- // This code was later copied to the JavaScript mono - repo and further modified and
3376
- // refactored over the years.
3377
- // Copyright (c) 2013 Onur Can Cakmak onur.cakmak@gmail.com and all TraceKit contributors.
3378
- //
3379
- // Permission is hereby granted, free of charge, to any person obtaining a copy of this
3380
- // software and associated documentation files(the 'Software'), to deal in the Software
3381
- // without restriction, including without limitation the rights to use, copy, modify,
3382
- // merge, publish, distribute, sublicense, and / or sell copies of the Software, and to
3383
- // permit persons to whom the Software is furnished to do so, subject to the following
3384
- // conditions:
3385
- //
3386
- // The above copyright notice and this permission notice shall be included in all copies
3387
- // or substantial portions of the Software.
3388
- //
3389
- // THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
3390
- // INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
3391
- // PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
3392
- // HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
3393
- // CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
3394
- // OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
3395
- const WEBPACK_ERROR_REGEXP = /\(error: (.*)\)/;
3396
- const STACKTRACE_FRAME_LIMIT = 50;
3397
- const UNKNOWN_FUNCTION = '?';
3398
- /** Node Stack line parser */
3399
- function node(getModule) {
3400
- const FILENAME_MATCH = /^\s*[-]{4,}$/;
3401
- const FULL_MATCH = /at (?:async )?(?:(.+?)\s+\()?(?:(.+):(\d+):(\d+)?|([^)]+))\)?/;
3402
- return line => {
3403
- const lineMatch = line.match(FULL_MATCH);
3404
- if (lineMatch) {
3405
- let object;
3406
- let method;
3407
- let functionName;
3408
- let typeName;
3409
- let methodName;
3410
- if (lineMatch[1]) {
3411
- functionName = lineMatch[1];
3412
- let methodStart = functionName.lastIndexOf('.');
3413
- if (functionName[methodStart - 1] === '.') {
3414
- methodStart--;
3415
- }
3416
- if (methodStart > 0) {
3417
- object = functionName.slice(0, methodStart);
3418
- method = functionName.slice(methodStart + 1);
3419
- const objectEnd = object.indexOf('.Module');
3420
- if (objectEnd > 0) {
3421
- functionName = functionName.slice(objectEnd + 1);
3422
- object = object.slice(0, objectEnd);
3423
- }
3424
- }
3425
- typeName = undefined;
3426
- }
3427
- if (method) {
3428
- typeName = object;
3429
- methodName = method;
3430
- }
3431
- if (method === '<anonymous>') {
3432
- methodName = undefined;
3433
- functionName = undefined;
3434
- }
3435
- if (functionName === undefined) {
3436
- methodName = methodName || UNKNOWN_FUNCTION;
3437
- functionName = typeName ? `${typeName}.${methodName}` : methodName;
3438
- }
3439
- let filename = lineMatch[2]?.startsWith('file://') ? lineMatch[2].slice(7) : lineMatch[2];
3440
- const isNative = lineMatch[5] === 'native';
3441
- // If it's a Windows path, trim the leading slash so that `/C:/foo` becomes `C:/foo`
3442
- if (filename?.match(/\/[A-Z]:/)) {
3443
- filename = filename.slice(1);
3444
- }
3445
- if (!filename && lineMatch[5] && !isNative) {
3446
- filename = lineMatch[5];
3447
- }
3448
- return {
3449
- filename: filename ? decodeURI(filename) : undefined,
3450
- module: getModule ? getModule(filename) : undefined,
3451
- function: functionName,
3452
- lineno: _parseIntOrUndefined(lineMatch[3]),
3453
- colno: _parseIntOrUndefined(lineMatch[4]),
3454
- in_app: filenameIsInApp(filename || '', isNative),
3455
- platform: 'node:javascript'
3456
- };
3457
- }
3458
- if (line.match(FILENAME_MATCH)) {
3459
- return {
3460
- filename: line,
3461
- platform: 'node:javascript'
3462
- };
3463
- }
3464
- return undefined;
3465
- };
3466
- }
3467
- /**
3468
- * Does this filename look like it's part of the app code?
3469
- */
3470
- function filenameIsInApp(filename, isNative = false) {
3471
- const isInternal = isNative || filename &&
3472
- // It's not internal if it's an absolute linux path
3473
- !filename.startsWith('/') &&
3474
- // It's not internal if it's an absolute windows path
3475
- !filename.match(/^[A-Z]:/) &&
3476
- // It's not internal if the path is starting with a dot
3477
- !filename.startsWith('.') &&
3478
- // It's not internal if the frame has a protocol. In node, this is usually the case if the file got pre-processed with a bundler like webpack
3479
- !filename.match(/^[a-zA-Z]([a-zA-Z0-9.\-+])*:\/\//); // Schema from: https://stackoverflow.com/a/3641782
3480
- // in_app is all that's not an internal Node function or a module within node_modules
3481
- // note that isNative appears to return true even for node core libraries
3482
- // see https://github.com/getsentry/raven-node/issues/176
3483
- return !isInternal && filename !== undefined && !filename.includes('node_modules/');
3484
- }
3485
- function _parseIntOrUndefined(input) {
3486
- return parseInt(input || '', 10) || undefined;
3487
- }
3488
- function nodeStackLineParser(getModule) {
3489
- return [90, node(getModule)];
3490
- }
3491
- function createStackParser(getModule) {
3492
- const parsers = [nodeStackLineParser(getModule)];
3493
- const sortedParsers = parsers.sort((a, b) => a[0] - b[0]).map(p => p[1]);
3494
- return (stack, skipFirstLines = 0) => {
3495
- const frames = [];
3496
- const lines = stack.split('\n');
3497
- for (let i = skipFirstLines; i < lines.length; i++) {
3498
- const line = lines[i];
3499
- // Ignore lines over 1kb as they are unlikely to be stack frames.
3500
- if (line.length > 1024) {
3501
- continue;
3502
- }
3503
- // https://github.com/getsentry/sentry-javascript/issues/5459
3504
- // Remove webpack (error: *) wrappers
3505
- const cleanedLine = WEBPACK_ERROR_REGEXP.test(line) ? line.replace(WEBPACK_ERROR_REGEXP, '$1') : line;
3506
- // https://github.com/getsentry/sentry-javascript/issues/7813
3507
- // Skip Error: lines
3508
- if (cleanedLine.match(/\S*Error: /)) {
3509
- continue;
3510
- }
3511
- for (const parser of sortedParsers) {
3512
- const frame = parser(cleanedLine);
3513
- if (frame) {
3514
- frames.push(frame);
3515
- break;
3516
- }
3517
- }
3518
- if (frames.length >= STACKTRACE_FRAME_LIMIT) {
3519
- break;
3520
- }
3521
- }
3522
- return reverseAndStripFrames(frames);
3523
- };
3524
- }
3525
- function reverseAndStripFrames(stack) {
3526
- if (!stack.length) {
3527
- return [];
3528
- }
3529
- const localStack = Array.from(stack);
3530
- localStack.reverse();
3531
- return localStack.slice(0, STACKTRACE_FRAME_LIMIT).map(frame => ({
3532
- ...frame,
3533
- filename: frame.filename || getLastStackFrame(localStack).filename,
3534
- function: frame.function || UNKNOWN_FUNCTION
3535
- }));
3536
- }
3537
- function getLastStackFrame(arr) {
3538
- return arr[arr.length - 1] || {};
3539
- }
3540
-
3541
- ErrorTracking.stackParser = createStackParser(createGetModuleFromFilename());
3542
- ErrorTracking.frameModifiers = [addSourceContext];
3543
- class PostHog extends PostHogBackendClient {
3544
- getLibraryId() {
3545
- return 'posthog-node';
3546
- }
3547
- }
3548
-
3549
- export { PostHog, PostHogSentryIntegration, createEventProcessor, sentryIntegration, setupExpressErrorHandler };
3550
- //# sourceMappingURL=index.mjs.map