posthog-node 4.2.0 → 4.2.2
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/CHANGELOG.md +8 -0
- package/CONTRIBUTING.md +14 -0
- package/lib/index.cjs.js +106 -273
- package/lib/index.cjs.js.map +1 -1
- package/lib/index.d.ts +23 -18
- package/lib/index.esm.js +106 -273
- package/lib/index.esm.js.map +1 -1
- package/lib/posthog-core/src/index.d.ts +5 -3
- package/lib/posthog-core/src/types.d.ts +16 -13
- package/lib/posthog-node/src/extensions/sentry-integration.d.ts +2 -2
- package/lib/posthog-node/src/feature-flags.d.ts +2 -1
- package/lib/posthog-node/src/fetch.d.ts +1 -1
- package/lib/posthog-node/src/posthog-node.d.ts +1 -1
- package/lib/posthog-node/src/types.d.ts +5 -5
- package/package.json +1 -1
- package/src/feature-flags.ts +14 -14
- package/src/posthog-node.ts +0 -1
package/lib/index.cjs.js
CHANGED
|
@@ -4,7 +4,7 @@ Object.defineProperty(exports, '__esModule', { value: true });
|
|
|
4
4
|
|
|
5
5
|
var rusha = require('rusha');
|
|
6
6
|
|
|
7
|
-
var version = "4.2.
|
|
7
|
+
var version = "4.2.2";
|
|
8
8
|
|
|
9
9
|
var PostHogPersistedProperty;
|
|
10
10
|
(function (PostHogPersistedProperty) {
|
|
@@ -22,6 +22,7 @@ var PostHogPersistedProperty;
|
|
|
22
22
|
PostHogPersistedProperty["GroupProperties"] = "group_properties";
|
|
23
23
|
PostHogPersistedProperty["InstalledAppBuild"] = "installed_app_build";
|
|
24
24
|
PostHogPersistedProperty["InstalledAppVersion"] = "installed_app_version";
|
|
25
|
+
PostHogPersistedProperty["SessionReplay"] = "session_replay";
|
|
25
26
|
})(PostHogPersistedProperty || (PostHogPersistedProperty = {}));
|
|
26
27
|
|
|
27
28
|
function assert(truthyValue, message) {
|
|
@@ -966,7 +967,7 @@ class PostHogCoreStateless {
|
|
|
966
967
|
this.maxBatchSize = Math.max(this.flushAt, options?.maxBatchSize ?? 100);
|
|
967
968
|
this.maxQueueSize = Math.max(this.flushAt, options?.maxQueueSize ?? 1000);
|
|
968
969
|
this.flushInterval = options?.flushInterval ?? 10000;
|
|
969
|
-
this.captureMode = options?.captureMode || '
|
|
970
|
+
this.captureMode = options?.captureMode || 'json';
|
|
970
971
|
// If enable is explicitly set to false we override the optout
|
|
971
972
|
this.defaultOptIn = options?.defaultOptIn ?? true;
|
|
972
973
|
this._retryOptions = {
|
|
@@ -983,11 +984,14 @@ class PostHogCoreStateless {
|
|
|
983
984
|
this._initPromise = Promise.resolve();
|
|
984
985
|
this._isInitialized = true;
|
|
985
986
|
}
|
|
987
|
+
logMsgIfDebug(fn) {
|
|
988
|
+
if (this.isDebug) {
|
|
989
|
+
fn();
|
|
990
|
+
}
|
|
991
|
+
}
|
|
986
992
|
wrap(fn) {
|
|
987
993
|
if (this.disabled) {
|
|
988
|
-
|
|
989
|
-
console.warn('[PostHog] The client is disabled');
|
|
990
|
-
}
|
|
994
|
+
this.logMsgIfDebug(() => console.warn('[PostHog] The client is disabled'));
|
|
991
995
|
return;
|
|
992
996
|
}
|
|
993
997
|
if (this._isInitialized) {
|
|
@@ -1031,6 +1035,9 @@ class PostHogCoreStateless {
|
|
|
1031
1035
|
get isDebug() {
|
|
1032
1036
|
return !!this.removeDebugCallback;
|
|
1033
1037
|
}
|
|
1038
|
+
get isDisabled() {
|
|
1039
|
+
return this.disabled;
|
|
1040
|
+
}
|
|
1034
1041
|
buildPayload(payload) {
|
|
1035
1042
|
return {
|
|
1036
1043
|
distinct_id: payload.distinct_id,
|
|
@@ -1227,7 +1234,7 @@ class PostHogCoreStateless {
|
|
|
1227
1234
|
const queue = this.getPersistedProperty(PostHogPersistedProperty.Queue) || [];
|
|
1228
1235
|
if (queue.length >= this.maxQueueSize) {
|
|
1229
1236
|
queue.shift();
|
|
1230
|
-
console.info('Queue is full, the oldest event is dropped.');
|
|
1237
|
+
this.logMsgIfDebug(() => console.info('Queue is full, the oldest event is dropped.'));
|
|
1231
1238
|
}
|
|
1232
1239
|
queue.push({ message });
|
|
1233
1240
|
this.setPersistedProperty(PostHogPersistedProperty.Queue, queue);
|
|
@@ -1359,33 +1366,45 @@ class PostHogCoreStateless {
|
|
|
1359
1366
|
}, { ...this._retryOptions, ...retryOptions });
|
|
1360
1367
|
}
|
|
1361
1368
|
async shutdown(shutdownTimeoutMs = 30000) {
|
|
1369
|
+
// A little tricky - we want to have a max shutdown time and enforce it, even if that means we have some
|
|
1370
|
+
// dangling promises. We'll keep track of the timeout and resolve/reject based on that.
|
|
1362
1371
|
await this._initPromise;
|
|
1372
|
+
let hasTimedOut = false;
|
|
1363
1373
|
this.clearFlushTimer();
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
break;
|
|
1374
|
+
const doShutdown = async () => {
|
|
1375
|
+
try {
|
|
1376
|
+
await Promise.all(Object.values(this.pendingPromises));
|
|
1377
|
+
while (true) {
|
|
1378
|
+
const queue = this.getPersistedProperty(PostHogPersistedProperty.Queue) || [];
|
|
1379
|
+
if (queue.length === 0) {
|
|
1380
|
+
break;
|
|
1381
|
+
}
|
|
1382
|
+
// flush again to make sure we send all events, some of which might've been added
|
|
1383
|
+
// while we were waiting for the pending promises to resolve
|
|
1384
|
+
// For example, see sendFeatureFlags in posthog-node/src/posthog-node.ts::capture
|
|
1385
|
+
await this.flush();
|
|
1386
|
+
if (hasTimedOut) {
|
|
1387
|
+
break;
|
|
1388
|
+
}
|
|
1380
1389
|
}
|
|
1381
1390
|
}
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1391
|
+
catch (e) {
|
|
1392
|
+
if (!isPostHogFetchError(e)) {
|
|
1393
|
+
throw e;
|
|
1394
|
+
}
|
|
1395
|
+
this.logMsgIfDebug(() => console.error('Error while shutting down PostHog', e));
|
|
1386
1396
|
}
|
|
1387
|
-
|
|
1388
|
-
|
|
1397
|
+
};
|
|
1398
|
+
return Promise.race([
|
|
1399
|
+
new Promise((_, reject) => {
|
|
1400
|
+
safeSetTimeout(() => {
|
|
1401
|
+
this.logMsgIfDebug(() => console.error('Timed out while shutting down PostHog'));
|
|
1402
|
+
hasTimedOut = true;
|
|
1403
|
+
reject('Timeout while shutting down PostHog. Some events may not have been sent.');
|
|
1404
|
+
}, shutdownTimeoutMs);
|
|
1405
|
+
}),
|
|
1406
|
+
doShutdown(),
|
|
1407
|
+
]);
|
|
1389
1408
|
}
|
|
1390
1409
|
}
|
|
1391
1410
|
|
|
@@ -1409,14 +1428,13 @@ class PostHogMemoryStorage {
|
|
|
1409
1428
|
* This is currently solved by using the global fetch if available instead.
|
|
1410
1429
|
* See https://github.com/PostHog/posthog-js-lite/issues/127 for more info
|
|
1411
1430
|
*/
|
|
1412
|
-
let _fetch =
|
|
1431
|
+
let _fetch =
|
|
1432
|
+
// eslint-disable-next-line @typescript-eslint/prefer-ts-expect-error
|
|
1413
1433
|
// @ts-ignore
|
|
1414
1434
|
typeof fetch !== 'undefined' ? fetch : typeof global.fetch !== 'undefined' ? global.fetch : undefined;
|
|
1415
|
-
|
|
1416
1435
|
if (!_fetch) {
|
|
1417
1436
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
1418
1437
|
const axios = require('axios');
|
|
1419
|
-
|
|
1420
1438
|
_fetch = async (url, options) => {
|
|
1421
1439
|
const res = await axios.request({
|
|
1422
1440
|
url,
|
|
@@ -1433,14 +1451,13 @@ if (!_fetch) {
|
|
|
1433
1451
|
json: async () => res.data
|
|
1434
1452
|
};
|
|
1435
1453
|
};
|
|
1436
|
-
}
|
|
1437
|
-
|
|
1438
|
-
|
|
1454
|
+
}
|
|
1455
|
+
// NOTE: We have to export this as default, even though we prefer named exports as we are relying on detecting "fetch" in the global scope
|
|
1439
1456
|
var fetch$1 = _fetch;
|
|
1440
1457
|
|
|
1458
|
+
// eslint-disable-next-line
|
|
1441
1459
|
const LONG_SCALE = 0xfffffffffffffff;
|
|
1442
1460
|
const NULL_VALUES_ALLOWED_OPERATORS = ['is_not'];
|
|
1443
|
-
|
|
1444
1461
|
class ClientError extends Error {
|
|
1445
1462
|
constructor(message) {
|
|
1446
1463
|
super();
|
|
@@ -1449,22 +1466,18 @@ class ClientError extends Error {
|
|
|
1449
1466
|
this.message = message;
|
|
1450
1467
|
Object.setPrototypeOf(this, ClientError.prototype);
|
|
1451
1468
|
}
|
|
1452
|
-
|
|
1453
1469
|
}
|
|
1454
|
-
|
|
1455
1470
|
class InconclusiveMatchError extends Error {
|
|
1456
1471
|
constructor(message) {
|
|
1457
1472
|
super(message);
|
|
1458
1473
|
this.name = this.constructor.name;
|
|
1459
|
-
Error.captureStackTrace(this, this.constructor);
|
|
1474
|
+
Error.captureStackTrace(this, this.constructor);
|
|
1475
|
+
// instanceof doesn't work in ES3 or ES5
|
|
1460
1476
|
// https://www.dannyguo.com/blog/how-to-fix-instanceof-not-working-for-custom-errors-in-typescript/
|
|
1461
1477
|
// this is the workaround
|
|
1462
|
-
|
|
1463
1478
|
Object.setPrototypeOf(this, InconclusiveMatchError.prototype);
|
|
1464
1479
|
}
|
|
1465
|
-
|
|
1466
1480
|
}
|
|
1467
|
-
|
|
1468
1481
|
class FeatureFlagsPoller {
|
|
1469
1482
|
constructor({
|
|
1470
1483
|
pollingInterval,
|
|
@@ -1486,81 +1499,69 @@ class FeatureFlagsPoller {
|
|
|
1486
1499
|
this.timeout = timeout;
|
|
1487
1500
|
this.projectApiKey = projectApiKey;
|
|
1488
1501
|
this.host = host;
|
|
1489
|
-
this.poller = undefined;
|
|
1490
|
-
|
|
1502
|
+
this.poller = undefined;
|
|
1503
|
+
// NOTE: as any is required here as the AbortSignal typing is slightly misaligned but works just fine
|
|
1491
1504
|
this.fetch = options.fetch || fetch$1;
|
|
1492
1505
|
this.onError = options.onError;
|
|
1493
1506
|
this.customHeaders = customHeaders;
|
|
1494
1507
|
void this.loadFeatureFlags();
|
|
1495
1508
|
}
|
|
1496
|
-
|
|
1497
1509
|
debug(enabled = true) {
|
|
1498
1510
|
this.debugMode = enabled;
|
|
1499
1511
|
}
|
|
1500
|
-
|
|
1512
|
+
logMsgIfDebug(fn) {
|
|
1513
|
+
if (this.debugMode) {
|
|
1514
|
+
fn();
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1501
1517
|
async getFeatureFlag(key, distinctId, groups = {}, personProperties = {}, groupProperties = {}) {
|
|
1502
1518
|
await this.loadFeatureFlags();
|
|
1503
1519
|
let response = undefined;
|
|
1504
1520
|
let featureFlag = undefined;
|
|
1505
|
-
|
|
1506
1521
|
if (!this.loadedSuccessfullyOnce) {
|
|
1507
1522
|
return response;
|
|
1508
1523
|
}
|
|
1509
|
-
|
|
1510
1524
|
for (const flag of this.featureFlags) {
|
|
1511
1525
|
if (key === flag.key) {
|
|
1512
1526
|
featureFlag = flag;
|
|
1513
1527
|
break;
|
|
1514
1528
|
}
|
|
1515
1529
|
}
|
|
1516
|
-
|
|
1517
1530
|
if (featureFlag !== undefined) {
|
|
1518
1531
|
try {
|
|
1519
1532
|
response = this.computeFlagLocally(featureFlag, distinctId, groups, personProperties, groupProperties);
|
|
1520
|
-
|
|
1521
|
-
if (this.debugMode) {
|
|
1522
|
-
console.debug(`Successfully computed flag locally: ${key} -> ${response}`);
|
|
1523
|
-
}
|
|
1533
|
+
this.logMsgIfDebug(() => console.debug(`Successfully computed flag locally: ${key} -> ${response}`));
|
|
1524
1534
|
} catch (e) {
|
|
1525
1535
|
if (e instanceof InconclusiveMatchError) {
|
|
1526
|
-
|
|
1527
|
-
console.debug(`InconclusiveMatchError when computing flag locally: ${key}: ${e}`);
|
|
1528
|
-
}
|
|
1536
|
+
this.logMsgIfDebug(() => console.debug(`InconclusiveMatchError when computing flag locally: ${key}: ${e}`));
|
|
1529
1537
|
} else if (e instanceof Error) {
|
|
1530
1538
|
this.onError?.(new Error(`Error computing flag locally: ${key}: ${e}`));
|
|
1531
1539
|
}
|
|
1532
1540
|
}
|
|
1533
1541
|
}
|
|
1534
|
-
|
|
1535
1542
|
return response;
|
|
1536
1543
|
}
|
|
1537
|
-
|
|
1538
1544
|
async computeFeatureFlagPayloadLocally(key, matchValue) {
|
|
1539
1545
|
await this.loadFeatureFlags();
|
|
1540
1546
|
let response = undefined;
|
|
1541
|
-
|
|
1542
1547
|
if (!this.loadedSuccessfullyOnce) {
|
|
1543
1548
|
return undefined;
|
|
1544
1549
|
}
|
|
1545
|
-
|
|
1546
1550
|
if (typeof matchValue == 'boolean') {
|
|
1547
1551
|
response = this.featureFlagsByKey?.[key]?.filters?.payloads?.[matchValue.toString()];
|
|
1548
1552
|
} else if (typeof matchValue == 'string') {
|
|
1549
1553
|
response = this.featureFlagsByKey?.[key]?.filters?.payloads?.[matchValue];
|
|
1550
|
-
}
|
|
1551
|
-
|
|
1552
|
-
|
|
1554
|
+
}
|
|
1555
|
+
// Undefined means a loading or missing data issue. Null means evaluation happened and there was no match
|
|
1553
1556
|
if (response === undefined || response === null) {
|
|
1554
1557
|
return null;
|
|
1555
1558
|
}
|
|
1556
|
-
|
|
1557
1559
|
try {
|
|
1558
1560
|
return JSON.parse(response);
|
|
1559
1561
|
} catch {
|
|
1560
1562
|
return response;
|
|
1561
1563
|
}
|
|
1562
1564
|
}
|
|
1563
|
-
|
|
1564
1565
|
async getAllFlagsAndPayloads(distinctId, groups = {}, personProperties = {}, groupProperties = {}) {
|
|
1565
1566
|
await this.loadFeatureFlags();
|
|
1566
1567
|
const response = {};
|
|
@@ -1571,7 +1572,6 @@ class FeatureFlagsPoller {
|
|
|
1571
1572
|
const matchValue = this.computeFlagLocally(flag, distinctId, groups, personProperties, groupProperties);
|
|
1572
1573
|
response[flag.key] = matchValue;
|
|
1573
1574
|
const matchPayload = await this.computeFeatureFlagPayloadLocally(flag.key, matchValue);
|
|
1574
|
-
|
|
1575
1575
|
if (matchPayload) {
|
|
1576
1576
|
payloads[flag.key] = matchPayload;
|
|
1577
1577
|
}
|
|
@@ -1579,7 +1579,6 @@ class FeatureFlagsPoller {
|
|
|
1579
1579
|
if (e instanceof InconclusiveMatchError) ; else if (e instanceof Error) {
|
|
1580
1580
|
this.onError?.(new Error(`Error computing flag locally: ${flag.key}: ${e}`));
|
|
1581
1581
|
}
|
|
1582
|
-
|
|
1583
1582
|
fallbackToDecide = true;
|
|
1584
1583
|
}
|
|
1585
1584
|
});
|
|
@@ -1589,56 +1588,41 @@ class FeatureFlagsPoller {
|
|
|
1589
1588
|
fallbackToDecide
|
|
1590
1589
|
};
|
|
1591
1590
|
}
|
|
1592
|
-
|
|
1593
1591
|
computeFlagLocally(flag, distinctId, groups = {}, personProperties = {}, groupProperties = {}) {
|
|
1594
1592
|
if (flag.ensure_experience_continuity) {
|
|
1595
1593
|
throw new InconclusiveMatchError('Flag has experience continuity enabled');
|
|
1596
1594
|
}
|
|
1597
|
-
|
|
1598
1595
|
if (!flag.active) {
|
|
1599
1596
|
return false;
|
|
1600
1597
|
}
|
|
1601
|
-
|
|
1602
1598
|
const flagFilters = flag.filters || {};
|
|
1603
1599
|
const aggregation_group_type_index = flagFilters.aggregation_group_type_index;
|
|
1604
|
-
|
|
1605
1600
|
if (aggregation_group_type_index != undefined) {
|
|
1606
1601
|
const groupName = this.groupTypeMapping[String(aggregation_group_type_index)];
|
|
1607
|
-
|
|
1608
1602
|
if (!groupName) {
|
|
1609
|
-
|
|
1610
|
-
console.warn(`[FEATURE FLAGS] Unknown group type index ${aggregation_group_type_index} for feature flag ${flag.key}`);
|
|
1611
|
-
}
|
|
1612
|
-
|
|
1603
|
+
this.logMsgIfDebug(() => console.warn(`[FEATURE FLAGS] Unknown group type index ${aggregation_group_type_index} for feature flag ${flag.key}`));
|
|
1613
1604
|
throw new InconclusiveMatchError('Flag has unknown group type index');
|
|
1614
1605
|
}
|
|
1615
|
-
|
|
1616
1606
|
if (!(groupName in groups)) {
|
|
1617
|
-
|
|
1618
|
-
console.warn(`[FEATURE FLAGS] Can't compute group feature flag: ${flag.key} without group names passed in`);
|
|
1619
|
-
}
|
|
1620
|
-
|
|
1607
|
+
this.logMsgIfDebug(() => console.warn(`[FEATURE FLAGS] Can't compute group feature flag: ${flag.key} without group names passed in`));
|
|
1621
1608
|
return false;
|
|
1622
1609
|
}
|
|
1623
|
-
|
|
1624
1610
|
const focusedGroupProperties = groupProperties[groupName];
|
|
1625
1611
|
return this.matchFeatureFlagProperties(flag, groups[groupName], focusedGroupProperties);
|
|
1626
1612
|
} else {
|
|
1627
1613
|
return this.matchFeatureFlagProperties(flag, distinctId, personProperties);
|
|
1628
1614
|
}
|
|
1629
1615
|
}
|
|
1630
|
-
|
|
1631
1616
|
matchFeatureFlagProperties(flag, distinctId, properties) {
|
|
1632
1617
|
const flagFilters = flag.filters || {};
|
|
1633
1618
|
const flagConditions = flagFilters.groups || [];
|
|
1634
1619
|
let isInconclusive = false;
|
|
1635
|
-
let result = undefined;
|
|
1620
|
+
let result = undefined;
|
|
1621
|
+
// # Stable sort conditions with variant overrides to the top. This ensures that if overrides are present, they are
|
|
1636
1622
|
// # evaluated first, and the variant override is applied to the first matching condition.
|
|
1637
|
-
|
|
1638
1623
|
const sortedFlagConditions = [...flagConditions].sort((conditionA, conditionB) => {
|
|
1639
1624
|
const AHasVariantOverride = !!conditionA.variant;
|
|
1640
1625
|
const BHasVariantOverride = !!conditionB.variant;
|
|
1641
|
-
|
|
1642
1626
|
if (AHasVariantOverride && BHasVariantOverride) {
|
|
1643
1627
|
return 0;
|
|
1644
1628
|
} else if (AHasVariantOverride) {
|
|
@@ -1649,19 +1633,16 @@ class FeatureFlagsPoller {
|
|
|
1649
1633
|
return 0;
|
|
1650
1634
|
}
|
|
1651
1635
|
});
|
|
1652
|
-
|
|
1653
1636
|
for (const condition of sortedFlagConditions) {
|
|
1654
1637
|
try {
|
|
1655
1638
|
if (this.isConditionMatch(flag, distinctId, condition, properties)) {
|
|
1656
1639
|
const variantOverride = condition.variant;
|
|
1657
1640
|
const flagVariants = flagFilters.multivariate?.variants || [];
|
|
1658
|
-
|
|
1659
1641
|
if (variantOverride && flagVariants.some(variant => variant.key === variantOverride)) {
|
|
1660
1642
|
result = variantOverride;
|
|
1661
1643
|
} else {
|
|
1662
1644
|
result = this.getMatchingVariant(flag, distinctId) || true;
|
|
1663
1645
|
}
|
|
1664
|
-
|
|
1665
1646
|
break;
|
|
1666
1647
|
}
|
|
1667
1648
|
} catch (e) {
|
|
@@ -1672,68 +1653,51 @@ class FeatureFlagsPoller {
|
|
|
1672
1653
|
}
|
|
1673
1654
|
}
|
|
1674
1655
|
}
|
|
1675
|
-
|
|
1676
1656
|
if (result !== undefined) {
|
|
1677
1657
|
return result;
|
|
1678
1658
|
} else if (isInconclusive) {
|
|
1679
1659
|
throw new InconclusiveMatchError("Can't determine if feature flag is enabled or not with given properties");
|
|
1680
|
-
}
|
|
1681
|
-
|
|
1682
|
-
|
|
1660
|
+
}
|
|
1661
|
+
// We can only return False when all conditions are False
|
|
1683
1662
|
return false;
|
|
1684
1663
|
}
|
|
1685
|
-
|
|
1686
1664
|
isConditionMatch(flag, distinctId, condition, properties) {
|
|
1687
1665
|
const rolloutPercentage = condition.rollout_percentage;
|
|
1688
|
-
|
|
1689
1666
|
const warnFunction = msg => {
|
|
1690
|
-
|
|
1691
|
-
console.warn(msg);
|
|
1692
|
-
}
|
|
1667
|
+
this.logMsgIfDebug(() => console.warn(msg));
|
|
1693
1668
|
};
|
|
1694
|
-
|
|
1695
1669
|
if ((condition.properties || []).length > 0) {
|
|
1696
1670
|
for (const prop of condition.properties) {
|
|
1697
1671
|
const propertyType = prop.type;
|
|
1698
1672
|
let matches = false;
|
|
1699
|
-
|
|
1700
1673
|
if (propertyType === 'cohort') {
|
|
1701
1674
|
matches = matchCohort(prop, properties, this.cohorts, this.debugMode);
|
|
1702
1675
|
} else {
|
|
1703
1676
|
matches = matchProperty(prop, properties, warnFunction);
|
|
1704
1677
|
}
|
|
1705
|
-
|
|
1706
1678
|
if (!matches) {
|
|
1707
1679
|
return false;
|
|
1708
1680
|
}
|
|
1709
1681
|
}
|
|
1710
|
-
|
|
1711
1682
|
if (rolloutPercentage == undefined) {
|
|
1712
1683
|
return true;
|
|
1713
1684
|
}
|
|
1714
1685
|
}
|
|
1715
|
-
|
|
1716
1686
|
if (rolloutPercentage != undefined && _hash(flag.key, distinctId) > rolloutPercentage / 100.0) {
|
|
1717
1687
|
return false;
|
|
1718
1688
|
}
|
|
1719
|
-
|
|
1720
1689
|
return true;
|
|
1721
1690
|
}
|
|
1722
|
-
|
|
1723
1691
|
getMatchingVariant(flag, distinctId) {
|
|
1724
1692
|
const hashValue = _hash(flag.key, distinctId, 'variant');
|
|
1725
|
-
|
|
1726
1693
|
const matchingVariant = this.variantLookupTable(flag).find(variant => {
|
|
1727
1694
|
return hashValue >= variant.valueMin && hashValue < variant.valueMax;
|
|
1728
1695
|
});
|
|
1729
|
-
|
|
1730
1696
|
if (matchingVariant) {
|
|
1731
1697
|
return matchingVariant.key;
|
|
1732
1698
|
}
|
|
1733
|
-
|
|
1734
1699
|
return undefined;
|
|
1735
1700
|
}
|
|
1736
|
-
|
|
1737
1701
|
variantLookupTable(flag) {
|
|
1738
1702
|
const lookupTable = [];
|
|
1739
1703
|
let valueMin = 0;
|
|
@@ -1751,40 +1715,31 @@ class FeatureFlagsPoller {
|
|
|
1751
1715
|
});
|
|
1752
1716
|
return lookupTable;
|
|
1753
1717
|
}
|
|
1754
|
-
|
|
1755
1718
|
async loadFeatureFlags(forceReload = false) {
|
|
1756
1719
|
if (!this.loadedSuccessfullyOnce || forceReload) {
|
|
1757
1720
|
await this._loadFeatureFlags();
|
|
1758
1721
|
}
|
|
1759
1722
|
}
|
|
1760
|
-
|
|
1761
1723
|
async _loadFeatureFlags() {
|
|
1762
1724
|
if (this.poller) {
|
|
1763
1725
|
clearTimeout(this.poller);
|
|
1764
1726
|
this.poller = undefined;
|
|
1765
1727
|
}
|
|
1766
|
-
|
|
1767
1728
|
this.poller = setTimeout(() => this._loadFeatureFlags(), this.pollingInterval);
|
|
1768
|
-
|
|
1769
1729
|
try {
|
|
1770
1730
|
const res = await this._requestFeatureFlagDefinitions();
|
|
1771
|
-
|
|
1772
1731
|
if (res && res.status === 401) {
|
|
1773
1732
|
throw new ClientError(`Your personalApiKey is invalid. Are you sure you're not using your Project API key? More information: https://posthog.com/docs/api/overview`);
|
|
1774
1733
|
}
|
|
1775
|
-
|
|
1776
1734
|
if (res && res.status !== 200) {
|
|
1777
1735
|
// something else went wrong, or the server is down.
|
|
1778
1736
|
// In this case, don't override existing flags
|
|
1779
1737
|
return;
|
|
1780
1738
|
}
|
|
1781
|
-
|
|
1782
1739
|
const responseJson = await res.json();
|
|
1783
|
-
|
|
1784
1740
|
if (!('flags' in responseJson)) {
|
|
1785
1741
|
this.onError?.(new Error(`Invalid response when getting feature flags: ${JSON.stringify(responseJson)}`));
|
|
1786
1742
|
}
|
|
1787
|
-
|
|
1788
1743
|
this.featureFlags = responseJson.flags || [];
|
|
1789
1744
|
this.featureFlagsByKey = this.featureFlags.reduce((acc, curr) => (acc[curr.key] = curr, acc), {});
|
|
1790
1745
|
this.groupTypeMapping = responseJson.group_type_mapping || {};
|
|
@@ -1798,18 +1753,17 @@ class FeatureFlagsPoller {
|
|
|
1798
1753
|
}
|
|
1799
1754
|
}
|
|
1800
1755
|
}
|
|
1801
|
-
|
|
1802
1756
|
async _requestFeatureFlagDefinitions() {
|
|
1803
1757
|
const url = `${this.host}/api/feature_flag/local_evaluation?token=${this.projectApiKey}&send_cohorts`;
|
|
1804
1758
|
const options = {
|
|
1805
1759
|
method: 'GET',
|
|
1806
|
-
headers: {
|
|
1760
|
+
headers: {
|
|
1761
|
+
...this.customHeaders,
|
|
1807
1762
|
'Content-Type': 'application/json',
|
|
1808
1763
|
Authorization: `Bearer ${this.personalApiKey}`
|
|
1809
1764
|
}
|
|
1810
1765
|
};
|
|
1811
1766
|
let abortTimeout = null;
|
|
1812
|
-
|
|
1813
1767
|
if (this.timeout && typeof this.timeout === 'number') {
|
|
1814
1768
|
const controller = new AbortController();
|
|
1815
1769
|
abortTimeout = safeSetTimeout(() => {
|
|
@@ -1817,62 +1771,50 @@ class FeatureFlagsPoller {
|
|
|
1817
1771
|
}, this.timeout);
|
|
1818
1772
|
options.signal = controller.signal;
|
|
1819
1773
|
}
|
|
1820
|
-
|
|
1821
1774
|
try {
|
|
1822
1775
|
return await this.fetch(url, options);
|
|
1823
1776
|
} finally {
|
|
1824
1777
|
clearTimeout(abortTimeout);
|
|
1825
1778
|
}
|
|
1826
1779
|
}
|
|
1827
|
-
|
|
1828
1780
|
stopPoller() {
|
|
1829
1781
|
clearTimeout(this.poller);
|
|
1830
1782
|
}
|
|
1831
|
-
|
|
1832
|
-
|
|
1783
|
+
}
|
|
1784
|
+
// # This function takes a distinct_id and a feature flag key and returns a float between 0 and 1.
|
|
1833
1785
|
// # Given the same distinct_id and key, it'll always return the same float. These floats are
|
|
1834
1786
|
// # uniformly distributed between 0 and 1, so if we want to show this feature to 20% of traffic
|
|
1835
1787
|
// # we can do _hash(key, distinct_id) < 0.2
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
1788
|
function _hash(key, distinctId, salt = '') {
|
|
1839
1789
|
// rusha is a fast sha1 implementation in pure javascript
|
|
1840
1790
|
const sha1Hash = rusha.createHash();
|
|
1841
1791
|
sha1Hash.update(`${key}.${distinctId}${salt}`);
|
|
1842
1792
|
return parseInt(sha1Hash.digest('hex').slice(0, 15), 16) / LONG_SCALE;
|
|
1843
1793
|
}
|
|
1844
|
-
|
|
1845
1794
|
function matchProperty(property, propertyValues, warnFunction) {
|
|
1846
1795
|
const key = property.key;
|
|
1847
1796
|
const value = property.value;
|
|
1848
1797
|
const operator = property.operator || 'exact';
|
|
1849
|
-
|
|
1850
1798
|
if (!(key in propertyValues)) {
|
|
1851
1799
|
throw new InconclusiveMatchError(`Property ${key} not found in propertyValues`);
|
|
1852
1800
|
} else if (operator === 'is_not_set') {
|
|
1853
1801
|
throw new InconclusiveMatchError(`Operator is_not_set is not supported`);
|
|
1854
1802
|
}
|
|
1855
|
-
|
|
1856
1803
|
const overrideValue = propertyValues[key];
|
|
1857
|
-
|
|
1858
1804
|
if (overrideValue == null && !NULL_VALUES_ALLOWED_OPERATORS.includes(operator)) {
|
|
1859
1805
|
// if the value is null, just fail the feature flag comparison
|
|
1860
1806
|
// this isn't an InconclusiveMatchError because the property value was provided.
|
|
1861
1807
|
if (warnFunction) {
|
|
1862
1808
|
warnFunction(`Property ${key} cannot have a value of null/undefined with the ${operator} operator`);
|
|
1863
1809
|
}
|
|
1864
|
-
|
|
1865
1810
|
return false;
|
|
1866
1811
|
}
|
|
1867
|
-
|
|
1868
1812
|
function computeExactMatch(value, overrideValue) {
|
|
1869
1813
|
if (Array.isArray(value)) {
|
|
1870
1814
|
return value.map(val => String(val).toLowerCase()).includes(String(overrideValue).toLowerCase());
|
|
1871
1815
|
}
|
|
1872
|
-
|
|
1873
1816
|
return String(value).toLowerCase() === String(overrideValue).toLowerCase();
|
|
1874
1817
|
}
|
|
1875
|
-
|
|
1876
1818
|
function compare(lhs, rhs, operator) {
|
|
1877
1819
|
if (operator === 'gt') {
|
|
1878
1820
|
return lhs > rhs;
|
|
@@ -1886,29 +1828,21 @@ function matchProperty(property, propertyValues, warnFunction) {
|
|
|
1886
1828
|
throw new Error(`Invalid operator: ${operator}`);
|
|
1887
1829
|
}
|
|
1888
1830
|
}
|
|
1889
|
-
|
|
1890
1831
|
switch (operator) {
|
|
1891
1832
|
case 'exact':
|
|
1892
1833
|
return computeExactMatch(value, overrideValue);
|
|
1893
|
-
|
|
1894
1834
|
case 'is_not':
|
|
1895
1835
|
return !computeExactMatch(value, overrideValue);
|
|
1896
|
-
|
|
1897
1836
|
case 'is_set':
|
|
1898
1837
|
return key in propertyValues;
|
|
1899
|
-
|
|
1900
1838
|
case 'icontains':
|
|
1901
1839
|
return String(overrideValue).toLowerCase().includes(String(value).toLowerCase());
|
|
1902
|
-
|
|
1903
1840
|
case 'not_icontains':
|
|
1904
1841
|
return !String(overrideValue).toLowerCase().includes(String(value).toLowerCase());
|
|
1905
|
-
|
|
1906
1842
|
case 'regex':
|
|
1907
1843
|
return isValidRegex(String(value)) && String(overrideValue).match(String(value)) !== null;
|
|
1908
|
-
|
|
1909
1844
|
case 'not_regex':
|
|
1910
1845
|
return isValidRegex(String(value)) && String(overrideValue).match(String(value)) === null;
|
|
1911
|
-
|
|
1912
1846
|
case 'gt':
|
|
1913
1847
|
case 'gte':
|
|
1914
1848
|
case 'lt':
|
|
@@ -1917,14 +1851,13 @@ function matchProperty(property, propertyValues, warnFunction) {
|
|
|
1917
1851
|
// :TRICKY: We adjust comparison based on the override value passed in,
|
|
1918
1852
|
// to make sure we handle both numeric and string comparisons appropriately.
|
|
1919
1853
|
let parsedValue = typeof value === 'number' ? value : null;
|
|
1920
|
-
|
|
1921
1854
|
if (typeof value === 'string') {
|
|
1922
1855
|
try {
|
|
1923
1856
|
parsedValue = parseFloat(value);
|
|
1924
|
-
} catch (err) {
|
|
1857
|
+
} catch (err) {
|
|
1858
|
+
// pass
|
|
1925
1859
|
}
|
|
1926
1860
|
}
|
|
1927
|
-
|
|
1928
1861
|
if (parsedValue != null && overrideValue != null) {
|
|
1929
1862
|
// check both null and undefined
|
|
1930
1863
|
if (typeof overrideValue === 'string') {
|
|
@@ -1936,66 +1869,50 @@ function matchProperty(property, propertyValues, warnFunction) {
|
|
|
1936
1869
|
return compare(String(overrideValue), String(value), operator);
|
|
1937
1870
|
}
|
|
1938
1871
|
}
|
|
1939
|
-
|
|
1940
1872
|
case 'is_date_after':
|
|
1941
1873
|
case 'is_date_before':
|
|
1942
1874
|
{
|
|
1943
1875
|
let parsedDate = relativeDateParseForFeatureFlagMatching(String(value));
|
|
1944
|
-
|
|
1945
1876
|
if (parsedDate == null) {
|
|
1946
1877
|
parsedDate = convertToDateTime(value);
|
|
1947
1878
|
}
|
|
1948
|
-
|
|
1949
1879
|
if (parsedDate == null) {
|
|
1950
1880
|
throw new InconclusiveMatchError(`Invalid date: ${value}`);
|
|
1951
1881
|
}
|
|
1952
|
-
|
|
1953
1882
|
const overrideDate = convertToDateTime(overrideValue);
|
|
1954
|
-
|
|
1955
1883
|
if (['is_date_before'].includes(operator)) {
|
|
1956
1884
|
return overrideDate < parsedDate;
|
|
1957
1885
|
}
|
|
1958
|
-
|
|
1959
1886
|
return overrideDate > parsedDate;
|
|
1960
1887
|
}
|
|
1961
|
-
|
|
1962
1888
|
default:
|
|
1963
1889
|
throw new InconclusiveMatchError(`Unknown operator: ${operator}`);
|
|
1964
1890
|
}
|
|
1965
1891
|
}
|
|
1966
|
-
|
|
1967
1892
|
function matchCohort(property, propertyValues, cohortProperties, debugMode = false) {
|
|
1968
1893
|
const cohortId = String(property.value);
|
|
1969
|
-
|
|
1970
1894
|
if (!(cohortId in cohortProperties)) {
|
|
1971
1895
|
throw new InconclusiveMatchError("can't match cohort without a given cohort property value");
|
|
1972
1896
|
}
|
|
1973
|
-
|
|
1974
1897
|
const propertyGroup = cohortProperties[cohortId];
|
|
1975
1898
|
return matchPropertyGroup(propertyGroup, propertyValues, cohortProperties, debugMode);
|
|
1976
1899
|
}
|
|
1977
|
-
|
|
1978
1900
|
function matchPropertyGroup(propertyGroup, propertyValues, cohortProperties, debugMode = false) {
|
|
1979
1901
|
if (!propertyGroup) {
|
|
1980
1902
|
return true;
|
|
1981
1903
|
}
|
|
1982
|
-
|
|
1983
1904
|
const propertyGroupType = propertyGroup.type;
|
|
1984
1905
|
const properties = propertyGroup.values;
|
|
1985
|
-
|
|
1986
1906
|
if (!properties || properties.length === 0) {
|
|
1987
1907
|
// empty groups are no-ops, always match
|
|
1988
1908
|
return true;
|
|
1989
1909
|
}
|
|
1990
|
-
|
|
1991
1910
|
let errorMatchingLocally = false;
|
|
1992
|
-
|
|
1993
1911
|
if ('values' in properties[0]) {
|
|
1994
1912
|
// a nested property group
|
|
1995
1913
|
for (const prop of properties) {
|
|
1996
1914
|
try {
|
|
1997
1915
|
const matches = matchPropertyGroup(prop, propertyValues, cohortProperties, debugMode);
|
|
1998
|
-
|
|
1999
1916
|
if (propertyGroupType === 'AND') {
|
|
2000
1917
|
if (!matches) {
|
|
2001
1918
|
return false;
|
|
@@ -2011,39 +1928,32 @@ function matchPropertyGroup(propertyGroup, propertyValues, cohortProperties, deb
|
|
|
2011
1928
|
if (debugMode) {
|
|
2012
1929
|
console.debug(`Failed to compute property ${prop} locally: ${err}`);
|
|
2013
1930
|
}
|
|
2014
|
-
|
|
2015
1931
|
errorMatchingLocally = true;
|
|
2016
1932
|
} else {
|
|
2017
1933
|
throw err;
|
|
2018
1934
|
}
|
|
2019
1935
|
}
|
|
2020
1936
|
}
|
|
2021
|
-
|
|
2022
1937
|
if (errorMatchingLocally) {
|
|
2023
1938
|
throw new InconclusiveMatchError("Can't match cohort without a given cohort property value");
|
|
2024
|
-
}
|
|
2025
|
-
|
|
2026
|
-
|
|
1939
|
+
}
|
|
1940
|
+
// if we get here, all matched in AND case, or none matched in OR case
|
|
2027
1941
|
return propertyGroupType === 'AND';
|
|
2028
1942
|
} else {
|
|
2029
1943
|
for (const prop of properties) {
|
|
2030
1944
|
try {
|
|
2031
1945
|
let matches;
|
|
2032
|
-
|
|
2033
1946
|
if (prop.type === 'cohort') {
|
|
2034
1947
|
matches = matchCohort(prop, propertyValues, cohortProperties, debugMode);
|
|
2035
1948
|
} else {
|
|
2036
1949
|
matches = matchProperty(prop, propertyValues);
|
|
2037
1950
|
}
|
|
2038
|
-
|
|
2039
1951
|
const negation = prop.negation || false;
|
|
2040
|
-
|
|
2041
1952
|
if (propertyGroupType === 'AND') {
|
|
2042
1953
|
// if negated property, do the inverse
|
|
2043
1954
|
if (!matches && !negation) {
|
|
2044
1955
|
return false;
|
|
2045
1956
|
}
|
|
2046
|
-
|
|
2047
1957
|
if (matches && negation) {
|
|
2048
1958
|
return false;
|
|
2049
1959
|
}
|
|
@@ -2052,7 +1962,6 @@ function matchPropertyGroup(propertyGroup, propertyValues, cohortProperties, deb
|
|
|
2052
1962
|
if (matches && !negation) {
|
|
2053
1963
|
return true;
|
|
2054
1964
|
}
|
|
2055
|
-
|
|
2056
1965
|
if (!matches && negation) {
|
|
2057
1966
|
return true;
|
|
2058
1967
|
}
|
|
@@ -2062,23 +1971,19 @@ function matchPropertyGroup(propertyGroup, propertyValues, cohortProperties, deb
|
|
|
2062
1971
|
if (debugMode) {
|
|
2063
1972
|
console.debug(`Failed to compute property ${prop} locally: ${err}`);
|
|
2064
1973
|
}
|
|
2065
|
-
|
|
2066
1974
|
errorMatchingLocally = true;
|
|
2067
1975
|
} else {
|
|
2068
1976
|
throw err;
|
|
2069
1977
|
}
|
|
2070
1978
|
}
|
|
2071
1979
|
}
|
|
2072
|
-
|
|
2073
1980
|
if (errorMatchingLocally) {
|
|
2074
1981
|
throw new InconclusiveMatchError("can't match cohort without a given cohort property value");
|
|
2075
|
-
}
|
|
2076
|
-
|
|
2077
|
-
|
|
1982
|
+
}
|
|
1983
|
+
// if we get here, all matched in AND case, or none matched in OR case
|
|
2078
1984
|
return propertyGroupType === 'AND';
|
|
2079
1985
|
}
|
|
2080
1986
|
}
|
|
2081
|
-
|
|
2082
1987
|
function isValidRegex(regex) {
|
|
2083
1988
|
try {
|
|
2084
1989
|
new RegExp(regex);
|
|
@@ -2087,42 +1992,33 @@ function isValidRegex(regex) {
|
|
|
2087
1992
|
return false;
|
|
2088
1993
|
}
|
|
2089
1994
|
}
|
|
2090
|
-
|
|
2091
1995
|
function convertToDateTime(value) {
|
|
2092
1996
|
if (value instanceof Date) {
|
|
2093
1997
|
return value;
|
|
2094
1998
|
} else if (typeof value === 'string' || typeof value === 'number') {
|
|
2095
1999
|
const date = new Date(value);
|
|
2096
|
-
|
|
2097
2000
|
if (!isNaN(date.valueOf())) {
|
|
2098
2001
|
return date;
|
|
2099
2002
|
}
|
|
2100
|
-
|
|
2101
2003
|
throw new InconclusiveMatchError(`${value} is in an invalid date format`);
|
|
2102
2004
|
} else {
|
|
2103
2005
|
throw new InconclusiveMatchError(`The date provided ${value} must be a string, number, or date object`);
|
|
2104
2006
|
}
|
|
2105
2007
|
}
|
|
2106
|
-
|
|
2107
2008
|
function relativeDateParseForFeatureFlagMatching(value) {
|
|
2108
2009
|
const regex = /^-?(?<number>[0-9]+)(?<interval>[a-z])$/;
|
|
2109
2010
|
const match = value.match(regex);
|
|
2110
2011
|
const parsedDt = new Date(new Date().toISOString());
|
|
2111
|
-
|
|
2112
2012
|
if (match) {
|
|
2113
2013
|
if (!match.groups) {
|
|
2114
2014
|
return null;
|
|
2115
2015
|
}
|
|
2116
|
-
|
|
2117
2016
|
const number = parseInt(match.groups['number']);
|
|
2118
|
-
|
|
2119
2017
|
if (number >= 10000) {
|
|
2120
2018
|
// Guard against overflow, disallow numbers greater than 10_000
|
|
2121
2019
|
return null;
|
|
2122
2020
|
}
|
|
2123
|
-
|
|
2124
2021
|
const interval = match.groups['interval'];
|
|
2125
|
-
|
|
2126
2022
|
if (interval == 'h') {
|
|
2127
2023
|
parsedDt.setUTCHours(parsedDt.getUTCHours() - number);
|
|
2128
2024
|
} else if (interval == 'd') {
|
|
@@ -2136,7 +2032,6 @@ function relativeDateParseForFeatureFlagMatching(value) {
|
|
|
2136
2032
|
} else {
|
|
2137
2033
|
return null;
|
|
2138
2034
|
}
|
|
2139
|
-
|
|
2140
2035
|
return parsedDt;
|
|
2141
2036
|
} else {
|
|
2142
2037
|
return null;
|
|
@@ -2144,15 +2039,13 @@ function relativeDateParseForFeatureFlagMatching(value) {
|
|
|
2144
2039
|
}
|
|
2145
2040
|
|
|
2146
2041
|
const THIRTY_SECONDS = 30 * 1000;
|
|
2147
|
-
const MAX_CACHE_SIZE = 50 * 1000;
|
|
2148
|
-
|
|
2042
|
+
const MAX_CACHE_SIZE = 50 * 1000;
|
|
2043
|
+
// The actual exported Nodejs API.
|
|
2149
2044
|
class PostHog extends PostHogCoreStateless {
|
|
2150
2045
|
constructor(apiKey, options = {}) {
|
|
2151
|
-
options.captureMode = options?.captureMode || 'json';
|
|
2152
2046
|
super(apiKey, options);
|
|
2153
2047
|
this._memoryStorage = new PostHogMemoryStorage();
|
|
2154
2048
|
this.options = options;
|
|
2155
|
-
|
|
2156
2049
|
if (options.personalApiKey) {
|
|
2157
2050
|
this.featureFlagsPoller = new FeatureFlagsPoller({
|
|
2158
2051
|
pollingInterval: typeof options.featureFlagsPollingInterval === 'number' ? options.featureFlagsPollingInterval : THIRTY_SECONDS,
|
|
@@ -2167,48 +2060,37 @@ class PostHog extends PostHogCoreStateless {
|
|
|
2167
2060
|
customHeaders: this.getCustomHeaders()
|
|
2168
2061
|
});
|
|
2169
2062
|
}
|
|
2170
|
-
|
|
2171
2063
|
this.distinctIdHasSentFlagCalls = {};
|
|
2172
2064
|
this.maxCacheSize = options.maxCacheSize || MAX_CACHE_SIZE;
|
|
2173
2065
|
}
|
|
2174
|
-
|
|
2175
2066
|
getPersistedProperty(key) {
|
|
2176
2067
|
return this._memoryStorage.getProperty(key);
|
|
2177
2068
|
}
|
|
2178
|
-
|
|
2179
2069
|
setPersistedProperty(key, value) {
|
|
2180
2070
|
return this._memoryStorage.setProperty(key, value);
|
|
2181
2071
|
}
|
|
2182
|
-
|
|
2183
2072
|
fetch(url, options) {
|
|
2184
2073
|
return this.options.fetch ? this.options.fetch(url, options) : fetch$1(url, options);
|
|
2185
2074
|
}
|
|
2186
|
-
|
|
2187
2075
|
getLibraryId() {
|
|
2188
2076
|
return 'posthog-node';
|
|
2189
2077
|
}
|
|
2190
|
-
|
|
2191
2078
|
getLibraryVersion() {
|
|
2192
2079
|
return version;
|
|
2193
2080
|
}
|
|
2194
|
-
|
|
2195
2081
|
getCustomUserAgent() {
|
|
2196
2082
|
return `${this.getLibraryId()}/${this.getLibraryVersion()}`;
|
|
2197
2083
|
}
|
|
2198
|
-
|
|
2199
2084
|
enable() {
|
|
2200
2085
|
return super.optIn();
|
|
2201
2086
|
}
|
|
2202
|
-
|
|
2203
2087
|
disable() {
|
|
2204
2088
|
return super.optOut();
|
|
2205
2089
|
}
|
|
2206
|
-
|
|
2207
2090
|
debug(enabled = true) {
|
|
2208
2091
|
super.debug(enabled);
|
|
2209
2092
|
this.featureFlagsPoller?.debug(enabled);
|
|
2210
2093
|
}
|
|
2211
|
-
|
|
2212
2094
|
capture({
|
|
2213
2095
|
distinctId,
|
|
2214
2096
|
event,
|
|
@@ -2226,65 +2108,55 @@ class PostHog extends PostHogCoreStateless {
|
|
|
2226
2108
|
uuid
|
|
2227
2109
|
});
|
|
2228
2110
|
};
|
|
2229
|
-
|
|
2230
2111
|
const _getFlags = (distinctId, groups, disableGeoip) => {
|
|
2231
2112
|
return super.getFeatureFlagsStateless(distinctId, groups, undefined, undefined, disableGeoip);
|
|
2232
|
-
};
|
|
2233
|
-
|
|
2234
|
-
|
|
2113
|
+
};
|
|
2114
|
+
// :TRICKY: If we flush, or need to shut down, to not lose events we want this promise to resolve before we flush
|
|
2235
2115
|
const capturePromise = Promise.resolve().then(async () => {
|
|
2236
2116
|
if (sendFeatureFlags) {
|
|
2237
2117
|
// If we are sending feature flags, we need to make sure we have the latest flags
|
|
2238
2118
|
// return await super.getFeatureFlagsStateless(distinctId, groups, undefined, undefined, disableGeoip)
|
|
2239
2119
|
return await _getFlags(distinctId, groups, disableGeoip);
|
|
2240
2120
|
}
|
|
2241
|
-
|
|
2242
2121
|
if ((this.featureFlagsPoller?.featureFlags?.length || 0) > 0) {
|
|
2243
2122
|
// Otherwise we may as well check for the flags locally and include them if there
|
|
2244
2123
|
const groupsWithStringValues = {};
|
|
2245
|
-
|
|
2246
2124
|
for (const [key, value] of Object.entries(groups || {})) {
|
|
2247
2125
|
groupsWithStringValues[key] = String(value);
|
|
2248
2126
|
}
|
|
2249
|
-
|
|
2250
2127
|
return await this.getAllFlags(distinctId, {
|
|
2251
2128
|
groups: groupsWithStringValues,
|
|
2252
2129
|
disableGeoip,
|
|
2253
2130
|
onlyEvaluateLocally: true
|
|
2254
2131
|
});
|
|
2255
2132
|
}
|
|
2256
|
-
|
|
2257
2133
|
return {};
|
|
2258
2134
|
}).then(flags => {
|
|
2259
2135
|
// Derive the relevant flag properties to add
|
|
2260
2136
|
const additionalProperties = {};
|
|
2261
|
-
|
|
2262
2137
|
if (flags) {
|
|
2263
2138
|
for (const [feature, variant] of Object.entries(flags)) {
|
|
2264
2139
|
additionalProperties[`$feature/${feature}`] = variant;
|
|
2265
2140
|
}
|
|
2266
2141
|
}
|
|
2267
|
-
|
|
2268
2142
|
const activeFlags = Object.keys(flags || {}).filter(flag => flags?.[flag] !== false);
|
|
2269
|
-
|
|
2270
2143
|
if (activeFlags.length > 0) {
|
|
2271
2144
|
additionalProperties['$active_feature_flags'] = activeFlags;
|
|
2272
2145
|
}
|
|
2273
|
-
|
|
2274
2146
|
return additionalProperties;
|
|
2275
2147
|
}).catch(() => {
|
|
2276
2148
|
// Something went wrong getting the flag info - we should capture the event anyways
|
|
2277
2149
|
return {};
|
|
2278
2150
|
}).then(additionalProperties => {
|
|
2279
2151
|
// No matter what - capture the event
|
|
2280
|
-
_capture({
|
|
2152
|
+
_capture({
|
|
2153
|
+
...additionalProperties,
|
|
2281
2154
|
...properties,
|
|
2282
2155
|
$groups: groups
|
|
2283
2156
|
});
|
|
2284
2157
|
});
|
|
2285
2158
|
this.addPendingPromise(capturePromise);
|
|
2286
2159
|
}
|
|
2287
|
-
|
|
2288
2160
|
identify({
|
|
2289
2161
|
distinctId,
|
|
2290
2162
|
properties,
|
|
@@ -2298,13 +2170,11 @@ class PostHog extends PostHogCoreStateless {
|
|
|
2298
2170
|
disableGeoip
|
|
2299
2171
|
});
|
|
2300
2172
|
}
|
|
2301
|
-
|
|
2302
2173
|
alias(data) {
|
|
2303
2174
|
super.aliasStateless(data.alias, data.distinctId, undefined, {
|
|
2304
2175
|
disableGeoip: data.disableGeoip
|
|
2305
2176
|
});
|
|
2306
2177
|
}
|
|
2307
|
-
|
|
2308
2178
|
async getFeatureFlag(key, distinctId, options) {
|
|
2309
2179
|
const {
|
|
2310
2180
|
groups,
|
|
@@ -2318,36 +2188,29 @@ class PostHog extends PostHogCoreStateless {
|
|
|
2318
2188
|
} = options || {};
|
|
2319
2189
|
const adjustedProperties = this.addLocalPersonAndGroupProperties(distinctId, groups, personProperties, groupProperties);
|
|
2320
2190
|
personProperties = adjustedProperties.allPersonProperties;
|
|
2321
|
-
groupProperties = adjustedProperties.allGroupProperties;
|
|
2322
|
-
|
|
2191
|
+
groupProperties = adjustedProperties.allGroupProperties;
|
|
2192
|
+
// set defaults
|
|
2323
2193
|
if (onlyEvaluateLocally == undefined) {
|
|
2324
2194
|
onlyEvaluateLocally = false;
|
|
2325
2195
|
}
|
|
2326
|
-
|
|
2327
2196
|
if (sendFeatureFlagEvents == undefined) {
|
|
2328
2197
|
sendFeatureFlagEvents = true;
|
|
2329
2198
|
}
|
|
2330
|
-
|
|
2331
2199
|
let response = await this.featureFlagsPoller?.getFeatureFlag(key, distinctId, groups, personProperties, groupProperties);
|
|
2332
2200
|
const flagWasLocallyEvaluated = response !== undefined;
|
|
2333
|
-
|
|
2334
2201
|
if (!flagWasLocallyEvaluated && !onlyEvaluateLocally) {
|
|
2335
2202
|
response = await super.getFeatureFlagStateless(key, distinctId, groups, personProperties, groupProperties, disableGeoip);
|
|
2336
2203
|
}
|
|
2337
|
-
|
|
2338
2204
|
const featureFlagReportedKey = `${key}_${response}`;
|
|
2339
|
-
|
|
2340
2205
|
if (sendFeatureFlagEvents && (!(distinctId in this.distinctIdHasSentFlagCalls) || !this.distinctIdHasSentFlagCalls[distinctId].includes(featureFlagReportedKey))) {
|
|
2341
2206
|
if (Object.keys(this.distinctIdHasSentFlagCalls).length >= this.maxCacheSize) {
|
|
2342
2207
|
this.distinctIdHasSentFlagCalls = {};
|
|
2343
2208
|
}
|
|
2344
|
-
|
|
2345
2209
|
if (Array.isArray(this.distinctIdHasSentFlagCalls[distinctId])) {
|
|
2346
2210
|
this.distinctIdHasSentFlagCalls[distinctId].push(featureFlagReportedKey);
|
|
2347
2211
|
} else {
|
|
2348
2212
|
this.distinctIdHasSentFlagCalls[distinctId] = [featureFlagReportedKey];
|
|
2349
2213
|
}
|
|
2350
|
-
|
|
2351
2214
|
this.capture({
|
|
2352
2215
|
distinctId,
|
|
2353
2216
|
event: '$feature_flag_called',
|
|
@@ -2361,10 +2224,8 @@ class PostHog extends PostHogCoreStateless {
|
|
|
2361
2224
|
disableGeoip
|
|
2362
2225
|
});
|
|
2363
2226
|
}
|
|
2364
|
-
|
|
2365
2227
|
return response;
|
|
2366
2228
|
}
|
|
2367
|
-
|
|
2368
2229
|
async getFeatureFlagPayload(key, distinctId, matchValue, options) {
|
|
2369
2230
|
const {
|
|
2370
2231
|
groups,
|
|
@@ -2379,56 +2240,45 @@ class PostHog extends PostHogCoreStateless {
|
|
|
2379
2240
|
const adjustedProperties = this.addLocalPersonAndGroupProperties(distinctId, groups, personProperties, groupProperties);
|
|
2380
2241
|
personProperties = adjustedProperties.allPersonProperties;
|
|
2381
2242
|
groupProperties = adjustedProperties.allGroupProperties;
|
|
2382
|
-
let response = undefined;
|
|
2383
|
-
|
|
2243
|
+
let response = undefined;
|
|
2244
|
+
// Try to get match value locally if not provided
|
|
2384
2245
|
if (!matchValue) {
|
|
2385
|
-
matchValue = await this.getFeatureFlag(key, distinctId, {
|
|
2246
|
+
matchValue = await this.getFeatureFlag(key, distinctId, {
|
|
2247
|
+
...options,
|
|
2386
2248
|
onlyEvaluateLocally: true
|
|
2387
2249
|
});
|
|
2388
2250
|
}
|
|
2389
|
-
|
|
2390
2251
|
if (matchValue) {
|
|
2391
2252
|
response = await this.featureFlagsPoller?.computeFeatureFlagPayloadLocally(key, matchValue);
|
|
2392
|
-
}
|
|
2393
|
-
|
|
2394
|
-
|
|
2253
|
+
}
|
|
2254
|
+
// set defaults
|
|
2395
2255
|
if (onlyEvaluateLocally == undefined) {
|
|
2396
2256
|
onlyEvaluateLocally = false;
|
|
2397
2257
|
}
|
|
2398
|
-
|
|
2399
2258
|
if (sendFeatureFlagEvents == undefined) {
|
|
2400
2259
|
sendFeatureFlagEvents = true;
|
|
2401
|
-
}
|
|
2402
|
-
|
|
2403
|
-
|
|
2260
|
+
}
|
|
2261
|
+
// set defaults
|
|
2404
2262
|
if (onlyEvaluateLocally == undefined) {
|
|
2405
2263
|
onlyEvaluateLocally = false;
|
|
2406
2264
|
}
|
|
2407
|
-
|
|
2408
2265
|
const payloadWasLocallyEvaluated = response !== undefined;
|
|
2409
|
-
|
|
2410
2266
|
if (!payloadWasLocallyEvaluated && !onlyEvaluateLocally) {
|
|
2411
2267
|
response = await super.getFeatureFlagPayloadStateless(key, distinctId, groups, personProperties, groupProperties, disableGeoip);
|
|
2412
2268
|
}
|
|
2413
|
-
|
|
2414
2269
|
return response;
|
|
2415
2270
|
}
|
|
2416
|
-
|
|
2417
2271
|
async isFeatureEnabled(key, distinctId, options) {
|
|
2418
2272
|
const feat = await this.getFeatureFlag(key, distinctId, options);
|
|
2419
|
-
|
|
2420
2273
|
if (feat === undefined) {
|
|
2421
2274
|
return undefined;
|
|
2422
2275
|
}
|
|
2423
|
-
|
|
2424
2276
|
return !!feat || false;
|
|
2425
2277
|
}
|
|
2426
|
-
|
|
2427
2278
|
async getAllFlags(distinctId, options) {
|
|
2428
2279
|
const response = await this.getAllFlagsAndPayloads(distinctId, options);
|
|
2429
2280
|
return response.featureFlags;
|
|
2430
2281
|
}
|
|
2431
|
-
|
|
2432
2282
|
async getAllFlagsAndPayloads(distinctId, options) {
|
|
2433
2283
|
const {
|
|
2434
2284
|
groups,
|
|
@@ -2441,39 +2291,36 @@ class PostHog extends PostHogCoreStateless {
|
|
|
2441
2291
|
} = options || {};
|
|
2442
2292
|
const adjustedProperties = this.addLocalPersonAndGroupProperties(distinctId, groups, personProperties, groupProperties);
|
|
2443
2293
|
personProperties = adjustedProperties.allPersonProperties;
|
|
2444
|
-
groupProperties = adjustedProperties.allGroupProperties;
|
|
2445
|
-
|
|
2294
|
+
groupProperties = adjustedProperties.allGroupProperties;
|
|
2295
|
+
// set defaults
|
|
2446
2296
|
if (onlyEvaluateLocally == undefined) {
|
|
2447
2297
|
onlyEvaluateLocally = false;
|
|
2448
2298
|
}
|
|
2449
|
-
|
|
2450
2299
|
const localEvaluationResult = await this.featureFlagsPoller?.getAllFlagsAndPayloads(distinctId, groups, personProperties, groupProperties);
|
|
2451
2300
|
let featureFlags = {};
|
|
2452
2301
|
let featureFlagPayloads = {};
|
|
2453
2302
|
let fallbackToDecide = true;
|
|
2454
|
-
|
|
2455
2303
|
if (localEvaluationResult) {
|
|
2456
2304
|
featureFlags = localEvaluationResult.response;
|
|
2457
2305
|
featureFlagPayloads = localEvaluationResult.payloads;
|
|
2458
2306
|
fallbackToDecide = localEvaluationResult.fallbackToDecide;
|
|
2459
2307
|
}
|
|
2460
|
-
|
|
2461
2308
|
if (fallbackToDecide && !onlyEvaluateLocally) {
|
|
2462
2309
|
const remoteEvaluationResult = await super.getFeatureFlagsAndPayloadsStateless(distinctId, groups, personProperties, groupProperties, disableGeoip);
|
|
2463
|
-
featureFlags = {
|
|
2310
|
+
featureFlags = {
|
|
2311
|
+
...featureFlags,
|
|
2464
2312
|
...(remoteEvaluationResult.flags || {})
|
|
2465
2313
|
};
|
|
2466
|
-
featureFlagPayloads = {
|
|
2314
|
+
featureFlagPayloads = {
|
|
2315
|
+
...featureFlagPayloads,
|
|
2467
2316
|
...(remoteEvaluationResult.payloads || {})
|
|
2468
2317
|
};
|
|
2469
2318
|
}
|
|
2470
|
-
|
|
2471
2319
|
return {
|
|
2472
2320
|
featureFlags,
|
|
2473
2321
|
featureFlagPayloads
|
|
2474
2322
|
};
|
|
2475
2323
|
}
|
|
2476
|
-
|
|
2477
2324
|
groupIdentify({
|
|
2478
2325
|
groupType,
|
|
2479
2326
|
groupKey,
|
|
@@ -2485,23 +2332,19 @@ class PostHog extends PostHogCoreStateless {
|
|
|
2485
2332
|
disableGeoip
|
|
2486
2333
|
}, distinctId);
|
|
2487
2334
|
}
|
|
2488
|
-
|
|
2489
2335
|
async reloadFeatureFlags() {
|
|
2490
2336
|
await this.featureFlagsPoller?.loadFeatureFlags(true);
|
|
2491
2337
|
}
|
|
2492
|
-
|
|
2493
2338
|
async shutdown(shutdownTimeoutMs) {
|
|
2494
2339
|
this.featureFlagsPoller?.stopPoller();
|
|
2495
2340
|
return super.shutdown(shutdownTimeoutMs);
|
|
2496
2341
|
}
|
|
2497
|
-
|
|
2498
2342
|
addLocalPersonAndGroupProperties(distinctId, groups, personProperties, groupProperties) {
|
|
2499
2343
|
const allPersonProperties = {
|
|
2500
2344
|
distinct_id: distinctId,
|
|
2501
2345
|
...(personProperties || {})
|
|
2502
2346
|
};
|
|
2503
2347
|
const allGroupProperties = {};
|
|
2504
|
-
|
|
2505
2348
|
if (groups) {
|
|
2506
2349
|
for (const groupName of Object.keys(groups)) {
|
|
2507
2350
|
allGroupProperties[groupName] = {
|
|
@@ -2510,13 +2353,11 @@ class PostHog extends PostHogCoreStateless {
|
|
|
2510
2353
|
};
|
|
2511
2354
|
}
|
|
2512
2355
|
}
|
|
2513
|
-
|
|
2514
2356
|
return {
|
|
2515
2357
|
allPersonProperties,
|
|
2516
2358
|
allGroupProperties
|
|
2517
2359
|
};
|
|
2518
2360
|
}
|
|
2519
|
-
|
|
2520
2361
|
}
|
|
2521
2362
|
|
|
2522
2363
|
/**
|
|
@@ -2547,26 +2388,21 @@ class PostHogSentryIntegration {
|
|
|
2547
2388
|
this.name = 'posthog-node';
|
|
2548
2389
|
this.posthogHost = posthog.options.host ?? 'https://us.i.posthog.com';
|
|
2549
2390
|
}
|
|
2550
|
-
|
|
2551
2391
|
setupOnce(addGlobalEventProcessor, getCurrentHub) {
|
|
2552
2392
|
addGlobalEventProcessor(event => {
|
|
2553
2393
|
if (event.exception?.values === undefined || event.exception.values.length === 0) {
|
|
2554
2394
|
return event;
|
|
2555
2395
|
}
|
|
2556
|
-
|
|
2557
2396
|
if (!event.tags) {
|
|
2558
2397
|
event.tags = {};
|
|
2559
2398
|
}
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2399
|
+
const sentry = getCurrentHub();
|
|
2400
|
+
// Get the PostHog user ID from a specific tag, which users can set on their Sentry scope as they need.
|
|
2563
2401
|
const userId = event.tags[PostHogSentryIntegration.POSTHOG_ID_TAG];
|
|
2564
|
-
|
|
2565
2402
|
if (userId === undefined) {
|
|
2566
2403
|
// If we can't find a user ID, don't bother linking the event. We won't be able to send anything meaningful to PostHog without it.
|
|
2567
2404
|
return event;
|
|
2568
2405
|
}
|
|
2569
|
-
|
|
2570
2406
|
event.tags['PostHog Person URL'] = new URL(`/person/${userId}`, this.posthogHost).toString();
|
|
2571
2407
|
const properties = {
|
|
2572
2408
|
// PostHog Exception Properties
|
|
@@ -2581,11 +2417,9 @@ class PostHogSentryIntegration {
|
|
|
2581
2417
|
$sentry_tags: event.tags
|
|
2582
2418
|
};
|
|
2583
2419
|
const projectId = sentry.getClient()?.getDsn()?.projectId;
|
|
2584
|
-
|
|
2585
2420
|
if (this.organization !== undefined && projectId !== undefined && event.event_id !== undefined) {
|
|
2586
2421
|
properties.$sentry_url = `${this.prefix ?? 'https://sentry.io/organizations'}/${this.organization}/issues/?project=${projectId}&query=${event.event_id}`;
|
|
2587
2422
|
}
|
|
2588
|
-
|
|
2589
2423
|
this.posthog.capture({
|
|
2590
2424
|
event: '$exception',
|
|
2591
2425
|
distinctId: userId,
|
|
@@ -2594,7 +2428,6 @@ class PostHogSentryIntegration {
|
|
|
2594
2428
|
return event;
|
|
2595
2429
|
});
|
|
2596
2430
|
}
|
|
2597
|
-
|
|
2598
2431
|
}
|
|
2599
2432
|
PostHogSentryIntegration.POSTHOG_ID_TAG = 'posthog_distinct_id';
|
|
2600
2433
|
|