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