posthog-node 5.8.8 → 5.9.0

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