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