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