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