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