posthog-node 5.6.0 → 5.8.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.
@@ -1,29 +1,30 @@
1
- import { posix, dirname, sep } from 'path';
1
+ import { dirname, posix, sep } from 'path';
2
2
  import { createReadStream } from 'node:fs';
3
3
  import { createInterface } from 'node:readline';
4
+ import { PostHogCoreStateless, getFeatureFlagValue } from '@posthog/core';
4
5
 
5
- /**
6
- * @file Adapted from [posthog-js](https://github.com/PostHog/posthog-js/blob/8157df935a4d0e71d2fefef7127aa85ee51c82d1/src/extensions/sentry-integration.ts) with modifications for the Node SDK.
6
+ /**
7
+ * @file Adapted from [posthog-js](https://github.com/PostHog/posthog-js/blob/8157df935a4d0e71d2fefef7127aa85ee51c82d1/src/extensions/sentry-integration.ts) with modifications for the Node SDK.
7
8
  */
8
- /**
9
- * Integrate Sentry with PostHog. This will add a direct link to the person in Sentry, and an $exception event in PostHog.
10
- *
11
- * ### Usage
12
- *
13
- * Sentry.init({
14
- * dsn: 'https://example',
15
- * integrations: [
16
- * new PostHogSentryIntegration(posthog)
17
- * ]
18
- * })
19
- *
20
- * Sentry.setTag(PostHogSentryIntegration.POSTHOG_ID_TAG, 'some distinct id');
21
- *
22
- * @param {Object} [posthog] The posthog object
23
- * @param {string} [organization] Optional: The Sentry organization, used to send a direct link from PostHog to Sentry
24
- * @param {Number} [projectId] Optional: The Sentry project id, used to send a direct link from PostHog to Sentry
25
- * @param {string} [prefix] Optional: Url of a self-hosted sentry instance (default: https://sentry.io/organizations/)
26
- * @param {SeverityLevel[] | '*'} [severityAllowList] Optional: send events matching the provided levels. Use '*' to send all events (default: ['error'])
9
+ /**
10
+ * Integrate Sentry with PostHog. This will add a direct link to the person in Sentry, and an $exception event in PostHog.
11
+ *
12
+ * ### Usage
13
+ *
14
+ * Sentry.init({
15
+ * dsn: 'https://example',
16
+ * integrations: [
17
+ * new PostHogSentryIntegration(posthog)
18
+ * ]
19
+ * })
20
+ *
21
+ * Sentry.setTag(PostHogSentryIntegration.POSTHOG_ID_TAG, 'some distinct id');
22
+ *
23
+ * @param {Object} [posthog] The posthog object
24
+ * @param {string} [organization] Optional: The Sentry organization, used to send a direct link from PostHog to Sentry
25
+ * @param {Number} [projectId] Optional: The Sentry project id, used to send a direct link from PostHog to Sentry
26
+ * @param {string} [prefix] Optional: Url of a self-hosted sentry instance (default: https://sentry.io/organizations/)
27
+ * @param {SeverityLevel[] | '*'} [severityAllowList] Optional: send events matching the provided levels. Use '*' to send all events (default: ['error'])
27
28
  */
28
29
  const NAME = 'posthog-node';
29
30
  function createEventProcessor(_posthog, {
@@ -117,411 +118,175 @@ class PostHogSentryIntegration {
117
118
  }
118
119
  PostHogSentryIntegration.POSTHOG_ID_TAG = 'posthog_distinct_id';
119
120
 
120
- // vendor from: https://github.com/LiosK/uuidv7/blob/f30b7a7faff73afbce0b27a46c638310f96912ba/src/index.ts
121
- // https://github.com/LiosK/uuidv7#license
122
- /**
123
- * uuidv7: An experimental implementation of the proposed UUID Version 7
124
- *
125
- * @license Apache-2.0
126
- * @copyright 2021-2023 LiosK
127
- * @packageDocumentation
128
- */
129
- const DIGITS = "0123456789abcdef";
130
- /** Represents a UUID as a 16-byte byte array. */
131
- class UUID {
132
- /** @param bytes - The 16-byte byte array representation. */
133
- constructor(bytes) {
134
- this.bytes = bytes;
135
- }
136
- /**
137
- * Creates an object from the internal representation, a 16-byte byte array
138
- * containing the binary UUID representation in the big-endian byte order.
139
- *
140
- * This method does NOT shallow-copy the argument, and thus the created object
141
- * holds the reference to the underlying buffer.
142
- *
143
- * @throws TypeError if the length of the argument is not 16.
144
- */
145
- static ofInner(bytes) {
146
- if (bytes.length !== 16) {
147
- throw new TypeError("not 128-bit length");
148
- }
149
- else {
150
- return new UUID(bytes);
151
- }
152
- }
153
- /**
154
- * Builds a byte array from UUIDv7 field values.
155
- *
156
- * @param unixTsMs - A 48-bit `unix_ts_ms` field value.
157
- * @param randA - A 12-bit `rand_a` field value.
158
- * @param randBHi - The higher 30 bits of 62-bit `rand_b` field value.
159
- * @param randBLo - The lower 32 bits of 62-bit `rand_b` field value.
160
- * @throws RangeError if any field value is out of the specified range.
161
- */
162
- static fromFieldsV7(unixTsMs, randA, randBHi, randBLo) {
163
- if (!Number.isInteger(unixTsMs) ||
164
- !Number.isInteger(randA) ||
165
- !Number.isInteger(randBHi) ||
166
- !Number.isInteger(randBLo) ||
167
- unixTsMs < 0 ||
168
- randA < 0 ||
169
- randBHi < 0 ||
170
- randBLo < 0 ||
171
- unixTsMs > 281474976710655 ||
172
- randA > 0xfff ||
173
- randBHi > 1073741823 ||
174
- randBLo > 4294967295) {
175
- throw new RangeError("invalid field value");
176
- }
177
- const bytes = new Uint8Array(16);
178
- bytes[0] = unixTsMs / 2 ** 40;
179
- bytes[1] = unixTsMs / 2 ** 32;
180
- bytes[2] = unixTsMs / 2 ** 24;
181
- bytes[3] = unixTsMs / 2 ** 16;
182
- bytes[4] = unixTsMs / 2 ** 8;
183
- bytes[5] = unixTsMs;
184
- bytes[6] = 0x70 | (randA >>> 8);
185
- bytes[7] = randA;
186
- bytes[8] = 0x80 | (randBHi >>> 24);
187
- bytes[9] = randBHi >>> 16;
188
- bytes[10] = randBHi >>> 8;
189
- bytes[11] = randBHi;
190
- bytes[12] = randBLo >>> 24;
191
- bytes[13] = randBLo >>> 16;
192
- bytes[14] = randBLo >>> 8;
193
- bytes[15] = randBLo;
194
- return new UUID(bytes);
195
- }
196
- /**
197
- * Builds a byte array from a string representation.
198
- *
199
- * This method accepts the following formats:
200
- *
201
- * - 32-digit hexadecimal format without hyphens: `0189dcd553117d408db09496a2eef37b`
202
- * - 8-4-4-4-12 hyphenated format: `0189dcd5-5311-7d40-8db0-9496a2eef37b`
203
- * - Hyphenated format with surrounding braces: `{0189dcd5-5311-7d40-8db0-9496a2eef37b}`
204
- * - RFC 4122 URN format: `urn:uuid:0189dcd5-5311-7d40-8db0-9496a2eef37b`
205
- *
206
- * Leading and trailing whitespaces represents an error.
207
- *
208
- * @throws SyntaxError if the argument could not parse as a valid UUID string.
209
- */
210
- static parse(uuid) {
211
- let hex = undefined;
212
- switch (uuid.length) {
213
- case 32:
214
- hex = /^[0-9a-f]{32}$/i.exec(uuid)?.[0];
215
- break;
216
- case 36:
217
- hex =
218
- /^([0-9a-f]{8})-([0-9a-f]{4})-([0-9a-f]{4})-([0-9a-f]{4})-([0-9a-f]{12})$/i
219
- .exec(uuid)
220
- ?.slice(1, 6)
221
- .join("");
222
- break;
223
- case 38:
224
- hex =
225
- /^\{([0-9a-f]{8})-([0-9a-f]{4})-([0-9a-f]{4})-([0-9a-f]{4})-([0-9a-f]{12})\}$/i
226
- .exec(uuid)
227
- ?.slice(1, 6)
228
- .join("");
229
- break;
230
- case 45:
231
- hex =
232
- /^urn:uuid:([0-9a-f]{8})-([0-9a-f]{4})-([0-9a-f]{4})-([0-9a-f]{4})-([0-9a-f]{12})$/i
233
- .exec(uuid)
234
- ?.slice(1, 6)
235
- .join("");
236
- break;
237
- }
238
- if (hex) {
239
- const inner = new Uint8Array(16);
240
- for (let i = 0; i < 16; i += 4) {
241
- const n = parseInt(hex.substring(2 * i, 2 * i + 8), 16);
242
- inner[i + 0] = n >>> 24;
243
- inner[i + 1] = n >>> 16;
244
- inner[i + 2] = n >>> 8;
245
- inner[i + 3] = n;
246
- }
247
- return new UUID(inner);
248
- }
249
- else {
250
- throw new SyntaxError("could not parse UUID string");
251
- }
252
- }
253
- /**
254
- * @returns The 8-4-4-4-12 canonical hexadecimal string representation
255
- * (`0189dcd5-5311-7d40-8db0-9496a2eef37b`).
256
- */
257
- toString() {
258
- let text = "";
259
- for (let i = 0; i < this.bytes.length; i++) {
260
- text += DIGITS.charAt(this.bytes[i] >>> 4);
261
- text += DIGITS.charAt(this.bytes[i] & 0xf);
262
- if (i === 3 || i === 5 || i === 7 || i === 9) {
263
- text += "-";
264
- }
265
- }
266
- return text;
267
- }
268
- /**
269
- * @returns The 32-digit hexadecimal representation without hyphens
270
- * (`0189dcd553117d408db09496a2eef37b`).
271
- */
272
- toHex() {
273
- let text = "";
274
- for (let i = 0; i < this.bytes.length; i++) {
275
- text += DIGITS.charAt(this.bytes[i] >>> 4);
276
- text += DIGITS.charAt(this.bytes[i] & 0xf);
277
- }
278
- return text;
279
- }
280
- /** @returns The 8-4-4-4-12 canonical hexadecimal string representation. */
281
- toJSON() {
282
- return this.toString();
283
- }
284
- /**
285
- * Reports the variant field value of the UUID or, if appropriate, "NIL" or
286
- * "MAX".
287
- *
288
- * For convenience, this method reports "NIL" or "MAX" if `this` represents
289
- * the Nil or Max UUID, although the Nil and Max UUIDs are technically
290
- * subsumed under the variants `0b0` and `0b111`, respectively.
291
- */
292
- getVariant() {
293
- const n = this.bytes[8] >>> 4;
294
- if (n < 0) {
295
- throw new Error("unreachable");
296
- }
297
- else if (n <= 0b0111) {
298
- return this.bytes.every((e) => e === 0) ? "NIL" : "VAR_0";
299
- }
300
- else if (n <= 0b1011) {
301
- return "VAR_10";
302
- }
303
- else if (n <= 0b1101) {
304
- return "VAR_110";
305
- }
306
- else if (n <= 0b1111) {
307
- return this.bytes.every((e) => e === 0xff) ? "MAX" : "VAR_RESERVED";
308
- }
309
- else {
310
- throw new Error("unreachable");
311
- }
312
- }
313
- /**
314
- * Returns the version field value of the UUID or `undefined` if the UUID does
315
- * not have the variant field value of `0b10`.
316
- */
317
- getVersion() {
318
- return this.getVariant() === "VAR_10" ? this.bytes[6] >>> 4 : undefined;
319
- }
320
- /** Creates an object from `this`. */
321
- clone() {
322
- return new UUID(this.bytes.slice(0));
323
- }
324
- /** Returns true if `this` is equivalent to `other`. */
325
- equals(other) {
326
- return this.compareTo(other) === 0;
327
- }
328
- /**
329
- * Returns a negative integer, zero, or positive integer if `this` is less
330
- * than, equal to, or greater than `other`, respectively.
331
- */
332
- compareTo(other) {
333
- for (let i = 0; i < 16; i++) {
334
- const diff = this.bytes[i] - other.bytes[i];
335
- if (diff !== 0) {
336
- return Math.sign(diff);
337
- }
338
- }
339
- return 0;
340
- }
341
- }
342
- /**
343
- * Encapsulates the monotonic counter state.
344
- *
345
- * This class provides APIs to utilize a separate counter state from that of the
346
- * global generator used by {@link uuidv7} and {@link uuidv7obj}. In addition to
347
- * the default {@link generate} method, this class has {@link generateOrAbort}
348
- * that is useful to absolutely guarantee the monotonically increasing order of
349
- * generated UUIDs. See their respective documentation for details.
350
- */
351
- class V7Generator {
352
- /**
353
- * Creates a generator object with the default random number generator, or
354
- * with the specified one if passed as an argument. The specified random
355
- * number generator should be cryptographically strong and securely seeded.
356
- */
357
- constructor(randomNumberGenerator) {
358
- this.timestamp = 0;
359
- this.counter = 0;
360
- this.random = randomNumberGenerator ?? getDefaultRandom();
361
- }
362
- /**
363
- * Generates a new UUIDv7 object from the current timestamp, or resets the
364
- * generator upon significant timestamp rollback.
365
- *
366
- * This method returns a monotonically increasing UUID by reusing the previous
367
- * timestamp even if the up-to-date timestamp is smaller than the immediately
368
- * preceding UUID's. However, when such a clock rollback is considered
369
- * significant (i.e., by more than ten seconds), this method resets the
370
- * generator and returns a new UUID based on the given timestamp, breaking the
371
- * increasing order of UUIDs.
372
- *
373
- * See {@link generateOrAbort} for the other mode of generation and
374
- * {@link generateOrResetCore} for the low-level primitive.
375
- */
376
- generate() {
377
- return this.generateOrResetCore(Date.now(), 10000);
378
- }
379
- /**
380
- * Generates a new UUIDv7 object from the current timestamp, or returns
381
- * `undefined` upon significant timestamp rollback.
382
- *
383
- * This method returns a monotonically increasing UUID by reusing the previous
384
- * timestamp even if the up-to-date timestamp is smaller than the immediately
385
- * preceding UUID's. However, when such a clock rollback is considered
386
- * significant (i.e., by more than ten seconds), this method aborts and
387
- * returns `undefined` immediately.
388
- *
389
- * See {@link generate} for the other mode of generation and
390
- * {@link generateOrAbortCore} for the low-level primitive.
391
- */
392
- generateOrAbort() {
393
- return this.generateOrAbortCore(Date.now(), 10000);
394
- }
395
- /**
396
- * Generates a new UUIDv7 object from the `unixTsMs` passed, or resets the
397
- * generator upon significant timestamp rollback.
398
- *
399
- * This method is equivalent to {@link generate} except that it takes a custom
400
- * timestamp and clock rollback allowance.
401
- *
402
- * @param rollbackAllowance - The amount of `unixTsMs` rollback that is
403
- * considered significant. A suggested value is `10_000` (milliseconds).
404
- * @throws RangeError if `unixTsMs` is not a 48-bit positive integer.
405
- */
406
- generateOrResetCore(unixTsMs, rollbackAllowance) {
407
- let value = this.generateOrAbortCore(unixTsMs, rollbackAllowance);
408
- if (value === undefined) {
409
- // reset state and resume
410
- this.timestamp = 0;
411
- value = this.generateOrAbortCore(unixTsMs, rollbackAllowance);
412
- }
413
- return value;
414
- }
415
- /**
416
- * Generates a new UUIDv7 object from the `unixTsMs` passed, or returns
417
- * `undefined` upon significant timestamp rollback.
418
- *
419
- * This method is equivalent to {@link generateOrAbort} except that it takes a
420
- * custom timestamp and clock rollback allowance.
421
- *
422
- * @param rollbackAllowance - The amount of `unixTsMs` rollback that is
423
- * considered significant. A suggested value is `10_000` (milliseconds).
424
- * @throws RangeError if `unixTsMs` is not a 48-bit positive integer.
425
- */
426
- generateOrAbortCore(unixTsMs, rollbackAllowance) {
427
- const MAX_COUNTER = 4398046511103;
428
- if (!Number.isInteger(unixTsMs) ||
429
- unixTsMs < 1 ||
430
- unixTsMs > 281474976710655) {
431
- throw new RangeError("`unixTsMs` must be a 48-bit positive integer");
432
- }
433
- else if (rollbackAllowance < 0 || rollbackAllowance > 281474976710655) {
434
- throw new RangeError("`rollbackAllowance` out of reasonable range");
435
- }
436
- if (unixTsMs > this.timestamp) {
437
- this.timestamp = unixTsMs;
438
- this.resetCounter();
439
- }
440
- else if (unixTsMs + rollbackAllowance >= this.timestamp) {
441
- // go on with previous timestamp if new one is not much smaller
442
- this.counter++;
443
- if (this.counter > MAX_COUNTER) {
444
- // increment timestamp at counter overflow
445
- this.timestamp++;
446
- this.resetCounter();
447
- }
448
- }
449
- else {
450
- // abort if clock went backwards to unbearable extent
451
- return undefined;
452
- }
453
- return UUID.fromFieldsV7(this.timestamp, Math.trunc(this.counter / 2 ** 30), this.counter & (2 ** 30 - 1), this.random.nextUint32());
454
- }
455
- /** Initializes the counter at a 42-bit random integer. */
456
- resetCounter() {
457
- this.counter =
458
- this.random.nextUint32() * 0x400 + (this.random.nextUint32() & 0x3ff);
459
- }
460
- /**
461
- * Generates a new UUIDv4 object utilizing the random number generator inside.
462
- *
463
- * @internal
464
- */
465
- generateV4() {
466
- const bytes = new Uint8Array(Uint32Array.of(this.random.nextUint32(), this.random.nextUint32(), this.random.nextUint32(), this.random.nextUint32()).buffer);
467
- bytes[6] = 0x40 | (bytes[6] >>> 4);
468
- bytes[8] = 0x80 | (bytes[8] >>> 2);
469
- return UUID.ofInner(bytes);
470
- }
471
- }
472
- /** A global flag to force use of cryptographically strong RNG. */
473
- // declare const UUIDV7_DENY_WEAK_RNG: boolean;
474
- /** Returns the default random number generator available in the environment. */
475
- const getDefaultRandom = () => {
476
- // fix: crypto isn't available in react-native, always use Math.random
477
- // // detect Web Crypto API
478
- // if (
479
- // typeof crypto !== "undefined" &&
480
- // typeof crypto.getRandomValues !== "undefined"
481
- // ) {
482
- // return new BufferedCryptoRandom();
483
- // } else {
484
- // // fall back on Math.random() unless the flag is set to true
485
- // if (typeof UUIDV7_DENY_WEAK_RNG !== "undefined" && UUIDV7_DENY_WEAK_RNG) {
486
- // throw new Error("no cryptographically strong RNG available");
487
- // }
488
- // return {
489
- // nextUint32: (): number =>
490
- // Math.trunc(Math.random() * 0x1_0000) * 0x1_0000 +
491
- // Math.trunc(Math.random() * 0x1_0000),
492
- // };
493
- // }
494
- return {
495
- nextUint32: () => Math.trunc(Math.random() * 65536) * 65536 +
496
- Math.trunc(Math.random() * 65536),
497
- };
498
- };
499
- // /**
500
- // * Wraps `crypto.getRandomValues()` to enable buffering; this uses a small
501
- // * buffer by default to avoid both unbearable throughput decline in some
502
- // * environments and the waste of time and space for unused values.
503
- // */
504
- // class BufferedCryptoRandom {
505
- // private readonly buffer = new Uint32Array(8);
506
- // private cursor = 0xffff;
507
- // nextUint32(): number {
508
- // if (this.cursor >= this.buffer.length) {
509
- // crypto.getRandomValues(this.buffer);
510
- // this.cursor = 0;
511
- // }
512
- // return this.buffer[this.cursor++];
513
- // }
514
- // }
515
- let defaultGenerator;
516
- /**
517
- * Generates a UUIDv7 string.
518
- *
519
- * @returns The 8-4-4-4-12 canonical hexadecimal string representation
520
- * ("xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx").
521
- */
522
- const uuidv7 = () => uuidv7obj().toString();
523
- /** Generates a UUIDv7 object. */
524
- const uuidv7obj = () => (defaultGenerator || (defaultGenerator = new V7Generator())).generate();
121
+ /*! For license information please see uuidv7.mjs.LICENSE.txt */
122
+ /**
123
+ * uuidv7: An experimental implementation of the proposed UUID Version 7
124
+ *
125
+ * @license Apache-2.0
126
+ * @copyright 2021-2023 LiosK
127
+ * @packageDocumentation
128
+ */ const DIGITS = "0123456789abcdef";
129
+ class UUID {
130
+ static ofInner(bytes) {
131
+ if (16 === bytes.length) return new UUID(bytes);
132
+ throw new TypeError("not 128-bit length");
133
+ }
134
+ static fromFieldsV7(unixTsMs, randA, randBHi, randBLo) {
135
+ 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");
136
+ const bytes = new Uint8Array(16);
137
+ bytes[0] = unixTsMs / 2 ** 40;
138
+ bytes[1] = unixTsMs / 2 ** 32;
139
+ bytes[2] = unixTsMs / 2 ** 24;
140
+ bytes[3] = unixTsMs / 2 ** 16;
141
+ bytes[4] = unixTsMs / 256;
142
+ bytes[5] = unixTsMs;
143
+ bytes[6] = 0x70 | randA >>> 8;
144
+ bytes[7] = randA;
145
+ bytes[8] = 0x80 | randBHi >>> 24;
146
+ bytes[9] = randBHi >>> 16;
147
+ bytes[10] = randBHi >>> 8;
148
+ bytes[11] = randBHi;
149
+ bytes[12] = randBLo >>> 24;
150
+ bytes[13] = randBLo >>> 16;
151
+ bytes[14] = randBLo >>> 8;
152
+ bytes[15] = randBLo;
153
+ return new UUID(bytes);
154
+ }
155
+ static parse(uuid) {
156
+ let hex;
157
+ switch(uuid.length){
158
+ case 32:
159
+ var _exec;
160
+ hex = null == (_exec = /^[0-9a-f]{32}$/i.exec(uuid)) ? void 0 : _exec[0];
161
+ break;
162
+ case 36:
163
+ var _exec1;
164
+ 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("");
165
+ break;
166
+ case 38:
167
+ var _exec2;
168
+ 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("");
169
+ break;
170
+ case 45:
171
+ var _exec3;
172
+ 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("");
173
+ break;
174
+ }
175
+ if (hex) {
176
+ const inner = new Uint8Array(16);
177
+ for(let i = 0; i < 16; i += 4){
178
+ const n = parseInt(hex.substring(2 * i, 2 * i + 8), 16);
179
+ inner[i + 0] = n >>> 24;
180
+ inner[i + 1] = n >>> 16;
181
+ inner[i + 2] = n >>> 8;
182
+ inner[i + 3] = n;
183
+ }
184
+ return new UUID(inner);
185
+ }
186
+ throw new SyntaxError("could not parse UUID string");
187
+ }
188
+ toString() {
189
+ let text = "";
190
+ for(let i = 0; i < this.bytes.length; i++){
191
+ text += DIGITS.charAt(this.bytes[i] >>> 4);
192
+ text += DIGITS.charAt(0xf & this.bytes[i]);
193
+ if (3 === i || 5 === i || 7 === i || 9 === i) text += "-";
194
+ }
195
+ return text;
196
+ }
197
+ toHex() {
198
+ let text = "";
199
+ for(let i = 0; i < this.bytes.length; i++){
200
+ text += DIGITS.charAt(this.bytes[i] >>> 4);
201
+ text += DIGITS.charAt(0xf & this.bytes[i]);
202
+ }
203
+ return text;
204
+ }
205
+ toJSON() {
206
+ return this.toString();
207
+ }
208
+ getVariant() {
209
+ const n = this.bytes[8] >>> 4;
210
+ if (n < 0) throw new Error("unreachable");
211
+ if (n <= 7) return this.bytes.every((e)=>0 === e) ? "NIL" : "VAR_0";
212
+ if (n <= 11) return "VAR_10";
213
+ if (n <= 13) return "VAR_110";
214
+ if (n <= 15) return this.bytes.every((e)=>0xff === e) ? "MAX" : "VAR_RESERVED";
215
+ else throw new Error("unreachable");
216
+ }
217
+ getVersion() {
218
+ return "VAR_10" === this.getVariant() ? this.bytes[6] >>> 4 : void 0;
219
+ }
220
+ clone() {
221
+ return new UUID(this.bytes.slice(0));
222
+ }
223
+ equals(other) {
224
+ return 0 === this.compareTo(other);
225
+ }
226
+ compareTo(other) {
227
+ for(let i = 0; i < 16; i++){
228
+ const diff = this.bytes[i] - other.bytes[i];
229
+ if (0 !== diff) return Math.sign(diff);
230
+ }
231
+ return 0;
232
+ }
233
+ constructor(bytes){
234
+ this.bytes = bytes;
235
+ }
236
+ }
237
+ class V7Generator {
238
+ generate() {
239
+ return this.generateOrResetCore(Date.now(), 10000);
240
+ }
241
+ generateOrAbort() {
242
+ return this.generateOrAbortCore(Date.now(), 10000);
243
+ }
244
+ generateOrResetCore(unixTsMs, rollbackAllowance) {
245
+ let value = this.generateOrAbortCore(unixTsMs, rollbackAllowance);
246
+ if (void 0 === value) {
247
+ this.timestamp = 0;
248
+ value = this.generateOrAbortCore(unixTsMs, rollbackAllowance);
249
+ }
250
+ return value;
251
+ }
252
+ generateOrAbortCore(unixTsMs, rollbackAllowance) {
253
+ const MAX_COUNTER = 0x3ffffffffff;
254
+ if (!Number.isInteger(unixTsMs) || unixTsMs < 1 || unixTsMs > 0xffffffffffff) throw new RangeError("`unixTsMs` must be a 48-bit positive integer");
255
+ if (rollbackAllowance < 0 || rollbackAllowance > 0xffffffffffff) throw new RangeError("`rollbackAllowance` out of reasonable range");
256
+ if (unixTsMs > this.timestamp) {
257
+ this.timestamp = unixTsMs;
258
+ this.resetCounter();
259
+ } else {
260
+ if (!(unixTsMs + rollbackAllowance >= this.timestamp)) return;
261
+ this.counter++;
262
+ if (this.counter > MAX_COUNTER) {
263
+ this.timestamp++;
264
+ this.resetCounter();
265
+ }
266
+ }
267
+ return UUID.fromFieldsV7(this.timestamp, Math.trunc(this.counter / 2 ** 30), this.counter & 2 ** 30 - 1, this.random.nextUint32());
268
+ }
269
+ resetCounter() {
270
+ this.counter = 0x400 * this.random.nextUint32() + (0x3ff & this.random.nextUint32());
271
+ }
272
+ generateV4() {
273
+ const bytes = new Uint8Array(Uint32Array.of(this.random.nextUint32(), this.random.nextUint32(), this.random.nextUint32(), this.random.nextUint32()).buffer);
274
+ bytes[6] = 0x40 | bytes[6] >>> 4;
275
+ bytes[8] = 0x80 | bytes[8] >>> 2;
276
+ return UUID.ofInner(bytes);
277
+ }
278
+ constructor(randomNumberGenerator){
279
+ this.timestamp = 0;
280
+ this.counter = 0;
281
+ this.random = null != randomNumberGenerator ? randomNumberGenerator : getDefaultRandom();
282
+ }
283
+ }
284
+ const getDefaultRandom = ()=>({
285
+ nextUint32: ()=>0x10000 * Math.trunc(0x10000 * Math.random()) + Math.trunc(0x10000 * Math.random())
286
+ });
287
+ let defaultGenerator;
288
+ const uuidv7 = ()=>uuidv7obj().toString();
289
+ const uuidv7obj = ()=>(defaultGenerator || (defaultGenerator = new V7Generator())).generate();
525
290
 
526
291
  // Portions of this file are derived from getsentry/sentry-javascript by Software, Inc. dba Sentry
527
292
  // Licensed under the MIT License
@@ -646,6 +411,7 @@ function isBuiltin(candidate, className) {
646
411
  }
647
412
 
648
413
  // Portions of this file are derived from getsentry/sentry-javascript by Software, Inc. dba Sentry
414
+ // Licensed under the MIT License
649
415
  async function propertiesFromUnknownInput(stackParser, frameModifiers, input, hint) {
650
416
  const providedMechanism = hint && hint.mechanism;
651
417
  const mechanism = providedMechanism || {
@@ -734,10 +500,10 @@ function getObjectClassName(obj) {
734
500
  // ignore errors here
735
501
  }
736
502
  }
737
- /**
738
- * Given any captured exception, extract its keys and create a sorted
739
- * and truncated list that will be used inside the event message.
740
- * eg. `Non-error exception captured with keys: foo, bar, baz`
503
+ /**
504
+ * Given any captured exception, extract its keys and create a sorted
505
+ * and truncated list that will be used inside the event message.
506
+ * eg. `Non-error exception captured with keys: foo, bar, baz`
741
507
  */
742
508
  function extractExceptionKeysForMessage(exception, maxLength = 40) {
743
509
  const keys = Object.keys(convertToPlainObject(exception));
@@ -767,13 +533,13 @@ function truncate(str, max = 0) {
767
533
  }
768
534
  return str.length <= max ? str : `${str.slice(0, max)}...`;
769
535
  }
770
- /**
771
- * Transforms any `Error` or `Event` into a plain object with all of their enumerable properties, and some of their
772
- * non-enumerable properties attached.
773
- *
774
- * @param value Initial source that we have to transform in order for it to be usable by the serializer
775
- * @returns An Event or Error turned into an object - or the value argument itself, when value is neither an Event nor
776
- * an Error.
536
+ /**
537
+ * Transforms any `Error` or `Event` into a plain object with all of their enumerable properties, and some of their
538
+ * non-enumerable properties attached.
539
+ *
540
+ * @param value Initial source that we have to transform in order for it to be usable by the serializer
541
+ * @returns An Event or Error turned into an object - or the value argument itself, when value is neither an Event nor
542
+ * an Error.
777
543
  */
778
544
  function convertToPlainObject(value) {
779
545
  if (isError(value)) {
@@ -821,8 +587,8 @@ function serializeEventTarget(target) {
821
587
  return '<unknown>';
822
588
  }
823
589
  }
824
- /**
825
- * Extracts stack frames from the error and builds an Exception
590
+ /**
591
+ * Extracts stack frames from the error and builds an Exception
826
592
  */
827
593
  async function exceptionFromError(stackParser, frameModifiers, error) {
828
594
  const exception = {
@@ -841,8 +607,8 @@ async function exceptionFromError(stackParser, frameModifiers, error) {
841
607
  }
842
608
  return exception;
843
609
  }
844
- /**
845
- * Extracts stack frames from the error.stack string
610
+ /**
611
+ * Extracts stack frames from the error.stack string
846
612
  */
847
613
  function parseStackFrames(stackParser, error) {
848
614
  return applyChunkIds(stackParser(error.stack || '', 1), stackParser);
@@ -857,6 +623,71 @@ function applyChunkIds(frames, parser) {
857
623
  return frames;
858
624
  }
859
625
 
626
+ const ObjProto = Object.prototype;
627
+ const type_utils_toString = ObjProto.toString;
628
+ const isNumber = (x)=>'[object Number]' == type_utils_toString.call(x);
629
+
630
+ function clampToRange(value, min, max, logger, fallbackValue) {
631
+ if (min > max) {
632
+ logger.warn('min cannot be greater than max.');
633
+ min = max;
634
+ }
635
+ if (isNumber(value)) if (value > max) {
636
+ logger.warn(' cannot be greater than max: ' + max + '. Using max value instead.');
637
+ return max;
638
+ } else {
639
+ if (!(value < min)) return value;
640
+ logger.warn(' cannot be less than min: ' + min + '. Using min value instead.');
641
+ return min;
642
+ }
643
+ logger.warn(' must be a number. using max or fallback. max: ' + max + ', fallback: ' + fallbackValue);
644
+ return clampToRange(max, min, max, logger);
645
+ }
646
+
647
+ class BucketedRateLimiter {
648
+ constructor(_options){
649
+ this._options = _options;
650
+ this._buckets = {};
651
+ this._refillBuckets = ()=>{
652
+ Object.keys(this._buckets).forEach((key)=>{
653
+ const newTokens = this._getBucket(key) + this._refillRate;
654
+ if (newTokens >= this._bucketSize) delete this._buckets[key];
655
+ else this._setBucket(key, newTokens);
656
+ });
657
+ };
658
+ this._getBucket = (key)=>this._buckets[String(key)];
659
+ this._setBucket = (key, value)=>{
660
+ this._buckets[String(key)] = value;
661
+ };
662
+ this.consumeRateLimit = (key)=>{
663
+ var _this__getBucket;
664
+ let tokens = null != (_this__getBucket = this._getBucket(key)) ? _this__getBucket : this._bucketSize;
665
+ tokens = Math.max(tokens - 1, 0);
666
+ if (0 === tokens) return true;
667
+ this._setBucket(key, tokens);
668
+ const hasReachedZero = 0 === tokens;
669
+ if (hasReachedZero) {
670
+ var _this__onBucketRateLimited, _this;
671
+ null == (_this__onBucketRateLimited = (_this = this)._onBucketRateLimited) || _this__onBucketRateLimited.call(_this, key);
672
+ }
673
+ return hasReachedZero;
674
+ };
675
+ this._onBucketRateLimited = this._options._onBucketRateLimited;
676
+ this._bucketSize = clampToRange(this._options.bucketSize, 0, 100, this._options._logger);
677
+ this._refillRate = clampToRange(this._options.refillRate, 0, this._bucketSize, this._options._logger);
678
+ this._refillInterval = clampToRange(this._options.refillInterval, 0, 86400000, this._options._logger);
679
+ setInterval(()=>{
680
+ this._refillBuckets();
681
+ }, this._refillInterval);
682
+ }
683
+ }
684
+
685
+ function safeSetTimeout(fn, timeout) {
686
+ const t = setTimeout(fn, timeout);
687
+ (null == t ? void 0 : t.unref) && (null == t || t.unref());
688
+ return t;
689
+ }
690
+
860
691
  const SHUTDOWN_TIMEOUT = 2000;
861
692
  class ErrorTracking {
862
693
  static async buildEventMessage(error, hint, distinctId, additionalProperties) {
@@ -878,9 +709,20 @@ class ErrorTracking {
878
709
  }
879
710
  };
880
711
  }
881
- constructor(client, options) {
712
+ constructor(client, options, _logger) {
882
713
  this.client = client;
883
714
  this._exceptionAutocaptureEnabled = options.enableExceptionAutocapture || false;
715
+ this._logger = _logger;
716
+ // by default captures ten exceptions before rate limiting by exception type
717
+ // refills at a rate of one token / 10 second period
718
+ // e.g. will capture 1 exception rate limited exception every 10 seconds until burst ends
719
+ this._rateLimiter = new BucketedRateLimiter({
720
+ refillRate: 1,
721
+ bucketSize: 10,
722
+ refillInterval: 10000,
723
+ // ten seconds in milliseconds
724
+ _logger: this._logger
725
+ });
884
726
  this.startAutocaptureIfEnabled();
885
727
  }
886
728
  startAutocaptureIfEnabled() {
@@ -890,7 +732,16 @@ class ErrorTracking {
890
732
  }
891
733
  }
892
734
  onException(exception, hint) {
893
- void ErrorTracking.buildEventMessage(exception, hint).then(msg => {
735
+ return ErrorTracking.buildEventMessage(exception, hint).then(msg => {
736
+ const exceptionProperties = msg.properties;
737
+ const exceptionType = exceptionProperties?.$exception_list[0].type ?? 'Exception';
738
+ const isRateLimited = this._rateLimiter.consumeRateLimit(exceptionType);
739
+ if (isRateLimited) {
740
+ this._logger.info('Skipping exception capture because of client rate limiting.', {
741
+ exception: exceptionType
742
+ });
743
+ return;
744
+ }
894
745
  this.client.capture(msg);
895
746
  });
896
747
  }
@@ -920,6 +771,7 @@ function setupExpressErrorHandler(_posthog, app) {
920
771
  }
921
772
 
922
773
  // Portions of this file are derived from getsentry/sentry-javascript by Software, Inc. dba Sentry
774
+ // Licensed under the MIT License
923
775
  /** Creates a function that gets the module name from a filename */
924
776
  function createGetModuleFromFilename(basePath = process.argv[1] ? dirname(process.argv[1]) : process.cwd(), isWindows = sep === '\\') {
925
777
  const normalizedBase = isWindows ? normalizeWindowsPath(basePath) : basePath;
@@ -999,6 +851,7 @@ class ReduceableCache {
999
851
  }
1000
852
 
1001
853
  // Portions of this file are derived from getsentry/sentry-javascript by Software, Inc. dba Sentry
854
+ // Licensed under the MIT License
1002
855
  const LRU_FILE_CONTENTS_CACHE = new ReduceableCache(25);
1003
856
  const LRU_FILE_CONTENTS_FS_READ_FAILED = new ReduceableCache(20);
1004
857
  const DEFAULT_LINES_OF_CONTEXT = 7;
@@ -1061,8 +914,8 @@ async function addSourceContext(frames) {
1061
914
  LRU_FILE_CONTENTS_CACHE.reduce();
1062
915
  return frames;
1063
916
  }
1064
- /**
1065
- * Extracts lines from a file and stores them in a cache.
917
+ /**
918
+ * Extracts lines from a file and stores them in a cache.
1066
919
  */
1067
920
  function getContextLinesFromFile(path, ranges, output) {
1068
921
  return new Promise(resolve => {
@@ -1146,8 +999,8 @@ function addSourceContextToFrames(frames, cache) {
1146
999
  }
1147
1000
  }
1148
1001
  }
1149
- /**
1150
- * Resolves context lines before and after the given line number and appends them to the frame;
1002
+ /**
1003
+ * Resolves context lines before and after the given line number and appends them to the frame;
1151
1004
  */
1152
1005
  function addContextToFrame(lineno, frame, contents) {
1153
1006
  // When there is no line number in the frame, attaching context is nonsensical and will even break grouping.
@@ -1185,28 +1038,28 @@ function addContextToFrame(lineno, frame, contents) {
1185
1038
  frame.post_context.push(line);
1186
1039
  }
1187
1040
  }
1188
- /**
1189
- * Clears the context lines from a frame, used to reset a frame to its original state
1190
- * if we fail to resolve all context lines for it.
1041
+ /**
1042
+ * Clears the context lines from a frame, used to reset a frame to its original state
1043
+ * if we fail to resolve all context lines for it.
1191
1044
  */
1192
1045
  function clearLineContext(frame) {
1193
1046
  delete frame.pre_context;
1194
1047
  delete frame.context_line;
1195
1048
  delete frame.post_context;
1196
1049
  }
1197
- /**
1198
- * Determines if context lines should be skipped for a file.
1199
- * - .min.(mjs|cjs|js) files are and not useful since they dont point to the original source
1200
- * - node: prefixed modules are part of the runtime and cannot be resolved to a file
1201
- * - data: skip json, wasm and inline js https://nodejs.org/api/esm.html#data-imports
1050
+ /**
1051
+ * Determines if context lines should be skipped for a file.
1052
+ * - .min.(mjs|cjs|js) files are and not useful since they dont point to the original source
1053
+ * - node: prefixed modules are part of the runtime and cannot be resolved to a file
1054
+ * - data: skip json, wasm and inline js https://nodejs.org/api/esm.html#data-imports
1202
1055
  */
1203
1056
  function shouldSkipContextLinesForFile(path) {
1204
1057
  // Test the most common prefix and extension first. These are the ones we
1205
1058
  // are most likely to see in user applications and are the ones we can break out of first.
1206
1059
  return path.startsWith('node:') || path.endsWith('.min.js') || path.endsWith('.min.cjs') || path.endsWith('.min.mjs') || path.startsWith('data:');
1207
1060
  }
1208
- /**
1209
- * Determines if we should skip contextlines based off the max lineno and colno values.
1061
+ /**
1062
+ * Determines if we should skip contextlines based off the max lineno and colno values.
1210
1063
  */
1211
1064
  function shouldSkipContextLinesForFrame(frame) {
1212
1065
  if (frame.lineno !== undefined && frame.lineno > MAX_CONTEXTLINES_LINENO) {
@@ -1217,8 +1070,8 @@ function shouldSkipContextLinesForFrame(frame) {
1217
1070
  }
1218
1071
  return false;
1219
1072
  }
1220
- /**
1221
- * Checks if we have all the contents that we need in the cache.
1073
+ /**
1074
+ * Checks if we have all the contents that we need in the cache.
1222
1075
  */
1223
1076
  function rangeExistsInContentCache(file, range) {
1224
1077
  const contents = LRU_FILE_CONTENTS_CACHE.get(file);
@@ -1232,9 +1085,9 @@ function rangeExistsInContentCache(file, range) {
1232
1085
  }
1233
1086
  return true;
1234
1087
  }
1235
- /**
1236
- * Creates contiguous ranges of lines to read from a file. In the case where context lines overlap,
1237
- * the ranges are merged to create a single range.
1088
+ /**
1089
+ * Creates contiguous ranges of lines to read from a file. In the case where context lines overlap,
1090
+ * the ranges are merged to create a single range.
1238
1091
  */
1239
1092
  function makeLineReaderRanges(lines) {
1240
1093
  if (!lines.length) {
@@ -1279,8 +1132,8 @@ function makeRangeStart(line) {
1279
1132
  function makeRangeEnd(line) {
1280
1133
  return line + DEFAULT_LINES_OF_CONTEXT;
1281
1134
  }
1282
- /**
1283
- * Get or init map value
1135
+ /**
1136
+ * Get or init map value
1284
1137
  */
1285
1138
  function emplace(map, key, contents) {
1286
1139
  const value = map.get(key);
@@ -1320,1092 +1173,18 @@ function snipLine(line, colno) {
1320
1173
  return newLine;
1321
1174
  }
1322
1175
 
1323
- var version = "5.6.0";
1176
+ var version = "5.8.0";
1324
1177
 
1325
- var PostHogPersistedProperty;
1326
- (function (PostHogPersistedProperty) {
1327
- PostHogPersistedProperty["AnonymousId"] = "anonymous_id";
1328
- PostHogPersistedProperty["DistinctId"] = "distinct_id";
1329
- PostHogPersistedProperty["Props"] = "props";
1330
- PostHogPersistedProperty["FeatureFlagDetails"] = "feature_flag_details";
1331
- PostHogPersistedProperty["FeatureFlags"] = "feature_flags";
1332
- PostHogPersistedProperty["FeatureFlagPayloads"] = "feature_flag_payloads";
1333
- PostHogPersistedProperty["BootstrapFeatureFlagDetails"] = "bootstrap_feature_flag_details";
1334
- PostHogPersistedProperty["BootstrapFeatureFlags"] = "bootstrap_feature_flags";
1335
- PostHogPersistedProperty["BootstrapFeatureFlagPayloads"] = "bootstrap_feature_flag_payloads";
1336
- PostHogPersistedProperty["OverrideFeatureFlags"] = "override_feature_flags";
1337
- PostHogPersistedProperty["Queue"] = "queue";
1338
- PostHogPersistedProperty["OptedOut"] = "opted_out";
1339
- PostHogPersistedProperty["SessionId"] = "session_id";
1340
- PostHogPersistedProperty["SessionStartTimestamp"] = "session_start_timestamp";
1341
- PostHogPersistedProperty["SessionLastTimestamp"] = "session_timestamp";
1342
- PostHogPersistedProperty["PersonProperties"] = "person_properties";
1343
- PostHogPersistedProperty["GroupProperties"] = "group_properties";
1344
- PostHogPersistedProperty["InstalledAppBuild"] = "installed_app_build";
1345
- PostHogPersistedProperty["InstalledAppVersion"] = "installed_app_version";
1346
- PostHogPersistedProperty["SessionReplay"] = "session_replay";
1347
- PostHogPersistedProperty["SurveyLastSeenDate"] = "survey_last_seen_date";
1348
- PostHogPersistedProperty["SurveysSeen"] = "surveys_seen";
1349
- PostHogPersistedProperty["Surveys"] = "surveys";
1350
- PostHogPersistedProperty["RemoteConfig"] = "remote_config";
1351
- PostHogPersistedProperty["FlagsEndpointWasHit"] = "flags_endpoint_was_hit";
1352
- })(PostHogPersistedProperty || (PostHogPersistedProperty = {}));
1353
- // Any key prefixed with `attr__` can be added
1354
- var Compression;
1355
- (function (Compression) {
1356
- Compression["GZipJS"] = "gzip-js";
1357
- Compression["Base64"] = "base64";
1358
- })(Compression || (Compression = {}));
1359
- var SurveyPosition;
1360
- (function (SurveyPosition) {
1361
- SurveyPosition["TopLeft"] = "top_left";
1362
- SurveyPosition["TopCenter"] = "top_center";
1363
- SurveyPosition["TopRight"] = "top_right";
1364
- SurveyPosition["MiddleLeft"] = "middle_left";
1365
- SurveyPosition["MiddleCenter"] = "middle_center";
1366
- SurveyPosition["MiddleRight"] = "middle_right";
1367
- SurveyPosition["Left"] = "left";
1368
- SurveyPosition["Right"] = "right";
1369
- SurveyPosition["Center"] = "center";
1370
- })(SurveyPosition || (SurveyPosition = {}));
1371
- var SurveyWidgetType;
1372
- (function (SurveyWidgetType) {
1373
- SurveyWidgetType["Button"] = "button";
1374
- SurveyWidgetType["Tab"] = "tab";
1375
- SurveyWidgetType["Selector"] = "selector";
1376
- })(SurveyWidgetType || (SurveyWidgetType = {}));
1377
- var SurveyType;
1378
- (function (SurveyType) {
1379
- SurveyType["Popover"] = "popover";
1380
- SurveyType["API"] = "api";
1381
- SurveyType["Widget"] = "widget";
1382
- })(SurveyType || (SurveyType = {}));
1383
- var SurveyQuestionDescriptionContentType;
1384
- (function (SurveyQuestionDescriptionContentType) {
1385
- SurveyQuestionDescriptionContentType["Html"] = "html";
1386
- SurveyQuestionDescriptionContentType["Text"] = "text";
1387
- })(SurveyQuestionDescriptionContentType || (SurveyQuestionDescriptionContentType = {}));
1388
- var SurveyRatingDisplay;
1389
- (function (SurveyRatingDisplay) {
1390
- SurveyRatingDisplay["Number"] = "number";
1391
- SurveyRatingDisplay["Emoji"] = "emoji";
1392
- })(SurveyRatingDisplay || (SurveyRatingDisplay = {}));
1393
- var SurveyQuestionType;
1394
- (function (SurveyQuestionType) {
1395
- SurveyQuestionType["Open"] = "open";
1396
- SurveyQuestionType["MultipleChoice"] = "multiple_choice";
1397
- SurveyQuestionType["SingleChoice"] = "single_choice";
1398
- SurveyQuestionType["Rating"] = "rating";
1399
- SurveyQuestionType["Link"] = "link";
1400
- })(SurveyQuestionType || (SurveyQuestionType = {}));
1401
- var SurveyQuestionBranchingType;
1402
- (function (SurveyQuestionBranchingType) {
1403
- SurveyQuestionBranchingType["NextQuestion"] = "next_question";
1404
- SurveyQuestionBranchingType["End"] = "end";
1405
- SurveyQuestionBranchingType["ResponseBased"] = "response_based";
1406
- SurveyQuestionBranchingType["SpecificQuestion"] = "specific_question";
1407
- })(SurveyQuestionBranchingType || (SurveyQuestionBranchingType = {}));
1408
- var SurveyMatchType;
1409
- (function (SurveyMatchType) {
1410
- SurveyMatchType["Regex"] = "regex";
1411
- SurveyMatchType["NotRegex"] = "not_regex";
1412
- SurveyMatchType["Exact"] = "exact";
1413
- SurveyMatchType["IsNot"] = "is_not";
1414
- SurveyMatchType["Icontains"] = "icontains";
1415
- SurveyMatchType["NotIcontains"] = "not_icontains";
1416
- })(SurveyMatchType || (SurveyMatchType = {}));
1417
- /** Sync with plugin-server/src/types.ts */
1418
- var ActionStepStringMatching;
1419
- (function (ActionStepStringMatching) {
1420
- ActionStepStringMatching["Contains"] = "contains";
1421
- ActionStepStringMatching["Exact"] = "exact";
1422
- ActionStepStringMatching["Regex"] = "regex";
1423
- })(ActionStepStringMatching || (ActionStepStringMatching = {}));
1424
-
1425
- const normalizeFlagsResponse = (flagsResponse) => {
1426
- if ('flags' in flagsResponse) {
1427
- // Convert v2 format to v1 format
1428
- const featureFlags = getFlagValuesFromFlags(flagsResponse.flags);
1429
- const featureFlagPayloads = getPayloadsFromFlags(flagsResponse.flags);
1430
- return {
1431
- ...flagsResponse,
1432
- featureFlags,
1433
- featureFlagPayloads,
1434
- };
1435
- }
1436
- else {
1437
- // Convert v1 format to v2 format
1438
- const featureFlags = flagsResponse.featureFlags ?? {};
1439
- const featureFlagPayloads = Object.fromEntries(Object.entries(flagsResponse.featureFlagPayloads || {}).map(([k, v]) => [k, parsePayload(v)]));
1440
- const flags = Object.fromEntries(Object.entries(featureFlags).map(([key, value]) => [
1441
- key,
1442
- getFlagDetailFromFlagAndPayload(key, value, featureFlagPayloads[key]),
1443
- ]));
1444
- return {
1445
- ...flagsResponse,
1446
- featureFlags,
1447
- featureFlagPayloads,
1448
- flags,
1449
- };
1450
- }
1451
- };
1452
- function getFlagDetailFromFlagAndPayload(key, value, payload) {
1453
- return {
1454
- key: key,
1455
- enabled: typeof value === 'string' ? true : value,
1456
- variant: typeof value === 'string' ? value : undefined,
1457
- reason: undefined,
1458
- metadata: {
1459
- id: undefined,
1460
- version: undefined,
1461
- payload: payload ? JSON.stringify(payload) : undefined,
1462
- description: undefined,
1463
- },
1464
- };
1465
- }
1466
- /**
1467
- * Get the flag values from the flags v4 response.
1468
- * @param flags - The flags
1469
- * @returns The flag values
1470
- */
1471
- const getFlagValuesFromFlags = (flags) => {
1472
- return Object.fromEntries(Object.entries(flags ?? {})
1473
- .map(([key, detail]) => [key, getFeatureFlagValue(detail)])
1474
- .filter(([, value]) => value !== undefined));
1475
- };
1476
- /**
1477
- * Get the payloads from the flags v4 response.
1478
- * @param flags - The flags
1479
- * @returns The payloads
1480
- */
1481
- const getPayloadsFromFlags = (flags) => {
1482
- const safeFlags = flags ?? {};
1483
- return Object.fromEntries(Object.keys(safeFlags)
1484
- .filter((flag) => {
1485
- const details = safeFlags[flag];
1486
- return details.enabled && details.metadata && details.metadata.payload !== undefined;
1487
- })
1488
- .map((flag) => {
1489
- const payload = safeFlags[flag].metadata?.payload;
1490
- return [flag, payload ? parsePayload(payload) : undefined];
1491
- }));
1492
- };
1493
- const getFeatureFlagValue = (detail) => {
1494
- return detail === undefined ? undefined : detail.variant ?? detail.enabled;
1495
- };
1496
- const parsePayload = (response) => {
1497
- if (typeof response !== 'string') {
1498
- return response;
1499
- }
1500
- try {
1501
- return JSON.parse(response);
1502
- }
1503
- catch {
1504
- return response;
1505
- }
1506
- };
1507
-
1508
- const STRING_FORMAT = 'utf8';
1509
- function assert(truthyValue, message) {
1510
- if (!truthyValue || typeof truthyValue !== 'string' || isEmpty(truthyValue)) {
1511
- throw new Error(message);
1512
- }
1513
- }
1514
- function isEmpty(truthyValue) {
1515
- if (truthyValue.trim().length === 0) {
1516
- return true;
1517
- }
1518
- return false;
1519
- }
1520
- function removeTrailingSlash(url) {
1521
- return url?.replace(/\/+$/, '');
1522
- }
1523
- async function retriable(fn, props) {
1524
- let lastError = null;
1525
- for (let i = 0; i < props.retryCount + 1; i++) {
1526
- if (i > 0) {
1527
- // don't wait when it's the last try
1528
- await new Promise((r) => setTimeout(r, props.retryDelay));
1529
- }
1530
- try {
1531
- const res = await fn();
1532
- return res;
1533
- }
1534
- catch (e) {
1535
- lastError = e;
1536
- if (!props.retryCheck(e)) {
1537
- throw e;
1538
- }
1539
- }
1540
- }
1541
- throw lastError;
1542
- }
1543
- function currentISOTime() {
1544
- return new Date().toISOString();
1545
- }
1546
- function safeSetTimeout(fn, timeout) {
1547
- // NOTE: we use this so rarely that it is totally fine to do `safeSetTimeout(fn, 0)``
1548
- // rather than setImmediate.
1549
- const t = setTimeout(fn, timeout);
1550
- // We unref if available to prevent Node.js hanging on exit
1551
- t?.unref && t?.unref();
1552
- return t;
1553
- }
1554
- function allSettled(promises) {
1555
- return Promise.all(promises.map((p) => (p ?? Promise.resolve()).then((value) => ({ status: 'fulfilled', value }), (reason) => ({ status: 'rejected', reason }))));
1556
- }
1557
-
1558
- /**
1559
- * Older browsers and some runtimes don't support this yet
1560
- * This API (as of 2025-05-07) is not available on React Native.
1561
- */
1562
- function isGzipSupported() {
1563
- return 'CompressionStream' in globalThis;
1564
- }
1565
- /**
1566
- * Gzip a string using Compression Streams API if it's available
1567
- */
1568
- async function gzipCompress(input, isDebug = true) {
1569
- try {
1570
- // Turn the string into a stream using a Blob, and then compress it
1571
- const dataStream = new Blob([input], {
1572
- type: 'text/plain',
1573
- }).stream();
1574
- const compressedStream = dataStream.pipeThrough(new CompressionStream('gzip'));
1575
- // Using a Response to easily extract the readablestream value. Decoding into a string for fetch
1576
- return await new Response(compressedStream).blob();
1577
- }
1578
- catch (error) {
1579
- if (isDebug) {
1580
- console.error('Failed to gzip compress data', error);
1581
- }
1582
- return null;
1583
- }
1584
- }
1585
-
1586
- class SimpleEventEmitter {
1587
- constructor() {
1588
- this.events = {};
1589
- this.events = {};
1590
- }
1591
- on(event, listener) {
1592
- if (!this.events[event]) {
1593
- this.events[event] = [];
1594
- }
1595
- this.events[event].push(listener);
1596
- return () => {
1597
- this.events[event] = this.events[event].filter((x) => x !== listener);
1598
- };
1599
- }
1600
- emit(event, payload) {
1601
- for (const listener of this.events[event] || []) {
1602
- listener(payload);
1603
- }
1604
- for (const listener of this.events['*'] || []) {
1605
- listener(event, payload);
1606
- }
1607
- }
1608
- }
1609
-
1610
- class PostHogFetchHttpError extends Error {
1611
- constructor(response, reqByteLength) {
1612
- super('HTTP error while fetching PostHog: status=' + response.status + ', reqByteLength=' + reqByteLength);
1613
- this.response = response;
1614
- this.reqByteLength = reqByteLength;
1615
- this.name = 'PostHogFetchHttpError';
1616
- }
1617
- get status() {
1618
- return this.response.status;
1619
- }
1620
- get text() {
1621
- return this.response.text();
1622
- }
1623
- get json() {
1624
- return this.response.json();
1625
- }
1626
- }
1627
- class PostHogFetchNetworkError extends Error {
1628
- constructor(error) {
1629
- // TRICKY: "cause" is a newer property but is just ignored otherwise. Cast to any to ignore the type issue.
1630
- // eslint-disable-next-line @typescript-eslint/prefer-ts-expect-error
1631
- // @ts-ignore
1632
- super('Network error while fetching PostHog', error instanceof Error ? { cause: error } : {});
1633
- this.error = error;
1634
- this.name = 'PostHogFetchNetworkError';
1635
- }
1636
- }
1637
- async function logFlushError(err) {
1638
- if (err instanceof PostHogFetchHttpError) {
1639
- let text = '';
1640
- try {
1641
- text = await err.text;
1642
- }
1643
- catch { }
1644
- console.error(`Error while flushing PostHog: message=${err.message}, response body=${text}`, err);
1645
- }
1646
- else {
1647
- console.error('Error while flushing PostHog', err);
1648
- }
1649
- return Promise.resolve();
1650
- }
1651
- function isPostHogFetchError(err) {
1652
- return typeof err === 'object' && (err instanceof PostHogFetchHttpError || err instanceof PostHogFetchNetworkError);
1653
- }
1654
- function isPostHogFetchContentTooLargeError(err) {
1655
- return typeof err === 'object' && err instanceof PostHogFetchHttpError && err.status === 413;
1656
- }
1657
- var QuotaLimitedFeature;
1658
- (function (QuotaLimitedFeature) {
1659
- QuotaLimitedFeature["FeatureFlags"] = "feature_flags";
1660
- QuotaLimitedFeature["Recordings"] = "recordings";
1661
- })(QuotaLimitedFeature || (QuotaLimitedFeature = {}));
1662
- class PostHogCoreStateless {
1663
- constructor(apiKey, options) {
1664
- this.flushPromise = null;
1665
- this.shutdownPromise = null;
1666
- this.pendingPromises = {};
1667
- // internal
1668
- this._events = new SimpleEventEmitter();
1669
- this._isInitialized = false;
1670
- assert(apiKey, "You must pass your PostHog project's api key.");
1671
- this.apiKey = apiKey;
1672
- this.host = removeTrailingSlash(options?.host || 'https://us.i.posthog.com');
1673
- this.flushAt = options?.flushAt ? Math.max(options?.flushAt, 1) : 20;
1674
- this.maxBatchSize = Math.max(this.flushAt, options?.maxBatchSize ?? 100);
1675
- this.maxQueueSize = Math.max(this.flushAt, options?.maxQueueSize ?? 1000);
1676
- this.flushInterval = options?.flushInterval ?? 10000;
1677
- this.preloadFeatureFlags = options?.preloadFeatureFlags ?? true;
1678
- // If enable is explicitly set to false we override the optout
1679
- this.defaultOptIn = options?.defaultOptIn ?? true;
1680
- this.disableSurveys = options?.disableSurveys ?? false;
1681
- this._retryOptions = {
1682
- retryCount: options?.fetchRetryCount ?? 3,
1683
- retryDelay: options?.fetchRetryDelay ?? 3000,
1684
- retryCheck: isPostHogFetchError,
1685
- };
1686
- this.requestTimeout = options?.requestTimeout ?? 10000; // 10 seconds
1687
- this.featureFlagsRequestTimeoutMs = options?.featureFlagsRequestTimeoutMs ?? 3000; // 3 seconds
1688
- this.remoteConfigRequestTimeoutMs = options?.remoteConfigRequestTimeoutMs ?? 3000; // 3 seconds
1689
- this.disableGeoip = options?.disableGeoip ?? true;
1690
- this.disabled = options?.disabled ?? false;
1691
- this.historicalMigration = options?.historicalMigration ?? false;
1692
- // Init promise allows the derived class to block calls until it is ready
1693
- this._initPromise = Promise.resolve();
1694
- this._isInitialized = true;
1695
- this.disableCompression = !isGzipSupported() || (options?.disableCompression ?? false);
1696
- }
1697
- logMsgIfDebug(fn) {
1698
- if (this.isDebug) {
1699
- fn();
1700
- }
1701
- }
1702
- wrap(fn) {
1703
- if (this.disabled) {
1704
- this.logMsgIfDebug(() => console.warn('[PostHog] The client is disabled'));
1705
- return;
1706
- }
1707
- if (this._isInitialized) {
1708
- // NOTE: We could also check for the "opt in" status here...
1709
- return fn();
1710
- }
1711
- this._initPromise.then(() => fn());
1712
- }
1713
- getCommonEventProperties() {
1714
- return {
1715
- $lib: this.getLibraryId(),
1716
- $lib_version: this.getLibraryVersion(),
1717
- };
1718
- }
1719
- get optedOut() {
1720
- return this.getPersistedProperty(PostHogPersistedProperty.OptedOut) ?? !this.defaultOptIn;
1721
- }
1722
- async optIn() {
1723
- this.wrap(() => {
1724
- this.setPersistedProperty(PostHogPersistedProperty.OptedOut, false);
1725
- });
1726
- }
1727
- async optOut() {
1728
- this.wrap(() => {
1729
- this.setPersistedProperty(PostHogPersistedProperty.OptedOut, true);
1730
- });
1731
- }
1732
- on(event, cb) {
1733
- return this._events.on(event, cb);
1734
- }
1735
- debug(enabled = true) {
1736
- this.removeDebugCallback?.();
1737
- if (enabled) {
1738
- const removeDebugCallback = this.on('*', (event, payload) => console.log('PostHog Debug', event, payload));
1739
- this.removeDebugCallback = () => {
1740
- removeDebugCallback();
1741
- this.removeDebugCallback = undefined;
1742
- };
1743
- }
1744
- }
1745
- get isDebug() {
1746
- return !!this.removeDebugCallback;
1747
- }
1748
- get isDisabled() {
1749
- return this.disabled;
1750
- }
1751
- buildPayload(payload) {
1752
- return {
1753
- distinct_id: payload.distinct_id,
1754
- event: payload.event,
1755
- properties: {
1756
- ...(payload.properties || {}),
1757
- ...this.getCommonEventProperties(), // Common PH props
1758
- },
1759
- };
1760
- }
1761
- addPendingPromise(promise) {
1762
- const promiseUUID = uuidv7();
1763
- this.pendingPromises[promiseUUID] = promise;
1764
- promise
1765
- .catch(() => { })
1766
- .finally(() => {
1767
- delete this.pendingPromises[promiseUUID];
1768
- });
1769
- return promise;
1770
- }
1771
- /***
1772
- *** TRACKING
1773
- ***/
1774
- identifyStateless(distinctId, properties, options) {
1775
- this.wrap(() => {
1776
- // The properties passed to identifyStateless are event properties.
1777
- // To add person properties, pass in all person properties to the `$set` and `$set_once` keys.
1778
- const payload = {
1779
- ...this.buildPayload({
1780
- distinct_id: distinctId,
1781
- event: '$identify',
1782
- properties,
1783
- }),
1784
- };
1785
- this.enqueue('identify', payload, options);
1786
- });
1787
- }
1788
- async identifyStatelessImmediate(distinctId, properties, options) {
1789
- const payload = {
1790
- ...this.buildPayload({
1791
- distinct_id: distinctId,
1792
- event: '$identify',
1793
- properties,
1794
- }),
1795
- };
1796
- await this.sendImmediate('identify', payload, options);
1797
- }
1798
- captureStateless(distinctId, event, properties, options) {
1799
- this.wrap(() => {
1800
- const payload = this.buildPayload({ distinct_id: distinctId, event, properties });
1801
- this.enqueue('capture', payload, options);
1802
- });
1803
- }
1804
- async captureStatelessImmediate(distinctId, event, properties, options) {
1805
- const payload = this.buildPayload({ distinct_id: distinctId, event, properties });
1806
- await this.sendImmediate('capture', payload, options);
1807
- }
1808
- aliasStateless(alias, distinctId, properties, options) {
1809
- this.wrap(() => {
1810
- const payload = this.buildPayload({
1811
- event: '$create_alias',
1812
- distinct_id: distinctId,
1813
- properties: {
1814
- ...(properties || {}),
1815
- distinct_id: distinctId,
1816
- alias,
1817
- },
1818
- });
1819
- this.enqueue('alias', payload, options);
1820
- });
1821
- }
1822
- async aliasStatelessImmediate(alias, distinctId, properties, options) {
1823
- const payload = this.buildPayload({
1824
- event: '$create_alias',
1825
- distinct_id: distinctId,
1826
- properties: {
1827
- ...(properties || {}),
1828
- distinct_id: distinctId,
1829
- alias,
1830
- },
1831
- });
1832
- await this.sendImmediate('alias', payload, options);
1833
- }
1834
- /***
1835
- *** GROUPS
1836
- ***/
1837
- groupIdentifyStateless(groupType, groupKey, groupProperties, options, distinctId, eventProperties) {
1838
- this.wrap(() => {
1839
- const payload = this.buildPayload({
1840
- distinct_id: distinctId || `$${groupType}_${groupKey}`,
1841
- event: '$groupidentify',
1842
- properties: {
1843
- $group_type: groupType,
1844
- $group_key: groupKey,
1845
- $group_set: groupProperties || {},
1846
- ...(eventProperties || {}),
1847
- },
1848
- });
1849
- this.enqueue('capture', payload, options);
1850
- });
1851
- }
1852
- async getRemoteConfig() {
1853
- await this._initPromise;
1854
- let host = this.host;
1855
- if (host === 'https://us.i.posthog.com') {
1856
- host = 'https://us-assets.i.posthog.com';
1857
- }
1858
- else if (host === 'https://eu.i.posthog.com') {
1859
- host = 'https://eu-assets.i.posthog.com';
1860
- }
1861
- const url = `${host}/array/${this.apiKey}/config`;
1862
- const fetchOptions = {
1863
- method: 'GET',
1864
- headers: { ...this.getCustomHeaders(), 'Content-Type': 'application/json' },
1865
- };
1866
- // Don't retry remote config API calls
1867
- return this.fetchWithRetry(url, fetchOptions, { retryCount: 0 }, this.remoteConfigRequestTimeoutMs)
1868
- .then((response) => response.json())
1869
- .catch((error) => {
1870
- this.logMsgIfDebug(() => console.error('Remote config could not be loaded', error));
1871
- this._events.emit('error', error);
1872
- return undefined;
1873
- });
1874
- }
1875
- /***
1876
- *** FEATURE FLAGS
1877
- ***/
1878
- async getFlags(distinctId, groups = {}, personProperties = {}, groupProperties = {}, extraPayload = {}) {
1879
- await this._initPromise;
1880
- const url = `${this.host}/flags/?v=2&config=true`;
1881
- const fetchOptions = {
1882
- method: 'POST',
1883
- headers: { ...this.getCustomHeaders(), 'Content-Type': 'application/json' },
1884
- body: JSON.stringify({
1885
- token: this.apiKey,
1886
- distinct_id: distinctId,
1887
- groups,
1888
- person_properties: personProperties,
1889
- group_properties: groupProperties,
1890
- ...extraPayload,
1891
- }),
1892
- };
1893
- this.logMsgIfDebug(() => console.log('PostHog Debug', 'Flags URL', url));
1894
- // Don't retry /flags API calls
1895
- return this.fetchWithRetry(url, fetchOptions, { retryCount: 0 }, this.featureFlagsRequestTimeoutMs)
1896
- .then((response) => response.json())
1897
- .then((response) => normalizeFlagsResponse(response))
1898
- .catch((error) => {
1899
- this._events.emit('error', error);
1900
- return undefined;
1901
- });
1902
- }
1903
- async getFeatureFlagStateless(key, distinctId, groups = {}, personProperties = {}, groupProperties = {}, disableGeoip) {
1904
- await this._initPromise;
1905
- const flagDetailResponse = await this.getFeatureFlagDetailStateless(key, distinctId, groups, personProperties, groupProperties, disableGeoip);
1906
- if (flagDetailResponse === undefined) {
1907
- // If we haven't loaded flags yet, or errored out, we respond with undefined
1908
- return {
1909
- response: undefined,
1910
- requestId: undefined,
1911
- };
1912
- }
1913
- let response = getFeatureFlagValue(flagDetailResponse.response);
1914
- if (response === undefined) {
1915
- // For cases where the flag is unknown, return false
1916
- response = false;
1917
- }
1918
- // If we have flags we either return the value (true or string) or false
1919
- return {
1920
- response,
1921
- requestId: flagDetailResponse.requestId,
1922
- };
1923
- }
1924
- async getFeatureFlagDetailStateless(key, distinctId, groups = {}, personProperties = {}, groupProperties = {}, disableGeoip) {
1925
- await this._initPromise;
1926
- const flagsResponse = await this.getFeatureFlagDetailsStateless(distinctId, groups, personProperties, groupProperties, disableGeoip, [key]);
1927
- if (flagsResponse === undefined) {
1928
- return undefined;
1929
- }
1930
- const featureFlags = flagsResponse.flags;
1931
- const flagDetail = featureFlags[key];
1932
- return {
1933
- response: flagDetail,
1934
- requestId: flagsResponse.requestId,
1935
- };
1936
- }
1937
- async getFeatureFlagPayloadStateless(key, distinctId, groups = {}, personProperties = {}, groupProperties = {}, disableGeoip) {
1938
- await this._initPromise;
1939
- const payloads = await this.getFeatureFlagPayloadsStateless(distinctId, groups, personProperties, groupProperties, disableGeoip, [key]);
1940
- if (!payloads) {
1941
- return undefined;
1942
- }
1943
- const response = payloads[key];
1944
- // Undefined means a loading or missing data issue. Null means evaluation happened and there was no match
1945
- if (response === undefined) {
1946
- return null;
1947
- }
1948
- return response;
1949
- }
1950
- async getFeatureFlagPayloadsStateless(distinctId, groups = {}, personProperties = {}, groupProperties = {}, disableGeoip, flagKeysToEvaluate) {
1951
- await this._initPromise;
1952
- const payloads = (await this.getFeatureFlagsAndPayloadsStateless(distinctId, groups, personProperties, groupProperties, disableGeoip, flagKeysToEvaluate)).payloads;
1953
- return payloads;
1954
- }
1955
- async getFeatureFlagsStateless(distinctId, groups = {}, personProperties = {}, groupProperties = {}, disableGeoip, flagKeysToEvaluate) {
1956
- await this._initPromise;
1957
- return await this.getFeatureFlagsAndPayloadsStateless(distinctId, groups, personProperties, groupProperties, disableGeoip, flagKeysToEvaluate);
1958
- }
1959
- async getFeatureFlagsAndPayloadsStateless(distinctId, groups = {}, personProperties = {}, groupProperties = {}, disableGeoip, flagKeysToEvaluate) {
1960
- await this._initPromise;
1961
- const featureFlagDetails = await this.getFeatureFlagDetailsStateless(distinctId, groups, personProperties, groupProperties, disableGeoip, flagKeysToEvaluate);
1962
- if (!featureFlagDetails) {
1963
- return {
1964
- flags: undefined,
1965
- payloads: undefined,
1966
- requestId: undefined,
1967
- };
1968
- }
1969
- return {
1970
- flags: featureFlagDetails.featureFlags,
1971
- payloads: featureFlagDetails.featureFlagPayloads,
1972
- requestId: featureFlagDetails.requestId,
1973
- };
1974
- }
1975
- async getFeatureFlagDetailsStateless(distinctId, groups = {}, personProperties = {}, groupProperties = {}, disableGeoip, flagKeysToEvaluate) {
1976
- await this._initPromise;
1977
- const extraPayload = {};
1978
- if (disableGeoip ?? this.disableGeoip) {
1979
- extraPayload['geoip_disable'] = true;
1980
- }
1981
- if (flagKeysToEvaluate) {
1982
- extraPayload['flag_keys_to_evaluate'] = flagKeysToEvaluate;
1983
- }
1984
- const flagsResponse = await this.getFlags(distinctId, groups, personProperties, groupProperties, extraPayload);
1985
- if (flagsResponse === undefined) {
1986
- // We probably errored out, so return undefined
1987
- return undefined;
1988
- }
1989
- // if there's an error on the flagsResponse, log a console error, but don't throw an error
1990
- if (flagsResponse.errorsWhileComputingFlags) {
1991
- console.error('[FEATURE FLAGS] Error while computing feature flags, some flags may be missing or incorrect. Learn more at https://posthog.com/docs/feature-flags/best-practices');
1992
- }
1993
- // Add check for quota limitation on feature flags
1994
- if (flagsResponse.quotaLimited?.includes(QuotaLimitedFeature.FeatureFlags)) {
1995
- console.warn('[FEATURE FLAGS] Feature flags quota limit exceeded - feature flags unavailable. Learn more about billing limits at https://posthog.com/docs/billing/limits-alerts');
1996
- return {
1997
- flags: {},
1998
- featureFlags: {},
1999
- featureFlagPayloads: {},
2000
- requestId: flagsResponse?.requestId,
2001
- };
2002
- }
2003
- return flagsResponse;
2004
- }
2005
- /***
2006
- *** SURVEYS
2007
- ***/
2008
- async getSurveysStateless() {
2009
- await this._initPromise;
2010
- if (this.disableSurveys === true) {
2011
- this.logMsgIfDebug(() => console.log('PostHog Debug', 'Loading surveys is disabled.'));
2012
- return [];
2013
- }
2014
- const url = `${this.host}/api/surveys/?token=${this.apiKey}`;
2015
- const fetchOptions = {
2016
- method: 'GET',
2017
- headers: { ...this.getCustomHeaders(), 'Content-Type': 'application/json' },
2018
- };
2019
- const response = await this.fetchWithRetry(url, fetchOptions)
2020
- .then((response) => {
2021
- if (response.status !== 200 || !response.json) {
2022
- const msg = `Surveys API could not be loaded: ${response.status}`;
2023
- const error = new Error(msg);
2024
- this.logMsgIfDebug(() => console.error(error));
2025
- this._events.emit('error', new Error(msg));
2026
- return undefined;
2027
- }
2028
- return response.json();
2029
- })
2030
- .catch((error) => {
2031
- this.logMsgIfDebug(() => console.error('Surveys API could not be loaded', error));
2032
- this._events.emit('error', error);
2033
- return undefined;
2034
- });
2035
- const newSurveys = response?.surveys;
2036
- if (newSurveys) {
2037
- this.logMsgIfDebug(() => console.log('PostHog Debug', 'Surveys fetched from API: ', JSON.stringify(newSurveys)));
2038
- }
2039
- return newSurveys ?? [];
2040
- }
2041
- get props() {
2042
- if (!this._props) {
2043
- this._props = this.getPersistedProperty(PostHogPersistedProperty.Props);
2044
- }
2045
- return this._props || {};
2046
- }
2047
- set props(val) {
2048
- this._props = val;
2049
- }
2050
- async register(properties) {
2051
- this.wrap(() => {
2052
- this.props = {
2053
- ...this.props,
2054
- ...properties,
2055
- };
2056
- this.setPersistedProperty(PostHogPersistedProperty.Props, this.props);
2057
- });
2058
- }
2059
- async unregister(property) {
2060
- this.wrap(() => {
2061
- delete this.props[property];
2062
- this.setPersistedProperty(PostHogPersistedProperty.Props, this.props);
2063
- });
2064
- }
2065
- /***
2066
- *** QUEUEING AND FLUSHING
2067
- ***/
2068
- enqueue(type, _message, options) {
2069
- this.wrap(() => {
2070
- if (this.optedOut) {
2071
- this._events.emit(type, `Library is disabled. Not sending event. To re-enable, call posthog.optIn()`);
2072
- return;
2073
- }
2074
- const message = this.prepareMessage(type, _message, options);
2075
- const queue = this.getPersistedProperty(PostHogPersistedProperty.Queue) || [];
2076
- if (queue.length >= this.maxQueueSize) {
2077
- queue.shift();
2078
- this.logMsgIfDebug(() => console.info('Queue is full, the oldest event is dropped.'));
2079
- }
2080
- queue.push({ message });
2081
- this.setPersistedProperty(PostHogPersistedProperty.Queue, queue);
2082
- this._events.emit(type, message);
2083
- // Flush queued events if we meet the flushAt length
2084
- if (queue.length >= this.flushAt) {
2085
- this.flushBackground();
2086
- }
2087
- if (this.flushInterval && !this._flushTimer) {
2088
- this._flushTimer = safeSetTimeout(() => this.flushBackground(), this.flushInterval);
2089
- }
2090
- });
2091
- }
2092
- async sendImmediate(type, _message, options) {
2093
- if (this.disabled) {
2094
- this.logMsgIfDebug(() => console.warn('[PostHog] The client is disabled'));
2095
- return;
2096
- }
2097
- if (!this._isInitialized) {
2098
- await this._initPromise;
2099
- }
2100
- if (this.optedOut) {
2101
- this._events.emit(type, `Library is disabled. Not sending event. To re-enable, call posthog.optIn()`);
2102
- return;
2103
- }
2104
- const data = {
2105
- api_key: this.apiKey,
2106
- batch: [this.prepareMessage(type, _message, options)],
2107
- sent_at: currentISOTime(),
2108
- };
2109
- if (this.historicalMigration) {
2110
- data.historical_migration = true;
2111
- }
2112
- const payload = JSON.stringify(data);
2113
- const url = `${this.host}/batch/`;
2114
- const gzippedPayload = !this.disableCompression ? await gzipCompress(payload, this.isDebug) : null;
2115
- const fetchOptions = {
2116
- method: 'POST',
2117
- headers: {
2118
- ...this.getCustomHeaders(),
2119
- 'Content-Type': 'application/json',
2120
- ...(gzippedPayload !== null && { 'Content-Encoding': 'gzip' }),
2121
- },
2122
- body: gzippedPayload || payload,
2123
- };
2124
- try {
2125
- await this.fetchWithRetry(url, fetchOptions);
2126
- }
2127
- catch (err) {
2128
- this._events.emit('error', err);
2129
- }
2130
- }
2131
- prepareMessage(type, _message, options) {
2132
- const message = {
2133
- ..._message,
2134
- type: type,
2135
- library: this.getLibraryId(),
2136
- library_version: this.getLibraryVersion(),
2137
- timestamp: options?.timestamp ? options?.timestamp : currentISOTime(),
2138
- uuid: options?.uuid ? options.uuid : uuidv7(),
2139
- };
2140
- const addGeoipDisableProperty = options?.disableGeoip ?? this.disableGeoip;
2141
- if (addGeoipDisableProperty) {
2142
- if (!message.properties) {
2143
- message.properties = {};
2144
- }
2145
- message['properties']['$geoip_disable'] = true;
2146
- }
2147
- if (message.distinctId) {
2148
- message.distinct_id = message.distinctId;
2149
- delete message.distinctId;
2150
- }
2151
- return message;
2152
- }
2153
- clearFlushTimer() {
2154
- if (this._flushTimer) {
2155
- clearTimeout(this._flushTimer);
2156
- this._flushTimer = undefined;
2157
- }
2158
- }
2159
- /**
2160
- * Helper for flushing the queue in the background
2161
- * Avoids unnecessary promise errors
2162
- */
2163
- flushBackground() {
2164
- void this.flush().catch(async (err) => {
2165
- await logFlushError(err);
2166
- });
2167
- }
2168
- /**
2169
- * Flushes the queue
2170
- *
2171
- * This function will return a promise that will resolve when the flush is complete,
2172
- * or reject if there was an error (for example if the server or network is down).
2173
- *
2174
- * If there is already a flush in progress, this function will wait for that flush to complete.
2175
- *
2176
- * It's recommended to do error handling in the callback of the promise.
2177
- *
2178
- * @example
2179
- * posthog.flush().then(() => {
2180
- * console.log('Flush complete')
2181
- * }).catch((err) => {
2182
- * console.error('Flush failed', err)
2183
- * })
2184
- *
2185
- *
2186
- * @throws PostHogFetchHttpError
2187
- * @throws PostHogFetchNetworkError
2188
- * @throws Error
2189
- */
2190
- async flush() {
2191
- // Wait for the current flush operation to finish (regardless of success or failure), then try to flush again.
2192
- // Use allSettled instead of finally to be defensive around flush throwing errors immediately rather than rejecting.
2193
- // Use a custom allSettled implementation to avoid issues with patching Promise on RN
2194
- const nextFlushPromise = allSettled([this.flushPromise]).then(() => {
2195
- return this._flush();
2196
- });
2197
- this.flushPromise = nextFlushPromise;
2198
- void this.addPendingPromise(nextFlushPromise);
2199
- allSettled([nextFlushPromise]).then(() => {
2200
- // If there are no others waiting to flush, clear the promise.
2201
- // We don't strictly need to do this, but it could make debugging easier
2202
- if (this.flushPromise === nextFlushPromise) {
2203
- this.flushPromise = null;
2204
- }
2205
- });
2206
- return nextFlushPromise;
2207
- }
2208
- getCustomHeaders() {
2209
- // Don't set the user agent if we're not on a browser. The latest spec allows
2210
- // the User-Agent header (see https://fetch.spec.whatwg.org/#terminology-headers
2211
- // and https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/setRequestHeader),
2212
- // but browsers such as Chrome and Safari have not caught up.
2213
- const customUserAgent = this.getCustomUserAgent();
2214
- const headers = {};
2215
- if (customUserAgent && customUserAgent !== '') {
2216
- headers['User-Agent'] = customUserAgent;
2217
- }
2218
- return headers;
2219
- }
2220
- async _flush() {
2221
- this.clearFlushTimer();
2222
- await this._initPromise;
2223
- let queue = this.getPersistedProperty(PostHogPersistedProperty.Queue) || [];
2224
- if (!queue.length) {
2225
- return;
2226
- }
2227
- const sentMessages = [];
2228
- const originalQueueLength = queue.length;
2229
- while (queue.length > 0 && sentMessages.length < originalQueueLength) {
2230
- const batchItems = queue.slice(0, this.maxBatchSize);
2231
- const batchMessages = batchItems.map((item) => item.message);
2232
- const persistQueueChange = () => {
2233
- const refreshedQueue = this.getPersistedProperty(PostHogPersistedProperty.Queue) || [];
2234
- const newQueue = refreshedQueue.slice(batchItems.length);
2235
- this.setPersistedProperty(PostHogPersistedProperty.Queue, newQueue);
2236
- queue = newQueue;
2237
- };
2238
- const data = {
2239
- api_key: this.apiKey,
2240
- batch: batchMessages,
2241
- sent_at: currentISOTime(),
2242
- };
2243
- if (this.historicalMigration) {
2244
- data.historical_migration = true;
2245
- }
2246
- const payload = JSON.stringify(data);
2247
- const url = `${this.host}/batch/`;
2248
- const gzippedPayload = !this.disableCompression ? await gzipCompress(payload, this.isDebug) : null;
2249
- const fetchOptions = {
2250
- method: 'POST',
2251
- headers: {
2252
- ...this.getCustomHeaders(),
2253
- 'Content-Type': 'application/json',
2254
- ...(gzippedPayload !== null && { 'Content-Encoding': 'gzip' }),
2255
- },
2256
- body: gzippedPayload || payload,
2257
- };
2258
- const retryOptions = {
2259
- retryCheck: (err) => {
2260
- // don't automatically retry on 413 errors, we want to reduce the batch size first
2261
- if (isPostHogFetchContentTooLargeError(err)) {
2262
- return false;
2263
- }
2264
- // otherwise, retry on network errors
2265
- return isPostHogFetchError(err);
2266
- },
2267
- };
2268
- try {
2269
- await this.fetchWithRetry(url, fetchOptions, retryOptions);
2270
- }
2271
- catch (err) {
2272
- if (isPostHogFetchContentTooLargeError(err) && batchMessages.length > 1) {
2273
- // if we get a 413 error, we want to reduce the batch size and try again
2274
- this.maxBatchSize = Math.max(1, Math.floor(batchMessages.length / 2));
2275
- this.logMsgIfDebug(() => console.warn(`Received 413 when sending batch of size ${batchMessages.length}, reducing batch size to ${this.maxBatchSize}`));
2276
- // do not persist the queue change, we want to retry the same batch
2277
- continue;
2278
- }
2279
- // depending on the error type, eg a malformed JSON or broken queue, it'll always return an error
2280
- // and this will be an endless loop, in this case, if the error isn't a network issue, we always remove the items from the queue
2281
- if (!(err instanceof PostHogFetchNetworkError)) {
2282
- persistQueueChange();
2283
- }
2284
- this._events.emit('error', err);
2285
- throw err;
2286
- }
2287
- persistQueueChange();
2288
- sentMessages.push(...batchMessages);
2289
- }
2290
- this._events.emit('flush', sentMessages);
2291
- }
2292
- async fetchWithRetry(url, options, retryOptions, requestTimeout) {
2293
- var _a;
2294
- (_a = AbortSignal).timeout ?? (_a.timeout = function timeout(ms) {
2295
- const ctrl = new AbortController();
2296
- setTimeout(() => ctrl.abort(), ms);
2297
- return ctrl.signal;
2298
- });
2299
- const body = options.body ? options.body : '';
2300
- let reqByteLength = -1;
2301
- try {
2302
- if (body instanceof Blob) {
2303
- reqByteLength = body.size;
2304
- }
2305
- else {
2306
- reqByteLength = Buffer.byteLength(body, STRING_FORMAT);
2307
- }
2308
- }
2309
- catch {
2310
- if (body instanceof Blob) {
2311
- reqByteLength = body.size;
2312
- }
2313
- else {
2314
- const encoded = new TextEncoder().encode(body);
2315
- reqByteLength = encoded.length;
2316
- }
2317
- }
2318
- return await retriable(async () => {
2319
- let res = null;
2320
- try {
2321
- res = await this.fetch(url, {
2322
- signal: AbortSignal.timeout(requestTimeout ?? this.requestTimeout),
2323
- ...options,
2324
- });
2325
- }
2326
- catch (e) {
2327
- // fetch will only throw on network errors or on timeouts
2328
- throw new PostHogFetchNetworkError(e);
2329
- }
2330
- // If we're in no-cors mode, we can't access the response status
2331
- // We only throw on HTTP errors if we're not in no-cors mode
2332
- // https://developer.mozilla.org/en-US/docs/Web/API/Request/mode#no-cors
2333
- const isNoCors = options.mode === 'no-cors';
2334
- if (!isNoCors && (res.status < 200 || res.status >= 400)) {
2335
- throw new PostHogFetchHttpError(res, reqByteLength);
2336
- }
2337
- return res;
2338
- }, { ...this._retryOptions, ...retryOptions });
2339
- }
2340
- async _shutdown(shutdownTimeoutMs = 30000) {
2341
- // A little tricky - we want to have a max shutdown time and enforce it, even if that means we have some
2342
- // dangling promises. We'll keep track of the timeout and resolve/reject based on that.
2343
- await this._initPromise;
2344
- let hasTimedOut = false;
2345
- this.clearFlushTimer();
2346
- const doShutdown = async () => {
2347
- try {
2348
- await Promise.all(Object.values(this.pendingPromises));
2349
- while (true) {
2350
- const queue = this.getPersistedProperty(PostHogPersistedProperty.Queue) || [];
2351
- if (queue.length === 0) {
2352
- break;
2353
- }
2354
- // flush again to make sure we send all events, some of which might've been added
2355
- // while we were waiting for the pending promises to resolve
2356
- // For example, see sendFeatureFlags in posthog-node/src/posthog-node.ts::capture
2357
- await this.flush();
2358
- if (hasTimedOut) {
2359
- break;
2360
- }
2361
- }
2362
- }
2363
- catch (e) {
2364
- if (!isPostHogFetchError(e)) {
2365
- throw e;
2366
- }
2367
- await logFlushError(e);
2368
- }
2369
- };
2370
- return Promise.race([
2371
- new Promise((_, reject) => {
2372
- safeSetTimeout(() => {
2373
- this.logMsgIfDebug(() => console.error('Timed out while shutting down PostHog'));
2374
- hasTimedOut = true;
2375
- reject('Timeout while shutting down PostHog. Some events may not have been sent.');
2376
- }, shutdownTimeoutMs);
2377
- }),
2378
- doShutdown(),
2379
- ]);
2380
- }
2381
- /**
2382
- * Call shutdown() once before the node process exits, so ensure that all events have been sent and all promises
2383
- * have resolved. Do not use this function if you intend to keep using this PostHog instance after calling it.
2384
- * @param shutdownTimeoutMs
2385
- */
2386
- async shutdown(shutdownTimeoutMs = 30000) {
2387
- if (this.shutdownPromise) {
2388
- this.logMsgIfDebug(() => console.warn('shutdown() called while already shutting down. shutdown() is meant to be called once before process exit - use flush() for per-request cleanup'));
2389
- }
2390
- else {
2391
- this.shutdownPromise = this._shutdown(shutdownTimeoutMs).finally(() => {
2392
- this.shutdownPromise = null;
2393
- });
2394
- }
2395
- return this.shutdownPromise;
2396
- }
2397
- }
2398
-
2399
- /**
2400
- * A lazy value that is only computed when needed. Inspired by C#'s Lazy<T> class.
1178
+ /**
1179
+ * A lazy value that is only computed when needed. Inspired by C#'s Lazy<T> class.
2401
1180
  */
2402
1181
  class Lazy {
2403
1182
  constructor(factory) {
2404
1183
  this.factory = factory;
2405
1184
  }
2406
- /**
2407
- * Gets the value, initializing it if necessary.
2408
- * Multiple concurrent calls will share the same initialization promise.
1185
+ /**
1186
+ * Gets the value, initializing it if necessary.
1187
+ * Multiple concurrent calls will share the same initialization promise.
2409
1188
  */
2410
1189
  async getValue() {
2411
1190
  if (this.value !== undefined) {
@@ -2425,15 +1204,15 @@ class Lazy {
2425
1204
  }
2426
1205
  return this.initializationPromise;
2427
1206
  }
2428
- /**
2429
- * Returns true if the value has been initialized.
1207
+ /**
1208
+ * Returns true if the value has been initialized.
2430
1209
  */
2431
1210
  isInitialized() {
2432
1211
  return this.value !== undefined;
2433
1212
  }
2434
- /**
2435
- * Returns a promise that resolves when the value is initialized.
2436
- * If already initialized, resolves immediately.
1213
+ /**
1214
+ * Returns a promise that resolves when the value is initialized.
1215
+ * If already initialized, resolves immediately.
2437
1216
  */
2438
1217
  async waitForInitialization() {
2439
1218
  if (this.isInitialized()) {
@@ -2562,7 +1341,8 @@ class FeatureFlagsPoller {
2562
1341
  featureFlag = this.featureFlagsByKey[key];
2563
1342
  if (featureFlag !== undefined) {
2564
1343
  try {
2565
- response = await this.computeFlagLocally(featureFlag, distinctId, groups, personProperties, groupProperties);
1344
+ const result = await this.computeFlagAndPayloadLocally(featureFlag, distinctId, groups, personProperties, groupProperties);
1345
+ response = result.value;
2566
1346
  this.logMsgIfDebug(() => console.debug(`Successfully computed flag locally: ${key} -> ${response}`));
2567
1347
  } catch (e) {
2568
1348
  if (e instanceof InconclusiveMatchError) {
@@ -2574,27 +1354,6 @@ class FeatureFlagsPoller {
2574
1354
  }
2575
1355
  return response;
2576
1356
  }
2577
- async computeFeatureFlagPayloadLocally(key, matchValue) {
2578
- await this.loadFeatureFlags();
2579
- let response = undefined;
2580
- if (!this.loadedSuccessfullyOnce) {
2581
- return undefined;
2582
- }
2583
- if (typeof matchValue == 'boolean') {
2584
- response = this.featureFlagsByKey?.[key]?.filters?.payloads?.[matchValue.toString()];
2585
- } else if (typeof matchValue == 'string') {
2586
- response = this.featureFlagsByKey?.[key]?.filters?.payloads?.[matchValue];
2587
- }
2588
- // Undefined means a loading or missing data issue. Null means evaluation happened and there was no match
2589
- if (response === undefined || response === null) {
2590
- return null;
2591
- }
2592
- try {
2593
- return JSON.parse(response);
2594
- } catch {
2595
- return response;
2596
- }
2597
- }
2598
1357
  async getAllFlagsAndPayloads(distinctId, groups = {}, personProperties = {}, groupProperties = {}, flagKeysToExplicitlyEvaluate) {
2599
1358
  await this.loadFeatureFlags();
2600
1359
  const response = {};
@@ -2603,9 +1362,11 @@ class FeatureFlagsPoller {
2603
1362
  const flagsToEvaluate = flagKeysToExplicitlyEvaluate ? flagKeysToExplicitlyEvaluate.map(key => this.featureFlagsByKey[key]).filter(Boolean) : this.featureFlags;
2604
1363
  await Promise.all(flagsToEvaluate.map(async flag => {
2605
1364
  try {
2606
- const matchValue = await this.computeFlagLocally(flag, distinctId, groups, personProperties, groupProperties);
1365
+ const {
1366
+ value: matchValue,
1367
+ payload: matchPayload
1368
+ } = await this.computeFlagAndPayloadLocally(flag, distinctId, groups, personProperties, groupProperties);
2607
1369
  response[flag.key] = matchValue;
2608
- const matchPayload = await this.computeFeatureFlagPayloadLocally(flag.key, matchValue);
2609
1370
  if (matchPayload) {
2610
1371
  payloads[flag.key] = matchPayload;
2611
1372
  }
@@ -2624,7 +1385,30 @@ class FeatureFlagsPoller {
2624
1385
  fallbackToFlags
2625
1386
  };
2626
1387
  }
2627
- async computeFlagLocally(flag, distinctId, groups = {}, personProperties = {}, groupProperties = {}) {
1388
+ async computeFlagAndPayloadLocally(flag, distinctId, groups = {}, personProperties = {}, groupProperties = {}, matchValue) {
1389
+ // Always ensure flags are loaded for payload computation
1390
+ await this.loadFeatureFlags();
1391
+ if (!this.loadedSuccessfullyOnce) {
1392
+ return {
1393
+ value: false,
1394
+ payload: null
1395
+ };
1396
+ }
1397
+ let flagValue;
1398
+ // If matchValue is provided, use it directly; otherwise evaluate the flag
1399
+ if (matchValue !== undefined) {
1400
+ flagValue = matchValue;
1401
+ } else {
1402
+ flagValue = await this.computeFlagValueLocally(flag, distinctId, groups, personProperties, groupProperties);
1403
+ }
1404
+ // Always compute payload based on the final flagValue (whether provided or computed)
1405
+ const payload = this.getFeatureFlagPayload(flag.key, flagValue);
1406
+ return {
1407
+ value: flagValue,
1408
+ payload
1409
+ };
1410
+ }
1411
+ async computeFlagValueLocally(flag, distinctId, groups = {}, personProperties = {}, groupProperties = {}) {
2628
1412
  if (flag.ensure_experience_continuity) {
2629
1413
  throw new InconclusiveMatchError('Flag has experience continuity enabled');
2630
1414
  }
@@ -2649,7 +1433,98 @@ class FeatureFlagsPoller {
2649
1433
  return await this.matchFeatureFlagProperties(flag, distinctId, personProperties);
2650
1434
  }
2651
1435
  }
2652
- async matchFeatureFlagProperties(flag, distinctId, properties) {
1436
+ getFeatureFlagPayload(key, flagValue) {
1437
+ let payload = null;
1438
+ if (flagValue !== false && flagValue !== null && flagValue !== undefined) {
1439
+ if (typeof flagValue == 'boolean') {
1440
+ payload = this.featureFlagsByKey?.[key]?.filters?.payloads?.[flagValue.toString()] || null;
1441
+ } else if (typeof flagValue == 'string') {
1442
+ payload = this.featureFlagsByKey?.[key]?.filters?.payloads?.[flagValue] || null;
1443
+ }
1444
+ if (payload !== null && payload !== undefined) {
1445
+ // If payload is already an object, return it directly
1446
+ if (typeof payload === 'object') {
1447
+ return payload;
1448
+ }
1449
+ // If payload is a string, try to parse it as JSON
1450
+ if (typeof payload === 'string') {
1451
+ try {
1452
+ return JSON.parse(payload);
1453
+ } catch {
1454
+ // If parsing fails, return the string as is
1455
+ return payload;
1456
+ }
1457
+ }
1458
+ // For other types, return as is
1459
+ return payload;
1460
+ }
1461
+ }
1462
+ return null;
1463
+ }
1464
+ async evaluateFlagDependency(property, distinctId, properties, evaluationCache) {
1465
+ const targetFlagKey = property.key;
1466
+ if (!this.featureFlagsByKey) {
1467
+ throw new InconclusiveMatchError('Feature flags not available for dependency evaluation');
1468
+ }
1469
+ // Check if dependency_chain is present - it should always be provided for flag dependencies
1470
+ if (!('dependency_chain' in property)) {
1471
+ throw new InconclusiveMatchError(`Flag dependency property for '${targetFlagKey}' is missing required 'dependency_chain' field`);
1472
+ }
1473
+ const dependencyChain = property.dependency_chain;
1474
+ // Check for missing or invalid dependency chain (This should never happen, but being defensive)
1475
+ if (!Array.isArray(dependencyChain)) {
1476
+ throw new InconclusiveMatchError(`Flag dependency property for '${targetFlagKey}' has an invalid 'dependency_chain' (expected array, got ${typeof dependencyChain})`);
1477
+ }
1478
+ // Handle circular dependency (empty chain means circular) (This should never happen, but being defensive)
1479
+ if (dependencyChain.length === 0) {
1480
+ throw new InconclusiveMatchError(`Circular dependency detected for flag '${targetFlagKey}' (empty dependency chain)`);
1481
+ }
1482
+ // Evaluate all dependencies in the chain order
1483
+ for (const depFlagKey of dependencyChain) {
1484
+ if (!(depFlagKey in evaluationCache)) {
1485
+ // Need to evaluate this dependency first
1486
+ const depFlag = this.featureFlagsByKey[depFlagKey];
1487
+ if (!depFlag) {
1488
+ // Missing flag dependency - cannot evaluate locally
1489
+ throw new InconclusiveMatchError(`Missing flag dependency '${depFlagKey}' for flag '${targetFlagKey}'`);
1490
+ } else if (!depFlag.active) {
1491
+ // Inactive flag evaluates to false
1492
+ evaluationCache[depFlagKey] = false;
1493
+ } else {
1494
+ // Recursively evaluate the dependency
1495
+ try {
1496
+ const depResult = await this.matchFeatureFlagProperties(depFlag, distinctId, properties, evaluationCache);
1497
+ evaluationCache[depFlagKey] = depResult;
1498
+ } catch (error) {
1499
+ // If we can't evaluate a dependency, store throw InconclusiveMatchError(`Missing flag dependency '${depFlagKey}' for flag '${targetFlagKey}'`)
1500
+ throw new InconclusiveMatchError(`Error evaluating flag dependency '${depFlagKey}' for flag '${targetFlagKey}': ${error}`);
1501
+ }
1502
+ }
1503
+ }
1504
+ // Check if dependency evaluation was inconclusive
1505
+ const cachedResult = evaluationCache[depFlagKey];
1506
+ if (cachedResult === null || cachedResult === undefined) {
1507
+ throw new InconclusiveMatchError(`Dependency '${depFlagKey}' could not be evaluated`);
1508
+ }
1509
+ }
1510
+ // The target flag is specified in property.key (This should match the last element in the dependency chain)
1511
+ const targetFlagValue = evaluationCache[targetFlagKey];
1512
+ return this.flagEvaluatesToExpectedValue(property.value, targetFlagValue);
1513
+ }
1514
+ flagEvaluatesToExpectedValue(expectedValue, flagValue) {
1515
+ // If the expected value is a boolean, then return true if the flag evaluated to true (or any string variant)
1516
+ // If the expected value is false, then only return true if the flag evaluated to false.
1517
+ if (typeof expectedValue === 'boolean') {
1518
+ return expectedValue === flagValue || typeof flagValue === 'string' && flagValue !== '' && expectedValue === true;
1519
+ }
1520
+ // If the expected value is a string, then return true if and only if the flag evaluated to the expected value.
1521
+ if (typeof expectedValue === 'string') {
1522
+ return flagValue === expectedValue;
1523
+ }
1524
+ // The `flag_evaluates_to` operator is not supported for numbers and arrays.
1525
+ return false;
1526
+ }
1527
+ async matchFeatureFlagProperties(flag, distinctId, properties, evaluationCache = {}) {
2653
1528
  const flagFilters = flag.filters || {};
2654
1529
  const flagConditions = flagFilters.groups || [];
2655
1530
  let isInconclusive = false;
@@ -2671,7 +1546,7 @@ class FeatureFlagsPoller {
2671
1546
  });
2672
1547
  for (const condition of sortedFlagConditions) {
2673
1548
  try {
2674
- if (await this.isConditionMatch(flag, distinctId, condition, properties)) {
1549
+ if (await this.isConditionMatch(flag, distinctId, condition, properties, evaluationCache)) {
2675
1550
  const variantOverride = condition.variant;
2676
1551
  const flagVariants = flagFilters.multivariate?.variants || [];
2677
1552
  if (variantOverride && flagVariants.some(variant => variant.key === variantOverride)) {
@@ -2697,7 +1572,7 @@ class FeatureFlagsPoller {
2697
1572
  // We can only return False when all conditions are False
2698
1573
  return false;
2699
1574
  }
2700
- async isConditionMatch(flag, distinctId, condition, properties) {
1575
+ async isConditionMatch(flag, distinctId, condition, properties, evaluationCache = {}) {
2701
1576
  const rolloutPercentage = condition.rollout_percentage;
2702
1577
  const warnFunction = msg => {
2703
1578
  this.logMsgIfDebug(() => console.warn(msg));
@@ -2709,8 +1584,7 @@ class FeatureFlagsPoller {
2709
1584
  if (propertyType === 'cohort') {
2710
1585
  matches = matchCohort(prop, properties, this.cohorts, this.debugMode);
2711
1586
  } else if (propertyType === 'flag') {
2712
- this.logMsgIfDebug(() => console.warn(`[FEATURE FLAGS] Flag dependency filters are not supported in local evaluation. ` + `Skipping condition for flag '${flag.key}' with dependency on flag '${prop.key || 'unknown'}'`));
2713
- continue;
1587
+ matches = await this.evaluateFlagDependency(prop, distinctId, properties, evaluationCache);
2714
1588
  } else {
2715
1589
  matches = matchProperty(prop, properties, warnFunction);
2716
1590
  }
@@ -2759,18 +1633,18 @@ class FeatureFlagsPoller {
2759
1633
  await this._loadFeatureFlags();
2760
1634
  }
2761
1635
  }
2762
- /**
2763
- * Returns true if the feature flags poller has loaded successfully at least once and has more than 0 feature flags.
2764
- * This is useful to check if local evaluation is ready before calling getFeatureFlag.
1636
+ /**
1637
+ * Returns true if the feature flags poller has loaded successfully at least once and has more than 0 feature flags.
1638
+ * This is useful to check if local evaluation is ready before calling getFeatureFlag.
2765
1639
  */
2766
1640
  isLocalEvaluationReady() {
2767
1641
  return (this.loadedSuccessfullyOnce ?? false) && (this.featureFlags?.length ?? 0) > 0;
2768
1642
  }
2769
- /**
2770
- * If a client is misconfigured with an invalid or improper API key, the polling interval is doubled each time
2771
- * until a successful request is made, up to a maximum of 60 seconds.
2772
- *
2773
- * @returns The polling interval to use for the next request.
1643
+ /**
1644
+ * If a client is misconfigured with an invalid or improper API key, the polling interval is doubled each time
1645
+ * until a successful request is made, up to a maximum of 60 seconds.
1646
+ *
1647
+ * @returns The polling interval to use for the next request.
2774
1648
  */
2775
1649
  getPollingInterval() {
2776
1650
  if (!this.shouldBeginExponentialBackoff) {
@@ -2975,6 +1849,10 @@ function matchProperty(property, propertyValues, warnFunction) {
2975
1849
  case 'is_date_after':
2976
1850
  case 'is_date_before':
2977
1851
  {
1852
+ // Boolean values should never be used with date operations
1853
+ if (typeof value === 'boolean') {
1854
+ throw new InconclusiveMatchError(`Date operations cannot be performed on boolean values`);
1855
+ }
2978
1856
  let parsedDate = relativeDateParseForFeatureFlagMatching(String(value));
2979
1857
  if (parsedDate == null) {
2980
1858
  parsedDate = convertToDateTime(value);
@@ -3158,6 +2036,37 @@ class PostHogMemoryStorage {
3158
2036
  }
3159
2037
  }
3160
2038
 
2039
+ const _createLogger = (prefix, logMsgIfDebug) => {
2040
+ const logger = {
2041
+ _log: (level, ...args) => {
2042
+ logMsgIfDebug(() => {
2043
+ const consoleLog = console[level];
2044
+ consoleLog(prefix, ...args);
2045
+ });
2046
+ },
2047
+ info: (...args) => {
2048
+ logger._log('log', ...args);
2049
+ },
2050
+ warn: (...args) => {
2051
+ logger._log('warn', ...args);
2052
+ },
2053
+ error: (...args) => {
2054
+ logger._log('error', ...args);
2055
+ },
2056
+ critical: (...args) => {
2057
+ // Critical errors are always logged to the console
2058
+ // eslint-disable-next-line no-console
2059
+ console.error(prefix, ...args);
2060
+ },
2061
+ uninitializedWarning: methodName => {
2062
+ logger.error(`You must initialize PostHog before calling ${methodName}`);
2063
+ },
2064
+ createLogger: additionalPrefix => _createLogger(`${prefix} ${additionalPrefix}`, logMsgIfDebug)
2065
+ };
2066
+ return logger;
2067
+ };
2068
+ const createLogger = logMsgIfDebug => _createLogger('[PostHog.js]', logMsgIfDebug);
2069
+
3161
2070
  // Standard local evaluation rate limit is 600 per minute (10 per second),
3162
2071
  // so the fastest a poller should ever be set is 100ms.
3163
2072
  const MINIMUM_POLLING_INTERVAL = 100;
@@ -3169,6 +2078,7 @@ class PostHogBackendClient extends PostHogCoreStateless {
3169
2078
  super(apiKey, options);
3170
2079
  this._memoryStorage = new PostHogMemoryStorage();
3171
2080
  this.options = options;
2081
+ this.logger = createLogger(this.logMsgIfDebug.bind(this));
3172
2082
  this.options.featureFlagsPollingInterval = typeof options.featureFlagsPollingInterval === 'number' ? Math.max(options.featureFlagsPollingInterval, MINIMUM_POLLING_INTERVAL) : THIRTY_SECONDS;
3173
2083
  if (options.personalApiKey) {
3174
2084
  if (options.personalApiKey.includes('phc_')) {
@@ -3182,6 +2092,7 @@ class PostHogBackendClient extends PostHogCoreStateless {
3182
2092
  personalApiKey: options.personalApiKey,
3183
2093
  projectApiKey: apiKey,
3184
2094
  timeout: options.requestTimeout ?? 10000,
2095
+ // 10 seconds
3185
2096
  host: this.host,
3186
2097
  fetch: options.fetch,
3187
2098
  onError: err => {
@@ -3194,7 +2105,7 @@ class PostHogBackendClient extends PostHogCoreStateless {
3194
2105
  });
3195
2106
  }
3196
2107
  }
3197
- this.errorTracking = new ErrorTracking(this, options);
2108
+ this.errorTracking = new ErrorTracking(this, options, this.logger);
3198
2109
  this.distinctIdHasSentFlagCalls = {};
3199
2110
  this.maxCacheSize = options.maxCacheSize || MAX_CACHE_SIZE;
3200
2111
  }
@@ -3237,11 +2148,25 @@ class PostHogBackendClient extends PostHogCoreStateless {
3237
2148
  disableGeoip,
3238
2149
  uuid
3239
2150
  } = props;
2151
+ // Run before_send if configured
2152
+ const eventMessage = this._runBeforeSend({
2153
+ distinctId,
2154
+ event,
2155
+ properties,
2156
+ groups,
2157
+ sendFeatureFlags,
2158
+ timestamp,
2159
+ disableGeoip,
2160
+ uuid
2161
+ });
2162
+ if (!eventMessage) {
2163
+ return;
2164
+ }
3240
2165
  const _capture = props => {
3241
- super.captureStateless(distinctId, event, props, {
3242
- timestamp,
3243
- disableGeoip,
3244
- uuid
2166
+ super.captureStateless(eventMessage.distinctId, eventMessage.event, props, {
2167
+ timestamp: eventMessage.timestamp,
2168
+ disableGeoip: eventMessage.disableGeoip,
2169
+ uuid: eventMessage.uuid
3245
2170
  });
3246
2171
  };
3247
2172
  // :TRICKY: If we flush, or need to shut down, to not lose events we want this promise to resolve before we flush
@@ -3276,8 +2201,8 @@ class PostHogBackendClient extends PostHogCoreStateless {
3276
2201
  // No matter what - capture the event
3277
2202
  _capture({
3278
2203
  ...additionalProperties,
3279
- ...properties,
3280
- $groups: groups
2204
+ ...(eventMessage.properties || {}),
2205
+ $groups: eventMessage.groups || groups
3281
2206
  });
3282
2207
  });
3283
2208
  this.addPendingPromise(capturePromise);
@@ -3296,11 +2221,25 @@ class PostHogBackendClient extends PostHogCoreStateless {
3296
2221
  disableGeoip,
3297
2222
  uuid
3298
2223
  } = props;
2224
+ // Run before_send if configured
2225
+ const eventMessage = this._runBeforeSend({
2226
+ distinctId,
2227
+ event,
2228
+ properties,
2229
+ groups,
2230
+ sendFeatureFlags,
2231
+ timestamp,
2232
+ disableGeoip,
2233
+ uuid
2234
+ });
2235
+ if (!eventMessage) {
2236
+ return;
2237
+ }
3299
2238
  const _capture = props => {
3300
- return super.captureStatelessImmediate(distinctId, event, props, {
3301
- timestamp,
3302
- disableGeoip,
3303
- uuid
2239
+ return super.captureStatelessImmediate(eventMessage.distinctId, eventMessage.event, props, {
2240
+ timestamp: eventMessage.timestamp,
2241
+ disableGeoip: eventMessage.disableGeoip,
2242
+ uuid: eventMessage.uuid
3304
2243
  });
3305
2244
  };
3306
2245
  const capturePromise = Promise.resolve().then(async () => {
@@ -3334,8 +2273,8 @@ class PostHogBackendClient extends PostHogCoreStateless {
3334
2273
  // No matter what - capture the event
3335
2274
  _capture({
3336
2275
  ...additionalProperties,
3337
- ...properties,
3338
- $groups: groups
2276
+ ...(eventMessage.properties || {}),
2277
+ $groups: eventMessage.groups || groups
3339
2278
  });
3340
2279
  });
3341
2280
  await capturePromise;
@@ -3487,19 +2426,17 @@ class PostHogBackendClient extends PostHogCoreStateless {
3487
2426
  let response = undefined;
3488
2427
  const localEvaluationEnabled = this.featureFlagsPoller !== undefined;
3489
2428
  if (localEvaluationEnabled) {
3490
- // Try to get match value locally if not provided
3491
- if (!matchValue) {
3492
- matchValue = await this.getFeatureFlag(key, distinctId, {
3493
- ...options,
3494
- onlyEvaluateLocally: true,
3495
- sendFeatureFlagEvents: false
3496
- });
3497
- }
3498
- if (matchValue) {
3499
- response = await this.featureFlagsPoller?.computeFeatureFlagPayloadLocally(key, matchValue);
2429
+ // Ensure flags are loaded before checking for the specific flag
2430
+ await this.featureFlagsPoller?.loadFeatureFlags();
2431
+ const flag = this.featureFlagsPoller?.featureFlagsByKey[key];
2432
+ if (flag) {
2433
+ const result = await this.featureFlagsPoller?.computeFlagAndPayloadLocally(flag, distinctId, groups, personProperties, groupProperties, matchValue);
2434
+ if (result) {
2435
+ matchValue = result.value;
2436
+ response = result.payload;
2437
+ }
3500
2438
  }
3501
2439
  }
3502
- //}
3503
2440
  // set defaults
3504
2441
  if (onlyEvaluateLocally == undefined) {
3505
2442
  onlyEvaluateLocally = false;
@@ -3507,10 +2444,6 @@ class PostHogBackendClient extends PostHogCoreStateless {
3507
2444
  if (sendFeatureFlagEvents == undefined) {
3508
2445
  sendFeatureFlagEvents = true;
3509
2446
  }
3510
- // set defaults
3511
- if (onlyEvaluateLocally == undefined) {
3512
- onlyEvaluateLocally = false;
3513
- }
3514
2447
  const payloadWasLocallyEvaluated = response !== undefined;
3515
2448
  if (!payloadWasLocallyEvaluated && !onlyEvaluateLocally) {
3516
2449
  response = await super.getFeatureFlagPayloadStateless(key, distinctId, groups, personProperties, groupProperties, disableGeoip);
@@ -3605,9 +2538,9 @@ class PostHogBackendClient extends PostHogCoreStateless {
3605
2538
  disableGeoip
3606
2539
  }, distinctId);
3607
2540
  }
3608
- /**
3609
- * Reloads the feature flag definitions from the server for local evaluation.
3610
- * This is useful to call if you want to ensure that the feature flags are up to date before calling getFeatureFlag.
2541
+ /**
2542
+ * Reloads the feature flag definitions from the server for local evaluation.
2543
+ * This is useful to call if you want to ensure that the feature flags are up to date before calling getFeatureFlag.
3611
2544
  */
3612
2545
  async reloadFeatureFlags() {
3613
2546
  await this.featureFlagsPoller?.loadFeatureFlags(true);
@@ -3620,7 +2553,7 @@ class PostHogBackendClient extends PostHogCoreStateless {
3620
2553
  if (!this.options.personalApiKey) {
3621
2554
  return undefined;
3622
2555
  }
3623
- const url = `${this.host}/api/projects/@current/feature_flags/${flagKey}/remote_config/`;
2556
+ const url = `${this.host}/api/projects/@current/feature_flags/${flagKey}/remote_config?token=${encodeURIComponent(this.apiKey)}`;
3624
2557
  const options = {
3625
2558
  method: 'GET',
3626
2559
  headers: {
@@ -3754,6 +2687,26 @@ class PostHogBackendClient extends PostHogCoreStateless {
3754
2687
  }, distinctId, additionalProperties);
3755
2688
  return await this.captureImmediate(evtMsg);
3756
2689
  }
2690
+ _runBeforeSend(eventMessage) {
2691
+ const beforeSend = this.options.before_send;
2692
+ if (!beforeSend) {
2693
+ return eventMessage;
2694
+ }
2695
+ const fns = Array.isArray(beforeSend) ? beforeSend : [beforeSend];
2696
+ let result = eventMessage;
2697
+ for (const fn of fns) {
2698
+ result = fn(result);
2699
+ if (!result) {
2700
+ this.logMsgIfDebug(() => console.info(`Event '${eventMessage.event}' was rejected in beforeSend function`));
2701
+ return null;
2702
+ }
2703
+ if (!result.properties || Object.keys(result.properties).length === 0) {
2704
+ const message = `Event '${result.event}' has no properties after beforeSend function, this is likely an error.`;
2705
+ this.logMsgIfDebug(() => console.warn(message));
2706
+ }
2707
+ }
2708
+ return result;
2709
+ }
3757
2710
  }
3758
2711
 
3759
2712
  // Portions of this file are derived from getsentry/sentry-javascript by Software, Inc. dba Sentry
@@ -3853,8 +2806,8 @@ function node(getModule) {
3853
2806
  return undefined;
3854
2807
  };
3855
2808
  }
3856
- /**
3857
- * Does this filename look like it's part of the app code?
2809
+ /**
2810
+ * Does this filename look like it's part of the app code?
3858
2811
  */
3859
2812
  function filenameIsInApp(filename, isNative = false) {
3860
2813
  const isInternal = isNative || filename &&