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/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.10.2";
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=3`;
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 decideResponse = await this.getFeatureFlagsStateless(distinctId, groups, personProperties, groupProperties, disableGeoip, [key]);
1181
- const featureFlags = decideResponse.flags;
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 = featureFlags[key];
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?.errorsWhileComputingFlags) {
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?.quotaLimited?.includes(QuotaLimitedFeature.FeatureFlags)) {
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: undefined,
1250
- payloads: undefined,
1363
+ flags: {},
1364
+ featureFlags: {},
1365
+ featureFlagPayloads: {},
1251
1366
  requestId: decideResponse?.requestId,
1252
1367
  };
1253
1368
  }
1254
- const flags = decideResponse?.featureFlags;
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 sha1Hash = createHash('sha1');
1959
- sha1Hash.update(`${key}.${distinctId}${salt}`);
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.getFeatureFlagStateless(key, distinctId, groups, personProperties, groupProperties, disableGeoip);
3270
- response = remoteResponse.response;
3271
- requestId = remoteResponse.requestId;
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
- // Try to get match value locally if not provided
3315
- if (!matchValue) {
3316
- matchValue = await this.getFeatureFlag(key, distinctId, {
3317
- ...options,
3318
- onlyEvaluateLocally: true
3319
- });
3320
- }
3321
- if (matchValue) {
3322
- response = await this.featureFlagsPoller?.computeFeatureFlagPayloadLocally(key, matchValue);
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 {