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 CHANGED
@@ -1,4 +1,18 @@
1
- # Next
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
- var version = "4.10.2";
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=3`;
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 decideResponse = await this.getFeatureFlagsStateless(distinctId, groups, personProperties, groupProperties, disableGeoip, [key]);
1185
- const featureFlags = decideResponse.flags;
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 = featureFlags[key];
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?.errorsWhileComputingFlags) {
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?.quotaLimited?.includes(QuotaLimitedFeature.FeatureFlags)) {
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: undefined,
1254
- payloads: undefined,
1385
+ flags: {},
1386
+ featureFlags: {},
1387
+ featureFlagPayloads: {},
1255
1388
  requestId: decideResponse?.requestId,
1256
1389
  };
1257
1390
  }
1258
- const flags = decideResponse?.featureFlags;
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 sha1Hash = node_crypto.createHash('sha1');
1963
- sha1Hash.update(`${key}.${distinctId}${salt}`);
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.getFeatureFlagStateless(key, distinctId, groups, personProperties, groupProperties, disableGeoip);
3274
- response = remoteResponse.response;
3275
- requestId = remoteResponse.requestId;
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
- // Try to get match value locally if not provided
3319
- if (!matchValue) {
3320
- matchValue = await this.getFeatureFlag(key, distinctId, {
3321
- ...options,
3322
- onlyEvaluateLocally: true
3323
- });
3324
- }
3325
- if (matchValue) {
3326
- response = await this.featureFlagsPoller?.computeFeatureFlagPayloadLocally(key, matchValue);
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 {