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,25 +1,27 @@
1
- /**
2
- * @file Adapted from [posthog-js](https://github.com/PostHog/posthog-js/blob/8157df935a4d0e71d2fefef7127aa85ee51c82d1/src/extensions/sentry-integration.ts) with modifications for the Node SDK.
1
+ import { PostHogCoreStateless, getFeatureFlagValue } from '@posthog/core';
2
+
3
+ /**
4
+ * @file Adapted from [posthog-js](https://github.com/PostHog/posthog-js/blob/8157df935a4d0e71d2fefef7127aa85ee51c82d1/src/extensions/sentry-integration.ts) with modifications for the Node SDK.
3
5
  */
4
- /**
5
- * Integrate Sentry with PostHog. This will add a direct link to the person in Sentry, and an $exception event in PostHog.
6
- *
7
- * ### Usage
8
- *
9
- * Sentry.init({
10
- * dsn: 'https://example',
11
- * integrations: [
12
- * new PostHogSentryIntegration(posthog)
13
- * ]
14
- * })
15
- *
16
- * Sentry.setTag(PostHogSentryIntegration.POSTHOG_ID_TAG, 'some distinct id');
17
- *
18
- * @param {Object} [posthog] The posthog object
19
- * @param {string} [organization] Optional: The Sentry organization, used to send a direct link from PostHog to Sentry
20
- * @param {Number} [projectId] Optional: The Sentry project id, used to send a direct link from PostHog to Sentry
21
- * @param {string} [prefix] Optional: Url of a self-hosted sentry instance (default: https://sentry.io/organizations/)
22
- * @param {SeverityLevel[] | '*'} [severityAllowList] Optional: send events matching the provided levels. Use '*' to send all events (default: ['error'])
6
+ /**
7
+ * Integrate Sentry with PostHog. This will add a direct link to the person in Sentry, and an $exception event in PostHog.
8
+ *
9
+ * ### Usage
10
+ *
11
+ * Sentry.init({
12
+ * dsn: 'https://example',
13
+ * integrations: [
14
+ * new PostHogSentryIntegration(posthog)
15
+ * ]
16
+ * })
17
+ *
18
+ * Sentry.setTag(PostHogSentryIntegration.POSTHOG_ID_TAG, 'some distinct id');
19
+ *
20
+ * @param {Object} [posthog] The posthog object
21
+ * @param {string} [organization] Optional: The Sentry organization, used to send a direct link from PostHog to Sentry
22
+ * @param {Number} [projectId] Optional: The Sentry project id, used to send a direct link from PostHog to Sentry
23
+ * @param {string} [prefix] Optional: Url of a self-hosted sentry instance (default: https://sentry.io/organizations/)
24
+ * @param {SeverityLevel[] | '*'} [severityAllowList] Optional: send events matching the provided levels. Use '*' to send all events (default: ['error'])
23
25
  */
24
26
  const NAME = 'posthog-node';
25
27
  function createEventProcessor(_posthog, {
@@ -113,411 +115,175 @@ class PostHogSentryIntegration {
113
115
  }
114
116
  PostHogSentryIntegration.POSTHOG_ID_TAG = 'posthog_distinct_id';
115
117
 
116
- // vendor from: https://github.com/LiosK/uuidv7/blob/f30b7a7faff73afbce0b27a46c638310f96912ba/src/index.ts
117
- // https://github.com/LiosK/uuidv7#license
118
- /**
119
- * uuidv7: An experimental implementation of the proposed UUID Version 7
120
- *
121
- * @license Apache-2.0
122
- * @copyright 2021-2023 LiosK
123
- * @packageDocumentation
124
- */
125
- const DIGITS = "0123456789abcdef";
126
- /** Represents a UUID as a 16-byte byte array. */
127
- class UUID {
128
- /** @param bytes - The 16-byte byte array representation. */
129
- constructor(bytes) {
130
- this.bytes = bytes;
131
- }
132
- /**
133
- * Creates an object from the internal representation, a 16-byte byte array
134
- * containing the binary UUID representation in the big-endian byte order.
135
- *
136
- * This method does NOT shallow-copy the argument, and thus the created object
137
- * holds the reference to the underlying buffer.
138
- *
139
- * @throws TypeError if the length of the argument is not 16.
140
- */
141
- static ofInner(bytes) {
142
- if (bytes.length !== 16) {
143
- throw new TypeError("not 128-bit length");
144
- }
145
- else {
146
- return new UUID(bytes);
147
- }
148
- }
149
- /**
150
- * Builds a byte array from UUIDv7 field values.
151
- *
152
- * @param unixTsMs - A 48-bit `unix_ts_ms` field value.
153
- * @param randA - A 12-bit `rand_a` field value.
154
- * @param randBHi - The higher 30 bits of 62-bit `rand_b` field value.
155
- * @param randBLo - The lower 32 bits of 62-bit `rand_b` field value.
156
- * @throws RangeError if any field value is out of the specified range.
157
- */
158
- static fromFieldsV7(unixTsMs, randA, randBHi, randBLo) {
159
- if (!Number.isInteger(unixTsMs) ||
160
- !Number.isInteger(randA) ||
161
- !Number.isInteger(randBHi) ||
162
- !Number.isInteger(randBLo) ||
163
- unixTsMs < 0 ||
164
- randA < 0 ||
165
- randBHi < 0 ||
166
- randBLo < 0 ||
167
- unixTsMs > 281474976710655 ||
168
- randA > 0xfff ||
169
- randBHi > 1073741823 ||
170
- randBLo > 4294967295) {
171
- throw new RangeError("invalid field value");
172
- }
173
- const bytes = new Uint8Array(16);
174
- bytes[0] = unixTsMs / 2 ** 40;
175
- bytes[1] = unixTsMs / 2 ** 32;
176
- bytes[2] = unixTsMs / 2 ** 24;
177
- bytes[3] = unixTsMs / 2 ** 16;
178
- bytes[4] = unixTsMs / 2 ** 8;
179
- bytes[5] = unixTsMs;
180
- bytes[6] = 0x70 | (randA >>> 8);
181
- bytes[7] = randA;
182
- bytes[8] = 0x80 | (randBHi >>> 24);
183
- bytes[9] = randBHi >>> 16;
184
- bytes[10] = randBHi >>> 8;
185
- bytes[11] = randBHi;
186
- bytes[12] = randBLo >>> 24;
187
- bytes[13] = randBLo >>> 16;
188
- bytes[14] = randBLo >>> 8;
189
- bytes[15] = randBLo;
190
- return new UUID(bytes);
191
- }
192
- /**
193
- * Builds a byte array from a string representation.
194
- *
195
- * This method accepts the following formats:
196
- *
197
- * - 32-digit hexadecimal format without hyphens: `0189dcd553117d408db09496a2eef37b`
198
- * - 8-4-4-4-12 hyphenated format: `0189dcd5-5311-7d40-8db0-9496a2eef37b`
199
- * - Hyphenated format with surrounding braces: `{0189dcd5-5311-7d40-8db0-9496a2eef37b}`
200
- * - RFC 4122 URN format: `urn:uuid:0189dcd5-5311-7d40-8db0-9496a2eef37b`
201
- *
202
- * Leading and trailing whitespaces represents an error.
203
- *
204
- * @throws SyntaxError if the argument could not parse as a valid UUID string.
205
- */
206
- static parse(uuid) {
207
- let hex = undefined;
208
- switch (uuid.length) {
209
- case 32:
210
- hex = /^[0-9a-f]{32}$/i.exec(uuid)?.[0];
211
- break;
212
- case 36:
213
- hex =
214
- /^([0-9a-f]{8})-([0-9a-f]{4})-([0-9a-f]{4})-([0-9a-f]{4})-([0-9a-f]{12})$/i
215
- .exec(uuid)
216
- ?.slice(1, 6)
217
- .join("");
218
- break;
219
- case 38:
220
- hex =
221
- /^\{([0-9a-f]{8})-([0-9a-f]{4})-([0-9a-f]{4})-([0-9a-f]{4})-([0-9a-f]{12})\}$/i
222
- .exec(uuid)
223
- ?.slice(1, 6)
224
- .join("");
225
- break;
226
- case 45:
227
- hex =
228
- /^urn:uuid:([0-9a-f]{8})-([0-9a-f]{4})-([0-9a-f]{4})-([0-9a-f]{4})-([0-9a-f]{12})$/i
229
- .exec(uuid)
230
- ?.slice(1, 6)
231
- .join("");
232
- break;
233
- }
234
- if (hex) {
235
- const inner = new Uint8Array(16);
236
- for (let i = 0; i < 16; i += 4) {
237
- const n = parseInt(hex.substring(2 * i, 2 * i + 8), 16);
238
- inner[i + 0] = n >>> 24;
239
- inner[i + 1] = n >>> 16;
240
- inner[i + 2] = n >>> 8;
241
- inner[i + 3] = n;
242
- }
243
- return new UUID(inner);
244
- }
245
- else {
246
- throw new SyntaxError("could not parse UUID string");
247
- }
248
- }
249
- /**
250
- * @returns The 8-4-4-4-12 canonical hexadecimal string representation
251
- * (`0189dcd5-5311-7d40-8db0-9496a2eef37b`).
252
- */
253
- toString() {
254
- let text = "";
255
- for (let i = 0; i < this.bytes.length; i++) {
256
- text += DIGITS.charAt(this.bytes[i] >>> 4);
257
- text += DIGITS.charAt(this.bytes[i] & 0xf);
258
- if (i === 3 || i === 5 || i === 7 || i === 9) {
259
- text += "-";
260
- }
261
- }
262
- return text;
263
- }
264
- /**
265
- * @returns The 32-digit hexadecimal representation without hyphens
266
- * (`0189dcd553117d408db09496a2eef37b`).
267
- */
268
- toHex() {
269
- let text = "";
270
- for (let i = 0; i < this.bytes.length; i++) {
271
- text += DIGITS.charAt(this.bytes[i] >>> 4);
272
- text += DIGITS.charAt(this.bytes[i] & 0xf);
273
- }
274
- return text;
275
- }
276
- /** @returns The 8-4-4-4-12 canonical hexadecimal string representation. */
277
- toJSON() {
278
- return this.toString();
279
- }
280
- /**
281
- * Reports the variant field value of the UUID or, if appropriate, "NIL" or
282
- * "MAX".
283
- *
284
- * For convenience, this method reports "NIL" or "MAX" if `this` represents
285
- * the Nil or Max UUID, although the Nil and Max UUIDs are technically
286
- * subsumed under the variants `0b0` and `0b111`, respectively.
287
- */
288
- getVariant() {
289
- const n = this.bytes[8] >>> 4;
290
- if (n < 0) {
291
- throw new Error("unreachable");
292
- }
293
- else if (n <= 0b0111) {
294
- return this.bytes.every((e) => e === 0) ? "NIL" : "VAR_0";
295
- }
296
- else if (n <= 0b1011) {
297
- return "VAR_10";
298
- }
299
- else if (n <= 0b1101) {
300
- return "VAR_110";
301
- }
302
- else if (n <= 0b1111) {
303
- return this.bytes.every((e) => e === 0xff) ? "MAX" : "VAR_RESERVED";
304
- }
305
- else {
306
- throw new Error("unreachable");
307
- }
308
- }
309
- /**
310
- * Returns the version field value of the UUID or `undefined` if the UUID does
311
- * not have the variant field value of `0b10`.
312
- */
313
- getVersion() {
314
- return this.getVariant() === "VAR_10" ? this.bytes[6] >>> 4 : undefined;
315
- }
316
- /** Creates an object from `this`. */
317
- clone() {
318
- return new UUID(this.bytes.slice(0));
319
- }
320
- /** Returns true if `this` is equivalent to `other`. */
321
- equals(other) {
322
- return this.compareTo(other) === 0;
323
- }
324
- /**
325
- * Returns a negative integer, zero, or positive integer if `this` is less
326
- * than, equal to, or greater than `other`, respectively.
327
- */
328
- compareTo(other) {
329
- for (let i = 0; i < 16; i++) {
330
- const diff = this.bytes[i] - other.bytes[i];
331
- if (diff !== 0) {
332
- return Math.sign(diff);
333
- }
334
- }
335
- return 0;
336
- }
337
- }
338
- /**
339
- * Encapsulates the monotonic counter state.
340
- *
341
- * This class provides APIs to utilize a separate counter state from that of the
342
- * global generator used by {@link uuidv7} and {@link uuidv7obj}. In addition to
343
- * the default {@link generate} method, this class has {@link generateOrAbort}
344
- * that is useful to absolutely guarantee the monotonically increasing order of
345
- * generated UUIDs. See their respective documentation for details.
346
- */
347
- class V7Generator {
348
- /**
349
- * Creates a generator object with the default random number generator, or
350
- * with the specified one if passed as an argument. The specified random
351
- * number generator should be cryptographically strong and securely seeded.
352
- */
353
- constructor(randomNumberGenerator) {
354
- this.timestamp = 0;
355
- this.counter = 0;
356
- this.random = randomNumberGenerator ?? getDefaultRandom();
357
- }
358
- /**
359
- * Generates a new UUIDv7 object from the current timestamp, or resets the
360
- * generator upon significant timestamp rollback.
361
- *
362
- * This method returns a monotonically increasing UUID by reusing the previous
363
- * timestamp even if the up-to-date timestamp is smaller than the immediately
364
- * preceding UUID's. However, when such a clock rollback is considered
365
- * significant (i.e., by more than ten seconds), this method resets the
366
- * generator and returns a new UUID based on the given timestamp, breaking the
367
- * increasing order of UUIDs.
368
- *
369
- * See {@link generateOrAbort} for the other mode of generation and
370
- * {@link generateOrResetCore} for the low-level primitive.
371
- */
372
- generate() {
373
- return this.generateOrResetCore(Date.now(), 10000);
374
- }
375
- /**
376
- * Generates a new UUIDv7 object from the current timestamp, or returns
377
- * `undefined` upon significant timestamp rollback.
378
- *
379
- * This method returns a monotonically increasing UUID by reusing the previous
380
- * timestamp even if the up-to-date timestamp is smaller than the immediately
381
- * preceding UUID's. However, when such a clock rollback is considered
382
- * significant (i.e., by more than ten seconds), this method aborts and
383
- * returns `undefined` immediately.
384
- *
385
- * See {@link generate} for the other mode of generation and
386
- * {@link generateOrAbortCore} for the low-level primitive.
387
- */
388
- generateOrAbort() {
389
- return this.generateOrAbortCore(Date.now(), 10000);
390
- }
391
- /**
392
- * Generates a new UUIDv7 object from the `unixTsMs` passed, or resets the
393
- * generator upon significant timestamp rollback.
394
- *
395
- * This method is equivalent to {@link generate} except that it takes a custom
396
- * timestamp and clock rollback allowance.
397
- *
398
- * @param rollbackAllowance - The amount of `unixTsMs` rollback that is
399
- * considered significant. A suggested value is `10_000` (milliseconds).
400
- * @throws RangeError if `unixTsMs` is not a 48-bit positive integer.
401
- */
402
- generateOrResetCore(unixTsMs, rollbackAllowance) {
403
- let value = this.generateOrAbortCore(unixTsMs, rollbackAllowance);
404
- if (value === undefined) {
405
- // reset state and resume
406
- this.timestamp = 0;
407
- value = this.generateOrAbortCore(unixTsMs, rollbackAllowance);
408
- }
409
- return value;
410
- }
411
- /**
412
- * Generates a new UUIDv7 object from the `unixTsMs` passed, or returns
413
- * `undefined` upon significant timestamp rollback.
414
- *
415
- * This method is equivalent to {@link generateOrAbort} except that it takes a
416
- * custom timestamp and clock rollback allowance.
417
- *
418
- * @param rollbackAllowance - The amount of `unixTsMs` rollback that is
419
- * considered significant. A suggested value is `10_000` (milliseconds).
420
- * @throws RangeError if `unixTsMs` is not a 48-bit positive integer.
421
- */
422
- generateOrAbortCore(unixTsMs, rollbackAllowance) {
423
- const MAX_COUNTER = 4398046511103;
424
- if (!Number.isInteger(unixTsMs) ||
425
- unixTsMs < 1 ||
426
- unixTsMs > 281474976710655) {
427
- throw new RangeError("`unixTsMs` must be a 48-bit positive integer");
428
- }
429
- else if (rollbackAllowance < 0 || rollbackAllowance > 281474976710655) {
430
- throw new RangeError("`rollbackAllowance` out of reasonable range");
431
- }
432
- if (unixTsMs > this.timestamp) {
433
- this.timestamp = unixTsMs;
434
- this.resetCounter();
435
- }
436
- else if (unixTsMs + rollbackAllowance >= this.timestamp) {
437
- // go on with previous timestamp if new one is not much smaller
438
- this.counter++;
439
- if (this.counter > MAX_COUNTER) {
440
- // increment timestamp at counter overflow
441
- this.timestamp++;
442
- this.resetCounter();
443
- }
444
- }
445
- else {
446
- // abort if clock went backwards to unbearable extent
447
- return undefined;
448
- }
449
- return UUID.fromFieldsV7(this.timestamp, Math.trunc(this.counter / 2 ** 30), this.counter & (2 ** 30 - 1), this.random.nextUint32());
450
- }
451
- /** Initializes the counter at a 42-bit random integer. */
452
- resetCounter() {
453
- this.counter =
454
- this.random.nextUint32() * 0x400 + (this.random.nextUint32() & 0x3ff);
455
- }
456
- /**
457
- * Generates a new UUIDv4 object utilizing the random number generator inside.
458
- *
459
- * @internal
460
- */
461
- generateV4() {
462
- const bytes = new Uint8Array(Uint32Array.of(this.random.nextUint32(), this.random.nextUint32(), this.random.nextUint32(), this.random.nextUint32()).buffer);
463
- bytes[6] = 0x40 | (bytes[6] >>> 4);
464
- bytes[8] = 0x80 | (bytes[8] >>> 2);
465
- return UUID.ofInner(bytes);
466
- }
467
- }
468
- /** A global flag to force use of cryptographically strong RNG. */
469
- // declare const UUIDV7_DENY_WEAK_RNG: boolean;
470
- /** Returns the default random number generator available in the environment. */
471
- const getDefaultRandom = () => {
472
- // fix: crypto isn't available in react-native, always use Math.random
473
- // // detect Web Crypto API
474
- // if (
475
- // typeof crypto !== "undefined" &&
476
- // typeof crypto.getRandomValues !== "undefined"
477
- // ) {
478
- // return new BufferedCryptoRandom();
479
- // } else {
480
- // // fall back on Math.random() unless the flag is set to true
481
- // if (typeof UUIDV7_DENY_WEAK_RNG !== "undefined" && UUIDV7_DENY_WEAK_RNG) {
482
- // throw new Error("no cryptographically strong RNG available");
483
- // }
484
- // return {
485
- // nextUint32: (): number =>
486
- // Math.trunc(Math.random() * 0x1_0000) * 0x1_0000 +
487
- // Math.trunc(Math.random() * 0x1_0000),
488
- // };
489
- // }
490
- return {
491
- nextUint32: () => Math.trunc(Math.random() * 65536) * 65536 +
492
- Math.trunc(Math.random() * 65536),
493
- };
494
- };
495
- // /**
496
- // * Wraps `crypto.getRandomValues()` to enable buffering; this uses a small
497
- // * buffer by default to avoid both unbearable throughput decline in some
498
- // * environments and the waste of time and space for unused values.
499
- // */
500
- // class BufferedCryptoRandom {
501
- // private readonly buffer = new Uint32Array(8);
502
- // private cursor = 0xffff;
503
- // nextUint32(): number {
504
- // if (this.cursor >= this.buffer.length) {
505
- // crypto.getRandomValues(this.buffer);
506
- // this.cursor = 0;
507
- // }
508
- // return this.buffer[this.cursor++];
509
- // }
510
- // }
511
- let defaultGenerator;
512
- /**
513
- * Generates a UUIDv7 string.
514
- *
515
- * @returns The 8-4-4-4-12 canonical hexadecimal string representation
516
- * ("xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx").
517
- */
518
- const uuidv7 = () => uuidv7obj().toString();
519
- /** Generates a UUIDv7 object. */
520
- const uuidv7obj = () => (defaultGenerator || (defaultGenerator = new V7Generator())).generate();
118
+ /*! For license information please see uuidv7.mjs.LICENSE.txt */
119
+ /**
120
+ * uuidv7: An experimental implementation of the proposed UUID Version 7
121
+ *
122
+ * @license Apache-2.0
123
+ * @copyright 2021-2023 LiosK
124
+ * @packageDocumentation
125
+ */ const DIGITS = "0123456789abcdef";
126
+ class UUID {
127
+ static ofInner(bytes) {
128
+ if (16 === bytes.length) return new UUID(bytes);
129
+ throw new TypeError("not 128-bit length");
130
+ }
131
+ static fromFieldsV7(unixTsMs, randA, randBHi, randBLo) {
132
+ 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");
133
+ const bytes = new Uint8Array(16);
134
+ bytes[0] = unixTsMs / 2 ** 40;
135
+ bytes[1] = unixTsMs / 2 ** 32;
136
+ bytes[2] = unixTsMs / 2 ** 24;
137
+ bytes[3] = unixTsMs / 2 ** 16;
138
+ bytes[4] = unixTsMs / 256;
139
+ bytes[5] = unixTsMs;
140
+ bytes[6] = 0x70 | randA >>> 8;
141
+ bytes[7] = randA;
142
+ bytes[8] = 0x80 | randBHi >>> 24;
143
+ bytes[9] = randBHi >>> 16;
144
+ bytes[10] = randBHi >>> 8;
145
+ bytes[11] = randBHi;
146
+ bytes[12] = randBLo >>> 24;
147
+ bytes[13] = randBLo >>> 16;
148
+ bytes[14] = randBLo >>> 8;
149
+ bytes[15] = randBLo;
150
+ return new UUID(bytes);
151
+ }
152
+ static parse(uuid) {
153
+ let hex;
154
+ switch(uuid.length){
155
+ case 32:
156
+ var _exec;
157
+ hex = null == (_exec = /^[0-9a-f]{32}$/i.exec(uuid)) ? void 0 : _exec[0];
158
+ break;
159
+ case 36:
160
+ var _exec1;
161
+ 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("");
162
+ break;
163
+ case 38:
164
+ var _exec2;
165
+ 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("");
166
+ break;
167
+ case 45:
168
+ var _exec3;
169
+ 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("");
170
+ break;
171
+ }
172
+ if (hex) {
173
+ const inner = new Uint8Array(16);
174
+ for(let i = 0; i < 16; i += 4){
175
+ const n = parseInt(hex.substring(2 * i, 2 * i + 8), 16);
176
+ inner[i + 0] = n >>> 24;
177
+ inner[i + 1] = n >>> 16;
178
+ inner[i + 2] = n >>> 8;
179
+ inner[i + 3] = n;
180
+ }
181
+ return new UUID(inner);
182
+ }
183
+ throw new SyntaxError("could not parse UUID string");
184
+ }
185
+ toString() {
186
+ let text = "";
187
+ for(let i = 0; i < this.bytes.length; i++){
188
+ text += DIGITS.charAt(this.bytes[i] >>> 4);
189
+ text += DIGITS.charAt(0xf & this.bytes[i]);
190
+ if (3 === i || 5 === i || 7 === i || 9 === i) text += "-";
191
+ }
192
+ return text;
193
+ }
194
+ toHex() {
195
+ let text = "";
196
+ for(let i = 0; i < this.bytes.length; i++){
197
+ text += DIGITS.charAt(this.bytes[i] >>> 4);
198
+ text += DIGITS.charAt(0xf & this.bytes[i]);
199
+ }
200
+ return text;
201
+ }
202
+ toJSON() {
203
+ return this.toString();
204
+ }
205
+ getVariant() {
206
+ const n = this.bytes[8] >>> 4;
207
+ if (n < 0) throw new Error("unreachable");
208
+ if (n <= 7) return this.bytes.every((e)=>0 === e) ? "NIL" : "VAR_0";
209
+ if (n <= 11) return "VAR_10";
210
+ if (n <= 13) return "VAR_110";
211
+ if (n <= 15) return this.bytes.every((e)=>0xff === e) ? "MAX" : "VAR_RESERVED";
212
+ else throw new Error("unreachable");
213
+ }
214
+ getVersion() {
215
+ return "VAR_10" === this.getVariant() ? this.bytes[6] >>> 4 : void 0;
216
+ }
217
+ clone() {
218
+ return new UUID(this.bytes.slice(0));
219
+ }
220
+ equals(other) {
221
+ return 0 === this.compareTo(other);
222
+ }
223
+ compareTo(other) {
224
+ for(let i = 0; i < 16; i++){
225
+ const diff = this.bytes[i] - other.bytes[i];
226
+ if (0 !== diff) return Math.sign(diff);
227
+ }
228
+ return 0;
229
+ }
230
+ constructor(bytes){
231
+ this.bytes = bytes;
232
+ }
233
+ }
234
+ class V7Generator {
235
+ generate() {
236
+ return this.generateOrResetCore(Date.now(), 10000);
237
+ }
238
+ generateOrAbort() {
239
+ return this.generateOrAbortCore(Date.now(), 10000);
240
+ }
241
+ generateOrResetCore(unixTsMs, rollbackAllowance) {
242
+ let value = this.generateOrAbortCore(unixTsMs, rollbackAllowance);
243
+ if (void 0 === value) {
244
+ this.timestamp = 0;
245
+ value = this.generateOrAbortCore(unixTsMs, rollbackAllowance);
246
+ }
247
+ return value;
248
+ }
249
+ generateOrAbortCore(unixTsMs, rollbackAllowance) {
250
+ const MAX_COUNTER = 0x3ffffffffff;
251
+ if (!Number.isInteger(unixTsMs) || unixTsMs < 1 || unixTsMs > 0xffffffffffff) throw new RangeError("`unixTsMs` must be a 48-bit positive integer");
252
+ if (rollbackAllowance < 0 || rollbackAllowance > 0xffffffffffff) throw new RangeError("`rollbackAllowance` out of reasonable range");
253
+ if (unixTsMs > this.timestamp) {
254
+ this.timestamp = unixTsMs;
255
+ this.resetCounter();
256
+ } else {
257
+ if (!(unixTsMs + rollbackAllowance >= this.timestamp)) return;
258
+ this.counter++;
259
+ if (this.counter > MAX_COUNTER) {
260
+ this.timestamp++;
261
+ this.resetCounter();
262
+ }
263
+ }
264
+ return UUID.fromFieldsV7(this.timestamp, Math.trunc(this.counter / 2 ** 30), this.counter & 2 ** 30 - 1, this.random.nextUint32());
265
+ }
266
+ resetCounter() {
267
+ this.counter = 0x400 * this.random.nextUint32() + (0x3ff & this.random.nextUint32());
268
+ }
269
+ generateV4() {
270
+ const bytes = new Uint8Array(Uint32Array.of(this.random.nextUint32(), this.random.nextUint32(), this.random.nextUint32(), this.random.nextUint32()).buffer);
271
+ bytes[6] = 0x40 | bytes[6] >>> 4;
272
+ bytes[8] = 0x80 | bytes[8] >>> 2;
273
+ return UUID.ofInner(bytes);
274
+ }
275
+ constructor(randomNumberGenerator){
276
+ this.timestamp = 0;
277
+ this.counter = 0;
278
+ this.random = null != randomNumberGenerator ? randomNumberGenerator : getDefaultRandom();
279
+ }
280
+ }
281
+ const getDefaultRandom = ()=>({
282
+ nextUint32: ()=>0x10000 * Math.trunc(0x10000 * Math.random()) + Math.trunc(0x10000 * Math.random())
283
+ });
284
+ let defaultGenerator;
285
+ const uuidv7 = ()=>uuidv7obj().toString();
286
+ const uuidv7obj = ()=>(defaultGenerator || (defaultGenerator = new V7Generator())).generate();
521
287
 
522
288
  // Portions of this file are derived from getsentry/sentry-javascript by Software, Inc. dba Sentry
523
289
  // Licensed under the MIT License
@@ -642,6 +408,7 @@ function isBuiltin(candidate, className) {
642
408
  }
643
409
 
644
410
  // Portions of this file are derived from getsentry/sentry-javascript by Software, Inc. dba Sentry
411
+ // Licensed under the MIT License
645
412
  async function propertiesFromUnknownInput(stackParser, frameModifiers, input, hint) {
646
413
  const providedMechanism = hint && hint.mechanism;
647
414
  const mechanism = providedMechanism || {
@@ -730,10 +497,10 @@ function getObjectClassName(obj) {
730
497
  // ignore errors here
731
498
  }
732
499
  }
733
- /**
734
- * Given any captured exception, extract its keys and create a sorted
735
- * and truncated list that will be used inside the event message.
736
- * eg. `Non-error exception captured with keys: foo, bar, baz`
500
+ /**
501
+ * Given any captured exception, extract its keys and create a sorted
502
+ * and truncated list that will be used inside the event message.
503
+ * eg. `Non-error exception captured with keys: foo, bar, baz`
737
504
  */
738
505
  function extractExceptionKeysForMessage(exception, maxLength = 40) {
739
506
  const keys = Object.keys(convertToPlainObject(exception));
@@ -763,13 +530,13 @@ function truncate(str, max = 0) {
763
530
  }
764
531
  return str.length <= max ? str : `${str.slice(0, max)}...`;
765
532
  }
766
- /**
767
- * Transforms any `Error` or `Event` into a plain object with all of their enumerable properties, and some of their
768
- * non-enumerable properties attached.
769
- *
770
- * @param value Initial source that we have to transform in order for it to be usable by the serializer
771
- * @returns An Event or Error turned into an object - or the value argument itself, when value is neither an Event nor
772
- * an Error.
533
+ /**
534
+ * Transforms any `Error` or `Event` into a plain object with all of their enumerable properties, and some of their
535
+ * non-enumerable properties attached.
536
+ *
537
+ * @param value Initial source that we have to transform in order for it to be usable by the serializer
538
+ * @returns An Event or Error turned into an object - or the value argument itself, when value is neither an Event nor
539
+ * an Error.
773
540
  */
774
541
  function convertToPlainObject(value) {
775
542
  if (isError(value)) {
@@ -817,8 +584,8 @@ function serializeEventTarget(target) {
817
584
  return '<unknown>';
818
585
  }
819
586
  }
820
- /**
821
- * Extracts stack frames from the error and builds an Exception
587
+ /**
588
+ * Extracts stack frames from the error and builds an Exception
822
589
  */
823
590
  async function exceptionFromError(stackParser, frameModifiers, error) {
824
591
  const exception = {
@@ -837,8 +604,8 @@ async function exceptionFromError(stackParser, frameModifiers, error) {
837
604
  }
838
605
  return exception;
839
606
  }
840
- /**
841
- * Extracts stack frames from the error.stack string
607
+ /**
608
+ * Extracts stack frames from the error.stack string
842
609
  */
843
610
  function parseStackFrames(stackParser, error) {
844
611
  return applyChunkIds(stackParser(error.stack || '', 1), stackParser);
@@ -853,6 +620,71 @@ function applyChunkIds(frames, parser) {
853
620
  return frames;
854
621
  }
855
622
 
623
+ const ObjProto = Object.prototype;
624
+ const type_utils_toString = ObjProto.toString;
625
+ const isNumber = (x)=>'[object Number]' == type_utils_toString.call(x);
626
+
627
+ function clampToRange(value, min, max, logger, fallbackValue) {
628
+ if (min > max) {
629
+ logger.warn('min cannot be greater than max.');
630
+ min = max;
631
+ }
632
+ if (isNumber(value)) if (value > max) {
633
+ logger.warn(' cannot be greater than max: ' + max + '. Using max value instead.');
634
+ return max;
635
+ } else {
636
+ if (!(value < min)) return value;
637
+ logger.warn(' cannot be less than min: ' + min + '. Using min value instead.');
638
+ return min;
639
+ }
640
+ logger.warn(' must be a number. using max or fallback. max: ' + max + ', fallback: ' + fallbackValue);
641
+ return clampToRange(max, min, max, logger);
642
+ }
643
+
644
+ class BucketedRateLimiter {
645
+ constructor(_options){
646
+ this._options = _options;
647
+ this._buckets = {};
648
+ this._refillBuckets = ()=>{
649
+ Object.keys(this._buckets).forEach((key)=>{
650
+ const newTokens = this._getBucket(key) + this._refillRate;
651
+ if (newTokens >= this._bucketSize) delete this._buckets[key];
652
+ else this._setBucket(key, newTokens);
653
+ });
654
+ };
655
+ this._getBucket = (key)=>this._buckets[String(key)];
656
+ this._setBucket = (key, value)=>{
657
+ this._buckets[String(key)] = value;
658
+ };
659
+ this.consumeRateLimit = (key)=>{
660
+ var _this__getBucket;
661
+ let tokens = null != (_this__getBucket = this._getBucket(key)) ? _this__getBucket : this._bucketSize;
662
+ tokens = Math.max(tokens - 1, 0);
663
+ if (0 === tokens) return true;
664
+ this._setBucket(key, tokens);
665
+ const hasReachedZero = 0 === tokens;
666
+ if (hasReachedZero) {
667
+ var _this__onBucketRateLimited, _this;
668
+ null == (_this__onBucketRateLimited = (_this = this)._onBucketRateLimited) || _this__onBucketRateLimited.call(_this, key);
669
+ }
670
+ return hasReachedZero;
671
+ };
672
+ this._onBucketRateLimited = this._options._onBucketRateLimited;
673
+ this._bucketSize = clampToRange(this._options.bucketSize, 0, 100, this._options._logger);
674
+ this._refillRate = clampToRange(this._options.refillRate, 0, this._bucketSize, this._options._logger);
675
+ this._refillInterval = clampToRange(this._options.refillInterval, 0, 86400000, this._options._logger);
676
+ setInterval(()=>{
677
+ this._refillBuckets();
678
+ }, this._refillInterval);
679
+ }
680
+ }
681
+
682
+ function safeSetTimeout(fn, timeout) {
683
+ const t = setTimeout(fn, timeout);
684
+ (null == t ? void 0 : t.unref) && (null == t || t.unref());
685
+ return t;
686
+ }
687
+
856
688
  const SHUTDOWN_TIMEOUT = 2000;
857
689
  class ErrorTracking {
858
690
  static async buildEventMessage(error, hint, distinctId, additionalProperties) {
@@ -874,9 +706,20 @@ class ErrorTracking {
874
706
  }
875
707
  };
876
708
  }
877
- constructor(client, options) {
709
+ constructor(client, options, _logger) {
878
710
  this.client = client;
879
711
  this._exceptionAutocaptureEnabled = options.enableExceptionAutocapture || false;
712
+ this._logger = _logger;
713
+ // by default captures ten exceptions before rate limiting by exception type
714
+ // refills at a rate of one token / 10 second period
715
+ // e.g. will capture 1 exception rate limited exception every 10 seconds until burst ends
716
+ this._rateLimiter = new BucketedRateLimiter({
717
+ refillRate: 1,
718
+ bucketSize: 10,
719
+ refillInterval: 10000,
720
+ // ten seconds in milliseconds
721
+ _logger: this._logger
722
+ });
880
723
  this.startAutocaptureIfEnabled();
881
724
  }
882
725
  startAutocaptureIfEnabled() {
@@ -886,7 +729,16 @@ class ErrorTracking {
886
729
  }
887
730
  }
888
731
  onException(exception, hint) {
889
- void ErrorTracking.buildEventMessage(exception, hint).then(msg => {
732
+ return ErrorTracking.buildEventMessage(exception, hint).then(msg => {
733
+ const exceptionProperties = msg.properties;
734
+ const exceptionType = exceptionProperties?.$exception_list[0].type ?? 'Exception';
735
+ const isRateLimited = this._rateLimiter.consumeRateLimit(exceptionType);
736
+ if (isRateLimited) {
737
+ this._logger.info('Skipping exception capture because of client rate limiting.', {
738
+ exception: exceptionType
739
+ });
740
+ return;
741
+ }
890
742
  this.client.capture(msg);
891
743
  });
892
744
  }
@@ -915,1092 +767,18 @@ function setupExpressErrorHandler(_posthog, app) {
915
767
  });
916
768
  }
917
769
 
918
- var version = "5.6.0";
919
-
920
- var PostHogPersistedProperty;
921
- (function (PostHogPersistedProperty) {
922
- PostHogPersistedProperty["AnonymousId"] = "anonymous_id";
923
- PostHogPersistedProperty["DistinctId"] = "distinct_id";
924
- PostHogPersistedProperty["Props"] = "props";
925
- PostHogPersistedProperty["FeatureFlagDetails"] = "feature_flag_details";
926
- PostHogPersistedProperty["FeatureFlags"] = "feature_flags";
927
- PostHogPersistedProperty["FeatureFlagPayloads"] = "feature_flag_payloads";
928
- PostHogPersistedProperty["BootstrapFeatureFlagDetails"] = "bootstrap_feature_flag_details";
929
- PostHogPersistedProperty["BootstrapFeatureFlags"] = "bootstrap_feature_flags";
930
- PostHogPersistedProperty["BootstrapFeatureFlagPayloads"] = "bootstrap_feature_flag_payloads";
931
- PostHogPersistedProperty["OverrideFeatureFlags"] = "override_feature_flags";
932
- PostHogPersistedProperty["Queue"] = "queue";
933
- PostHogPersistedProperty["OptedOut"] = "opted_out";
934
- PostHogPersistedProperty["SessionId"] = "session_id";
935
- PostHogPersistedProperty["SessionStartTimestamp"] = "session_start_timestamp";
936
- PostHogPersistedProperty["SessionLastTimestamp"] = "session_timestamp";
937
- PostHogPersistedProperty["PersonProperties"] = "person_properties";
938
- PostHogPersistedProperty["GroupProperties"] = "group_properties";
939
- PostHogPersistedProperty["InstalledAppBuild"] = "installed_app_build";
940
- PostHogPersistedProperty["InstalledAppVersion"] = "installed_app_version";
941
- PostHogPersistedProperty["SessionReplay"] = "session_replay";
942
- PostHogPersistedProperty["SurveyLastSeenDate"] = "survey_last_seen_date";
943
- PostHogPersistedProperty["SurveysSeen"] = "surveys_seen";
944
- PostHogPersistedProperty["Surveys"] = "surveys";
945
- PostHogPersistedProperty["RemoteConfig"] = "remote_config";
946
- PostHogPersistedProperty["FlagsEndpointWasHit"] = "flags_endpoint_was_hit";
947
- })(PostHogPersistedProperty || (PostHogPersistedProperty = {}));
948
- // Any key prefixed with `attr__` can be added
949
- var Compression;
950
- (function (Compression) {
951
- Compression["GZipJS"] = "gzip-js";
952
- Compression["Base64"] = "base64";
953
- })(Compression || (Compression = {}));
954
- var SurveyPosition;
955
- (function (SurveyPosition) {
956
- SurveyPosition["TopLeft"] = "top_left";
957
- SurveyPosition["TopCenter"] = "top_center";
958
- SurveyPosition["TopRight"] = "top_right";
959
- SurveyPosition["MiddleLeft"] = "middle_left";
960
- SurveyPosition["MiddleCenter"] = "middle_center";
961
- SurveyPosition["MiddleRight"] = "middle_right";
962
- SurveyPosition["Left"] = "left";
963
- SurveyPosition["Right"] = "right";
964
- SurveyPosition["Center"] = "center";
965
- })(SurveyPosition || (SurveyPosition = {}));
966
- var SurveyWidgetType;
967
- (function (SurveyWidgetType) {
968
- SurveyWidgetType["Button"] = "button";
969
- SurveyWidgetType["Tab"] = "tab";
970
- SurveyWidgetType["Selector"] = "selector";
971
- })(SurveyWidgetType || (SurveyWidgetType = {}));
972
- var SurveyType;
973
- (function (SurveyType) {
974
- SurveyType["Popover"] = "popover";
975
- SurveyType["API"] = "api";
976
- SurveyType["Widget"] = "widget";
977
- })(SurveyType || (SurveyType = {}));
978
- var SurveyQuestionDescriptionContentType;
979
- (function (SurveyQuestionDescriptionContentType) {
980
- SurveyQuestionDescriptionContentType["Html"] = "html";
981
- SurveyQuestionDescriptionContentType["Text"] = "text";
982
- })(SurveyQuestionDescriptionContentType || (SurveyQuestionDescriptionContentType = {}));
983
- var SurveyRatingDisplay;
984
- (function (SurveyRatingDisplay) {
985
- SurveyRatingDisplay["Number"] = "number";
986
- SurveyRatingDisplay["Emoji"] = "emoji";
987
- })(SurveyRatingDisplay || (SurveyRatingDisplay = {}));
988
- var SurveyQuestionType;
989
- (function (SurveyQuestionType) {
990
- SurveyQuestionType["Open"] = "open";
991
- SurveyQuestionType["MultipleChoice"] = "multiple_choice";
992
- SurveyQuestionType["SingleChoice"] = "single_choice";
993
- SurveyQuestionType["Rating"] = "rating";
994
- SurveyQuestionType["Link"] = "link";
995
- })(SurveyQuestionType || (SurveyQuestionType = {}));
996
- var SurveyQuestionBranchingType;
997
- (function (SurveyQuestionBranchingType) {
998
- SurveyQuestionBranchingType["NextQuestion"] = "next_question";
999
- SurveyQuestionBranchingType["End"] = "end";
1000
- SurveyQuestionBranchingType["ResponseBased"] = "response_based";
1001
- SurveyQuestionBranchingType["SpecificQuestion"] = "specific_question";
1002
- })(SurveyQuestionBranchingType || (SurveyQuestionBranchingType = {}));
1003
- var SurveyMatchType;
1004
- (function (SurveyMatchType) {
1005
- SurveyMatchType["Regex"] = "regex";
1006
- SurveyMatchType["NotRegex"] = "not_regex";
1007
- SurveyMatchType["Exact"] = "exact";
1008
- SurveyMatchType["IsNot"] = "is_not";
1009
- SurveyMatchType["Icontains"] = "icontains";
1010
- SurveyMatchType["NotIcontains"] = "not_icontains";
1011
- })(SurveyMatchType || (SurveyMatchType = {}));
1012
- /** Sync with plugin-server/src/types.ts */
1013
- var ActionStepStringMatching;
1014
- (function (ActionStepStringMatching) {
1015
- ActionStepStringMatching["Contains"] = "contains";
1016
- ActionStepStringMatching["Exact"] = "exact";
1017
- ActionStepStringMatching["Regex"] = "regex";
1018
- })(ActionStepStringMatching || (ActionStepStringMatching = {}));
1019
-
1020
- const normalizeFlagsResponse = (flagsResponse) => {
1021
- if ('flags' in flagsResponse) {
1022
- // Convert v2 format to v1 format
1023
- const featureFlags = getFlagValuesFromFlags(flagsResponse.flags);
1024
- const featureFlagPayloads = getPayloadsFromFlags(flagsResponse.flags);
1025
- return {
1026
- ...flagsResponse,
1027
- featureFlags,
1028
- featureFlagPayloads,
1029
- };
1030
- }
1031
- else {
1032
- // Convert v1 format to v2 format
1033
- const featureFlags = flagsResponse.featureFlags ?? {};
1034
- const featureFlagPayloads = Object.fromEntries(Object.entries(flagsResponse.featureFlagPayloads || {}).map(([k, v]) => [k, parsePayload(v)]));
1035
- const flags = Object.fromEntries(Object.entries(featureFlags).map(([key, value]) => [
1036
- key,
1037
- getFlagDetailFromFlagAndPayload(key, value, featureFlagPayloads[key]),
1038
- ]));
1039
- return {
1040
- ...flagsResponse,
1041
- featureFlags,
1042
- featureFlagPayloads,
1043
- flags,
1044
- };
1045
- }
1046
- };
1047
- function getFlagDetailFromFlagAndPayload(key, value, payload) {
1048
- return {
1049
- key: key,
1050
- enabled: typeof value === 'string' ? true : value,
1051
- variant: typeof value === 'string' ? value : undefined,
1052
- reason: undefined,
1053
- metadata: {
1054
- id: undefined,
1055
- version: undefined,
1056
- payload: payload ? JSON.stringify(payload) : undefined,
1057
- description: undefined,
1058
- },
1059
- };
1060
- }
1061
- /**
1062
- * Get the flag values from the flags v4 response.
1063
- * @param flags - The flags
1064
- * @returns The flag values
1065
- */
1066
- const getFlagValuesFromFlags = (flags) => {
1067
- return Object.fromEntries(Object.entries(flags ?? {})
1068
- .map(([key, detail]) => [key, getFeatureFlagValue(detail)])
1069
- .filter(([, value]) => value !== undefined));
1070
- };
1071
- /**
1072
- * Get the payloads from the flags v4 response.
1073
- * @param flags - The flags
1074
- * @returns The payloads
1075
- */
1076
- const getPayloadsFromFlags = (flags) => {
1077
- const safeFlags = flags ?? {};
1078
- return Object.fromEntries(Object.keys(safeFlags)
1079
- .filter((flag) => {
1080
- const details = safeFlags[flag];
1081
- return details.enabled && details.metadata && details.metadata.payload !== undefined;
1082
- })
1083
- .map((flag) => {
1084
- const payload = safeFlags[flag].metadata?.payload;
1085
- return [flag, payload ? parsePayload(payload) : undefined];
1086
- }));
1087
- };
1088
- const getFeatureFlagValue = (detail) => {
1089
- return detail === undefined ? undefined : detail.variant ?? detail.enabled;
1090
- };
1091
- const parsePayload = (response) => {
1092
- if (typeof response !== 'string') {
1093
- return response;
1094
- }
1095
- try {
1096
- return JSON.parse(response);
1097
- }
1098
- catch {
1099
- return response;
1100
- }
1101
- };
1102
-
1103
- const STRING_FORMAT = 'utf8';
1104
- function assert(truthyValue, message) {
1105
- if (!truthyValue || typeof truthyValue !== 'string' || isEmpty(truthyValue)) {
1106
- throw new Error(message);
1107
- }
1108
- }
1109
- function isEmpty(truthyValue) {
1110
- if (truthyValue.trim().length === 0) {
1111
- return true;
1112
- }
1113
- return false;
1114
- }
1115
- function removeTrailingSlash(url) {
1116
- return url?.replace(/\/+$/, '');
1117
- }
1118
- async function retriable(fn, props) {
1119
- let lastError = null;
1120
- for (let i = 0; i < props.retryCount + 1; i++) {
1121
- if (i > 0) {
1122
- // don't wait when it's the last try
1123
- await new Promise((r) => setTimeout(r, props.retryDelay));
1124
- }
1125
- try {
1126
- const res = await fn();
1127
- return res;
1128
- }
1129
- catch (e) {
1130
- lastError = e;
1131
- if (!props.retryCheck(e)) {
1132
- throw e;
1133
- }
1134
- }
1135
- }
1136
- throw lastError;
1137
- }
1138
- function currentISOTime() {
1139
- return new Date().toISOString();
1140
- }
1141
- function safeSetTimeout(fn, timeout) {
1142
- // NOTE: we use this so rarely that it is totally fine to do `safeSetTimeout(fn, 0)``
1143
- // rather than setImmediate.
1144
- const t = setTimeout(fn, timeout);
1145
- // We unref if available to prevent Node.js hanging on exit
1146
- t?.unref && t?.unref();
1147
- return t;
1148
- }
1149
- function allSettled(promises) {
1150
- return Promise.all(promises.map((p) => (p ?? Promise.resolve()).then((value) => ({ status: 'fulfilled', value }), (reason) => ({ status: 'rejected', reason }))));
1151
- }
1152
-
1153
- /**
1154
- * Older browsers and some runtimes don't support this yet
1155
- * This API (as of 2025-05-07) is not available on React Native.
1156
- */
1157
- function isGzipSupported() {
1158
- return 'CompressionStream' in globalThis;
1159
- }
1160
- /**
1161
- * Gzip a string using Compression Streams API if it's available
1162
- */
1163
- async function gzipCompress(input, isDebug = true) {
1164
- try {
1165
- // Turn the string into a stream using a Blob, and then compress it
1166
- const dataStream = new Blob([input], {
1167
- type: 'text/plain',
1168
- }).stream();
1169
- const compressedStream = dataStream.pipeThrough(new CompressionStream('gzip'));
1170
- // Using a Response to easily extract the readablestream value. Decoding into a string for fetch
1171
- return await new Response(compressedStream).blob();
1172
- }
1173
- catch (error) {
1174
- if (isDebug) {
1175
- console.error('Failed to gzip compress data', error);
1176
- }
1177
- return null;
1178
- }
1179
- }
1180
-
1181
- class SimpleEventEmitter {
1182
- constructor() {
1183
- this.events = {};
1184
- this.events = {};
1185
- }
1186
- on(event, listener) {
1187
- if (!this.events[event]) {
1188
- this.events[event] = [];
1189
- }
1190
- this.events[event].push(listener);
1191
- return () => {
1192
- this.events[event] = this.events[event].filter((x) => x !== listener);
1193
- };
1194
- }
1195
- emit(event, payload) {
1196
- for (const listener of this.events[event] || []) {
1197
- listener(payload);
1198
- }
1199
- for (const listener of this.events['*'] || []) {
1200
- listener(event, payload);
1201
- }
1202
- }
1203
- }
1204
-
1205
- class PostHogFetchHttpError extends Error {
1206
- constructor(response, reqByteLength) {
1207
- super('HTTP error while fetching PostHog: status=' + response.status + ', reqByteLength=' + reqByteLength);
1208
- this.response = response;
1209
- this.reqByteLength = reqByteLength;
1210
- this.name = 'PostHogFetchHttpError';
1211
- }
1212
- get status() {
1213
- return this.response.status;
1214
- }
1215
- get text() {
1216
- return this.response.text();
1217
- }
1218
- get json() {
1219
- return this.response.json();
1220
- }
1221
- }
1222
- class PostHogFetchNetworkError extends Error {
1223
- constructor(error) {
1224
- // TRICKY: "cause" is a newer property but is just ignored otherwise. Cast to any to ignore the type issue.
1225
- // eslint-disable-next-line @typescript-eslint/prefer-ts-expect-error
1226
- // @ts-ignore
1227
- super('Network error while fetching PostHog', error instanceof Error ? { cause: error } : {});
1228
- this.error = error;
1229
- this.name = 'PostHogFetchNetworkError';
1230
- }
1231
- }
1232
- async function logFlushError(err) {
1233
- if (err instanceof PostHogFetchHttpError) {
1234
- let text = '';
1235
- try {
1236
- text = await err.text;
1237
- }
1238
- catch { }
1239
- console.error(`Error while flushing PostHog: message=${err.message}, response body=${text}`, err);
1240
- }
1241
- else {
1242
- console.error('Error while flushing PostHog', err);
1243
- }
1244
- return Promise.resolve();
1245
- }
1246
- function isPostHogFetchError(err) {
1247
- return typeof err === 'object' && (err instanceof PostHogFetchHttpError || err instanceof PostHogFetchNetworkError);
1248
- }
1249
- function isPostHogFetchContentTooLargeError(err) {
1250
- return typeof err === 'object' && err instanceof PostHogFetchHttpError && err.status === 413;
1251
- }
1252
- var QuotaLimitedFeature;
1253
- (function (QuotaLimitedFeature) {
1254
- QuotaLimitedFeature["FeatureFlags"] = "feature_flags";
1255
- QuotaLimitedFeature["Recordings"] = "recordings";
1256
- })(QuotaLimitedFeature || (QuotaLimitedFeature = {}));
1257
- class PostHogCoreStateless {
1258
- constructor(apiKey, options) {
1259
- this.flushPromise = null;
1260
- this.shutdownPromise = null;
1261
- this.pendingPromises = {};
1262
- // internal
1263
- this._events = new SimpleEventEmitter();
1264
- this._isInitialized = false;
1265
- assert(apiKey, "You must pass your PostHog project's api key.");
1266
- this.apiKey = apiKey;
1267
- this.host = removeTrailingSlash(options?.host || 'https://us.i.posthog.com');
1268
- this.flushAt = options?.flushAt ? Math.max(options?.flushAt, 1) : 20;
1269
- this.maxBatchSize = Math.max(this.flushAt, options?.maxBatchSize ?? 100);
1270
- this.maxQueueSize = Math.max(this.flushAt, options?.maxQueueSize ?? 1000);
1271
- this.flushInterval = options?.flushInterval ?? 10000;
1272
- this.preloadFeatureFlags = options?.preloadFeatureFlags ?? true;
1273
- // If enable is explicitly set to false we override the optout
1274
- this.defaultOptIn = options?.defaultOptIn ?? true;
1275
- this.disableSurveys = options?.disableSurveys ?? false;
1276
- this._retryOptions = {
1277
- retryCount: options?.fetchRetryCount ?? 3,
1278
- retryDelay: options?.fetchRetryDelay ?? 3000,
1279
- retryCheck: isPostHogFetchError,
1280
- };
1281
- this.requestTimeout = options?.requestTimeout ?? 10000; // 10 seconds
1282
- this.featureFlagsRequestTimeoutMs = options?.featureFlagsRequestTimeoutMs ?? 3000; // 3 seconds
1283
- this.remoteConfigRequestTimeoutMs = options?.remoteConfigRequestTimeoutMs ?? 3000; // 3 seconds
1284
- this.disableGeoip = options?.disableGeoip ?? true;
1285
- this.disabled = options?.disabled ?? false;
1286
- this.historicalMigration = options?.historicalMigration ?? false;
1287
- // Init promise allows the derived class to block calls until it is ready
1288
- this._initPromise = Promise.resolve();
1289
- this._isInitialized = true;
1290
- this.disableCompression = !isGzipSupported() || (options?.disableCompression ?? false);
1291
- }
1292
- logMsgIfDebug(fn) {
1293
- if (this.isDebug) {
1294
- fn();
1295
- }
1296
- }
1297
- wrap(fn) {
1298
- if (this.disabled) {
1299
- this.logMsgIfDebug(() => console.warn('[PostHog] The client is disabled'));
1300
- return;
1301
- }
1302
- if (this._isInitialized) {
1303
- // NOTE: We could also check for the "opt in" status here...
1304
- return fn();
1305
- }
1306
- this._initPromise.then(() => fn());
1307
- }
1308
- getCommonEventProperties() {
1309
- return {
1310
- $lib: this.getLibraryId(),
1311
- $lib_version: this.getLibraryVersion(),
1312
- };
1313
- }
1314
- get optedOut() {
1315
- return this.getPersistedProperty(PostHogPersistedProperty.OptedOut) ?? !this.defaultOptIn;
1316
- }
1317
- async optIn() {
1318
- this.wrap(() => {
1319
- this.setPersistedProperty(PostHogPersistedProperty.OptedOut, false);
1320
- });
1321
- }
1322
- async optOut() {
1323
- this.wrap(() => {
1324
- this.setPersistedProperty(PostHogPersistedProperty.OptedOut, true);
1325
- });
1326
- }
1327
- on(event, cb) {
1328
- return this._events.on(event, cb);
1329
- }
1330
- debug(enabled = true) {
1331
- this.removeDebugCallback?.();
1332
- if (enabled) {
1333
- const removeDebugCallback = this.on('*', (event, payload) => console.log('PostHog Debug', event, payload));
1334
- this.removeDebugCallback = () => {
1335
- removeDebugCallback();
1336
- this.removeDebugCallback = undefined;
1337
- };
1338
- }
1339
- }
1340
- get isDebug() {
1341
- return !!this.removeDebugCallback;
1342
- }
1343
- get isDisabled() {
1344
- return this.disabled;
1345
- }
1346
- buildPayload(payload) {
1347
- return {
1348
- distinct_id: payload.distinct_id,
1349
- event: payload.event,
1350
- properties: {
1351
- ...(payload.properties || {}),
1352
- ...this.getCommonEventProperties(), // Common PH props
1353
- },
1354
- };
1355
- }
1356
- addPendingPromise(promise) {
1357
- const promiseUUID = uuidv7();
1358
- this.pendingPromises[promiseUUID] = promise;
1359
- promise
1360
- .catch(() => { })
1361
- .finally(() => {
1362
- delete this.pendingPromises[promiseUUID];
1363
- });
1364
- return promise;
1365
- }
1366
- /***
1367
- *** TRACKING
1368
- ***/
1369
- identifyStateless(distinctId, properties, options) {
1370
- this.wrap(() => {
1371
- // The properties passed to identifyStateless are event properties.
1372
- // To add person properties, pass in all person properties to the `$set` and `$set_once` keys.
1373
- const payload = {
1374
- ...this.buildPayload({
1375
- distinct_id: distinctId,
1376
- event: '$identify',
1377
- properties,
1378
- }),
1379
- };
1380
- this.enqueue('identify', payload, options);
1381
- });
1382
- }
1383
- async identifyStatelessImmediate(distinctId, properties, options) {
1384
- const payload = {
1385
- ...this.buildPayload({
1386
- distinct_id: distinctId,
1387
- event: '$identify',
1388
- properties,
1389
- }),
1390
- };
1391
- await this.sendImmediate('identify', payload, options);
1392
- }
1393
- captureStateless(distinctId, event, properties, options) {
1394
- this.wrap(() => {
1395
- const payload = this.buildPayload({ distinct_id: distinctId, event, properties });
1396
- this.enqueue('capture', payload, options);
1397
- });
1398
- }
1399
- async captureStatelessImmediate(distinctId, event, properties, options) {
1400
- const payload = this.buildPayload({ distinct_id: distinctId, event, properties });
1401
- await this.sendImmediate('capture', payload, options);
1402
- }
1403
- aliasStateless(alias, distinctId, properties, options) {
1404
- this.wrap(() => {
1405
- const payload = this.buildPayload({
1406
- event: '$create_alias',
1407
- distinct_id: distinctId,
1408
- properties: {
1409
- ...(properties || {}),
1410
- distinct_id: distinctId,
1411
- alias,
1412
- },
1413
- });
1414
- this.enqueue('alias', payload, options);
1415
- });
1416
- }
1417
- async aliasStatelessImmediate(alias, distinctId, properties, options) {
1418
- const payload = this.buildPayload({
1419
- event: '$create_alias',
1420
- distinct_id: distinctId,
1421
- properties: {
1422
- ...(properties || {}),
1423
- distinct_id: distinctId,
1424
- alias,
1425
- },
1426
- });
1427
- await this.sendImmediate('alias', payload, options);
1428
- }
1429
- /***
1430
- *** GROUPS
1431
- ***/
1432
- groupIdentifyStateless(groupType, groupKey, groupProperties, options, distinctId, eventProperties) {
1433
- this.wrap(() => {
1434
- const payload = this.buildPayload({
1435
- distinct_id: distinctId || `$${groupType}_${groupKey}`,
1436
- event: '$groupidentify',
1437
- properties: {
1438
- $group_type: groupType,
1439
- $group_key: groupKey,
1440
- $group_set: groupProperties || {},
1441
- ...(eventProperties || {}),
1442
- },
1443
- });
1444
- this.enqueue('capture', payload, options);
1445
- });
1446
- }
1447
- async getRemoteConfig() {
1448
- await this._initPromise;
1449
- let host = this.host;
1450
- if (host === 'https://us.i.posthog.com') {
1451
- host = 'https://us-assets.i.posthog.com';
1452
- }
1453
- else if (host === 'https://eu.i.posthog.com') {
1454
- host = 'https://eu-assets.i.posthog.com';
1455
- }
1456
- const url = `${host}/array/${this.apiKey}/config`;
1457
- const fetchOptions = {
1458
- method: 'GET',
1459
- headers: { ...this.getCustomHeaders(), 'Content-Type': 'application/json' },
1460
- };
1461
- // Don't retry remote config API calls
1462
- return this.fetchWithRetry(url, fetchOptions, { retryCount: 0 }, this.remoteConfigRequestTimeoutMs)
1463
- .then((response) => response.json())
1464
- .catch((error) => {
1465
- this.logMsgIfDebug(() => console.error('Remote config could not be loaded', error));
1466
- this._events.emit('error', error);
1467
- return undefined;
1468
- });
1469
- }
1470
- /***
1471
- *** FEATURE FLAGS
1472
- ***/
1473
- async getFlags(distinctId, groups = {}, personProperties = {}, groupProperties = {}, extraPayload = {}) {
1474
- await this._initPromise;
1475
- const url = `${this.host}/flags/?v=2&config=true`;
1476
- const fetchOptions = {
1477
- method: 'POST',
1478
- headers: { ...this.getCustomHeaders(), 'Content-Type': 'application/json' },
1479
- body: JSON.stringify({
1480
- token: this.apiKey,
1481
- distinct_id: distinctId,
1482
- groups,
1483
- person_properties: personProperties,
1484
- group_properties: groupProperties,
1485
- ...extraPayload,
1486
- }),
1487
- };
1488
- this.logMsgIfDebug(() => console.log('PostHog Debug', 'Flags URL', url));
1489
- // Don't retry /flags API calls
1490
- return this.fetchWithRetry(url, fetchOptions, { retryCount: 0 }, this.featureFlagsRequestTimeoutMs)
1491
- .then((response) => response.json())
1492
- .then((response) => normalizeFlagsResponse(response))
1493
- .catch((error) => {
1494
- this._events.emit('error', error);
1495
- return undefined;
1496
- });
1497
- }
1498
- async getFeatureFlagStateless(key, distinctId, groups = {}, personProperties = {}, groupProperties = {}, disableGeoip) {
1499
- await this._initPromise;
1500
- const flagDetailResponse = await this.getFeatureFlagDetailStateless(key, distinctId, groups, personProperties, groupProperties, disableGeoip);
1501
- if (flagDetailResponse === undefined) {
1502
- // If we haven't loaded flags yet, or errored out, we respond with undefined
1503
- return {
1504
- response: undefined,
1505
- requestId: undefined,
1506
- };
1507
- }
1508
- let response = getFeatureFlagValue(flagDetailResponse.response);
1509
- if (response === undefined) {
1510
- // For cases where the flag is unknown, return false
1511
- response = false;
1512
- }
1513
- // If we have flags we either return the value (true or string) or false
1514
- return {
1515
- response,
1516
- requestId: flagDetailResponse.requestId,
1517
- };
1518
- }
1519
- async getFeatureFlagDetailStateless(key, distinctId, groups = {}, personProperties = {}, groupProperties = {}, disableGeoip) {
1520
- await this._initPromise;
1521
- const flagsResponse = await this.getFeatureFlagDetailsStateless(distinctId, groups, personProperties, groupProperties, disableGeoip, [key]);
1522
- if (flagsResponse === undefined) {
1523
- return undefined;
1524
- }
1525
- const featureFlags = flagsResponse.flags;
1526
- const flagDetail = featureFlags[key];
1527
- return {
1528
- response: flagDetail,
1529
- requestId: flagsResponse.requestId,
1530
- };
1531
- }
1532
- async getFeatureFlagPayloadStateless(key, distinctId, groups = {}, personProperties = {}, groupProperties = {}, disableGeoip) {
1533
- await this._initPromise;
1534
- const payloads = await this.getFeatureFlagPayloadsStateless(distinctId, groups, personProperties, groupProperties, disableGeoip, [key]);
1535
- if (!payloads) {
1536
- return undefined;
1537
- }
1538
- const response = payloads[key];
1539
- // Undefined means a loading or missing data issue. Null means evaluation happened and there was no match
1540
- if (response === undefined) {
1541
- return null;
1542
- }
1543
- return response;
1544
- }
1545
- async getFeatureFlagPayloadsStateless(distinctId, groups = {}, personProperties = {}, groupProperties = {}, disableGeoip, flagKeysToEvaluate) {
1546
- await this._initPromise;
1547
- const payloads = (await this.getFeatureFlagsAndPayloadsStateless(distinctId, groups, personProperties, groupProperties, disableGeoip, flagKeysToEvaluate)).payloads;
1548
- return payloads;
1549
- }
1550
- async getFeatureFlagsStateless(distinctId, groups = {}, personProperties = {}, groupProperties = {}, disableGeoip, flagKeysToEvaluate) {
1551
- await this._initPromise;
1552
- return await this.getFeatureFlagsAndPayloadsStateless(distinctId, groups, personProperties, groupProperties, disableGeoip, flagKeysToEvaluate);
1553
- }
1554
- async getFeatureFlagsAndPayloadsStateless(distinctId, groups = {}, personProperties = {}, groupProperties = {}, disableGeoip, flagKeysToEvaluate) {
1555
- await this._initPromise;
1556
- const featureFlagDetails = await this.getFeatureFlagDetailsStateless(distinctId, groups, personProperties, groupProperties, disableGeoip, flagKeysToEvaluate);
1557
- if (!featureFlagDetails) {
1558
- return {
1559
- flags: undefined,
1560
- payloads: undefined,
1561
- requestId: undefined,
1562
- };
1563
- }
1564
- return {
1565
- flags: featureFlagDetails.featureFlags,
1566
- payloads: featureFlagDetails.featureFlagPayloads,
1567
- requestId: featureFlagDetails.requestId,
1568
- };
1569
- }
1570
- async getFeatureFlagDetailsStateless(distinctId, groups = {}, personProperties = {}, groupProperties = {}, disableGeoip, flagKeysToEvaluate) {
1571
- await this._initPromise;
1572
- const extraPayload = {};
1573
- if (disableGeoip ?? this.disableGeoip) {
1574
- extraPayload['geoip_disable'] = true;
1575
- }
1576
- if (flagKeysToEvaluate) {
1577
- extraPayload['flag_keys_to_evaluate'] = flagKeysToEvaluate;
1578
- }
1579
- const flagsResponse = await this.getFlags(distinctId, groups, personProperties, groupProperties, extraPayload);
1580
- if (flagsResponse === undefined) {
1581
- // We probably errored out, so return undefined
1582
- return undefined;
1583
- }
1584
- // if there's an error on the flagsResponse, log a console error, but don't throw an error
1585
- if (flagsResponse.errorsWhileComputingFlags) {
1586
- 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');
1587
- }
1588
- // Add check for quota limitation on feature flags
1589
- if (flagsResponse.quotaLimited?.includes(QuotaLimitedFeature.FeatureFlags)) {
1590
- 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');
1591
- return {
1592
- flags: {},
1593
- featureFlags: {},
1594
- featureFlagPayloads: {},
1595
- requestId: flagsResponse?.requestId,
1596
- };
1597
- }
1598
- return flagsResponse;
1599
- }
1600
- /***
1601
- *** SURVEYS
1602
- ***/
1603
- async getSurveysStateless() {
1604
- await this._initPromise;
1605
- if (this.disableSurveys === true) {
1606
- this.logMsgIfDebug(() => console.log('PostHog Debug', 'Loading surveys is disabled.'));
1607
- return [];
1608
- }
1609
- const url = `${this.host}/api/surveys/?token=${this.apiKey}`;
1610
- const fetchOptions = {
1611
- method: 'GET',
1612
- headers: { ...this.getCustomHeaders(), 'Content-Type': 'application/json' },
1613
- };
1614
- const response = await this.fetchWithRetry(url, fetchOptions)
1615
- .then((response) => {
1616
- if (response.status !== 200 || !response.json) {
1617
- const msg = `Surveys API could not be loaded: ${response.status}`;
1618
- const error = new Error(msg);
1619
- this.logMsgIfDebug(() => console.error(error));
1620
- this._events.emit('error', new Error(msg));
1621
- return undefined;
1622
- }
1623
- return response.json();
1624
- })
1625
- .catch((error) => {
1626
- this.logMsgIfDebug(() => console.error('Surveys API could not be loaded', error));
1627
- this._events.emit('error', error);
1628
- return undefined;
1629
- });
1630
- const newSurveys = response?.surveys;
1631
- if (newSurveys) {
1632
- this.logMsgIfDebug(() => console.log('PostHog Debug', 'Surveys fetched from API: ', JSON.stringify(newSurveys)));
1633
- }
1634
- return newSurveys ?? [];
1635
- }
1636
- get props() {
1637
- if (!this._props) {
1638
- this._props = this.getPersistedProperty(PostHogPersistedProperty.Props);
1639
- }
1640
- return this._props || {};
1641
- }
1642
- set props(val) {
1643
- this._props = val;
1644
- }
1645
- async register(properties) {
1646
- this.wrap(() => {
1647
- this.props = {
1648
- ...this.props,
1649
- ...properties,
1650
- };
1651
- this.setPersistedProperty(PostHogPersistedProperty.Props, this.props);
1652
- });
1653
- }
1654
- async unregister(property) {
1655
- this.wrap(() => {
1656
- delete this.props[property];
1657
- this.setPersistedProperty(PostHogPersistedProperty.Props, this.props);
1658
- });
1659
- }
1660
- /***
1661
- *** QUEUEING AND FLUSHING
1662
- ***/
1663
- enqueue(type, _message, options) {
1664
- this.wrap(() => {
1665
- if (this.optedOut) {
1666
- this._events.emit(type, `Library is disabled. Not sending event. To re-enable, call posthog.optIn()`);
1667
- return;
1668
- }
1669
- const message = this.prepareMessage(type, _message, options);
1670
- const queue = this.getPersistedProperty(PostHogPersistedProperty.Queue) || [];
1671
- if (queue.length >= this.maxQueueSize) {
1672
- queue.shift();
1673
- this.logMsgIfDebug(() => console.info('Queue is full, the oldest event is dropped.'));
1674
- }
1675
- queue.push({ message });
1676
- this.setPersistedProperty(PostHogPersistedProperty.Queue, queue);
1677
- this._events.emit(type, message);
1678
- // Flush queued events if we meet the flushAt length
1679
- if (queue.length >= this.flushAt) {
1680
- this.flushBackground();
1681
- }
1682
- if (this.flushInterval && !this._flushTimer) {
1683
- this._flushTimer = safeSetTimeout(() => this.flushBackground(), this.flushInterval);
1684
- }
1685
- });
1686
- }
1687
- async sendImmediate(type, _message, options) {
1688
- if (this.disabled) {
1689
- this.logMsgIfDebug(() => console.warn('[PostHog] The client is disabled'));
1690
- return;
1691
- }
1692
- if (!this._isInitialized) {
1693
- await this._initPromise;
1694
- }
1695
- if (this.optedOut) {
1696
- this._events.emit(type, `Library is disabled. Not sending event. To re-enable, call posthog.optIn()`);
1697
- return;
1698
- }
1699
- const data = {
1700
- api_key: this.apiKey,
1701
- batch: [this.prepareMessage(type, _message, options)],
1702
- sent_at: currentISOTime(),
1703
- };
1704
- if (this.historicalMigration) {
1705
- data.historical_migration = true;
1706
- }
1707
- const payload = JSON.stringify(data);
1708
- const url = `${this.host}/batch/`;
1709
- const gzippedPayload = !this.disableCompression ? await gzipCompress(payload, this.isDebug) : null;
1710
- const fetchOptions = {
1711
- method: 'POST',
1712
- headers: {
1713
- ...this.getCustomHeaders(),
1714
- 'Content-Type': 'application/json',
1715
- ...(gzippedPayload !== null && { 'Content-Encoding': 'gzip' }),
1716
- },
1717
- body: gzippedPayload || payload,
1718
- };
1719
- try {
1720
- await this.fetchWithRetry(url, fetchOptions);
1721
- }
1722
- catch (err) {
1723
- this._events.emit('error', err);
1724
- }
1725
- }
1726
- prepareMessage(type, _message, options) {
1727
- const message = {
1728
- ..._message,
1729
- type: type,
1730
- library: this.getLibraryId(),
1731
- library_version: this.getLibraryVersion(),
1732
- timestamp: options?.timestamp ? options?.timestamp : currentISOTime(),
1733
- uuid: options?.uuid ? options.uuid : uuidv7(),
1734
- };
1735
- const addGeoipDisableProperty = options?.disableGeoip ?? this.disableGeoip;
1736
- if (addGeoipDisableProperty) {
1737
- if (!message.properties) {
1738
- message.properties = {};
1739
- }
1740
- message['properties']['$geoip_disable'] = true;
1741
- }
1742
- if (message.distinctId) {
1743
- message.distinct_id = message.distinctId;
1744
- delete message.distinctId;
1745
- }
1746
- return message;
1747
- }
1748
- clearFlushTimer() {
1749
- if (this._flushTimer) {
1750
- clearTimeout(this._flushTimer);
1751
- this._flushTimer = undefined;
1752
- }
1753
- }
1754
- /**
1755
- * Helper for flushing the queue in the background
1756
- * Avoids unnecessary promise errors
1757
- */
1758
- flushBackground() {
1759
- void this.flush().catch(async (err) => {
1760
- await logFlushError(err);
1761
- });
1762
- }
1763
- /**
1764
- * Flushes the queue
1765
- *
1766
- * This function will return a promise that will resolve when the flush is complete,
1767
- * or reject if there was an error (for example if the server or network is down).
1768
- *
1769
- * If there is already a flush in progress, this function will wait for that flush to complete.
1770
- *
1771
- * It's recommended to do error handling in the callback of the promise.
1772
- *
1773
- * @example
1774
- * posthog.flush().then(() => {
1775
- * console.log('Flush complete')
1776
- * }).catch((err) => {
1777
- * console.error('Flush failed', err)
1778
- * })
1779
- *
1780
- *
1781
- * @throws PostHogFetchHttpError
1782
- * @throws PostHogFetchNetworkError
1783
- * @throws Error
1784
- */
1785
- async flush() {
1786
- // Wait for the current flush operation to finish (regardless of success or failure), then try to flush again.
1787
- // Use allSettled instead of finally to be defensive around flush throwing errors immediately rather than rejecting.
1788
- // Use a custom allSettled implementation to avoid issues with patching Promise on RN
1789
- const nextFlushPromise = allSettled([this.flushPromise]).then(() => {
1790
- return this._flush();
1791
- });
1792
- this.flushPromise = nextFlushPromise;
1793
- void this.addPendingPromise(nextFlushPromise);
1794
- allSettled([nextFlushPromise]).then(() => {
1795
- // If there are no others waiting to flush, clear the promise.
1796
- // We don't strictly need to do this, but it could make debugging easier
1797
- if (this.flushPromise === nextFlushPromise) {
1798
- this.flushPromise = null;
1799
- }
1800
- });
1801
- return nextFlushPromise;
1802
- }
1803
- getCustomHeaders() {
1804
- // Don't set the user agent if we're not on a browser. The latest spec allows
1805
- // the User-Agent header (see https://fetch.spec.whatwg.org/#terminology-headers
1806
- // and https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/setRequestHeader),
1807
- // but browsers such as Chrome and Safari have not caught up.
1808
- const customUserAgent = this.getCustomUserAgent();
1809
- const headers = {};
1810
- if (customUserAgent && customUserAgent !== '') {
1811
- headers['User-Agent'] = customUserAgent;
1812
- }
1813
- return headers;
1814
- }
1815
- async _flush() {
1816
- this.clearFlushTimer();
1817
- await this._initPromise;
1818
- let queue = this.getPersistedProperty(PostHogPersistedProperty.Queue) || [];
1819
- if (!queue.length) {
1820
- return;
1821
- }
1822
- const sentMessages = [];
1823
- const originalQueueLength = queue.length;
1824
- while (queue.length > 0 && sentMessages.length < originalQueueLength) {
1825
- const batchItems = queue.slice(0, this.maxBatchSize);
1826
- const batchMessages = batchItems.map((item) => item.message);
1827
- const persistQueueChange = () => {
1828
- const refreshedQueue = this.getPersistedProperty(PostHogPersistedProperty.Queue) || [];
1829
- const newQueue = refreshedQueue.slice(batchItems.length);
1830
- this.setPersistedProperty(PostHogPersistedProperty.Queue, newQueue);
1831
- queue = newQueue;
1832
- };
1833
- const data = {
1834
- api_key: this.apiKey,
1835
- batch: batchMessages,
1836
- sent_at: currentISOTime(),
1837
- };
1838
- if (this.historicalMigration) {
1839
- data.historical_migration = true;
1840
- }
1841
- const payload = JSON.stringify(data);
1842
- const url = `${this.host}/batch/`;
1843
- const gzippedPayload = !this.disableCompression ? await gzipCompress(payload, this.isDebug) : null;
1844
- const fetchOptions = {
1845
- method: 'POST',
1846
- headers: {
1847
- ...this.getCustomHeaders(),
1848
- 'Content-Type': 'application/json',
1849
- ...(gzippedPayload !== null && { 'Content-Encoding': 'gzip' }),
1850
- },
1851
- body: gzippedPayload || payload,
1852
- };
1853
- const retryOptions = {
1854
- retryCheck: (err) => {
1855
- // don't automatically retry on 413 errors, we want to reduce the batch size first
1856
- if (isPostHogFetchContentTooLargeError(err)) {
1857
- return false;
1858
- }
1859
- // otherwise, retry on network errors
1860
- return isPostHogFetchError(err);
1861
- },
1862
- };
1863
- try {
1864
- await this.fetchWithRetry(url, fetchOptions, retryOptions);
1865
- }
1866
- catch (err) {
1867
- if (isPostHogFetchContentTooLargeError(err) && batchMessages.length > 1) {
1868
- // if we get a 413 error, we want to reduce the batch size and try again
1869
- this.maxBatchSize = Math.max(1, Math.floor(batchMessages.length / 2));
1870
- this.logMsgIfDebug(() => console.warn(`Received 413 when sending batch of size ${batchMessages.length}, reducing batch size to ${this.maxBatchSize}`));
1871
- // do not persist the queue change, we want to retry the same batch
1872
- continue;
1873
- }
1874
- // depending on the error type, eg a malformed JSON or broken queue, it'll always return an error
1875
- // 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
1876
- if (!(err instanceof PostHogFetchNetworkError)) {
1877
- persistQueueChange();
1878
- }
1879
- this._events.emit('error', err);
1880
- throw err;
1881
- }
1882
- persistQueueChange();
1883
- sentMessages.push(...batchMessages);
1884
- }
1885
- this._events.emit('flush', sentMessages);
1886
- }
1887
- async fetchWithRetry(url, options, retryOptions, requestTimeout) {
1888
- var _a;
1889
- (_a = AbortSignal).timeout ?? (_a.timeout = function timeout(ms) {
1890
- const ctrl = new AbortController();
1891
- setTimeout(() => ctrl.abort(), ms);
1892
- return ctrl.signal;
1893
- });
1894
- const body = options.body ? options.body : '';
1895
- let reqByteLength = -1;
1896
- try {
1897
- if (body instanceof Blob) {
1898
- reqByteLength = body.size;
1899
- }
1900
- else {
1901
- reqByteLength = Buffer.byteLength(body, STRING_FORMAT);
1902
- }
1903
- }
1904
- catch {
1905
- if (body instanceof Blob) {
1906
- reqByteLength = body.size;
1907
- }
1908
- else {
1909
- const encoded = new TextEncoder().encode(body);
1910
- reqByteLength = encoded.length;
1911
- }
1912
- }
1913
- return await retriable(async () => {
1914
- let res = null;
1915
- try {
1916
- res = await this.fetch(url, {
1917
- signal: AbortSignal.timeout(requestTimeout ?? this.requestTimeout),
1918
- ...options,
1919
- });
1920
- }
1921
- catch (e) {
1922
- // fetch will only throw on network errors or on timeouts
1923
- throw new PostHogFetchNetworkError(e);
1924
- }
1925
- // If we're in no-cors mode, we can't access the response status
1926
- // We only throw on HTTP errors if we're not in no-cors mode
1927
- // https://developer.mozilla.org/en-US/docs/Web/API/Request/mode#no-cors
1928
- const isNoCors = options.mode === 'no-cors';
1929
- if (!isNoCors && (res.status < 200 || res.status >= 400)) {
1930
- throw new PostHogFetchHttpError(res, reqByteLength);
1931
- }
1932
- return res;
1933
- }, { ...this._retryOptions, ...retryOptions });
1934
- }
1935
- async _shutdown(shutdownTimeoutMs = 30000) {
1936
- // A little tricky - we want to have a max shutdown time and enforce it, even if that means we have some
1937
- // dangling promises. We'll keep track of the timeout and resolve/reject based on that.
1938
- await this._initPromise;
1939
- let hasTimedOut = false;
1940
- this.clearFlushTimer();
1941
- const doShutdown = async () => {
1942
- try {
1943
- await Promise.all(Object.values(this.pendingPromises));
1944
- while (true) {
1945
- const queue = this.getPersistedProperty(PostHogPersistedProperty.Queue) || [];
1946
- if (queue.length === 0) {
1947
- break;
1948
- }
1949
- // flush again to make sure we send all events, some of which might've been added
1950
- // while we were waiting for the pending promises to resolve
1951
- // For example, see sendFeatureFlags in posthog-node/src/posthog-node.ts::capture
1952
- await this.flush();
1953
- if (hasTimedOut) {
1954
- break;
1955
- }
1956
- }
1957
- }
1958
- catch (e) {
1959
- if (!isPostHogFetchError(e)) {
1960
- throw e;
1961
- }
1962
- await logFlushError(e);
1963
- }
1964
- };
1965
- return Promise.race([
1966
- new Promise((_, reject) => {
1967
- safeSetTimeout(() => {
1968
- this.logMsgIfDebug(() => console.error('Timed out while shutting down PostHog'));
1969
- hasTimedOut = true;
1970
- reject('Timeout while shutting down PostHog. Some events may not have been sent.');
1971
- }, shutdownTimeoutMs);
1972
- }),
1973
- doShutdown(),
1974
- ]);
1975
- }
1976
- /**
1977
- * Call shutdown() once before the node process exits, so ensure that all events have been sent and all promises
1978
- * have resolved. Do not use this function if you intend to keep using this PostHog instance after calling it.
1979
- * @param shutdownTimeoutMs
1980
- */
1981
- async shutdown(shutdownTimeoutMs = 30000) {
1982
- if (this.shutdownPromise) {
1983
- 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'));
1984
- }
1985
- else {
1986
- this.shutdownPromise = this._shutdown(shutdownTimeoutMs).finally(() => {
1987
- this.shutdownPromise = null;
1988
- });
1989
- }
1990
- return this.shutdownPromise;
1991
- }
1992
- }
770
+ var version = "5.8.0";
1993
771
 
1994
- /**
1995
- * A lazy value that is only computed when needed. Inspired by C#'s Lazy<T> class.
772
+ /**
773
+ * A lazy value that is only computed when needed. Inspired by C#'s Lazy<T> class.
1996
774
  */
1997
775
  class Lazy {
1998
776
  constructor(factory) {
1999
777
  this.factory = factory;
2000
778
  }
2001
- /**
2002
- * Gets the value, initializing it if necessary.
2003
- * Multiple concurrent calls will share the same initialization promise.
779
+ /**
780
+ * Gets the value, initializing it if necessary.
781
+ * Multiple concurrent calls will share the same initialization promise.
2004
782
  */
2005
783
  async getValue() {
2006
784
  if (this.value !== undefined) {
@@ -2020,15 +798,15 @@ class Lazy {
2020
798
  }
2021
799
  return this.initializationPromise;
2022
800
  }
2023
- /**
2024
- * Returns true if the value has been initialized.
801
+ /**
802
+ * Returns true if the value has been initialized.
2025
803
  */
2026
804
  isInitialized() {
2027
805
  return this.value !== undefined;
2028
806
  }
2029
- /**
2030
- * Returns a promise that resolves when the value is initialized.
2031
- * If already initialized, resolves immediately.
807
+ /**
808
+ * Returns a promise that resolves when the value is initialized.
809
+ * If already initialized, resolves immediately.
2032
810
  */
2033
811
  async waitForInitialization() {
2034
812
  if (this.isInitialized()) {
@@ -2157,7 +935,8 @@ class FeatureFlagsPoller {
2157
935
  featureFlag = this.featureFlagsByKey[key];
2158
936
  if (featureFlag !== undefined) {
2159
937
  try {
2160
- response = await this.computeFlagLocally(featureFlag, distinctId, groups, personProperties, groupProperties);
938
+ const result = await this.computeFlagAndPayloadLocally(featureFlag, distinctId, groups, personProperties, groupProperties);
939
+ response = result.value;
2161
940
  this.logMsgIfDebug(() => console.debug(`Successfully computed flag locally: ${key} -> ${response}`));
2162
941
  } catch (e) {
2163
942
  if (e instanceof InconclusiveMatchError) {
@@ -2169,27 +948,6 @@ class FeatureFlagsPoller {
2169
948
  }
2170
949
  return response;
2171
950
  }
2172
- async computeFeatureFlagPayloadLocally(key, matchValue) {
2173
- await this.loadFeatureFlags();
2174
- let response = undefined;
2175
- if (!this.loadedSuccessfullyOnce) {
2176
- return undefined;
2177
- }
2178
- if (typeof matchValue == 'boolean') {
2179
- response = this.featureFlagsByKey?.[key]?.filters?.payloads?.[matchValue.toString()];
2180
- } else if (typeof matchValue == 'string') {
2181
- response = this.featureFlagsByKey?.[key]?.filters?.payloads?.[matchValue];
2182
- }
2183
- // Undefined means a loading or missing data issue. Null means evaluation happened and there was no match
2184
- if (response === undefined || response === null) {
2185
- return null;
2186
- }
2187
- try {
2188
- return JSON.parse(response);
2189
- } catch {
2190
- return response;
2191
- }
2192
- }
2193
951
  async getAllFlagsAndPayloads(distinctId, groups = {}, personProperties = {}, groupProperties = {}, flagKeysToExplicitlyEvaluate) {
2194
952
  await this.loadFeatureFlags();
2195
953
  const response = {};
@@ -2198,9 +956,11 @@ class FeatureFlagsPoller {
2198
956
  const flagsToEvaluate = flagKeysToExplicitlyEvaluate ? flagKeysToExplicitlyEvaluate.map(key => this.featureFlagsByKey[key]).filter(Boolean) : this.featureFlags;
2199
957
  await Promise.all(flagsToEvaluate.map(async flag => {
2200
958
  try {
2201
- const matchValue = await this.computeFlagLocally(flag, distinctId, groups, personProperties, groupProperties);
959
+ const {
960
+ value: matchValue,
961
+ payload: matchPayload
962
+ } = await this.computeFlagAndPayloadLocally(flag, distinctId, groups, personProperties, groupProperties);
2202
963
  response[flag.key] = matchValue;
2203
- const matchPayload = await this.computeFeatureFlagPayloadLocally(flag.key, matchValue);
2204
964
  if (matchPayload) {
2205
965
  payloads[flag.key] = matchPayload;
2206
966
  }
@@ -2219,7 +979,30 @@ class FeatureFlagsPoller {
2219
979
  fallbackToFlags
2220
980
  };
2221
981
  }
2222
- async computeFlagLocally(flag, distinctId, groups = {}, personProperties = {}, groupProperties = {}) {
982
+ async computeFlagAndPayloadLocally(flag, distinctId, groups = {}, personProperties = {}, groupProperties = {}, matchValue) {
983
+ // Always ensure flags are loaded for payload computation
984
+ await this.loadFeatureFlags();
985
+ if (!this.loadedSuccessfullyOnce) {
986
+ return {
987
+ value: false,
988
+ payload: null
989
+ };
990
+ }
991
+ let flagValue;
992
+ // If matchValue is provided, use it directly; otherwise evaluate the flag
993
+ if (matchValue !== undefined) {
994
+ flagValue = matchValue;
995
+ } else {
996
+ flagValue = await this.computeFlagValueLocally(flag, distinctId, groups, personProperties, groupProperties);
997
+ }
998
+ // Always compute payload based on the final flagValue (whether provided or computed)
999
+ const payload = this.getFeatureFlagPayload(flag.key, flagValue);
1000
+ return {
1001
+ value: flagValue,
1002
+ payload
1003
+ };
1004
+ }
1005
+ async computeFlagValueLocally(flag, distinctId, groups = {}, personProperties = {}, groupProperties = {}) {
2223
1006
  if (flag.ensure_experience_continuity) {
2224
1007
  throw new InconclusiveMatchError('Flag has experience continuity enabled');
2225
1008
  }
@@ -2244,7 +1027,98 @@ class FeatureFlagsPoller {
2244
1027
  return await this.matchFeatureFlagProperties(flag, distinctId, personProperties);
2245
1028
  }
2246
1029
  }
2247
- async matchFeatureFlagProperties(flag, distinctId, properties) {
1030
+ getFeatureFlagPayload(key, flagValue) {
1031
+ let payload = null;
1032
+ if (flagValue !== false && flagValue !== null && flagValue !== undefined) {
1033
+ if (typeof flagValue == 'boolean') {
1034
+ payload = this.featureFlagsByKey?.[key]?.filters?.payloads?.[flagValue.toString()] || null;
1035
+ } else if (typeof flagValue == 'string') {
1036
+ payload = this.featureFlagsByKey?.[key]?.filters?.payloads?.[flagValue] || null;
1037
+ }
1038
+ if (payload !== null && payload !== undefined) {
1039
+ // If payload is already an object, return it directly
1040
+ if (typeof payload === 'object') {
1041
+ return payload;
1042
+ }
1043
+ // If payload is a string, try to parse it as JSON
1044
+ if (typeof payload === 'string') {
1045
+ try {
1046
+ return JSON.parse(payload);
1047
+ } catch {
1048
+ // If parsing fails, return the string as is
1049
+ return payload;
1050
+ }
1051
+ }
1052
+ // For other types, return as is
1053
+ return payload;
1054
+ }
1055
+ }
1056
+ return null;
1057
+ }
1058
+ async evaluateFlagDependency(property, distinctId, properties, evaluationCache) {
1059
+ const targetFlagKey = property.key;
1060
+ if (!this.featureFlagsByKey) {
1061
+ throw new InconclusiveMatchError('Feature flags not available for dependency evaluation');
1062
+ }
1063
+ // Check if dependency_chain is present - it should always be provided for flag dependencies
1064
+ if (!('dependency_chain' in property)) {
1065
+ throw new InconclusiveMatchError(`Flag dependency property for '${targetFlagKey}' is missing required 'dependency_chain' field`);
1066
+ }
1067
+ const dependencyChain = property.dependency_chain;
1068
+ // Check for missing or invalid dependency chain (This should never happen, but being defensive)
1069
+ if (!Array.isArray(dependencyChain)) {
1070
+ throw new InconclusiveMatchError(`Flag dependency property for '${targetFlagKey}' has an invalid 'dependency_chain' (expected array, got ${typeof dependencyChain})`);
1071
+ }
1072
+ // Handle circular dependency (empty chain means circular) (This should never happen, but being defensive)
1073
+ if (dependencyChain.length === 0) {
1074
+ throw new InconclusiveMatchError(`Circular dependency detected for flag '${targetFlagKey}' (empty dependency chain)`);
1075
+ }
1076
+ // Evaluate all dependencies in the chain order
1077
+ for (const depFlagKey of dependencyChain) {
1078
+ if (!(depFlagKey in evaluationCache)) {
1079
+ // Need to evaluate this dependency first
1080
+ const depFlag = this.featureFlagsByKey[depFlagKey];
1081
+ if (!depFlag) {
1082
+ // Missing flag dependency - cannot evaluate locally
1083
+ throw new InconclusiveMatchError(`Missing flag dependency '${depFlagKey}' for flag '${targetFlagKey}'`);
1084
+ } else if (!depFlag.active) {
1085
+ // Inactive flag evaluates to false
1086
+ evaluationCache[depFlagKey] = false;
1087
+ } else {
1088
+ // Recursively evaluate the dependency
1089
+ try {
1090
+ const depResult = await this.matchFeatureFlagProperties(depFlag, distinctId, properties, evaluationCache);
1091
+ evaluationCache[depFlagKey] = depResult;
1092
+ } catch (error) {
1093
+ // If we can't evaluate a dependency, store throw InconclusiveMatchError(`Missing flag dependency '${depFlagKey}' for flag '${targetFlagKey}'`)
1094
+ throw new InconclusiveMatchError(`Error evaluating flag dependency '${depFlagKey}' for flag '${targetFlagKey}': ${error}`);
1095
+ }
1096
+ }
1097
+ }
1098
+ // Check if dependency evaluation was inconclusive
1099
+ const cachedResult = evaluationCache[depFlagKey];
1100
+ if (cachedResult === null || cachedResult === undefined) {
1101
+ throw new InconclusiveMatchError(`Dependency '${depFlagKey}' could not be evaluated`);
1102
+ }
1103
+ }
1104
+ // The target flag is specified in property.key (This should match the last element in the dependency chain)
1105
+ const targetFlagValue = evaluationCache[targetFlagKey];
1106
+ return this.flagEvaluatesToExpectedValue(property.value, targetFlagValue);
1107
+ }
1108
+ flagEvaluatesToExpectedValue(expectedValue, flagValue) {
1109
+ // If the expected value is a boolean, then return true if the flag evaluated to true (or any string variant)
1110
+ // If the expected value is false, then only return true if the flag evaluated to false.
1111
+ if (typeof expectedValue === 'boolean') {
1112
+ return expectedValue === flagValue || typeof flagValue === 'string' && flagValue !== '' && expectedValue === true;
1113
+ }
1114
+ // If the expected value is a string, then return true if and only if the flag evaluated to the expected value.
1115
+ if (typeof expectedValue === 'string') {
1116
+ return flagValue === expectedValue;
1117
+ }
1118
+ // The `flag_evaluates_to` operator is not supported for numbers and arrays.
1119
+ return false;
1120
+ }
1121
+ async matchFeatureFlagProperties(flag, distinctId, properties, evaluationCache = {}) {
2248
1122
  const flagFilters = flag.filters || {};
2249
1123
  const flagConditions = flagFilters.groups || [];
2250
1124
  let isInconclusive = false;
@@ -2266,7 +1140,7 @@ class FeatureFlagsPoller {
2266
1140
  });
2267
1141
  for (const condition of sortedFlagConditions) {
2268
1142
  try {
2269
- if (await this.isConditionMatch(flag, distinctId, condition, properties)) {
1143
+ if (await this.isConditionMatch(flag, distinctId, condition, properties, evaluationCache)) {
2270
1144
  const variantOverride = condition.variant;
2271
1145
  const flagVariants = flagFilters.multivariate?.variants || [];
2272
1146
  if (variantOverride && flagVariants.some(variant => variant.key === variantOverride)) {
@@ -2292,7 +1166,7 @@ class FeatureFlagsPoller {
2292
1166
  // We can only return False when all conditions are False
2293
1167
  return false;
2294
1168
  }
2295
- async isConditionMatch(flag, distinctId, condition, properties) {
1169
+ async isConditionMatch(flag, distinctId, condition, properties, evaluationCache = {}) {
2296
1170
  const rolloutPercentage = condition.rollout_percentage;
2297
1171
  const warnFunction = msg => {
2298
1172
  this.logMsgIfDebug(() => console.warn(msg));
@@ -2304,8 +1178,7 @@ class FeatureFlagsPoller {
2304
1178
  if (propertyType === 'cohort') {
2305
1179
  matches = matchCohort(prop, properties, this.cohorts, this.debugMode);
2306
1180
  } else if (propertyType === 'flag') {
2307
- 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'}'`));
2308
- continue;
1181
+ matches = await this.evaluateFlagDependency(prop, distinctId, properties, evaluationCache);
2309
1182
  } else {
2310
1183
  matches = matchProperty(prop, properties, warnFunction);
2311
1184
  }
@@ -2354,18 +1227,18 @@ class FeatureFlagsPoller {
2354
1227
  await this._loadFeatureFlags();
2355
1228
  }
2356
1229
  }
2357
- /**
2358
- * Returns true if the feature flags poller has loaded successfully at least once and has more than 0 feature flags.
2359
- * This is useful to check if local evaluation is ready before calling getFeatureFlag.
1230
+ /**
1231
+ * Returns true if the feature flags poller has loaded successfully at least once and has more than 0 feature flags.
1232
+ * This is useful to check if local evaluation is ready before calling getFeatureFlag.
2360
1233
  */
2361
1234
  isLocalEvaluationReady() {
2362
1235
  return (this.loadedSuccessfullyOnce ?? false) && (this.featureFlags?.length ?? 0) > 0;
2363
1236
  }
2364
- /**
2365
- * If a client is misconfigured with an invalid or improper API key, the polling interval is doubled each time
2366
- * until a successful request is made, up to a maximum of 60 seconds.
2367
- *
2368
- * @returns The polling interval to use for the next request.
1237
+ /**
1238
+ * If a client is misconfigured with an invalid or improper API key, the polling interval is doubled each time
1239
+ * until a successful request is made, up to a maximum of 60 seconds.
1240
+ *
1241
+ * @returns The polling interval to use for the next request.
2369
1242
  */
2370
1243
  getPollingInterval() {
2371
1244
  if (!this.shouldBeginExponentialBackoff) {
@@ -2570,6 +1443,10 @@ function matchProperty(property, propertyValues, warnFunction) {
2570
1443
  case 'is_date_after':
2571
1444
  case 'is_date_before':
2572
1445
  {
1446
+ // Boolean values should never be used with date operations
1447
+ if (typeof value === 'boolean') {
1448
+ throw new InconclusiveMatchError(`Date operations cannot be performed on boolean values`);
1449
+ }
2573
1450
  let parsedDate = relativeDateParseForFeatureFlagMatching(String(value));
2574
1451
  if (parsedDate == null) {
2575
1452
  parsedDate = convertToDateTime(value);
@@ -2753,6 +1630,37 @@ class PostHogMemoryStorage {
2753
1630
  }
2754
1631
  }
2755
1632
 
1633
+ const _createLogger = (prefix, logMsgIfDebug) => {
1634
+ const logger = {
1635
+ _log: (level, ...args) => {
1636
+ logMsgIfDebug(() => {
1637
+ const consoleLog = console[level];
1638
+ consoleLog(prefix, ...args);
1639
+ });
1640
+ },
1641
+ info: (...args) => {
1642
+ logger._log('log', ...args);
1643
+ },
1644
+ warn: (...args) => {
1645
+ logger._log('warn', ...args);
1646
+ },
1647
+ error: (...args) => {
1648
+ logger._log('error', ...args);
1649
+ },
1650
+ critical: (...args) => {
1651
+ // Critical errors are always logged to the console
1652
+ // eslint-disable-next-line no-console
1653
+ console.error(prefix, ...args);
1654
+ },
1655
+ uninitializedWarning: methodName => {
1656
+ logger.error(`You must initialize PostHog before calling ${methodName}`);
1657
+ },
1658
+ createLogger: additionalPrefix => _createLogger(`${prefix} ${additionalPrefix}`, logMsgIfDebug)
1659
+ };
1660
+ return logger;
1661
+ };
1662
+ const createLogger = logMsgIfDebug => _createLogger('[PostHog.js]', logMsgIfDebug);
1663
+
2756
1664
  // Standard local evaluation rate limit is 600 per minute (10 per second),
2757
1665
  // so the fastest a poller should ever be set is 100ms.
2758
1666
  const MINIMUM_POLLING_INTERVAL = 100;
@@ -2764,6 +1672,7 @@ class PostHogBackendClient extends PostHogCoreStateless {
2764
1672
  super(apiKey, options);
2765
1673
  this._memoryStorage = new PostHogMemoryStorage();
2766
1674
  this.options = options;
1675
+ this.logger = createLogger(this.logMsgIfDebug.bind(this));
2767
1676
  this.options.featureFlagsPollingInterval = typeof options.featureFlagsPollingInterval === 'number' ? Math.max(options.featureFlagsPollingInterval, MINIMUM_POLLING_INTERVAL) : THIRTY_SECONDS;
2768
1677
  if (options.personalApiKey) {
2769
1678
  if (options.personalApiKey.includes('phc_')) {
@@ -2777,6 +1686,7 @@ class PostHogBackendClient extends PostHogCoreStateless {
2777
1686
  personalApiKey: options.personalApiKey,
2778
1687
  projectApiKey: apiKey,
2779
1688
  timeout: options.requestTimeout ?? 10000,
1689
+ // 10 seconds
2780
1690
  host: this.host,
2781
1691
  fetch: options.fetch,
2782
1692
  onError: err => {
@@ -2789,7 +1699,7 @@ class PostHogBackendClient extends PostHogCoreStateless {
2789
1699
  });
2790
1700
  }
2791
1701
  }
2792
- this.errorTracking = new ErrorTracking(this, options);
1702
+ this.errorTracking = new ErrorTracking(this, options, this.logger);
2793
1703
  this.distinctIdHasSentFlagCalls = {};
2794
1704
  this.maxCacheSize = options.maxCacheSize || MAX_CACHE_SIZE;
2795
1705
  }
@@ -2832,11 +1742,25 @@ class PostHogBackendClient extends PostHogCoreStateless {
2832
1742
  disableGeoip,
2833
1743
  uuid
2834
1744
  } = props;
1745
+ // Run before_send if configured
1746
+ const eventMessage = this._runBeforeSend({
1747
+ distinctId,
1748
+ event,
1749
+ properties,
1750
+ groups,
1751
+ sendFeatureFlags,
1752
+ timestamp,
1753
+ disableGeoip,
1754
+ uuid
1755
+ });
1756
+ if (!eventMessage) {
1757
+ return;
1758
+ }
2835
1759
  const _capture = props => {
2836
- super.captureStateless(distinctId, event, props, {
2837
- timestamp,
2838
- disableGeoip,
2839
- uuid
1760
+ super.captureStateless(eventMessage.distinctId, eventMessage.event, props, {
1761
+ timestamp: eventMessage.timestamp,
1762
+ disableGeoip: eventMessage.disableGeoip,
1763
+ uuid: eventMessage.uuid
2840
1764
  });
2841
1765
  };
2842
1766
  // :TRICKY: If we flush, or need to shut down, to not lose events we want this promise to resolve before we flush
@@ -2871,8 +1795,8 @@ class PostHogBackendClient extends PostHogCoreStateless {
2871
1795
  // No matter what - capture the event
2872
1796
  _capture({
2873
1797
  ...additionalProperties,
2874
- ...properties,
2875
- $groups: groups
1798
+ ...(eventMessage.properties || {}),
1799
+ $groups: eventMessage.groups || groups
2876
1800
  });
2877
1801
  });
2878
1802
  this.addPendingPromise(capturePromise);
@@ -2891,11 +1815,25 @@ class PostHogBackendClient extends PostHogCoreStateless {
2891
1815
  disableGeoip,
2892
1816
  uuid
2893
1817
  } = props;
1818
+ // Run before_send if configured
1819
+ const eventMessage = this._runBeforeSend({
1820
+ distinctId,
1821
+ event,
1822
+ properties,
1823
+ groups,
1824
+ sendFeatureFlags,
1825
+ timestamp,
1826
+ disableGeoip,
1827
+ uuid
1828
+ });
1829
+ if (!eventMessage) {
1830
+ return;
1831
+ }
2894
1832
  const _capture = props => {
2895
- return super.captureStatelessImmediate(distinctId, event, props, {
2896
- timestamp,
2897
- disableGeoip,
2898
- uuid
1833
+ return super.captureStatelessImmediate(eventMessage.distinctId, eventMessage.event, props, {
1834
+ timestamp: eventMessage.timestamp,
1835
+ disableGeoip: eventMessage.disableGeoip,
1836
+ uuid: eventMessage.uuid
2899
1837
  });
2900
1838
  };
2901
1839
  const capturePromise = Promise.resolve().then(async () => {
@@ -2929,8 +1867,8 @@ class PostHogBackendClient extends PostHogCoreStateless {
2929
1867
  // No matter what - capture the event
2930
1868
  _capture({
2931
1869
  ...additionalProperties,
2932
- ...properties,
2933
- $groups: groups
1870
+ ...(eventMessage.properties || {}),
1871
+ $groups: eventMessage.groups || groups
2934
1872
  });
2935
1873
  });
2936
1874
  await capturePromise;
@@ -3082,19 +2020,17 @@ class PostHogBackendClient extends PostHogCoreStateless {
3082
2020
  let response = undefined;
3083
2021
  const localEvaluationEnabled = this.featureFlagsPoller !== undefined;
3084
2022
  if (localEvaluationEnabled) {
3085
- // Try to get match value locally if not provided
3086
- if (!matchValue) {
3087
- matchValue = await this.getFeatureFlag(key, distinctId, {
3088
- ...options,
3089
- onlyEvaluateLocally: true,
3090
- sendFeatureFlagEvents: false
3091
- });
3092
- }
3093
- if (matchValue) {
3094
- response = await this.featureFlagsPoller?.computeFeatureFlagPayloadLocally(key, matchValue);
2023
+ // Ensure flags are loaded before checking for the specific flag
2024
+ await this.featureFlagsPoller?.loadFeatureFlags();
2025
+ const flag = this.featureFlagsPoller?.featureFlagsByKey[key];
2026
+ if (flag) {
2027
+ const result = await this.featureFlagsPoller?.computeFlagAndPayloadLocally(flag, distinctId, groups, personProperties, groupProperties, matchValue);
2028
+ if (result) {
2029
+ matchValue = result.value;
2030
+ response = result.payload;
2031
+ }
3095
2032
  }
3096
2033
  }
3097
- //}
3098
2034
  // set defaults
3099
2035
  if (onlyEvaluateLocally == undefined) {
3100
2036
  onlyEvaluateLocally = false;
@@ -3102,10 +2038,6 @@ class PostHogBackendClient extends PostHogCoreStateless {
3102
2038
  if (sendFeatureFlagEvents == undefined) {
3103
2039
  sendFeatureFlagEvents = true;
3104
2040
  }
3105
- // set defaults
3106
- if (onlyEvaluateLocally == undefined) {
3107
- onlyEvaluateLocally = false;
3108
- }
3109
2041
  const payloadWasLocallyEvaluated = response !== undefined;
3110
2042
  if (!payloadWasLocallyEvaluated && !onlyEvaluateLocally) {
3111
2043
  response = await super.getFeatureFlagPayloadStateless(key, distinctId, groups, personProperties, groupProperties, disableGeoip);
@@ -3200,9 +2132,9 @@ class PostHogBackendClient extends PostHogCoreStateless {
3200
2132
  disableGeoip
3201
2133
  }, distinctId);
3202
2134
  }
3203
- /**
3204
- * Reloads the feature flag definitions from the server for local evaluation.
3205
- * This is useful to call if you want to ensure that the feature flags are up to date before calling getFeatureFlag.
2135
+ /**
2136
+ * Reloads the feature flag definitions from the server for local evaluation.
2137
+ * This is useful to call if you want to ensure that the feature flags are up to date before calling getFeatureFlag.
3206
2138
  */
3207
2139
  async reloadFeatureFlags() {
3208
2140
  await this.featureFlagsPoller?.loadFeatureFlags(true);
@@ -3215,7 +2147,7 @@ class PostHogBackendClient extends PostHogCoreStateless {
3215
2147
  if (!this.options.personalApiKey) {
3216
2148
  return undefined;
3217
2149
  }
3218
- const url = `${this.host}/api/projects/@current/feature_flags/${flagKey}/remote_config/`;
2150
+ const url = `${this.host}/api/projects/@current/feature_flags/${flagKey}/remote_config?token=${encodeURIComponent(this.apiKey)}`;
3219
2151
  const options = {
3220
2152
  method: 'GET',
3221
2153
  headers: {
@@ -3349,6 +2281,26 @@ class PostHogBackendClient extends PostHogCoreStateless {
3349
2281
  }, distinctId, additionalProperties);
3350
2282
  return await this.captureImmediate(evtMsg);
3351
2283
  }
2284
+ _runBeforeSend(eventMessage) {
2285
+ const beforeSend = this.options.before_send;
2286
+ if (!beforeSend) {
2287
+ return eventMessage;
2288
+ }
2289
+ const fns = Array.isArray(beforeSend) ? beforeSend : [beforeSend];
2290
+ let result = eventMessage;
2291
+ for (const fn of fns) {
2292
+ result = fn(result);
2293
+ if (!result) {
2294
+ this.logMsgIfDebug(() => console.info(`Event '${eventMessage.event}' was rejected in beforeSend function`));
2295
+ return null;
2296
+ }
2297
+ if (!result.properties || Object.keys(result.properties).length === 0) {
2298
+ const message = `Event '${result.event}' has no properties after beforeSend function, this is likely an error.`;
2299
+ this.logMsgIfDebug(() => console.warn(message));
2300
+ }
2301
+ }
2302
+ return result;
2303
+ }
3352
2304
  }
3353
2305
 
3354
2306
  // Portions of this file are derived from getsentry/sentry-javascript by Software, Inc. dba Sentry
@@ -3431,7 +2383,7 @@ function node(getModule) {
3431
2383
  }
3432
2384
  return {
3433
2385
  filename: filename ? decodeURI(filename) : undefined,
3434
- module: getModule ? getModule(filename) : undefined,
2386
+ module: undefined,
3435
2387
  function: functionName,
3436
2388
  lineno: _parseIntOrUndefined(lineMatch[3]),
3437
2389
  colno: _parseIntOrUndefined(lineMatch[4]),
@@ -3448,8 +2400,8 @@ function node(getModule) {
3448
2400
  return undefined;
3449
2401
  };
3450
2402
  }
3451
- /**
3452
- * Does this filename look like it's part of the app code?
2403
+ /**
2404
+ * Does this filename look like it's part of the app code?
3453
2405
  */
3454
2406
  function filenameIsInApp(filename, isNative = false) {
3455
2407
  const isInternal = isNative || filename &&
@@ -3470,10 +2422,10 @@ function _parseIntOrUndefined(input) {
3470
2422
  return parseInt(input || '', 10) || undefined;
3471
2423
  }
3472
2424
  function nodeStackLineParser(getModule) {
3473
- return [90, node(getModule)];
2425
+ return [90, node()];
3474
2426
  }
3475
2427
  function createStackParser(getModule) {
3476
- const parsers = [nodeStackLineParser(getModule)];
2428
+ const parsers = [nodeStackLineParser()];
3477
2429
  const sortedParsers = parsers.sort((a, b) => a[0] - b[0]).map(p => p[1]);
3478
2430
  return (stack, skipFirstLines = 0) => {
3479
2431
  const frames = [];