posthog-node 5.0.0 → 5.1.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,5 +1,13 @@
1
1
  # Next
2
2
 
3
+ # 5.1.1 - 2025-06-16
4
+
5
+ 1. fix: Handle double-encoded JSON payloads from the remote config endpoint
6
+
7
+ # 5.1.0 - 2025-06-12
8
+
9
+ 1. chore: use `/flags?v=2&config=true` instead of `/decide?v=4` for the flag evaluation backend
10
+
3
11
  # 5.0.0 - 2025-06-10
4
12
 
5
13
  ## Removed
@@ -935,7 +935,7 @@ function setupExpressErrorHandler(_posthog, app) {
935
935
  });
936
936
  }
937
937
 
938
- var version = "5.0.0";
938
+ var version = "5.1.1";
939
939
 
940
940
  var PostHogPersistedProperty;
941
941
  (function (PostHogPersistedProperty) {
@@ -959,11 +959,11 @@ var PostHogPersistedProperty;
959
959
  PostHogPersistedProperty["InstalledAppBuild"] = "installed_app_build";
960
960
  PostHogPersistedProperty["InstalledAppVersion"] = "installed_app_version";
961
961
  PostHogPersistedProperty["SessionReplay"] = "session_replay";
962
- PostHogPersistedProperty["DecideEndpointWasHit"] = "decide_endpoint_was_hit";
963
962
  PostHogPersistedProperty["SurveyLastSeenDate"] = "survey_last_seen_date";
964
963
  PostHogPersistedProperty["SurveysSeen"] = "surveys_seen";
965
964
  PostHogPersistedProperty["Surveys"] = "surveys";
966
965
  PostHogPersistedProperty["RemoteConfig"] = "remote_config";
966
+ PostHogPersistedProperty["FlagsEndpointWasHit"] = "flags_endpoint_was_hit";
967
967
  })(PostHogPersistedProperty || (PostHogPersistedProperty = {}));
968
968
  // Any key prefixed with `attr__` can be added
969
969
  var Compression;
@@ -1031,27 +1031,27 @@ var ActionStepStringMatching;
1031
1031
  ActionStepStringMatching["Regex"] = "regex";
1032
1032
  })(ActionStepStringMatching || (ActionStepStringMatching = {}));
1033
1033
 
1034
- const normalizeDecideResponse = (decideResponse) => {
1035
- if ('flags' in decideResponse) {
1036
- // Convert v4 format to v3 format
1037
- const featureFlags = getFlagValuesFromFlags(decideResponse.flags);
1038
- const featureFlagPayloads = getPayloadsFromFlags(decideResponse.flags);
1034
+ const normalizeFlagsResponse = (flagsResponse) => {
1035
+ if ('flags' in flagsResponse) {
1036
+ // Convert v2 format to v1 format
1037
+ const featureFlags = getFlagValuesFromFlags(flagsResponse.flags);
1038
+ const featureFlagPayloads = getPayloadsFromFlags(flagsResponse.flags);
1039
1039
  return {
1040
- ...decideResponse,
1040
+ ...flagsResponse,
1041
1041
  featureFlags,
1042
1042
  featureFlagPayloads,
1043
1043
  };
1044
1044
  }
1045
1045
  else {
1046
- // Convert v3 format to v4 format
1047
- const featureFlags = decideResponse.featureFlags ?? {};
1048
- const featureFlagPayloads = Object.fromEntries(Object.entries(decideResponse.featureFlagPayloads || {}).map(([k, v]) => [k, parsePayload(v)]));
1046
+ // Convert v1 format to v2 format
1047
+ const featureFlags = flagsResponse.featureFlags ?? {};
1048
+ const featureFlagPayloads = Object.fromEntries(Object.entries(flagsResponse.featureFlagPayloads || {}).map(([k, v]) => [k, parsePayload(v)]));
1049
1049
  const flags = Object.fromEntries(Object.entries(featureFlags).map(([key, value]) => [
1050
1050
  key,
1051
1051
  getFlagDetailFromFlagAndPayload(key, value, featureFlagPayloads[key]),
1052
1052
  ]));
1053
1053
  return {
1054
- ...decideResponse,
1054
+ ...flagsResponse,
1055
1055
  featureFlags,
1056
1056
  featureFlagPayloads,
1057
1057
  flags,
@@ -1114,90 +1114,6 @@ const parsePayload = (response) => {
1114
1114
  }
1115
1115
  };
1116
1116
 
1117
- // Rollout constants
1118
- const NEW_FLAGS_ROLLOUT_PERCENTAGE = 1;
1119
- // The fnv1a hashes of the tokens that are explicitly excluded from the rollout
1120
- // see https://github.com/PostHog/posthog-js-lite/blob/main/posthog-core/src/utils.ts#L84
1121
- // are hashed API tokens from our top 10 for each category supported by this SDK.
1122
- const NEW_FLAGS_EXCLUDED_HASHES = new Set([
1123
- // Node
1124
- '61be3dd8',
1125
- '96f6df5f',
1126
- '8cfdba9b',
1127
- 'bf027177',
1128
- 'e59430a8',
1129
- '7fa5500b',
1130
- '569798e9',
1131
- '04809ff7',
1132
- '0ebc61a5',
1133
- '32de7f98',
1134
- '3beeb69a',
1135
- '12d34ad9',
1136
- '733853ec',
1137
- '0645bb64',
1138
- '5dcbee21',
1139
- 'b1f95fa3',
1140
- '2189e408',
1141
- '82b460c2',
1142
- '3a8cc979',
1143
- '29ef8843',
1144
- '2cdbf767',
1145
- '38084b54',
1146
- // React Native
1147
- '50f9f8de',
1148
- '41d0df91',
1149
- '5c236689',
1150
- 'c11aedd3',
1151
- 'ada46672',
1152
- 'f4331ee1',
1153
- '42fed62a',
1154
- 'c957462c',
1155
- 'd62f705a',
1156
- // Web (lots of teams per org, hence lots of API tokens)
1157
- 'e0162666',
1158
- '01b3e5cf',
1159
- '441cef7f',
1160
- 'bb9cafee',
1161
- '8f348eb0',
1162
- 'b2553f3a',
1163
- '97469d7d',
1164
- '39f21a76',
1165
- '03706dcc',
1166
- '27d50569',
1167
- '307584a7',
1168
- '6433e92e',
1169
- '150c7fbb',
1170
- '49f57f22',
1171
- '3772f65b',
1172
- '01eb8256',
1173
- '3c9e9234',
1174
- 'f853c7f7',
1175
- 'c0ac4b67',
1176
- 'cd609d40',
1177
- '10ca9b1a',
1178
- '8a87f11b',
1179
- '8e8e5216',
1180
- '1f6b63b3',
1181
- 'db7943dd',
1182
- '79b7164c',
1183
- '07f78e33',
1184
- '2d21b6fd',
1185
- '952db5ee',
1186
- 'a7d3b43f',
1187
- '1924dd9c',
1188
- '84e1b8f6',
1189
- 'dff631b6',
1190
- 'c5aa8a79',
1191
- 'fa133a95',
1192
- '498a4508',
1193
- '24748755',
1194
- '98f3d658',
1195
- '21bbda67',
1196
- '7dbfed69',
1197
- 'be3ec24c',
1198
- 'fc80b8e2',
1199
- '75cc0998',
1200
- ]);
1201
1117
  const STRING_FORMAT = 'utf8';
1202
1118
  function assert(truthyValue, message) {
1203
1119
  if (!truthyValue || typeof truthyValue !== 'string' || isEmpty(truthyValue)) {
@@ -1244,30 +1160,6 @@ function safeSetTimeout(fn, timeout) {
1244
1160
  t?.unref && t?.unref();
1245
1161
  return t;
1246
1162
  }
1247
- // FNV-1a hash function
1248
- // https://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function
1249
- // I know, I know, I'm rolling my own hash function, but I didn't want to take on
1250
- // a crypto dependency and this is just temporary anyway
1251
- function fnv1a(str) {
1252
- let hash = 0x811c9dc5; // FNV offset basis
1253
- for (let i = 0; i < str.length; i++) {
1254
- hash ^= str.charCodeAt(i);
1255
- hash += (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24);
1256
- }
1257
- // Convert to hex string, padding to 8 chars
1258
- return (hash >>> 0).toString(16).padStart(8, '0');
1259
- }
1260
- function isTokenInRollout(token, percentage = 0, excludedHashes) {
1261
- const tokenHash = fnv1a(token);
1262
- // Check excluded hashes (we're explicitly including these tokens from the rollout)
1263
- if (excludedHashes?.has(tokenHash)) {
1264
- return false;
1265
- }
1266
- // Convert hash to int and divide by max value to get number between 0-1
1267
- const hashInt = parseInt(tokenHash, 16);
1268
- const hashFloat = hashInt / 0xffffffff;
1269
- return hashFloat < percentage;
1270
- }
1271
1163
  function allSettled(promises) {
1272
1164
  return Promise.all(promises.map((p) => (p ?? Promise.resolve()).then((value) => ({ status: 'fulfilled', value }), (reason) => ({ status: 'rejected', reason }))));
1273
1165
  }
@@ -1592,13 +1484,9 @@ class PostHogCoreStateless {
1592
1484
  /***
1593
1485
  *** FEATURE FLAGS
1594
1486
  ***/
1595
- async getDecide(distinctId, groups = {}, personProperties = {}, groupProperties = {}, extraPayload = {}) {
1487
+ async getFlags(distinctId, groups = {}, personProperties = {}, groupProperties = {}, extraPayload = {}) {
1596
1488
  await this._initPromise;
1597
- // Check if the API token is in the new flags rollout
1598
- // This is a temporary measure to ensure that we can still use the old flags API
1599
- // while we migrate to the new flags API
1600
- const useFlags = isTokenInRollout(this.apiKey, NEW_FLAGS_ROLLOUT_PERCENTAGE, NEW_FLAGS_EXCLUDED_HASHES);
1601
- const url = useFlags ? `${this.host}/flags/?v=2` : `${this.host}/decide/?v=4`;
1489
+ const url = `${this.host}/flags/?v=2&config=true`;
1602
1490
  const fetchOptions = {
1603
1491
  method: 'POST',
1604
1492
  headers: { ...this.getCustomHeaders(), 'Content-Type': 'application/json' },
@@ -1611,11 +1499,11 @@ class PostHogCoreStateless {
1611
1499
  ...extraPayload,
1612
1500
  }),
1613
1501
  };
1614
- this.logMsgIfDebug(() => console.log('PostHog Debug', 'Decide URL', url));
1615
- // Don't retry /decide API calls
1502
+ this.logMsgIfDebug(() => console.log('PostHog Debug', 'Flags URL', url));
1503
+ // Don't retry /flags API calls
1616
1504
  return this.fetchWithRetry(url, fetchOptions, { retryCount: 0 }, this.featureFlagsRequestTimeoutMs)
1617
1505
  .then((response) => response.json())
1618
- .then((response) => normalizeDecideResponse(response))
1506
+ .then((response) => normalizeFlagsResponse(response))
1619
1507
  .catch((error) => {
1620
1508
  this._events.emit('error', error);
1621
1509
  return undefined;
@@ -1644,15 +1532,15 @@ class PostHogCoreStateless {
1644
1532
  }
1645
1533
  async getFeatureFlagDetailStateless(key, distinctId, groups = {}, personProperties = {}, groupProperties = {}, disableGeoip) {
1646
1534
  await this._initPromise;
1647
- const decideResponse = await this.getFeatureFlagDetailsStateless(distinctId, groups, personProperties, groupProperties, disableGeoip, [key]);
1648
- if (decideResponse === undefined) {
1535
+ const flagsResponse = await this.getFeatureFlagDetailsStateless(distinctId, groups, personProperties, groupProperties, disableGeoip, [key]);
1536
+ if (flagsResponse === undefined) {
1649
1537
  return undefined;
1650
1538
  }
1651
- const featureFlags = decideResponse.flags;
1539
+ const featureFlags = flagsResponse.flags;
1652
1540
  const flagDetail = featureFlags[key];
1653
1541
  return {
1654
1542
  response: flagDetail,
1655
- requestId: decideResponse.requestId,
1543
+ requestId: flagsResponse.requestId,
1656
1544
  };
1657
1545
  }
1658
1546
  async getFeatureFlagPayloadStateless(key, distinctId, groups = {}, personProperties = {}, groupProperties = {}, disableGeoip) {
@@ -1702,26 +1590,26 @@ class PostHogCoreStateless {
1702
1590
  if (flagKeysToEvaluate) {
1703
1591
  extraPayload['flag_keys_to_evaluate'] = flagKeysToEvaluate;
1704
1592
  }
1705
- const decideResponse = await this.getDecide(distinctId, groups, personProperties, groupProperties, extraPayload);
1706
- if (decideResponse === undefined) {
1593
+ const flagsResponse = await this.getFlags(distinctId, groups, personProperties, groupProperties, extraPayload);
1594
+ if (flagsResponse === undefined) {
1707
1595
  // We probably errored out, so return undefined
1708
1596
  return undefined;
1709
1597
  }
1710
- // if there's an error on the decideResponse, log a console error, but don't throw an error
1711
- if (decideResponse.errorsWhileComputingFlags) {
1598
+ // if there's an error on the flagsResponse, log a console error, but don't throw an error
1599
+ if (flagsResponse.errorsWhileComputingFlags) {
1712
1600
  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');
1713
1601
  }
1714
1602
  // Add check for quota limitation on feature flags
1715
- if (decideResponse.quotaLimited?.includes(QuotaLimitedFeature.FeatureFlags)) {
1603
+ if (flagsResponse.quotaLimited?.includes(QuotaLimitedFeature.FeatureFlags)) {
1716
1604
  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');
1717
1605
  return {
1718
1606
  flags: {},
1719
1607
  featureFlags: {},
1720
1608
  featureFlagPayloads: {},
1721
- requestId: decideResponse?.requestId,
1609
+ requestId: flagsResponse?.requestId,
1722
1610
  };
1723
1611
  }
1724
- return decideResponse;
1612
+ return flagsResponse;
1725
1613
  }
1726
1614
  /***
1727
1615
  *** SURVEYS
@@ -2325,7 +2213,7 @@ class FeatureFlagsPoller {
2325
2213
  await this.loadFeatureFlags();
2326
2214
  const response = {};
2327
2215
  const payloads = {};
2328
- let fallbackToDecide = this.featureFlags.length == 0;
2216
+ let fallbackToFlags = this.featureFlags.length == 0;
2329
2217
  await Promise.all(this.featureFlags.map(async flag => {
2330
2218
  try {
2331
2219
  const matchValue = await this.computeFlagLocally(flag, distinctId, groups, personProperties, groupProperties);
@@ -2338,13 +2226,13 @@ class FeatureFlagsPoller {
2338
2226
  if (e instanceof InconclusiveMatchError) ; else if (e instanceof Error) {
2339
2227
  this.onError?.(new Error(`Error computing flag locally: ${flag.key}: ${e}`));
2340
2228
  }
2341
- fallbackToDecide = true;
2229
+ fallbackToFlags = true;
2342
2230
  }
2343
2231
  }));
2344
2232
  return {
2345
2233
  response,
2346
2234
  payloads,
2347
- fallbackToDecide
2235
+ fallbackToFlags
2348
2236
  };
2349
2237
  }
2350
2238
  async computeFlagLocally(flag, distinctId, groups = {}, personProperties = {}, groupProperties = {}) {
@@ -3276,7 +3164,24 @@ class PostHogBackendClient extends PostHogCoreStateless {
3276
3164
  return response;
3277
3165
  }
3278
3166
  async getRemoteConfigPayload(flagKey) {
3279
- return (await this.featureFlagsPoller?._requestRemoteConfigPayload(flagKey))?.json();
3167
+ const response = await this.featureFlagsPoller?._requestRemoteConfigPayload(flagKey);
3168
+ if (!response) {
3169
+ return undefined;
3170
+ }
3171
+ const parsed = await response.json();
3172
+ // The payload from the endpoint is stored as a JSON encoded string. So when we return
3173
+ // it, it's effectively double encoded. As far as we know, we should never get single-encoded
3174
+ // JSON, but we'll be defensive here just in case.
3175
+ if (typeof parsed === 'string') {
3176
+ try {
3177
+ // If the parsed value is a string, try parsing it again to handle double-encoded JSON
3178
+ return JSON.parse(parsed);
3179
+ } catch (e) {
3180
+ // If second parse fails, return the string as is
3181
+ return parsed;
3182
+ }
3183
+ }
3184
+ return parsed;
3280
3185
  }
3281
3186
  async isFeatureEnabled(key, distinctId, options) {
3282
3187
  const feat = await this.getFeatureFlag(key, distinctId, options);
@@ -3309,13 +3214,13 @@ class PostHogBackendClient extends PostHogCoreStateless {
3309
3214
  const localEvaluationResult = await this.featureFlagsPoller?.getAllFlagsAndPayloads(distinctId, groups, personProperties, groupProperties);
3310
3215
  let featureFlags = {};
3311
3216
  let featureFlagPayloads = {};
3312
- let fallbackToDecide = true;
3217
+ let fallbackToFlags = true;
3313
3218
  if (localEvaluationResult) {
3314
3219
  featureFlags = localEvaluationResult.response;
3315
3220
  featureFlagPayloads = localEvaluationResult.payloads;
3316
- fallbackToDecide = localEvaluationResult.fallbackToDecide;
3221
+ fallbackToFlags = localEvaluationResult.fallbackToFlags;
3317
3222
  }
3318
- if (fallbackToDecide && !onlyEvaluateLocally) {
3223
+ if (fallbackToFlags && !onlyEvaluateLocally) {
3319
3224
  const remoteEvaluationResult = await super.getFeatureFlagsAndPayloadsStateless(distinctId, groups, personProperties, groupProperties, disableGeoip);
3320
3225
  featureFlags = {
3321
3226
  ...featureFlags,