posthog-node 4.10.2 → 4.11.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 +16 -2
- package/lib/index.cjs.js +293 -64
- package/lib/index.cjs.js.map +1 -1
- package/lib/index.d.ts +73 -13
- package/lib/index.esm.js +275 -64
- package/lib/index.esm.js.map +1 -1
- package/lib/posthog-core/src/featureFlagUtils.d.ts +34 -0
- package/lib/posthog-core/src/index.d.ts +18 -7
- package/lib/posthog-core/src/types.d.ts +74 -6
- package/lib/posthog-node/src/crypto-helpers.d.ts +3 -0
- package/lib/posthog-node/src/crypto.d.ts +2 -0
- package/lib/posthog-node/src/feature-flags.d.ts +8 -8
- package/lib/posthog-node/src/lazy.d.ts +23 -0
- package/lib/posthog-node/src/posthog-node.d.ts +4 -3
- package/lib/posthog-node/src/types.d.ts +3 -3
- package/lib/posthog-node/test/test-utils.d.ts +7 -0
- package/package.json +1 -1
- package/src/crypto-helpers.ts +36 -0
- package/src/crypto.ts +22 -0
- package/src/feature-flags.ts +42 -41
- package/src/lazy.ts +55 -0
- package/src/posthog-node.ts +36 -17
- package/src/types.ts +3 -3
- package/test/crypto.spec.ts +36 -0
- package/test/feature-flags.decide.spec.ts +380 -0
- package/test/feature-flags.spec.ts +3 -45
- package/test/lazy.spec.ts +71 -0
- package/test/posthog-node.spec.ts +19 -19
- package/test/test-utils.ts +36 -1
- package/benchmarks/rusha-vs-native.mjs +0 -70
package/lib/index.esm.js
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
|
-
import { createHash } from 'node:crypto';
|
|
2
1
|
import { createReadStream } from 'node:fs';
|
|
3
2
|
import { createInterface } from 'node:readline';
|
|
4
3
|
import { posix, dirname, sep } from 'node:path';
|
|
5
4
|
|
|
6
|
-
var version = "4.
|
|
5
|
+
var version = "4.11.1";
|
|
7
6
|
|
|
8
7
|
var PostHogPersistedProperty;
|
|
9
8
|
(function (PostHogPersistedProperty) {
|
|
10
9
|
PostHogPersistedProperty["AnonymousId"] = "anonymous_id";
|
|
11
10
|
PostHogPersistedProperty["DistinctId"] = "distinct_id";
|
|
12
11
|
PostHogPersistedProperty["Props"] = "props";
|
|
12
|
+
PostHogPersistedProperty["FeatureFlagDetails"] = "feature_flag_details";
|
|
13
13
|
PostHogPersistedProperty["FeatureFlags"] = "feature_flags";
|
|
14
14
|
PostHogPersistedProperty["FeatureFlagPayloads"] = "feature_flag_payloads";
|
|
15
|
+
PostHogPersistedProperty["BootstrapFeatureFlagDetails"] = "bootstrap_feature_flag_details";
|
|
15
16
|
PostHogPersistedProperty["BootstrapFeatureFlags"] = "bootstrap_feature_flags";
|
|
16
17
|
PostHogPersistedProperty["BootstrapFeatureFlagPayloads"] = "bootstrap_feature_flag_payloads";
|
|
17
18
|
PostHogPersistedProperty["OverrideFeatureFlags"] = "override_feature_flags";
|
|
@@ -31,11 +32,100 @@ var PostHogPersistedProperty;
|
|
|
31
32
|
PostHogPersistedProperty["RemoteConfig"] = "remote_config";
|
|
32
33
|
})(PostHogPersistedProperty || (PostHogPersistedProperty = {}));
|
|
33
34
|
|
|
35
|
+
const normalizeDecideResponse = (decideResponse) => {
|
|
36
|
+
if ('flags' in decideResponse) {
|
|
37
|
+
// Convert v4 format to v3 format
|
|
38
|
+
const featureFlags = getFlagValuesFromFlags(decideResponse.flags);
|
|
39
|
+
const featureFlagPayloads = getPayloadsFromFlags(decideResponse.flags);
|
|
40
|
+
return {
|
|
41
|
+
...decideResponse,
|
|
42
|
+
featureFlags,
|
|
43
|
+
featureFlagPayloads,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
// Convert v3 format to v4 format
|
|
48
|
+
const featureFlags = decideResponse.featureFlags ?? {};
|
|
49
|
+
const featureFlagPayloads = Object.fromEntries(Object.entries(decideResponse.featureFlagPayloads || {}).map(([k, v]) => [k, parsePayload(v)]));
|
|
50
|
+
const flags = Object.fromEntries(Object.entries(featureFlags).map(([key, value]) => [
|
|
51
|
+
key,
|
|
52
|
+
getFlagDetailFromFlagAndPayload(key, value, featureFlagPayloads[key]),
|
|
53
|
+
]));
|
|
54
|
+
return {
|
|
55
|
+
...decideResponse,
|
|
56
|
+
featureFlags,
|
|
57
|
+
featureFlagPayloads,
|
|
58
|
+
flags,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
function getFlagDetailFromFlagAndPayload(key, value, payload) {
|
|
63
|
+
return {
|
|
64
|
+
key: key,
|
|
65
|
+
enabled: typeof value === 'string' ? true : value,
|
|
66
|
+
variant: typeof value === 'string' ? value : undefined,
|
|
67
|
+
reason: undefined,
|
|
68
|
+
metadata: {
|
|
69
|
+
id: undefined,
|
|
70
|
+
version: undefined,
|
|
71
|
+
payload: payload ? JSON.stringify(payload) : undefined,
|
|
72
|
+
description: undefined,
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Get the flag values from the flags v4 response.
|
|
78
|
+
* @param flags - The flags
|
|
79
|
+
* @returns The flag values
|
|
80
|
+
*/
|
|
81
|
+
const getFlagValuesFromFlags = (flags) => {
|
|
82
|
+
return Object.fromEntries(Object.entries(flags ?? {})
|
|
83
|
+
.map(([key, detail]) => [key, getFeatureFlagValue(detail)])
|
|
84
|
+
.filter(([, value]) => value !== undefined));
|
|
85
|
+
};
|
|
86
|
+
/**
|
|
87
|
+
* Get the payloads from the flags v4 response.
|
|
88
|
+
* @param flags - The flags
|
|
89
|
+
* @returns The payloads
|
|
90
|
+
*/
|
|
91
|
+
const getPayloadsFromFlags = (flags) => {
|
|
92
|
+
const safeFlags = flags ?? {};
|
|
93
|
+
return Object.fromEntries(Object.keys(safeFlags)
|
|
94
|
+
.filter((flag) => {
|
|
95
|
+
const details = safeFlags[flag];
|
|
96
|
+
return details.enabled && details.metadata && details.metadata.payload !== undefined;
|
|
97
|
+
})
|
|
98
|
+
.map((flag) => {
|
|
99
|
+
const payload = safeFlags[flag].metadata?.payload;
|
|
100
|
+
return [flag, payload ? parsePayload(payload) : undefined];
|
|
101
|
+
}));
|
|
102
|
+
};
|
|
103
|
+
const getFeatureFlagValue = (detail) => {
|
|
104
|
+
return detail === undefined ? undefined : detail.variant ?? detail.enabled;
|
|
105
|
+
};
|
|
106
|
+
const parsePayload = (response) => {
|
|
107
|
+
if (typeof response !== 'string') {
|
|
108
|
+
return response;
|
|
109
|
+
}
|
|
110
|
+
try {
|
|
111
|
+
return JSON.parse(response);
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return response;
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
|
|
34
118
|
function assert(truthyValue, message) {
|
|
35
|
-
if (!truthyValue) {
|
|
119
|
+
if (!truthyValue || typeof truthyValue !== 'string' || isEmpty(truthyValue)) {
|
|
36
120
|
throw new Error(message);
|
|
37
121
|
}
|
|
38
122
|
}
|
|
123
|
+
function isEmpty(truthyValue) {
|
|
124
|
+
if (truthyValue.trim().length === 0) {
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
39
129
|
function removeTrailingSlash(url) {
|
|
40
130
|
return url?.replace(/\/+$/, '');
|
|
41
131
|
}
|
|
@@ -1154,7 +1244,7 @@ class PostHogCoreStateless {
|
|
|
1154
1244
|
***/
|
|
1155
1245
|
async getDecide(distinctId, groups = {}, personProperties = {}, groupProperties = {}, extraPayload = {}) {
|
|
1156
1246
|
await this._initPromise;
|
|
1157
|
-
const url = `${this.host}/decide/?v=
|
|
1247
|
+
const url = `${this.host}/decide/?v=4`;
|
|
1158
1248
|
const fetchOptions = {
|
|
1159
1249
|
method: 'POST',
|
|
1160
1250
|
headers: { ...this.getCustomHeaders(), 'Content-Type': 'application/json' },
|
|
@@ -1170,6 +1260,7 @@ class PostHogCoreStateless {
|
|
|
1170
1260
|
// Don't retry /decide API calls
|
|
1171
1261
|
return this.fetchWithRetry(url, fetchOptions, { retryCount: 0 }, this.featureFlagsRequestTimeoutMs)
|
|
1172
1262
|
.then((response) => response.json())
|
|
1263
|
+
.then((response) => normalizeDecideResponse(response))
|
|
1173
1264
|
.catch((error) => {
|
|
1174
1265
|
this._events.emit('error', error);
|
|
1175
1266
|
return undefined;
|
|
@@ -1177,17 +1268,15 @@ class PostHogCoreStateless {
|
|
|
1177
1268
|
}
|
|
1178
1269
|
async getFeatureFlagStateless(key, distinctId, groups = {}, personProperties = {}, groupProperties = {}, disableGeoip) {
|
|
1179
1270
|
await this._initPromise;
|
|
1180
|
-
const
|
|
1181
|
-
|
|
1182
|
-
if (!featureFlags) {
|
|
1271
|
+
const flagDetailResponse = await this.getFeatureFlagDetailStateless(key, distinctId, groups, personProperties, groupProperties, disableGeoip);
|
|
1272
|
+
if (flagDetailResponse === undefined) {
|
|
1183
1273
|
// If we haven't loaded flags yet, or errored out, we respond with undefined
|
|
1184
1274
|
return {
|
|
1185
1275
|
response: undefined,
|
|
1186
1276
|
requestId: undefined,
|
|
1187
1277
|
};
|
|
1188
1278
|
}
|
|
1189
|
-
let response =
|
|
1190
|
-
// `/decide` v3 returns all flags
|
|
1279
|
+
let response = getFeatureFlagValue(flagDetailResponse.response);
|
|
1191
1280
|
if (response === undefined) {
|
|
1192
1281
|
// For cases where the flag is unknown, return false
|
|
1193
1282
|
response = false;
|
|
@@ -1195,6 +1284,19 @@ class PostHogCoreStateless {
|
|
|
1195
1284
|
// If we have flags we either return the value (true or string) or false
|
|
1196
1285
|
return {
|
|
1197
1286
|
response,
|
|
1287
|
+
requestId: flagDetailResponse.requestId,
|
|
1288
|
+
};
|
|
1289
|
+
}
|
|
1290
|
+
async getFeatureFlagDetailStateless(key, distinctId, groups = {}, personProperties = {}, groupProperties = {}, disableGeoip) {
|
|
1291
|
+
await this._initPromise;
|
|
1292
|
+
const decideResponse = await this.getFeatureFlagDetailsStateless(distinctId, groups, personProperties, groupProperties, disableGeoip, [key]);
|
|
1293
|
+
if (decideResponse === undefined) {
|
|
1294
|
+
return undefined;
|
|
1295
|
+
}
|
|
1296
|
+
const featureFlags = decideResponse.flags;
|
|
1297
|
+
const flagDetail = featureFlags[key];
|
|
1298
|
+
return {
|
|
1299
|
+
response: flagDetail,
|
|
1198
1300
|
requestId: decideResponse.requestId,
|
|
1199
1301
|
};
|
|
1200
1302
|
}
|
|
@@ -1216,19 +1318,27 @@ class PostHogCoreStateless {
|
|
|
1216
1318
|
const payloads = (await this.getFeatureFlagsAndPayloadsStateless(distinctId, groups, personProperties, groupProperties, disableGeoip, flagKeysToEvaluate)).payloads;
|
|
1217
1319
|
return payloads;
|
|
1218
1320
|
}
|
|
1219
|
-
_parsePayload(response) {
|
|
1220
|
-
try {
|
|
1221
|
-
return JSON.parse(response);
|
|
1222
|
-
}
|
|
1223
|
-
catch {
|
|
1224
|
-
return response;
|
|
1225
|
-
}
|
|
1226
|
-
}
|
|
1227
1321
|
async getFeatureFlagsStateless(distinctId, groups = {}, personProperties = {}, groupProperties = {}, disableGeoip, flagKeysToEvaluate) {
|
|
1228
1322
|
await this._initPromise;
|
|
1229
1323
|
return await this.getFeatureFlagsAndPayloadsStateless(distinctId, groups, personProperties, groupProperties, disableGeoip, flagKeysToEvaluate);
|
|
1230
1324
|
}
|
|
1231
1325
|
async getFeatureFlagsAndPayloadsStateless(distinctId, groups = {}, personProperties = {}, groupProperties = {}, disableGeoip, flagKeysToEvaluate) {
|
|
1326
|
+
await this._initPromise;
|
|
1327
|
+
const featureFlagDetails = await this.getFeatureFlagDetailsStateless(distinctId, groups, personProperties, groupProperties, disableGeoip, flagKeysToEvaluate);
|
|
1328
|
+
if (!featureFlagDetails) {
|
|
1329
|
+
return {
|
|
1330
|
+
flags: undefined,
|
|
1331
|
+
payloads: undefined,
|
|
1332
|
+
requestId: undefined,
|
|
1333
|
+
};
|
|
1334
|
+
}
|
|
1335
|
+
return {
|
|
1336
|
+
flags: featureFlagDetails.featureFlags,
|
|
1337
|
+
payloads: featureFlagDetails.featureFlagPayloads,
|
|
1338
|
+
requestId: featureFlagDetails.requestId,
|
|
1339
|
+
};
|
|
1340
|
+
}
|
|
1341
|
+
async getFeatureFlagDetailsStateless(distinctId, groups = {}, personProperties = {}, groupProperties = {}, disableGeoip, flagKeysToEvaluate) {
|
|
1232
1342
|
await this._initPromise;
|
|
1233
1343
|
const extraPayload = {};
|
|
1234
1344
|
if (disableGeoip ?? this.disableGeoip) {
|
|
@@ -1238,30 +1348,25 @@ class PostHogCoreStateless {
|
|
|
1238
1348
|
extraPayload['flag_keys_to_evaluate'] = flagKeysToEvaluate;
|
|
1239
1349
|
}
|
|
1240
1350
|
const decideResponse = await this.getDecide(distinctId, groups, personProperties, groupProperties, extraPayload);
|
|
1351
|
+
if (decideResponse === undefined) {
|
|
1352
|
+
// We probably errored out, so return undefined
|
|
1353
|
+
return undefined;
|
|
1354
|
+
}
|
|
1241
1355
|
// if there's an error on the decideResponse, log a console error, but don't throw an error
|
|
1242
|
-
if (decideResponse
|
|
1356
|
+
if (decideResponse.errorsWhileComputingFlags) {
|
|
1243
1357
|
console.error('[FEATURE FLAGS] Error while computing feature flags, some flags may be missing or incorrect. Learn more at https://posthog.com/docs/feature-flags/best-practices');
|
|
1244
1358
|
}
|
|
1245
1359
|
// Add check for quota limitation on feature flags
|
|
1246
|
-
if (decideResponse
|
|
1360
|
+
if (decideResponse.quotaLimited?.includes(QuotaLimitedFeature.FeatureFlags)) {
|
|
1247
1361
|
console.warn('[FEATURE FLAGS] Feature flags quota limit exceeded - feature flags unavailable. Learn more about billing limits at https://posthog.com/docs/billing/limits-alerts');
|
|
1248
1362
|
return {
|
|
1249
|
-
flags:
|
|
1250
|
-
|
|
1363
|
+
flags: {},
|
|
1364
|
+
featureFlags: {},
|
|
1365
|
+
featureFlagPayloads: {},
|
|
1251
1366
|
requestId: decideResponse?.requestId,
|
|
1252
1367
|
};
|
|
1253
1368
|
}
|
|
1254
|
-
|
|
1255
|
-
const payloads = decideResponse?.featureFlagPayloads;
|
|
1256
|
-
let parsedPayloads = payloads;
|
|
1257
|
-
if (payloads) {
|
|
1258
|
-
parsedPayloads = Object.fromEntries(Object.entries(payloads).map(([k, v]) => [k, this._parsePayload(v)]));
|
|
1259
|
-
}
|
|
1260
|
-
return {
|
|
1261
|
-
flags,
|
|
1262
|
-
payloads: parsedPayloads,
|
|
1263
|
-
requestId: decideResponse?.requestId,
|
|
1264
|
-
};
|
|
1369
|
+
return decideResponse;
|
|
1265
1370
|
}
|
|
1266
1371
|
/***
|
|
1267
1372
|
*** SURVEYS
|
|
@@ -1548,6 +1653,100 @@ if (!_fetch) {
|
|
|
1548
1653
|
// 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
|
|
1549
1654
|
var fetch$1 = _fetch;
|
|
1550
1655
|
|
|
1656
|
+
/**
|
|
1657
|
+
* A lazy value that is only computed when needed. Inspired by C#'s Lazy<T> class.
|
|
1658
|
+
*/
|
|
1659
|
+
class Lazy {
|
|
1660
|
+
constructor(factory) {
|
|
1661
|
+
this.factory = factory;
|
|
1662
|
+
}
|
|
1663
|
+
/**
|
|
1664
|
+
* Gets the value, initializing it if necessary.
|
|
1665
|
+
* Multiple concurrent calls will share the same initialization promise.
|
|
1666
|
+
*/
|
|
1667
|
+
async getValue() {
|
|
1668
|
+
if (this.value !== undefined) {
|
|
1669
|
+
return this.value;
|
|
1670
|
+
}
|
|
1671
|
+
if (this.initializationPromise === undefined) {
|
|
1672
|
+
this.initializationPromise = (async () => {
|
|
1673
|
+
try {
|
|
1674
|
+
const result = await this.factory();
|
|
1675
|
+
this.value = result;
|
|
1676
|
+
return result;
|
|
1677
|
+
} finally {
|
|
1678
|
+
// Clear the promise so we can retry if needed
|
|
1679
|
+
this.initializationPromise = undefined;
|
|
1680
|
+
}
|
|
1681
|
+
})();
|
|
1682
|
+
}
|
|
1683
|
+
return this.initializationPromise;
|
|
1684
|
+
}
|
|
1685
|
+
/**
|
|
1686
|
+
* Returns true if the value has been initialized.
|
|
1687
|
+
*/
|
|
1688
|
+
isInitialized() {
|
|
1689
|
+
return this.value !== undefined;
|
|
1690
|
+
}
|
|
1691
|
+
/**
|
|
1692
|
+
* Returns a promise that resolves when the value is initialized.
|
|
1693
|
+
* If already initialized, resolves immediately.
|
|
1694
|
+
*/
|
|
1695
|
+
async waitForInitialization() {
|
|
1696
|
+
if (this.isInitialized()) {
|
|
1697
|
+
return;
|
|
1698
|
+
}
|
|
1699
|
+
await this.getValue();
|
|
1700
|
+
}
|
|
1701
|
+
}
|
|
1702
|
+
|
|
1703
|
+
/// <reference lib="dom" />
|
|
1704
|
+
const nodeCrypto = new Lazy(async () => {
|
|
1705
|
+
try {
|
|
1706
|
+
return await import('crypto');
|
|
1707
|
+
} catch {
|
|
1708
|
+
return undefined;
|
|
1709
|
+
}
|
|
1710
|
+
});
|
|
1711
|
+
async function getNodeCrypto() {
|
|
1712
|
+
return await nodeCrypto.getValue();
|
|
1713
|
+
}
|
|
1714
|
+
const webCrypto = new Lazy(async () => {
|
|
1715
|
+
if (typeof globalThis.crypto?.subtle !== 'undefined') {
|
|
1716
|
+
return globalThis.crypto.subtle;
|
|
1717
|
+
}
|
|
1718
|
+
try {
|
|
1719
|
+
// Node.js: use built-in webcrypto and assign it if needed
|
|
1720
|
+
const crypto = await nodeCrypto.getValue();
|
|
1721
|
+
if (crypto?.webcrypto?.subtle) {
|
|
1722
|
+
return crypto.webcrypto.subtle;
|
|
1723
|
+
}
|
|
1724
|
+
} catch {
|
|
1725
|
+
// Ignore if not available
|
|
1726
|
+
}
|
|
1727
|
+
return undefined;
|
|
1728
|
+
});
|
|
1729
|
+
async function getWebCrypto() {
|
|
1730
|
+
return await webCrypto.getValue();
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
/// <reference lib="dom" />
|
|
1734
|
+
async function hashSHA1(text) {
|
|
1735
|
+
// Try Node.js crypto first
|
|
1736
|
+
const nodeCrypto = await getNodeCrypto();
|
|
1737
|
+
if (nodeCrypto) {
|
|
1738
|
+
return nodeCrypto.createHash('sha1').update(text).digest('hex');
|
|
1739
|
+
}
|
|
1740
|
+
const webCrypto = await getWebCrypto();
|
|
1741
|
+
// Fall back to Web Crypto API
|
|
1742
|
+
if (webCrypto) {
|
|
1743
|
+
const hashBuffer = await webCrypto.digest('SHA-1', new TextEncoder().encode(text));
|
|
1744
|
+
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
1745
|
+
return hashArray.map(byte => byte.toString(16).padStart(2, '0')).join('');
|
|
1746
|
+
}
|
|
1747
|
+
throw new Error('No crypto implementation available. Tried Node Crypto API and Web SubtleCrypto API');
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1551
1750
|
// eslint-disable-next-line
|
|
1552
1751
|
const LONG_SCALE = 0xfffffffffffffff;
|
|
1553
1752
|
const NULL_VALUES_ALLOWED_OPERATORS = ['is_not'];
|
|
@@ -1623,7 +1822,7 @@ class FeatureFlagsPoller {
|
|
|
1623
1822
|
}
|
|
1624
1823
|
if (featureFlag !== undefined) {
|
|
1625
1824
|
try {
|
|
1626
|
-
response = this.computeFlagLocally(featureFlag, distinctId, groups, personProperties, groupProperties);
|
|
1825
|
+
response = await this.computeFlagLocally(featureFlag, distinctId, groups, personProperties, groupProperties);
|
|
1627
1826
|
this.logMsgIfDebug(() => console.debug(`Successfully computed flag locally: ${key} -> ${response}`));
|
|
1628
1827
|
} catch (e) {
|
|
1629
1828
|
if (e instanceof InconclusiveMatchError) {
|
|
@@ -1661,9 +1860,9 @@ class FeatureFlagsPoller {
|
|
|
1661
1860
|
const response = {};
|
|
1662
1861
|
const payloads = {};
|
|
1663
1862
|
let fallbackToDecide = this.featureFlags.length == 0;
|
|
1664
|
-
this.featureFlags.map(async flag => {
|
|
1863
|
+
await Promise.all(this.featureFlags.map(async flag => {
|
|
1665
1864
|
try {
|
|
1666
|
-
const matchValue = this.computeFlagLocally(flag, distinctId, groups, personProperties, groupProperties);
|
|
1865
|
+
const matchValue = await this.computeFlagLocally(flag, distinctId, groups, personProperties, groupProperties);
|
|
1667
1866
|
response[flag.key] = matchValue;
|
|
1668
1867
|
const matchPayload = await this.computeFeatureFlagPayloadLocally(flag.key, matchValue);
|
|
1669
1868
|
if (matchPayload) {
|
|
@@ -1675,14 +1874,14 @@ class FeatureFlagsPoller {
|
|
|
1675
1874
|
}
|
|
1676
1875
|
fallbackToDecide = true;
|
|
1677
1876
|
}
|
|
1678
|
-
});
|
|
1877
|
+
}));
|
|
1679
1878
|
return {
|
|
1680
1879
|
response,
|
|
1681
1880
|
payloads,
|
|
1682
1881
|
fallbackToDecide
|
|
1683
1882
|
};
|
|
1684
1883
|
}
|
|
1685
|
-
computeFlagLocally(flag, distinctId, groups = {}, personProperties = {}, groupProperties = {}) {
|
|
1884
|
+
async computeFlagLocally(flag, distinctId, groups = {}, personProperties = {}, groupProperties = {}) {
|
|
1686
1885
|
if (flag.ensure_experience_continuity) {
|
|
1687
1886
|
throw new InconclusiveMatchError('Flag has experience continuity enabled');
|
|
1688
1887
|
}
|
|
@@ -1702,12 +1901,12 @@ class FeatureFlagsPoller {
|
|
|
1702
1901
|
return false;
|
|
1703
1902
|
}
|
|
1704
1903
|
const focusedGroupProperties = groupProperties[groupName];
|
|
1705
|
-
return this.matchFeatureFlagProperties(flag, groups[groupName], focusedGroupProperties);
|
|
1904
|
+
return await this.matchFeatureFlagProperties(flag, groups[groupName], focusedGroupProperties);
|
|
1706
1905
|
} else {
|
|
1707
|
-
return this.matchFeatureFlagProperties(flag, distinctId, personProperties);
|
|
1906
|
+
return await this.matchFeatureFlagProperties(flag, distinctId, personProperties);
|
|
1708
1907
|
}
|
|
1709
1908
|
}
|
|
1710
|
-
matchFeatureFlagProperties(flag, distinctId, properties) {
|
|
1909
|
+
async matchFeatureFlagProperties(flag, distinctId, properties) {
|
|
1711
1910
|
const flagFilters = flag.filters || {};
|
|
1712
1911
|
const flagConditions = flagFilters.groups || [];
|
|
1713
1912
|
let isInconclusive = false;
|
|
@@ -1729,13 +1928,13 @@ class FeatureFlagsPoller {
|
|
|
1729
1928
|
});
|
|
1730
1929
|
for (const condition of sortedFlagConditions) {
|
|
1731
1930
|
try {
|
|
1732
|
-
if (this.isConditionMatch(flag, distinctId, condition, properties)) {
|
|
1931
|
+
if (await this.isConditionMatch(flag, distinctId, condition, properties)) {
|
|
1733
1932
|
const variantOverride = condition.variant;
|
|
1734
1933
|
const flagVariants = flagFilters.multivariate?.variants || [];
|
|
1735
1934
|
if (variantOverride && flagVariants.some(variant => variant.key === variantOverride)) {
|
|
1736
1935
|
result = variantOverride;
|
|
1737
1936
|
} else {
|
|
1738
|
-
result = this.getMatchingVariant(flag, distinctId) || true;
|
|
1937
|
+
result = (await this.getMatchingVariant(flag, distinctId)) || true;
|
|
1739
1938
|
}
|
|
1740
1939
|
break;
|
|
1741
1940
|
}
|
|
@@ -1755,7 +1954,7 @@ class FeatureFlagsPoller {
|
|
|
1755
1954
|
// We can only return False when all conditions are False
|
|
1756
1955
|
return false;
|
|
1757
1956
|
}
|
|
1758
|
-
isConditionMatch(flag, distinctId, condition, properties) {
|
|
1957
|
+
async isConditionMatch(flag, distinctId, condition, properties) {
|
|
1759
1958
|
const rolloutPercentage = condition.rollout_percentage;
|
|
1760
1959
|
const warnFunction = msg => {
|
|
1761
1960
|
this.logMsgIfDebug(() => console.warn(msg));
|
|
@@ -1777,13 +1976,13 @@ class FeatureFlagsPoller {
|
|
|
1777
1976
|
return true;
|
|
1778
1977
|
}
|
|
1779
1978
|
}
|
|
1780
|
-
if (rolloutPercentage != undefined && _hash(flag.key, distinctId) > rolloutPercentage / 100.0) {
|
|
1979
|
+
if (rolloutPercentage != undefined && (await _hash(flag.key, distinctId)) > rolloutPercentage / 100.0) {
|
|
1781
1980
|
return false;
|
|
1782
1981
|
}
|
|
1783
1982
|
return true;
|
|
1784
1983
|
}
|
|
1785
|
-
getMatchingVariant(flag, distinctId) {
|
|
1786
|
-
const hashValue = _hash(flag.key, distinctId, 'variant');
|
|
1984
|
+
async getMatchingVariant(flag, distinctId) {
|
|
1985
|
+
const hashValue = await _hash(flag.key, distinctId, 'variant');
|
|
1787
1986
|
const matchingVariant = this.variantLookupTable(flag).find(variant => {
|
|
1788
1987
|
return hashValue >= variant.valueMin && hashValue < variant.valueMax;
|
|
1789
1988
|
});
|
|
@@ -1954,10 +2153,9 @@ class FeatureFlagsPoller {
|
|
|
1954
2153
|
// # Given the same distinct_id and key, it'll always return the same float. These floats are
|
|
1955
2154
|
// # uniformly distributed between 0 and 1, so if we want to show this feature to 20% of traffic
|
|
1956
2155
|
// # we can do _hash(key, distinct_id) < 0.2
|
|
1957
|
-
function _hash(key, distinctId, salt = '') {
|
|
1958
|
-
const
|
|
1959
|
-
|
|
1960
|
-
return parseInt(sha1Hash.digest('hex').slice(0, 15), 16) / LONG_SCALE;
|
|
2156
|
+
async function _hash(key, distinctId, salt = '') {
|
|
2157
|
+
const hashString = await hashSHA1(`${key}.${distinctId}${salt}`);
|
|
2158
|
+
return parseInt(hashString.slice(0, 15), 16) / LONG_SCALE;
|
|
1961
2159
|
}
|
|
1962
2160
|
function matchProperty(property, propertyValues, warnFunction) {
|
|
1963
2161
|
const key = property.key;
|
|
@@ -3200,7 +3398,7 @@ class PostHog extends PostHogCoreStateless {
|
|
|
3200
3398
|
additionalProperties[`$feature/${feature}`] = variant;
|
|
3201
3399
|
}
|
|
3202
3400
|
}
|
|
3203
|
-
const activeFlags = Object.keys(flags || {}).filter(flag => flags?.[flag] !== false);
|
|
3401
|
+
const activeFlags = Object.keys(flags || {}).filter(flag => flags?.[flag] !== false).sort();
|
|
3204
3402
|
if (activeFlags.length > 0) {
|
|
3205
3403
|
additionalProperties['$active_feature_flags'] = activeFlags;
|
|
3206
3404
|
}
|
|
@@ -3265,10 +3463,15 @@ class PostHog extends PostHogCoreStateless {
|
|
|
3265
3463
|
let response = await this.featureFlagsPoller?.getFeatureFlag(key, distinctId, groups, personProperties, groupProperties);
|
|
3266
3464
|
const flagWasLocallyEvaluated = response !== undefined;
|
|
3267
3465
|
let requestId = undefined;
|
|
3466
|
+
let flagDetail = undefined;
|
|
3268
3467
|
if (!flagWasLocallyEvaluated && !onlyEvaluateLocally) {
|
|
3269
|
-
const remoteResponse = await super.
|
|
3270
|
-
|
|
3271
|
-
|
|
3468
|
+
const remoteResponse = await super.getFeatureFlagDetailStateless(key, distinctId, groups, personProperties, groupProperties, disableGeoip);
|
|
3469
|
+
if (remoteResponse === undefined) {
|
|
3470
|
+
return undefined;
|
|
3471
|
+
}
|
|
3472
|
+
flagDetail = remoteResponse.response;
|
|
3473
|
+
response = getFeatureFlagValue(flagDetail);
|
|
3474
|
+
requestId = remoteResponse?.requestId;
|
|
3272
3475
|
}
|
|
3273
3476
|
const featureFlagReportedKey = `${key}_${response}`;
|
|
3274
3477
|
if (sendFeatureFlagEvents && (!(distinctId in this.distinctIdHasSentFlagCalls) || !this.distinctIdHasSentFlagCalls[distinctId].includes(featureFlagReportedKey))) {
|
|
@@ -3286,6 +3489,9 @@ class PostHog extends PostHogCoreStateless {
|
|
|
3286
3489
|
properties: {
|
|
3287
3490
|
$feature_flag: key,
|
|
3288
3491
|
$feature_flag_response: response,
|
|
3492
|
+
$feature_flag_id: flagDetail?.metadata?.id,
|
|
3493
|
+
$feature_flag_version: flagDetail?.metadata?.version,
|
|
3494
|
+
$feature_flag_reason: flagDetail?.reason?.description ?? flagDetail?.reason?.code,
|
|
3289
3495
|
locally_evaluated: flagWasLocallyEvaluated,
|
|
3290
3496
|
[`$feature/${key}`]: response,
|
|
3291
3497
|
$feature_flag_request_id: requestId
|
|
@@ -3311,16 +3517,21 @@ class PostHog extends PostHogCoreStateless {
|
|
|
3311
3517
|
personProperties = adjustedProperties.allPersonProperties;
|
|
3312
3518
|
groupProperties = adjustedProperties.allGroupProperties;
|
|
3313
3519
|
let response = undefined;
|
|
3314
|
-
|
|
3315
|
-
if (
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3520
|
+
const localEvaluationEnabled = this.featureFlagsPoller !== undefined;
|
|
3521
|
+
if (localEvaluationEnabled) {
|
|
3522
|
+
// Try to get match value locally if not provided
|
|
3523
|
+
if (!matchValue) {
|
|
3524
|
+
matchValue = await this.getFeatureFlag(key, distinctId, {
|
|
3525
|
+
...options,
|
|
3526
|
+
onlyEvaluateLocally: true,
|
|
3527
|
+
sendFeatureFlagEvents: false
|
|
3528
|
+
});
|
|
3529
|
+
}
|
|
3530
|
+
if (matchValue) {
|
|
3531
|
+
response = await this.featureFlagsPoller?.computeFeatureFlagPayloadLocally(key, matchValue);
|
|
3532
|
+
}
|
|
3323
3533
|
}
|
|
3534
|
+
//}
|
|
3324
3535
|
// set defaults
|
|
3325
3536
|
if (onlyEvaluateLocally == undefined) {
|
|
3326
3537
|
onlyEvaluateLocally = false;
|
|
@@ -3350,7 +3561,7 @@ class PostHog extends PostHogCoreStateless {
|
|
|
3350
3561
|
}
|
|
3351
3562
|
async getAllFlags(distinctId, options) {
|
|
3352
3563
|
const response = await this.getAllFlagsAndPayloads(distinctId, options);
|
|
3353
|
-
return response.featureFlags;
|
|
3564
|
+
return response.featureFlags || {};
|
|
3354
3565
|
}
|
|
3355
3566
|
async getAllFlagsAndPayloads(distinctId, options) {
|
|
3356
3567
|
const {
|