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/CHANGELOG.md
CHANGED
|
@@ -1,4 +1,18 @@
|
|
|
1
|
-
#
|
|
1
|
+
# 4.11.1 - 2025-03-28
|
|
2
|
+
|
|
3
|
+
## Fixed
|
|
4
|
+
|
|
5
|
+
1. `getFeatureFlag`, `isFeatureEnabled`, and `getAllFlagsAndPayloads` now return `undefined` if the flag is not found.
|
|
6
|
+
|
|
7
|
+
# 4.11.0 - 2025-03-28
|
|
8
|
+
|
|
9
|
+
## Added
|
|
10
|
+
|
|
11
|
+
1. `$feature_flag_called` event now includes additional properties such as `feature_flag_id`, `feature_flag_version`, `feature_flag_reason`, and `feature_flag_request_id`.
|
|
12
|
+
|
|
13
|
+
## Fixed
|
|
14
|
+
|
|
15
|
+
1. apiKey cannot be empty.
|
|
2
16
|
|
|
3
17
|
# 4.10.2 - 2025-03-06
|
|
4
18
|
|
|
@@ -301,4 +315,4 @@ Breaking changes:
|
|
|
301
315
|
What's new:
|
|
302
316
|
|
|
303
317
|
1. You can now evaluate feature flags locally (i.e. without sending a request to your PostHog servers) by setting a personal API key, and passing in groups and person properties to `isFeatureEnabled` and `getFeatureFlag` calls.
|
|
304
|
-
2. Introduces a `getAllFlags` method that returns all feature flags. This is useful for when you want to seed your frontend with some initial flags, given a user ID.
|
|
318
|
+
2. Introduces a `getAllFlags` method that returns all feature flags. This is useful for when you want to seed your frontend with some initial flags, given a user ID.
|
package/lib/index.cjs.js
CHANGED
|
@@ -2,20 +2,39 @@
|
|
|
2
2
|
|
|
3
3
|
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
4
|
|
|
5
|
-
var node_crypto = require('node:crypto');
|
|
6
5
|
var node_fs = require('node:fs');
|
|
7
6
|
var node_readline = require('node:readline');
|
|
8
7
|
var node_path = require('node:path');
|
|
9
8
|
|
|
10
|
-
|
|
9
|
+
function _interopNamespace(e) {
|
|
10
|
+
if (e && e.__esModule) return e;
|
|
11
|
+
var n = Object.create(null);
|
|
12
|
+
if (e) {
|
|
13
|
+
Object.keys(e).forEach(function (k) {
|
|
14
|
+
if (k !== 'default') {
|
|
15
|
+
var d = Object.getOwnPropertyDescriptor(e, k);
|
|
16
|
+
Object.defineProperty(n, k, d.get ? d : {
|
|
17
|
+
enumerable: true,
|
|
18
|
+
get: function () { return e[k]; }
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
n["default"] = e;
|
|
24
|
+
return Object.freeze(n);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
var version = "4.11.1";
|
|
11
28
|
|
|
12
29
|
var PostHogPersistedProperty;
|
|
13
30
|
(function (PostHogPersistedProperty) {
|
|
14
31
|
PostHogPersistedProperty["AnonymousId"] = "anonymous_id";
|
|
15
32
|
PostHogPersistedProperty["DistinctId"] = "distinct_id";
|
|
16
33
|
PostHogPersistedProperty["Props"] = "props";
|
|
34
|
+
PostHogPersistedProperty["FeatureFlagDetails"] = "feature_flag_details";
|
|
17
35
|
PostHogPersistedProperty["FeatureFlags"] = "feature_flags";
|
|
18
36
|
PostHogPersistedProperty["FeatureFlagPayloads"] = "feature_flag_payloads";
|
|
37
|
+
PostHogPersistedProperty["BootstrapFeatureFlagDetails"] = "bootstrap_feature_flag_details";
|
|
19
38
|
PostHogPersistedProperty["BootstrapFeatureFlags"] = "bootstrap_feature_flags";
|
|
20
39
|
PostHogPersistedProperty["BootstrapFeatureFlagPayloads"] = "bootstrap_feature_flag_payloads";
|
|
21
40
|
PostHogPersistedProperty["OverrideFeatureFlags"] = "override_feature_flags";
|
|
@@ -35,11 +54,100 @@ var PostHogPersistedProperty;
|
|
|
35
54
|
PostHogPersistedProperty["RemoteConfig"] = "remote_config";
|
|
36
55
|
})(PostHogPersistedProperty || (PostHogPersistedProperty = {}));
|
|
37
56
|
|
|
57
|
+
const normalizeDecideResponse = (decideResponse) => {
|
|
58
|
+
if ('flags' in decideResponse) {
|
|
59
|
+
// Convert v4 format to v3 format
|
|
60
|
+
const featureFlags = getFlagValuesFromFlags(decideResponse.flags);
|
|
61
|
+
const featureFlagPayloads = getPayloadsFromFlags(decideResponse.flags);
|
|
62
|
+
return {
|
|
63
|
+
...decideResponse,
|
|
64
|
+
featureFlags,
|
|
65
|
+
featureFlagPayloads,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
// Convert v3 format to v4 format
|
|
70
|
+
const featureFlags = decideResponse.featureFlags ?? {};
|
|
71
|
+
const featureFlagPayloads = Object.fromEntries(Object.entries(decideResponse.featureFlagPayloads || {}).map(([k, v]) => [k, parsePayload(v)]));
|
|
72
|
+
const flags = Object.fromEntries(Object.entries(featureFlags).map(([key, value]) => [
|
|
73
|
+
key,
|
|
74
|
+
getFlagDetailFromFlagAndPayload(key, value, featureFlagPayloads[key]),
|
|
75
|
+
]));
|
|
76
|
+
return {
|
|
77
|
+
...decideResponse,
|
|
78
|
+
featureFlags,
|
|
79
|
+
featureFlagPayloads,
|
|
80
|
+
flags,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
function getFlagDetailFromFlagAndPayload(key, value, payload) {
|
|
85
|
+
return {
|
|
86
|
+
key: key,
|
|
87
|
+
enabled: typeof value === 'string' ? true : value,
|
|
88
|
+
variant: typeof value === 'string' ? value : undefined,
|
|
89
|
+
reason: undefined,
|
|
90
|
+
metadata: {
|
|
91
|
+
id: undefined,
|
|
92
|
+
version: undefined,
|
|
93
|
+
payload: payload ? JSON.stringify(payload) : undefined,
|
|
94
|
+
description: undefined,
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Get the flag values from the flags v4 response.
|
|
100
|
+
* @param flags - The flags
|
|
101
|
+
* @returns The flag values
|
|
102
|
+
*/
|
|
103
|
+
const getFlagValuesFromFlags = (flags) => {
|
|
104
|
+
return Object.fromEntries(Object.entries(flags ?? {})
|
|
105
|
+
.map(([key, detail]) => [key, getFeatureFlagValue(detail)])
|
|
106
|
+
.filter(([, value]) => value !== undefined));
|
|
107
|
+
};
|
|
108
|
+
/**
|
|
109
|
+
* Get the payloads from the flags v4 response.
|
|
110
|
+
* @param flags - The flags
|
|
111
|
+
* @returns The payloads
|
|
112
|
+
*/
|
|
113
|
+
const getPayloadsFromFlags = (flags) => {
|
|
114
|
+
const safeFlags = flags ?? {};
|
|
115
|
+
return Object.fromEntries(Object.keys(safeFlags)
|
|
116
|
+
.filter((flag) => {
|
|
117
|
+
const details = safeFlags[flag];
|
|
118
|
+
return details.enabled && details.metadata && details.metadata.payload !== undefined;
|
|
119
|
+
})
|
|
120
|
+
.map((flag) => {
|
|
121
|
+
const payload = safeFlags[flag].metadata?.payload;
|
|
122
|
+
return [flag, payload ? parsePayload(payload) : undefined];
|
|
123
|
+
}));
|
|
124
|
+
};
|
|
125
|
+
const getFeatureFlagValue = (detail) => {
|
|
126
|
+
return detail === undefined ? undefined : detail.variant ?? detail.enabled;
|
|
127
|
+
};
|
|
128
|
+
const parsePayload = (response) => {
|
|
129
|
+
if (typeof response !== 'string') {
|
|
130
|
+
return response;
|
|
131
|
+
}
|
|
132
|
+
try {
|
|
133
|
+
return JSON.parse(response);
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
return response;
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
|
|
38
140
|
function assert(truthyValue, message) {
|
|
39
|
-
if (!truthyValue) {
|
|
141
|
+
if (!truthyValue || typeof truthyValue !== 'string' || isEmpty(truthyValue)) {
|
|
40
142
|
throw new Error(message);
|
|
41
143
|
}
|
|
42
144
|
}
|
|
145
|
+
function isEmpty(truthyValue) {
|
|
146
|
+
if (truthyValue.trim().length === 0) {
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
43
151
|
function removeTrailingSlash(url) {
|
|
44
152
|
return url?.replace(/\/+$/, '');
|
|
45
153
|
}
|
|
@@ -1158,7 +1266,7 @@ class PostHogCoreStateless {
|
|
|
1158
1266
|
***/
|
|
1159
1267
|
async getDecide(distinctId, groups = {}, personProperties = {}, groupProperties = {}, extraPayload = {}) {
|
|
1160
1268
|
await this._initPromise;
|
|
1161
|
-
const url = `${this.host}/decide/?v=
|
|
1269
|
+
const url = `${this.host}/decide/?v=4`;
|
|
1162
1270
|
const fetchOptions = {
|
|
1163
1271
|
method: 'POST',
|
|
1164
1272
|
headers: { ...this.getCustomHeaders(), 'Content-Type': 'application/json' },
|
|
@@ -1174,6 +1282,7 @@ class PostHogCoreStateless {
|
|
|
1174
1282
|
// Don't retry /decide API calls
|
|
1175
1283
|
return this.fetchWithRetry(url, fetchOptions, { retryCount: 0 }, this.featureFlagsRequestTimeoutMs)
|
|
1176
1284
|
.then((response) => response.json())
|
|
1285
|
+
.then((response) => normalizeDecideResponse(response))
|
|
1177
1286
|
.catch((error) => {
|
|
1178
1287
|
this._events.emit('error', error);
|
|
1179
1288
|
return undefined;
|
|
@@ -1181,17 +1290,15 @@ class PostHogCoreStateless {
|
|
|
1181
1290
|
}
|
|
1182
1291
|
async getFeatureFlagStateless(key, distinctId, groups = {}, personProperties = {}, groupProperties = {}, disableGeoip) {
|
|
1183
1292
|
await this._initPromise;
|
|
1184
|
-
const
|
|
1185
|
-
|
|
1186
|
-
if (!featureFlags) {
|
|
1293
|
+
const flagDetailResponse = await this.getFeatureFlagDetailStateless(key, distinctId, groups, personProperties, groupProperties, disableGeoip);
|
|
1294
|
+
if (flagDetailResponse === undefined) {
|
|
1187
1295
|
// If we haven't loaded flags yet, or errored out, we respond with undefined
|
|
1188
1296
|
return {
|
|
1189
1297
|
response: undefined,
|
|
1190
1298
|
requestId: undefined,
|
|
1191
1299
|
};
|
|
1192
1300
|
}
|
|
1193
|
-
let response =
|
|
1194
|
-
// `/decide` v3 returns all flags
|
|
1301
|
+
let response = getFeatureFlagValue(flagDetailResponse.response);
|
|
1195
1302
|
if (response === undefined) {
|
|
1196
1303
|
// For cases where the flag is unknown, return false
|
|
1197
1304
|
response = false;
|
|
@@ -1199,6 +1306,19 @@ class PostHogCoreStateless {
|
|
|
1199
1306
|
// If we have flags we either return the value (true or string) or false
|
|
1200
1307
|
return {
|
|
1201
1308
|
response,
|
|
1309
|
+
requestId: flagDetailResponse.requestId,
|
|
1310
|
+
};
|
|
1311
|
+
}
|
|
1312
|
+
async getFeatureFlagDetailStateless(key, distinctId, groups = {}, personProperties = {}, groupProperties = {}, disableGeoip) {
|
|
1313
|
+
await this._initPromise;
|
|
1314
|
+
const decideResponse = await this.getFeatureFlagDetailsStateless(distinctId, groups, personProperties, groupProperties, disableGeoip, [key]);
|
|
1315
|
+
if (decideResponse === undefined) {
|
|
1316
|
+
return undefined;
|
|
1317
|
+
}
|
|
1318
|
+
const featureFlags = decideResponse.flags;
|
|
1319
|
+
const flagDetail = featureFlags[key];
|
|
1320
|
+
return {
|
|
1321
|
+
response: flagDetail,
|
|
1202
1322
|
requestId: decideResponse.requestId,
|
|
1203
1323
|
};
|
|
1204
1324
|
}
|
|
@@ -1220,19 +1340,27 @@ class PostHogCoreStateless {
|
|
|
1220
1340
|
const payloads = (await this.getFeatureFlagsAndPayloadsStateless(distinctId, groups, personProperties, groupProperties, disableGeoip, flagKeysToEvaluate)).payloads;
|
|
1221
1341
|
return payloads;
|
|
1222
1342
|
}
|
|
1223
|
-
_parsePayload(response) {
|
|
1224
|
-
try {
|
|
1225
|
-
return JSON.parse(response);
|
|
1226
|
-
}
|
|
1227
|
-
catch {
|
|
1228
|
-
return response;
|
|
1229
|
-
}
|
|
1230
|
-
}
|
|
1231
1343
|
async getFeatureFlagsStateless(distinctId, groups = {}, personProperties = {}, groupProperties = {}, disableGeoip, flagKeysToEvaluate) {
|
|
1232
1344
|
await this._initPromise;
|
|
1233
1345
|
return await this.getFeatureFlagsAndPayloadsStateless(distinctId, groups, personProperties, groupProperties, disableGeoip, flagKeysToEvaluate);
|
|
1234
1346
|
}
|
|
1235
1347
|
async getFeatureFlagsAndPayloadsStateless(distinctId, groups = {}, personProperties = {}, groupProperties = {}, disableGeoip, flagKeysToEvaluate) {
|
|
1348
|
+
await this._initPromise;
|
|
1349
|
+
const featureFlagDetails = await this.getFeatureFlagDetailsStateless(distinctId, groups, personProperties, groupProperties, disableGeoip, flagKeysToEvaluate);
|
|
1350
|
+
if (!featureFlagDetails) {
|
|
1351
|
+
return {
|
|
1352
|
+
flags: undefined,
|
|
1353
|
+
payloads: undefined,
|
|
1354
|
+
requestId: undefined,
|
|
1355
|
+
};
|
|
1356
|
+
}
|
|
1357
|
+
return {
|
|
1358
|
+
flags: featureFlagDetails.featureFlags,
|
|
1359
|
+
payloads: featureFlagDetails.featureFlagPayloads,
|
|
1360
|
+
requestId: featureFlagDetails.requestId,
|
|
1361
|
+
};
|
|
1362
|
+
}
|
|
1363
|
+
async getFeatureFlagDetailsStateless(distinctId, groups = {}, personProperties = {}, groupProperties = {}, disableGeoip, flagKeysToEvaluate) {
|
|
1236
1364
|
await this._initPromise;
|
|
1237
1365
|
const extraPayload = {};
|
|
1238
1366
|
if (disableGeoip ?? this.disableGeoip) {
|
|
@@ -1242,30 +1370,25 @@ class PostHogCoreStateless {
|
|
|
1242
1370
|
extraPayload['flag_keys_to_evaluate'] = flagKeysToEvaluate;
|
|
1243
1371
|
}
|
|
1244
1372
|
const decideResponse = await this.getDecide(distinctId, groups, personProperties, groupProperties, extraPayload);
|
|
1373
|
+
if (decideResponse === undefined) {
|
|
1374
|
+
// We probably errored out, so return undefined
|
|
1375
|
+
return undefined;
|
|
1376
|
+
}
|
|
1245
1377
|
// if there's an error on the decideResponse, log a console error, but don't throw an error
|
|
1246
|
-
if (decideResponse
|
|
1378
|
+
if (decideResponse.errorsWhileComputingFlags) {
|
|
1247
1379
|
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');
|
|
1248
1380
|
}
|
|
1249
1381
|
// Add check for quota limitation on feature flags
|
|
1250
|
-
if (decideResponse
|
|
1382
|
+
if (decideResponse.quotaLimited?.includes(QuotaLimitedFeature.FeatureFlags)) {
|
|
1251
1383
|
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');
|
|
1252
1384
|
return {
|
|
1253
|
-
flags:
|
|
1254
|
-
|
|
1385
|
+
flags: {},
|
|
1386
|
+
featureFlags: {},
|
|
1387
|
+
featureFlagPayloads: {},
|
|
1255
1388
|
requestId: decideResponse?.requestId,
|
|
1256
1389
|
};
|
|
1257
1390
|
}
|
|
1258
|
-
|
|
1259
|
-
const payloads = decideResponse?.featureFlagPayloads;
|
|
1260
|
-
let parsedPayloads = payloads;
|
|
1261
|
-
if (payloads) {
|
|
1262
|
-
parsedPayloads = Object.fromEntries(Object.entries(payloads).map(([k, v]) => [k, this._parsePayload(v)]));
|
|
1263
|
-
}
|
|
1264
|
-
return {
|
|
1265
|
-
flags,
|
|
1266
|
-
payloads: parsedPayloads,
|
|
1267
|
-
requestId: decideResponse?.requestId,
|
|
1268
|
-
};
|
|
1391
|
+
return decideResponse;
|
|
1269
1392
|
}
|
|
1270
1393
|
/***
|
|
1271
1394
|
*** SURVEYS
|
|
@@ -1552,6 +1675,100 @@ if (!_fetch) {
|
|
|
1552
1675
|
// 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
|
|
1553
1676
|
var fetch$1 = _fetch;
|
|
1554
1677
|
|
|
1678
|
+
/**
|
|
1679
|
+
* A lazy value that is only computed when needed. Inspired by C#'s Lazy<T> class.
|
|
1680
|
+
*/
|
|
1681
|
+
class Lazy {
|
|
1682
|
+
constructor(factory) {
|
|
1683
|
+
this.factory = factory;
|
|
1684
|
+
}
|
|
1685
|
+
/**
|
|
1686
|
+
* Gets the value, initializing it if necessary.
|
|
1687
|
+
* Multiple concurrent calls will share the same initialization promise.
|
|
1688
|
+
*/
|
|
1689
|
+
async getValue() {
|
|
1690
|
+
if (this.value !== undefined) {
|
|
1691
|
+
return this.value;
|
|
1692
|
+
}
|
|
1693
|
+
if (this.initializationPromise === undefined) {
|
|
1694
|
+
this.initializationPromise = (async () => {
|
|
1695
|
+
try {
|
|
1696
|
+
const result = await this.factory();
|
|
1697
|
+
this.value = result;
|
|
1698
|
+
return result;
|
|
1699
|
+
} finally {
|
|
1700
|
+
// Clear the promise so we can retry if needed
|
|
1701
|
+
this.initializationPromise = undefined;
|
|
1702
|
+
}
|
|
1703
|
+
})();
|
|
1704
|
+
}
|
|
1705
|
+
return this.initializationPromise;
|
|
1706
|
+
}
|
|
1707
|
+
/**
|
|
1708
|
+
* Returns true if the value has been initialized.
|
|
1709
|
+
*/
|
|
1710
|
+
isInitialized() {
|
|
1711
|
+
return this.value !== undefined;
|
|
1712
|
+
}
|
|
1713
|
+
/**
|
|
1714
|
+
* Returns a promise that resolves when the value is initialized.
|
|
1715
|
+
* If already initialized, resolves immediately.
|
|
1716
|
+
*/
|
|
1717
|
+
async waitForInitialization() {
|
|
1718
|
+
if (this.isInitialized()) {
|
|
1719
|
+
return;
|
|
1720
|
+
}
|
|
1721
|
+
await this.getValue();
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
/// <reference lib="dom" />
|
|
1726
|
+
const nodeCrypto = new Lazy(async () => {
|
|
1727
|
+
try {
|
|
1728
|
+
return await Promise.resolve().then(function () { return /*#__PURE__*/_interopNamespace(require('crypto')); });
|
|
1729
|
+
} catch {
|
|
1730
|
+
return undefined;
|
|
1731
|
+
}
|
|
1732
|
+
});
|
|
1733
|
+
async function getNodeCrypto() {
|
|
1734
|
+
return await nodeCrypto.getValue();
|
|
1735
|
+
}
|
|
1736
|
+
const webCrypto = new Lazy(async () => {
|
|
1737
|
+
if (typeof globalThis.crypto?.subtle !== 'undefined') {
|
|
1738
|
+
return globalThis.crypto.subtle;
|
|
1739
|
+
}
|
|
1740
|
+
try {
|
|
1741
|
+
// Node.js: use built-in webcrypto and assign it if needed
|
|
1742
|
+
const crypto = await nodeCrypto.getValue();
|
|
1743
|
+
if (crypto?.webcrypto?.subtle) {
|
|
1744
|
+
return crypto.webcrypto.subtle;
|
|
1745
|
+
}
|
|
1746
|
+
} catch {
|
|
1747
|
+
// Ignore if not available
|
|
1748
|
+
}
|
|
1749
|
+
return undefined;
|
|
1750
|
+
});
|
|
1751
|
+
async function getWebCrypto() {
|
|
1752
|
+
return await webCrypto.getValue();
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
/// <reference lib="dom" />
|
|
1756
|
+
async function hashSHA1(text) {
|
|
1757
|
+
// Try Node.js crypto first
|
|
1758
|
+
const nodeCrypto = await getNodeCrypto();
|
|
1759
|
+
if (nodeCrypto) {
|
|
1760
|
+
return nodeCrypto.createHash('sha1').update(text).digest('hex');
|
|
1761
|
+
}
|
|
1762
|
+
const webCrypto = await getWebCrypto();
|
|
1763
|
+
// Fall back to Web Crypto API
|
|
1764
|
+
if (webCrypto) {
|
|
1765
|
+
const hashBuffer = await webCrypto.digest('SHA-1', new TextEncoder().encode(text));
|
|
1766
|
+
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
1767
|
+
return hashArray.map(byte => byte.toString(16).padStart(2, '0')).join('');
|
|
1768
|
+
}
|
|
1769
|
+
throw new Error('No crypto implementation available. Tried Node Crypto API and Web SubtleCrypto API');
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1555
1772
|
// eslint-disable-next-line
|
|
1556
1773
|
const LONG_SCALE = 0xfffffffffffffff;
|
|
1557
1774
|
const NULL_VALUES_ALLOWED_OPERATORS = ['is_not'];
|
|
@@ -1627,7 +1844,7 @@ class FeatureFlagsPoller {
|
|
|
1627
1844
|
}
|
|
1628
1845
|
if (featureFlag !== undefined) {
|
|
1629
1846
|
try {
|
|
1630
|
-
response = this.computeFlagLocally(featureFlag, distinctId, groups, personProperties, groupProperties);
|
|
1847
|
+
response = await this.computeFlagLocally(featureFlag, distinctId, groups, personProperties, groupProperties);
|
|
1631
1848
|
this.logMsgIfDebug(() => console.debug(`Successfully computed flag locally: ${key} -> ${response}`));
|
|
1632
1849
|
} catch (e) {
|
|
1633
1850
|
if (e instanceof InconclusiveMatchError) {
|
|
@@ -1665,9 +1882,9 @@ class FeatureFlagsPoller {
|
|
|
1665
1882
|
const response = {};
|
|
1666
1883
|
const payloads = {};
|
|
1667
1884
|
let fallbackToDecide = this.featureFlags.length == 0;
|
|
1668
|
-
this.featureFlags.map(async flag => {
|
|
1885
|
+
await Promise.all(this.featureFlags.map(async flag => {
|
|
1669
1886
|
try {
|
|
1670
|
-
const matchValue = this.computeFlagLocally(flag, distinctId, groups, personProperties, groupProperties);
|
|
1887
|
+
const matchValue = await this.computeFlagLocally(flag, distinctId, groups, personProperties, groupProperties);
|
|
1671
1888
|
response[flag.key] = matchValue;
|
|
1672
1889
|
const matchPayload = await this.computeFeatureFlagPayloadLocally(flag.key, matchValue);
|
|
1673
1890
|
if (matchPayload) {
|
|
@@ -1679,14 +1896,14 @@ class FeatureFlagsPoller {
|
|
|
1679
1896
|
}
|
|
1680
1897
|
fallbackToDecide = true;
|
|
1681
1898
|
}
|
|
1682
|
-
});
|
|
1899
|
+
}));
|
|
1683
1900
|
return {
|
|
1684
1901
|
response,
|
|
1685
1902
|
payloads,
|
|
1686
1903
|
fallbackToDecide
|
|
1687
1904
|
};
|
|
1688
1905
|
}
|
|
1689
|
-
computeFlagLocally(flag, distinctId, groups = {}, personProperties = {}, groupProperties = {}) {
|
|
1906
|
+
async computeFlagLocally(flag, distinctId, groups = {}, personProperties = {}, groupProperties = {}) {
|
|
1690
1907
|
if (flag.ensure_experience_continuity) {
|
|
1691
1908
|
throw new InconclusiveMatchError('Flag has experience continuity enabled');
|
|
1692
1909
|
}
|
|
@@ -1706,12 +1923,12 @@ class FeatureFlagsPoller {
|
|
|
1706
1923
|
return false;
|
|
1707
1924
|
}
|
|
1708
1925
|
const focusedGroupProperties = groupProperties[groupName];
|
|
1709
|
-
return this.matchFeatureFlagProperties(flag, groups[groupName], focusedGroupProperties);
|
|
1926
|
+
return await this.matchFeatureFlagProperties(flag, groups[groupName], focusedGroupProperties);
|
|
1710
1927
|
} else {
|
|
1711
|
-
return this.matchFeatureFlagProperties(flag, distinctId, personProperties);
|
|
1928
|
+
return await this.matchFeatureFlagProperties(flag, distinctId, personProperties);
|
|
1712
1929
|
}
|
|
1713
1930
|
}
|
|
1714
|
-
matchFeatureFlagProperties(flag, distinctId, properties) {
|
|
1931
|
+
async matchFeatureFlagProperties(flag, distinctId, properties) {
|
|
1715
1932
|
const flagFilters = flag.filters || {};
|
|
1716
1933
|
const flagConditions = flagFilters.groups || [];
|
|
1717
1934
|
let isInconclusive = false;
|
|
@@ -1733,13 +1950,13 @@ class FeatureFlagsPoller {
|
|
|
1733
1950
|
});
|
|
1734
1951
|
for (const condition of sortedFlagConditions) {
|
|
1735
1952
|
try {
|
|
1736
|
-
if (this.isConditionMatch(flag, distinctId, condition, properties)) {
|
|
1953
|
+
if (await this.isConditionMatch(flag, distinctId, condition, properties)) {
|
|
1737
1954
|
const variantOverride = condition.variant;
|
|
1738
1955
|
const flagVariants = flagFilters.multivariate?.variants || [];
|
|
1739
1956
|
if (variantOverride && flagVariants.some(variant => variant.key === variantOverride)) {
|
|
1740
1957
|
result = variantOverride;
|
|
1741
1958
|
} else {
|
|
1742
|
-
result = this.getMatchingVariant(flag, distinctId) || true;
|
|
1959
|
+
result = (await this.getMatchingVariant(flag, distinctId)) || true;
|
|
1743
1960
|
}
|
|
1744
1961
|
break;
|
|
1745
1962
|
}
|
|
@@ -1759,7 +1976,7 @@ class FeatureFlagsPoller {
|
|
|
1759
1976
|
// We can only return False when all conditions are False
|
|
1760
1977
|
return false;
|
|
1761
1978
|
}
|
|
1762
|
-
isConditionMatch(flag, distinctId, condition, properties) {
|
|
1979
|
+
async isConditionMatch(flag, distinctId, condition, properties) {
|
|
1763
1980
|
const rolloutPercentage = condition.rollout_percentage;
|
|
1764
1981
|
const warnFunction = msg => {
|
|
1765
1982
|
this.logMsgIfDebug(() => console.warn(msg));
|
|
@@ -1781,13 +1998,13 @@ class FeatureFlagsPoller {
|
|
|
1781
1998
|
return true;
|
|
1782
1999
|
}
|
|
1783
2000
|
}
|
|
1784
|
-
if (rolloutPercentage != undefined && _hash(flag.key, distinctId) > rolloutPercentage / 100.0) {
|
|
2001
|
+
if (rolloutPercentage != undefined && (await _hash(flag.key, distinctId)) > rolloutPercentage / 100.0) {
|
|
1785
2002
|
return false;
|
|
1786
2003
|
}
|
|
1787
2004
|
return true;
|
|
1788
2005
|
}
|
|
1789
|
-
getMatchingVariant(flag, distinctId) {
|
|
1790
|
-
const hashValue = _hash(flag.key, distinctId, 'variant');
|
|
2006
|
+
async getMatchingVariant(flag, distinctId) {
|
|
2007
|
+
const hashValue = await _hash(flag.key, distinctId, 'variant');
|
|
1791
2008
|
const matchingVariant = this.variantLookupTable(flag).find(variant => {
|
|
1792
2009
|
return hashValue >= variant.valueMin && hashValue < variant.valueMax;
|
|
1793
2010
|
});
|
|
@@ -1958,10 +2175,9 @@ class FeatureFlagsPoller {
|
|
|
1958
2175
|
// # Given the same distinct_id and key, it'll always return the same float. These floats are
|
|
1959
2176
|
// # uniformly distributed between 0 and 1, so if we want to show this feature to 20% of traffic
|
|
1960
2177
|
// # we can do _hash(key, distinct_id) < 0.2
|
|
1961
|
-
function _hash(key, distinctId, salt = '') {
|
|
1962
|
-
const
|
|
1963
|
-
|
|
1964
|
-
return parseInt(sha1Hash.digest('hex').slice(0, 15), 16) / LONG_SCALE;
|
|
2178
|
+
async function _hash(key, distinctId, salt = '') {
|
|
2179
|
+
const hashString = await hashSHA1(`${key}.${distinctId}${salt}`);
|
|
2180
|
+
return parseInt(hashString.slice(0, 15), 16) / LONG_SCALE;
|
|
1965
2181
|
}
|
|
1966
2182
|
function matchProperty(property, propertyValues, warnFunction) {
|
|
1967
2183
|
const key = property.key;
|
|
@@ -3204,7 +3420,7 @@ class PostHog extends PostHogCoreStateless {
|
|
|
3204
3420
|
additionalProperties[`$feature/${feature}`] = variant;
|
|
3205
3421
|
}
|
|
3206
3422
|
}
|
|
3207
|
-
const activeFlags = Object.keys(flags || {}).filter(flag => flags?.[flag] !== false);
|
|
3423
|
+
const activeFlags = Object.keys(flags || {}).filter(flag => flags?.[flag] !== false).sort();
|
|
3208
3424
|
if (activeFlags.length > 0) {
|
|
3209
3425
|
additionalProperties['$active_feature_flags'] = activeFlags;
|
|
3210
3426
|
}
|
|
@@ -3269,10 +3485,15 @@ class PostHog extends PostHogCoreStateless {
|
|
|
3269
3485
|
let response = await this.featureFlagsPoller?.getFeatureFlag(key, distinctId, groups, personProperties, groupProperties);
|
|
3270
3486
|
const flagWasLocallyEvaluated = response !== undefined;
|
|
3271
3487
|
let requestId = undefined;
|
|
3488
|
+
let flagDetail = undefined;
|
|
3272
3489
|
if (!flagWasLocallyEvaluated && !onlyEvaluateLocally) {
|
|
3273
|
-
const remoteResponse = await super.
|
|
3274
|
-
|
|
3275
|
-
|
|
3490
|
+
const remoteResponse = await super.getFeatureFlagDetailStateless(key, distinctId, groups, personProperties, groupProperties, disableGeoip);
|
|
3491
|
+
if (remoteResponse === undefined) {
|
|
3492
|
+
return undefined;
|
|
3493
|
+
}
|
|
3494
|
+
flagDetail = remoteResponse.response;
|
|
3495
|
+
response = getFeatureFlagValue(flagDetail);
|
|
3496
|
+
requestId = remoteResponse?.requestId;
|
|
3276
3497
|
}
|
|
3277
3498
|
const featureFlagReportedKey = `${key}_${response}`;
|
|
3278
3499
|
if (sendFeatureFlagEvents && (!(distinctId in this.distinctIdHasSentFlagCalls) || !this.distinctIdHasSentFlagCalls[distinctId].includes(featureFlagReportedKey))) {
|
|
@@ -3290,6 +3511,9 @@ class PostHog extends PostHogCoreStateless {
|
|
|
3290
3511
|
properties: {
|
|
3291
3512
|
$feature_flag: key,
|
|
3292
3513
|
$feature_flag_response: response,
|
|
3514
|
+
$feature_flag_id: flagDetail?.metadata?.id,
|
|
3515
|
+
$feature_flag_version: flagDetail?.metadata?.version,
|
|
3516
|
+
$feature_flag_reason: flagDetail?.reason?.description ?? flagDetail?.reason?.code,
|
|
3293
3517
|
locally_evaluated: flagWasLocallyEvaluated,
|
|
3294
3518
|
[`$feature/${key}`]: response,
|
|
3295
3519
|
$feature_flag_request_id: requestId
|
|
@@ -3315,16 +3539,21 @@ class PostHog extends PostHogCoreStateless {
|
|
|
3315
3539
|
personProperties = adjustedProperties.allPersonProperties;
|
|
3316
3540
|
groupProperties = adjustedProperties.allGroupProperties;
|
|
3317
3541
|
let response = undefined;
|
|
3318
|
-
|
|
3319
|
-
if (
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3542
|
+
const localEvaluationEnabled = this.featureFlagsPoller !== undefined;
|
|
3543
|
+
if (localEvaluationEnabled) {
|
|
3544
|
+
// Try to get match value locally if not provided
|
|
3545
|
+
if (!matchValue) {
|
|
3546
|
+
matchValue = await this.getFeatureFlag(key, distinctId, {
|
|
3547
|
+
...options,
|
|
3548
|
+
onlyEvaluateLocally: true,
|
|
3549
|
+
sendFeatureFlagEvents: false
|
|
3550
|
+
});
|
|
3551
|
+
}
|
|
3552
|
+
if (matchValue) {
|
|
3553
|
+
response = await this.featureFlagsPoller?.computeFeatureFlagPayloadLocally(key, matchValue);
|
|
3554
|
+
}
|
|
3327
3555
|
}
|
|
3556
|
+
//}
|
|
3328
3557
|
// set defaults
|
|
3329
3558
|
if (onlyEvaluateLocally == undefined) {
|
|
3330
3559
|
onlyEvaluateLocally = false;
|
|
@@ -3354,7 +3583,7 @@ class PostHog extends PostHogCoreStateless {
|
|
|
3354
3583
|
}
|
|
3355
3584
|
async getAllFlags(distinctId, options) {
|
|
3356
3585
|
const response = await this.getAllFlagsAndPayloads(distinctId, options);
|
|
3357
|
-
return response.featureFlags;
|
|
3586
|
+
return response.featureFlags || {};
|
|
3358
3587
|
}
|
|
3359
3588
|
async getAllFlagsAndPayloads(distinctId, options) {
|
|
3360
3589
|
const {
|