posthog-node 4.7.0 → 4.8.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
+ # 4.8.1 – 2025-02-26
4
+
5
+ 1. Supports gracefully handling quotaLimited responses from the PostHog API for feature flag evaluation
6
+
7
+ # 4.8.0 - 2025-02-26
8
+
9
+ 1. Add guardrails and exponential error backoff in the feature flag local evaluation poller to prevent high rates of 401/403 traffic towards `/local_evaluation`
10
+
3
11
  # 4.7.0 - 2025-02-20
4
12
 
5
13
  ## Added
package/lib/index.cjs.js CHANGED
@@ -7,7 +7,7 @@ var node_fs = require('node:fs');
7
7
  var node_readline = require('node:readline');
8
8
  var node_path = require('node:path');
9
9
 
10
- var version = "4.7.0";
10
+ var version = "4.8.1";
11
11
 
12
12
  var PostHogPersistedProperty;
13
13
  (function (PostHogPersistedProperty) {
@@ -958,6 +958,11 @@ class PostHogFetchNetworkError extends Error {
958
958
  function isPostHogFetchError(err) {
959
959
  return typeof err === 'object' && (err instanceof PostHogFetchHttpError || err instanceof PostHogFetchNetworkError);
960
960
  }
961
+ var QuotaLimitedFeature;
962
+ (function (QuotaLimitedFeature) {
963
+ QuotaLimitedFeature["FeatureFlags"] = "feature_flags";
964
+ QuotaLimitedFeature["Recordings"] = "recordings";
965
+ })(QuotaLimitedFeature || (QuotaLimitedFeature = {}));
961
966
  class PostHogCoreStateless {
962
967
  constructor(apiKey, options) {
963
968
  this.flushPromise = null;
@@ -1201,6 +1206,14 @@ class PostHogCoreStateless {
1201
1206
  extraPayload['geoip_disable'] = true;
1202
1207
  }
1203
1208
  const decideResponse = await this.getDecide(distinctId, groups, personProperties, groupProperties, extraPayload);
1209
+ // Add check for quota limitation on feature flags
1210
+ if (decideResponse?.quotaLimited?.includes(QuotaLimitedFeature.FeatureFlags)) {
1211
+ 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');
1212
+ return {
1213
+ flags: undefined,
1214
+ payloads: undefined,
1215
+ };
1216
+ }
1204
1217
  const flags = decideResponse?.featureFlags;
1205
1218
  const payloads = decideResponse?.featureFlagPayloads;
1206
1219
  let parsedPayloads = payloads;
@@ -1495,6 +1508,8 @@ class FeatureFlagsPoller {
1495
1508
  ...options
1496
1509
  }) {
1497
1510
  this.debugMode = false;
1511
+ this.lastRequestWasAuthenticationError = false;
1512
+ this.authenticationErrorCount = 0;
1498
1513
  this.pollingInterval = pollingInterval;
1499
1514
  this.personalApiKey = personalApiKey;
1500
1515
  this.featureFlags = [];
@@ -1506,7 +1521,6 @@ class FeatureFlagsPoller {
1506
1521
  this.projectApiKey = projectApiKey;
1507
1522
  this.host = host;
1508
1523
  this.poller = undefined;
1509
- // NOTE: as any is required here as the AbortSignal typing is slightly misaligned but works just fine
1510
1524
  this.fetch = options.fetch || fetch$1;
1511
1525
  this.onError = options.onError;
1512
1526
  this.customHeaders = customHeaders;
@@ -1726,16 +1740,45 @@ class FeatureFlagsPoller {
1726
1740
  await this._loadFeatureFlags();
1727
1741
  }
1728
1742
  }
1743
+ /**
1744
+ * If a client is misconfigured with an invalid or improper API key, the polling interval is doubled each time
1745
+ * until a successful request is made, up to a maximum of 60 seconds.
1746
+ *
1747
+ * @returns The polling interval to use for the next request.
1748
+ */
1749
+ getPollingInterval() {
1750
+ if (!this.lastRequestWasAuthenticationError) {
1751
+ return this.pollingInterval;
1752
+ }
1753
+ return Math.min(SIXTY_SECONDS, this.pollingInterval * 2 ** this.authenticationErrorCount);
1754
+ }
1729
1755
  async _loadFeatureFlags() {
1730
1756
  if (this.poller) {
1731
1757
  clearTimeout(this.poller);
1732
1758
  this.poller = undefined;
1733
1759
  }
1734
- this.poller = setTimeout(() => this._loadFeatureFlags(), this.pollingInterval);
1760
+ this.poller = setTimeout(() => this._loadFeatureFlags(), this.getPollingInterval());
1735
1761
  try {
1736
1762
  const res = await this._requestFeatureFlagDefinitions();
1737
1763
  if (res && res.status === 401) {
1738
- throw new ClientError(`Your personalApiKey is invalid. Are you sure you're not using your Project API key? More information: https://posthog.com/docs/api/overview`);
1764
+ this.lastRequestWasAuthenticationError = true;
1765
+ this.authenticationErrorCount += 1;
1766
+ throw new ClientError(`Your project key or personal API key is invalid. Setting next polling interval to ${this.getPollingInterval()}ms. More information: https://posthog.com/docs/api#rate-limiting`);
1767
+ }
1768
+ if (res && res.status === 403) {
1769
+ this.lastRequestWasAuthenticationError = true;
1770
+ this.authenticationErrorCount += 1;
1771
+ throw new ClientError(`Your personal API key does not have permission to fetch feature flag definitions for local evaluation. Setting next polling interval to ${this.getPollingInterval()}ms. Are you sure you're using the correct personal and Project API key pair? More information: https://posthog.com/docs/api/overview`);
1772
+ }
1773
+ if (res && res.status === 402) {
1774
+ // Quota limited - clear all flags
1775
+ console.warn('[FEATURE FLAGS] Feature flags quota limit exceeded - unsetting all local flags. Learn more about billing limits at https://posthog.com/docs/billing/limits-alerts');
1776
+ this.featureFlags = [];
1777
+ this.featureFlagsByKey = {};
1778
+ this.groupTypeMapping = {};
1779
+ this.cohorts = {};
1780
+ this.loadedSuccessfullyOnce = false;
1781
+ return;
1739
1782
  }
1740
1783
  if (res && res.status !== 200) {
1741
1784
  // something else went wrong, or the server is down.
@@ -1751,6 +1794,8 @@ class FeatureFlagsPoller {
1751
1794
  this.groupTypeMapping = responseJson.group_type_mapping || {};
1752
1795
  this.cohorts = responseJson.cohorts || [];
1753
1796
  this.loadedSuccessfullyOnce = true;
1797
+ this.lastRequestWasAuthenticationError = false;
1798
+ this.authenticationErrorCount = 0;
1754
1799
  } catch (err) {
1755
1800
  // if an error that is not an instance of ClientError is thrown
1756
1801
  // we silently ignore the error when reloading feature flags
@@ -2939,7 +2984,11 @@ class ErrorTracking {
2939
2984
  }
2940
2985
  }
2941
2986
 
2987
+ // Standard local evaluation rate limit is 600 per minute (10 per second),
2988
+ // so the fastest a poller should ever be set is 100ms.
2989
+ const MINIMUM_POLLING_INTERVAL = 100;
2942
2990
  const THIRTY_SECONDS = 30 * 1000;
2991
+ const SIXTY_SECONDS = 60 * 1000;
2943
2992
  const MAX_CACHE_SIZE = 50 * 1000;
2944
2993
  // The actual exported Nodejs API.
2945
2994
  class PostHog extends PostHogCoreStateless {
@@ -2947,9 +2996,13 @@ class PostHog extends PostHogCoreStateless {
2947
2996
  super(apiKey, options);
2948
2997
  this._memoryStorage = new PostHogMemoryStorage();
2949
2998
  this.options = options;
2999
+ this.options.featureFlagsPollingInterval = typeof options.featureFlagsPollingInterval === 'number' ? Math.max(options.featureFlagsPollingInterval, MINIMUM_POLLING_INTERVAL) : THIRTY_SECONDS;
2950
3000
  if (options.personalApiKey) {
3001
+ if (options.personalApiKey.includes('phc_')) {
3002
+ throw new Error('Your Personal API key is invalid. These keys are prefixed with "phx_" and can be created in PostHog project settings.');
3003
+ }
2951
3004
  this.featureFlagsPoller = new FeatureFlagsPoller({
2952
- pollingInterval: typeof options.featureFlagsPollingInterval === 'number' ? options.featureFlagsPollingInterval : THIRTY_SECONDS,
3005
+ pollingInterval: this.options.featureFlagsPollingInterval,
2953
3006
  personalApiKey: options.personalApiKey,
2954
3007
  projectApiKey: apiKey,
2955
3008
  timeout: options.requestTimeout ?? 10000,
@@ -3404,8 +3457,11 @@ function setupExpressErrorHandler(_posthog, app) {
3404
3457
  });
3405
3458
  }
3406
3459
 
3460
+ exports.MINIMUM_POLLING_INTERVAL = MINIMUM_POLLING_INTERVAL;
3407
3461
  exports.PostHog = PostHog;
3408
3462
  exports.PostHogSentryIntegration = PostHogSentryIntegration;
3463
+ exports.SIXTY_SECONDS = SIXTY_SECONDS;
3464
+ exports.THIRTY_SECONDS = THIRTY_SECONDS;
3409
3465
  exports.createEventProcessor = createEventProcessor;
3410
3466
  exports.sentryIntegration = sentryIntegration;
3411
3467
  exports.setupExpressErrorHandler = setupExpressErrorHandler;