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.
@@ -913,7 +913,7 @@ function setupExpressErrorHandler(_posthog, app) {
913
913
  });
914
914
  }
915
915
 
916
- var version = "5.0.0";
916
+ var version = "5.1.1";
917
917
 
918
918
  var PostHogPersistedProperty;
919
919
  (function (PostHogPersistedProperty) {
@@ -937,11 +937,11 @@ var PostHogPersistedProperty;
937
937
  PostHogPersistedProperty["InstalledAppBuild"] = "installed_app_build";
938
938
  PostHogPersistedProperty["InstalledAppVersion"] = "installed_app_version";
939
939
  PostHogPersistedProperty["SessionReplay"] = "session_replay";
940
- PostHogPersistedProperty["DecideEndpointWasHit"] = "decide_endpoint_was_hit";
941
940
  PostHogPersistedProperty["SurveyLastSeenDate"] = "survey_last_seen_date";
942
941
  PostHogPersistedProperty["SurveysSeen"] = "surveys_seen";
943
942
  PostHogPersistedProperty["Surveys"] = "surveys";
944
943
  PostHogPersistedProperty["RemoteConfig"] = "remote_config";
944
+ PostHogPersistedProperty["FlagsEndpointWasHit"] = "flags_endpoint_was_hit";
945
945
  })(PostHogPersistedProperty || (PostHogPersistedProperty = {}));
946
946
  // Any key prefixed with `attr__` can be added
947
947
  var Compression;
@@ -1009,27 +1009,27 @@ var ActionStepStringMatching;
1009
1009
  ActionStepStringMatching["Regex"] = "regex";
1010
1010
  })(ActionStepStringMatching || (ActionStepStringMatching = {}));
1011
1011
 
1012
- const normalizeDecideResponse = (decideResponse) => {
1013
- if ('flags' in decideResponse) {
1014
- // Convert v4 format to v3 format
1015
- const featureFlags = getFlagValuesFromFlags(decideResponse.flags);
1016
- const featureFlagPayloads = getPayloadsFromFlags(decideResponse.flags);
1012
+ const normalizeFlagsResponse = (flagsResponse) => {
1013
+ if ('flags' in flagsResponse) {
1014
+ // Convert v2 format to v1 format
1015
+ const featureFlags = getFlagValuesFromFlags(flagsResponse.flags);
1016
+ const featureFlagPayloads = getPayloadsFromFlags(flagsResponse.flags);
1017
1017
  return {
1018
- ...decideResponse,
1018
+ ...flagsResponse,
1019
1019
  featureFlags,
1020
1020
  featureFlagPayloads,
1021
1021
  };
1022
1022
  }
1023
1023
  else {
1024
- // Convert v3 format to v4 format
1025
- const featureFlags = decideResponse.featureFlags ?? {};
1026
- const featureFlagPayloads = Object.fromEntries(Object.entries(decideResponse.featureFlagPayloads || {}).map(([k, v]) => [k, parsePayload(v)]));
1024
+ // Convert v1 format to v2 format
1025
+ const featureFlags = flagsResponse.featureFlags ?? {};
1026
+ const featureFlagPayloads = Object.fromEntries(Object.entries(flagsResponse.featureFlagPayloads || {}).map(([k, v]) => [k, parsePayload(v)]));
1027
1027
  const flags = Object.fromEntries(Object.entries(featureFlags).map(([key, value]) => [
1028
1028
  key,
1029
1029
  getFlagDetailFromFlagAndPayload(key, value, featureFlagPayloads[key]),
1030
1030
  ]));
1031
1031
  return {
1032
- ...decideResponse,
1032
+ ...flagsResponse,
1033
1033
  featureFlags,
1034
1034
  featureFlagPayloads,
1035
1035
  flags,
@@ -1092,90 +1092,6 @@ const parsePayload = (response) => {
1092
1092
  }
1093
1093
  };
1094
1094
 
1095
- // Rollout constants
1096
- const NEW_FLAGS_ROLLOUT_PERCENTAGE = 1;
1097
- // The fnv1a hashes of the tokens that are explicitly excluded from the rollout
1098
- // see https://github.com/PostHog/posthog-js-lite/blob/main/posthog-core/src/utils.ts#L84
1099
- // are hashed API tokens from our top 10 for each category supported by this SDK.
1100
- const NEW_FLAGS_EXCLUDED_HASHES = new Set([
1101
- // Node
1102
- '61be3dd8',
1103
- '96f6df5f',
1104
- '8cfdba9b',
1105
- 'bf027177',
1106
- 'e59430a8',
1107
- '7fa5500b',
1108
- '569798e9',
1109
- '04809ff7',
1110
- '0ebc61a5',
1111
- '32de7f98',
1112
- '3beeb69a',
1113
- '12d34ad9',
1114
- '733853ec',
1115
- '0645bb64',
1116
- '5dcbee21',
1117
- 'b1f95fa3',
1118
- '2189e408',
1119
- '82b460c2',
1120
- '3a8cc979',
1121
- '29ef8843',
1122
- '2cdbf767',
1123
- '38084b54',
1124
- // React Native
1125
- '50f9f8de',
1126
- '41d0df91',
1127
- '5c236689',
1128
- 'c11aedd3',
1129
- 'ada46672',
1130
- 'f4331ee1',
1131
- '42fed62a',
1132
- 'c957462c',
1133
- 'd62f705a',
1134
- // Web (lots of teams per org, hence lots of API tokens)
1135
- 'e0162666',
1136
- '01b3e5cf',
1137
- '441cef7f',
1138
- 'bb9cafee',
1139
- '8f348eb0',
1140
- 'b2553f3a',
1141
- '97469d7d',
1142
- '39f21a76',
1143
- '03706dcc',
1144
- '27d50569',
1145
- '307584a7',
1146
- '6433e92e',
1147
- '150c7fbb',
1148
- '49f57f22',
1149
- '3772f65b',
1150
- '01eb8256',
1151
- '3c9e9234',
1152
- 'f853c7f7',
1153
- 'c0ac4b67',
1154
- 'cd609d40',
1155
- '10ca9b1a',
1156
- '8a87f11b',
1157
- '8e8e5216',
1158
- '1f6b63b3',
1159
- 'db7943dd',
1160
- '79b7164c',
1161
- '07f78e33',
1162
- '2d21b6fd',
1163
- '952db5ee',
1164
- 'a7d3b43f',
1165
- '1924dd9c',
1166
- '84e1b8f6',
1167
- 'dff631b6',
1168
- 'c5aa8a79',
1169
- 'fa133a95',
1170
- '498a4508',
1171
- '24748755',
1172
- '98f3d658',
1173
- '21bbda67',
1174
- '7dbfed69',
1175
- 'be3ec24c',
1176
- 'fc80b8e2',
1177
- '75cc0998',
1178
- ]);
1179
1095
  const STRING_FORMAT = 'utf8';
1180
1096
  function assert(truthyValue, message) {
1181
1097
  if (!truthyValue || typeof truthyValue !== 'string' || isEmpty(truthyValue)) {
@@ -1222,30 +1138,6 @@ function safeSetTimeout(fn, timeout) {
1222
1138
  t?.unref && t?.unref();
1223
1139
  return t;
1224
1140
  }
1225
- // FNV-1a hash function
1226
- // https://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function
1227
- // I know, I know, I'm rolling my own hash function, but I didn't want to take on
1228
- // a crypto dependency and this is just temporary anyway
1229
- function fnv1a(str) {
1230
- let hash = 0x811c9dc5; // FNV offset basis
1231
- for (let i = 0; i < str.length; i++) {
1232
- hash ^= str.charCodeAt(i);
1233
- hash += (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24);
1234
- }
1235
- // Convert to hex string, padding to 8 chars
1236
- return (hash >>> 0).toString(16).padStart(8, '0');
1237
- }
1238
- function isTokenInRollout(token, percentage = 0, excludedHashes) {
1239
- const tokenHash = fnv1a(token);
1240
- // Check excluded hashes (we're explicitly including these tokens from the rollout)
1241
- if (excludedHashes?.has(tokenHash)) {
1242
- return false;
1243
- }
1244
- // Convert hash to int and divide by max value to get number between 0-1
1245
- const hashInt = parseInt(tokenHash, 16);
1246
- const hashFloat = hashInt / 0xffffffff;
1247
- return hashFloat < percentage;
1248
- }
1249
1141
  function allSettled(promises) {
1250
1142
  return Promise.all(promises.map((p) => (p ?? Promise.resolve()).then((value) => ({ status: 'fulfilled', value }), (reason) => ({ status: 'rejected', reason }))));
1251
1143
  }
@@ -1570,13 +1462,9 @@ class PostHogCoreStateless {
1570
1462
  /***
1571
1463
  *** FEATURE FLAGS
1572
1464
  ***/
1573
- async getDecide(distinctId, groups = {}, personProperties = {}, groupProperties = {}, extraPayload = {}) {
1465
+ async getFlags(distinctId, groups = {}, personProperties = {}, groupProperties = {}, extraPayload = {}) {
1574
1466
  await this._initPromise;
1575
- // Check if the API token is in the new flags rollout
1576
- // This is a temporary measure to ensure that we can still use the old flags API
1577
- // while we migrate to the new flags API
1578
- const useFlags = isTokenInRollout(this.apiKey, NEW_FLAGS_ROLLOUT_PERCENTAGE, NEW_FLAGS_EXCLUDED_HASHES);
1579
- const url = useFlags ? `${this.host}/flags/?v=2` : `${this.host}/decide/?v=4`;
1467
+ const url = `${this.host}/flags/?v=2&config=true`;
1580
1468
  const fetchOptions = {
1581
1469
  method: 'POST',
1582
1470
  headers: { ...this.getCustomHeaders(), 'Content-Type': 'application/json' },
@@ -1589,11 +1477,11 @@ class PostHogCoreStateless {
1589
1477
  ...extraPayload,
1590
1478
  }),
1591
1479
  };
1592
- this.logMsgIfDebug(() => console.log('PostHog Debug', 'Decide URL', url));
1593
- // Don't retry /decide API calls
1480
+ this.logMsgIfDebug(() => console.log('PostHog Debug', 'Flags URL', url));
1481
+ // Don't retry /flags API calls
1594
1482
  return this.fetchWithRetry(url, fetchOptions, { retryCount: 0 }, this.featureFlagsRequestTimeoutMs)
1595
1483
  .then((response) => response.json())
1596
- .then((response) => normalizeDecideResponse(response))
1484
+ .then((response) => normalizeFlagsResponse(response))
1597
1485
  .catch((error) => {
1598
1486
  this._events.emit('error', error);
1599
1487
  return undefined;
@@ -1622,15 +1510,15 @@ class PostHogCoreStateless {
1622
1510
  }
1623
1511
  async getFeatureFlagDetailStateless(key, distinctId, groups = {}, personProperties = {}, groupProperties = {}, disableGeoip) {
1624
1512
  await this._initPromise;
1625
- const decideResponse = await this.getFeatureFlagDetailsStateless(distinctId, groups, personProperties, groupProperties, disableGeoip, [key]);
1626
- if (decideResponse === undefined) {
1513
+ const flagsResponse = await this.getFeatureFlagDetailsStateless(distinctId, groups, personProperties, groupProperties, disableGeoip, [key]);
1514
+ if (flagsResponse === undefined) {
1627
1515
  return undefined;
1628
1516
  }
1629
- const featureFlags = decideResponse.flags;
1517
+ const featureFlags = flagsResponse.flags;
1630
1518
  const flagDetail = featureFlags[key];
1631
1519
  return {
1632
1520
  response: flagDetail,
1633
- requestId: decideResponse.requestId,
1521
+ requestId: flagsResponse.requestId,
1634
1522
  };
1635
1523
  }
1636
1524
  async getFeatureFlagPayloadStateless(key, distinctId, groups = {}, personProperties = {}, groupProperties = {}, disableGeoip) {
@@ -1680,26 +1568,26 @@ class PostHogCoreStateless {
1680
1568
  if (flagKeysToEvaluate) {
1681
1569
  extraPayload['flag_keys_to_evaluate'] = flagKeysToEvaluate;
1682
1570
  }
1683
- const decideResponse = await this.getDecide(distinctId, groups, personProperties, groupProperties, extraPayload);
1684
- if (decideResponse === undefined) {
1571
+ const flagsResponse = await this.getFlags(distinctId, groups, personProperties, groupProperties, extraPayload);
1572
+ if (flagsResponse === undefined) {
1685
1573
  // We probably errored out, so return undefined
1686
1574
  return undefined;
1687
1575
  }
1688
- // if there's an error on the decideResponse, log a console error, but don't throw an error
1689
- if (decideResponse.errorsWhileComputingFlags) {
1576
+ // if there's an error on the flagsResponse, log a console error, but don't throw an error
1577
+ if (flagsResponse.errorsWhileComputingFlags) {
1690
1578
  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');
1691
1579
  }
1692
1580
  // Add check for quota limitation on feature flags
1693
- if (decideResponse.quotaLimited?.includes(QuotaLimitedFeature.FeatureFlags)) {
1581
+ if (flagsResponse.quotaLimited?.includes(QuotaLimitedFeature.FeatureFlags)) {
1694
1582
  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');
1695
1583
  return {
1696
1584
  flags: {},
1697
1585
  featureFlags: {},
1698
1586
  featureFlagPayloads: {},
1699
- requestId: decideResponse?.requestId,
1587
+ requestId: flagsResponse?.requestId,
1700
1588
  };
1701
1589
  }
1702
- return decideResponse;
1590
+ return flagsResponse;
1703
1591
  }
1704
1592
  /***
1705
1593
  *** SURVEYS
@@ -2303,7 +2191,7 @@ class FeatureFlagsPoller {
2303
2191
  await this.loadFeatureFlags();
2304
2192
  const response = {};
2305
2193
  const payloads = {};
2306
- let fallbackToDecide = this.featureFlags.length == 0;
2194
+ let fallbackToFlags = this.featureFlags.length == 0;
2307
2195
  await Promise.all(this.featureFlags.map(async flag => {
2308
2196
  try {
2309
2197
  const matchValue = await this.computeFlagLocally(flag, distinctId, groups, personProperties, groupProperties);
@@ -2316,13 +2204,13 @@ class FeatureFlagsPoller {
2316
2204
  if (e instanceof InconclusiveMatchError) ; else if (e instanceof Error) {
2317
2205
  this.onError?.(new Error(`Error computing flag locally: ${flag.key}: ${e}`));
2318
2206
  }
2319
- fallbackToDecide = true;
2207
+ fallbackToFlags = true;
2320
2208
  }
2321
2209
  }));
2322
2210
  return {
2323
2211
  response,
2324
2212
  payloads,
2325
- fallbackToDecide
2213
+ fallbackToFlags
2326
2214
  };
2327
2215
  }
2328
2216
  async computeFlagLocally(flag, distinctId, groups = {}, personProperties = {}, groupProperties = {}) {
@@ -3254,7 +3142,24 @@ class PostHogBackendClient extends PostHogCoreStateless {
3254
3142
  return response;
3255
3143
  }
3256
3144
  async getRemoteConfigPayload(flagKey) {
3257
- return (await this.featureFlagsPoller?._requestRemoteConfigPayload(flagKey))?.json();
3145
+ const response = await this.featureFlagsPoller?._requestRemoteConfigPayload(flagKey);
3146
+ if (!response) {
3147
+ return undefined;
3148
+ }
3149
+ const parsed = await response.json();
3150
+ // The payload from the endpoint is stored as a JSON encoded string. So when we return
3151
+ // it, it's effectively double encoded. As far as we know, we should never get single-encoded
3152
+ // JSON, but we'll be defensive here just in case.
3153
+ if (typeof parsed === 'string') {
3154
+ try {
3155
+ // If the parsed value is a string, try parsing it again to handle double-encoded JSON
3156
+ return JSON.parse(parsed);
3157
+ } catch (e) {
3158
+ // If second parse fails, return the string as is
3159
+ return parsed;
3160
+ }
3161
+ }
3162
+ return parsed;
3258
3163
  }
3259
3164
  async isFeatureEnabled(key, distinctId, options) {
3260
3165
  const feat = await this.getFeatureFlag(key, distinctId, options);
@@ -3287,13 +3192,13 @@ class PostHogBackendClient extends PostHogCoreStateless {
3287
3192
  const localEvaluationResult = await this.featureFlagsPoller?.getAllFlagsAndPayloads(distinctId, groups, personProperties, groupProperties);
3288
3193
  let featureFlags = {};
3289
3194
  let featureFlagPayloads = {};
3290
- let fallbackToDecide = true;
3195
+ let fallbackToFlags = true;
3291
3196
  if (localEvaluationResult) {
3292
3197
  featureFlags = localEvaluationResult.response;
3293
3198
  featureFlagPayloads = localEvaluationResult.payloads;
3294
- fallbackToDecide = localEvaluationResult.fallbackToDecide;
3199
+ fallbackToFlags = localEvaluationResult.fallbackToFlags;
3295
3200
  }
3296
- if (fallbackToDecide && !onlyEvaluateLocally) {
3201
+ if (fallbackToFlags && !onlyEvaluateLocally) {
3297
3202
  const remoteEvaluationResult = await super.getFeatureFlagsAndPayloadsStateless(distinctId, groups, personProperties, groupProperties, disableGeoip);
3298
3203
  featureFlags = {
3299
3204
  ...featureFlags,