posthog-node 4.10.0 → 4.10.2

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,14 +1,24 @@
1
1
  # Next
2
2
 
3
- # 4.10.0 – 2025-03-06
3
+ # 4.10.2 - 2025-03-06
4
+
5
+ 1. Add: log error message when feature flags have computation errors.
6
+
7
+ # 4.10.1 – 2025-03-06
8
+
9
+ 1. Fix: only set `platform` on PostHog exception frame properties
10
+ 1. Fix: prevent fetch floods when rate-limited.
11
+
12
+
13
+ # 4.10.0 – 2025-03-06
4
14
 
5
15
  1. Attach requestId to $feature_flag_called if present in /decide response
6
16
 
7
- # 4.9.0 – 2025-03-04
17
+ # 4.9.0 – 2025-03-04
8
18
 
9
19
  1. Allow feature flags to be evaluated individually when local evaluation is not being used
10
20
 
11
- # 4.8.1 – 2025-02-26
21
+ # 4.8.1 – 2025-02-26
12
22
 
13
23
  1. Supports gracefully handling quotaLimited responses from the PostHog API for feature flag evaluation
14
24
 
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.10.0";
10
+ var version = "4.10.2";
11
11
 
12
12
  var PostHogPersistedProperty;
13
13
  (function (PostHogPersistedProperty) {
@@ -29,6 +29,10 @@ var PostHogPersistedProperty;
29
29
  PostHogPersistedProperty["InstalledAppVersion"] = "installed_app_version";
30
30
  PostHogPersistedProperty["SessionReplay"] = "session_replay";
31
31
  PostHogPersistedProperty["DecideEndpointWasHit"] = "decide_endpoint_was_hit";
32
+ PostHogPersistedProperty["SurveyLastSeenDate"] = "survey_last_seen_date";
33
+ PostHogPersistedProperty["SurveysSeen"] = "surveys_seen";
34
+ PostHogPersistedProperty["Surveys"] = "surveys";
35
+ PostHogPersistedProperty["RemoteConfig"] = "remote_config";
32
36
  })(PostHogPersistedProperty || (PostHogPersistedProperty = {}));
33
37
 
34
38
  function assert(truthyValue, message) {
@@ -966,10 +970,6 @@ var QuotaLimitedFeature;
966
970
  class PostHogCoreStateless {
967
971
  constructor(apiKey, options) {
968
972
  this.flushPromise = null;
969
- this.disableGeoip = true;
970
- this.historicalMigration = false;
971
- this.disabled = false;
972
- this.defaultOptIn = true;
973
973
  this.pendingPromises = {};
974
974
  // internal
975
975
  this._events = new SimpleEventEmitter();
@@ -982,8 +982,10 @@ class PostHogCoreStateless {
982
982
  this.maxQueueSize = Math.max(this.flushAt, options?.maxQueueSize ?? 1000);
983
983
  this.flushInterval = options?.flushInterval ?? 10000;
984
984
  this.captureMode = options?.captureMode || 'json';
985
+ this.preloadFeatureFlags = options?.preloadFeatureFlags ?? true;
985
986
  // If enable is explicitly set to false we override the optout
986
987
  this.defaultOptIn = options?.defaultOptIn ?? true;
988
+ this.disableSurveys = options?.disableSurveys ?? false;
987
989
  this._retryOptions = {
988
990
  retryCount: options?.fetchRetryCount ?? 3,
989
991
  retryDelay: options?.fetchRetryDelay ?? 3000,
@@ -991,6 +993,7 @@ class PostHogCoreStateless {
991
993
  };
992
994
  this.requestTimeout = options?.requestTimeout ?? 10000; // 10 seconds
993
995
  this.featureFlagsRequestTimeoutMs = options?.featureFlagsRequestTimeoutMs ?? 3000; // 3 seconds
996
+ this.remoteConfigRequestTimeoutMs = options?.remoteConfigRequestTimeoutMs ?? 3000; // 3 seconds
994
997
  this.disableGeoip = options?.disableGeoip ?? true;
995
998
  this.disabled = options?.disabled ?? false;
996
999
  this.historicalMigration = options?.historicalMigration ?? false;
@@ -1127,6 +1130,29 @@ class PostHogCoreStateless {
1127
1130
  this.enqueue('capture', payload, options);
1128
1131
  });
1129
1132
  }
1133
+ async getRemoteConfig() {
1134
+ await this._initPromise;
1135
+ let host = this.host;
1136
+ if (host === 'https://us.i.posthog.com') {
1137
+ host = 'https://us-assets.i.posthog.com';
1138
+ }
1139
+ else if (host === 'https://eu.i.posthog.com') {
1140
+ host = 'https://eu-assets.i.posthog.com';
1141
+ }
1142
+ const url = `${host}/array/${this.apiKey}/config`;
1143
+ const fetchOptions = {
1144
+ method: 'GET',
1145
+ headers: { ...this.getCustomHeaders(), 'Content-Type': 'application/json' },
1146
+ };
1147
+ // Don't retry remote config API calls
1148
+ return this.fetchWithRetry(url, fetchOptions, { retryCount: 0 }, this.remoteConfigRequestTimeoutMs)
1149
+ .then((response) => response.json())
1150
+ .catch((error) => {
1151
+ this.logMsgIfDebug(() => console.error('Remote config could not be loaded', error));
1152
+ this._events.emit('error', error);
1153
+ return undefined;
1154
+ });
1155
+ }
1130
1156
  /***
1131
1157
  *** FEATURE FLAGS
1132
1158
  ***/
@@ -1216,6 +1242,10 @@ class PostHogCoreStateless {
1216
1242
  extraPayload['flag_keys_to_evaluate'] = flagKeysToEvaluate;
1217
1243
  }
1218
1244
  const decideResponse = await this.getDecide(distinctId, groups, personProperties, groupProperties, extraPayload);
1245
+ // if there's an error on the decideResponse, log a console error, but don't throw an error
1246
+ if (decideResponse?.errorsWhileComputingFlags) {
1247
+ 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
+ }
1219
1249
  // Add check for quota limitation on feature flags
1220
1250
  if (decideResponse?.quotaLimited?.includes(QuotaLimitedFeature.FeatureFlags)) {
1221
1251
  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');
@@ -1237,6 +1267,42 @@ class PostHogCoreStateless {
1237
1267
  requestId: decideResponse?.requestId,
1238
1268
  };
1239
1269
  }
1270
+ /***
1271
+ *** SURVEYS
1272
+ ***/
1273
+ async getSurveysStateless() {
1274
+ await this._initPromise;
1275
+ if (this.disableSurveys === true) {
1276
+ this.logMsgIfDebug(() => console.log('Loading surveys is disabled.'));
1277
+ return [];
1278
+ }
1279
+ const url = `${this.host}/api/surveys/?token=${this.apiKey}`;
1280
+ const fetchOptions = {
1281
+ method: 'GET',
1282
+ headers: { ...this.getCustomHeaders(), 'Content-Type': 'application/json' },
1283
+ };
1284
+ const response = await this.fetchWithRetry(url, fetchOptions)
1285
+ .then((response) => {
1286
+ if (response.status !== 200 || !response.json) {
1287
+ const msg = `Surveys API could not be loaded: ${response.status}`;
1288
+ const error = new Error(msg);
1289
+ this.logMsgIfDebug(() => console.error(error));
1290
+ this._events.emit('error', new Error(msg));
1291
+ return undefined;
1292
+ }
1293
+ return response.json();
1294
+ })
1295
+ .catch((error) => {
1296
+ this.logMsgIfDebug(() => console.error('Surveys API could not be loaded', error));
1297
+ this._events.emit('error', error);
1298
+ return undefined;
1299
+ });
1300
+ const newSurveys = response?.surveys;
1301
+ if (newSurveys) {
1302
+ this.logMsgIfDebug(() => console.log('PostHog Debug', 'Surveys fetched from API: ', JSON.stringify(newSurveys)));
1303
+ }
1304
+ return newSurveys ?? [];
1305
+ }
1240
1306
  /***
1241
1307
  *** QUEUEING AND FLUSHING
1242
1308
  ***/
@@ -1520,8 +1586,8 @@ class FeatureFlagsPoller {
1520
1586
  ...options
1521
1587
  }) {
1522
1588
  this.debugMode = false;
1523
- this.lastRequestWasAuthenticationError = false;
1524
- this.authenticationErrorCount = 0;
1589
+ this.shouldBeginExponentialBackoff = false;
1590
+ this.backOffCount = 0;
1525
1591
  this.pollingInterval = pollingInterval;
1526
1592
  this.personalApiKey = personalApiKey;
1527
1593
  this.featureFlags = [];
@@ -1759,10 +1825,10 @@ class FeatureFlagsPoller {
1759
1825
  * @returns The polling interval to use for the next request.
1760
1826
  */
1761
1827
  getPollingInterval() {
1762
- if (!this.lastRequestWasAuthenticationError) {
1828
+ if (!this.shouldBeginExponentialBackoff) {
1763
1829
  return this.pollingInterval;
1764
1830
  }
1765
- return Math.min(SIXTY_SECONDS, this.pollingInterval * 2 ** this.authenticationErrorCount);
1831
+ return Math.min(SIXTY_SECONDS, this.pollingInterval * 2 ** this.backOffCount);
1766
1832
  }
1767
1833
  async _loadFeatureFlags() {
1768
1834
  if (this.poller) {
@@ -1772,45 +1838,69 @@ class FeatureFlagsPoller {
1772
1838
  this.poller = setTimeout(() => this._loadFeatureFlags(), this.getPollingInterval());
1773
1839
  try {
1774
1840
  const res = await this._requestFeatureFlagDefinitions();
1775
- if (res && res.status === 401) {
1776
- this.lastRequestWasAuthenticationError = true;
1777
- this.authenticationErrorCount += 1;
1778
- 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`);
1779
- }
1780
- if (res && res.status === 403) {
1781
- this.lastRequestWasAuthenticationError = true;
1782
- this.authenticationErrorCount += 1;
1783
- 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`);
1784
- }
1785
- if (res && res.status === 402) {
1786
- // Quota limited - clear all flags
1787
- 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');
1788
- this.featureFlags = [];
1789
- this.featureFlagsByKey = {};
1790
- this.groupTypeMapping = {};
1791
- this.cohorts = {};
1792
- this.loadedSuccessfullyOnce = false;
1841
+ // Handle undefined res case, this shouldn't happen, but it doesn't hurt to handle it anyway
1842
+ if (!res) {
1843
+ // Don't override existing flags when something goes wrong
1793
1844
  return;
1794
1845
  }
1795
- if (res && res.status !== 200) {
1796
- // something else went wrong, or the server is down.
1797
- // In this case, don't override existing flags
1798
- return;
1799
- }
1800
- const responseJson = await res.json();
1801
- if (!('flags' in responseJson)) {
1802
- this.onError?.(new Error(`Invalid response when getting feature flags: ${JSON.stringify(responseJson)}`));
1846
+ // NB ON ERROR HANDLING & `loadedSuccessfullyOnce`:
1847
+ //
1848
+ // `loadedSuccessfullyOnce` indicates we've successfully loaded a valid set of flags at least once.
1849
+ // If we set it to `true` in an error scenario (e.g. 402 Over Quota, 401 Invalid Key, etc.),
1850
+ // any manual call to `loadFeatureFlags()` (without forceReload) will skip refetching entirely,
1851
+ // leaving us stuck with zero or outdated flags. The poller does keep running, but we also want
1852
+ // manual reloads to be possible as soon as the error condition is resolved.
1853
+ //
1854
+ // Therefore, on error statuses, we do *not* set `loadedSuccessfullyOnce = true`, ensuring that
1855
+ // both the background poller and any subsequent manual calls can keep trying to load flags
1856
+ // once the issue (quota, permission, rate limit, etc.) is resolved.
1857
+ switch (res.status) {
1858
+ case 401:
1859
+ // Invalid API key
1860
+ this.shouldBeginExponentialBackoff = true;
1861
+ this.backOffCount += 1;
1862
+ 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`);
1863
+ case 402:
1864
+ // Quota exceeded - clear all flags
1865
+ 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');
1866
+ this.featureFlags = [];
1867
+ this.featureFlagsByKey = {};
1868
+ this.groupTypeMapping = {};
1869
+ this.cohorts = {};
1870
+ return;
1871
+ case 403:
1872
+ // Permissions issue
1873
+ this.shouldBeginExponentialBackoff = true;
1874
+ this.backOffCount += 1;
1875
+ 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`);
1876
+ case 429:
1877
+ // Rate limited
1878
+ this.shouldBeginExponentialBackoff = true;
1879
+ this.backOffCount += 1;
1880
+ throw new ClientError(`You are being rate limited. Setting next polling interval to ${this.getPollingInterval()}ms. More information: https://posthog.com/docs/api#rate-limiting`);
1881
+ case 200:
1882
+ {
1883
+ // Process successful response
1884
+ const responseJson = await res.json();
1885
+ if (!('flags' in responseJson)) {
1886
+ this.onError?.(new Error(`Invalid response when getting feature flags: ${JSON.stringify(responseJson)}`));
1887
+ return;
1888
+ }
1889
+ this.featureFlags = responseJson.flags || [];
1890
+ this.featureFlagsByKey = this.featureFlags.reduce((acc, curr) => (acc[curr.key] = curr, acc), {});
1891
+ this.groupTypeMapping = responseJson.group_type_mapping || {};
1892
+ this.cohorts = responseJson.cohorts || {};
1893
+ this.loadedSuccessfullyOnce = true;
1894
+ this.shouldBeginExponentialBackoff = false;
1895
+ this.backOffCount = 0;
1896
+ break;
1897
+ }
1898
+ default:
1899
+ // Something else went wrong, or the server is down.
1900
+ // In this case, don't override existing flags
1901
+ return;
1803
1902
  }
1804
- this.featureFlags = responseJson.flags || [];
1805
- this.featureFlagsByKey = this.featureFlags.reduce((acc, curr) => (acc[curr.key] = curr, acc), {});
1806
- this.groupTypeMapping = responseJson.group_type_mapping || {};
1807
- this.cohorts = responseJson.cohorts || [];
1808
- this.loadedSuccessfullyOnce = true;
1809
- this.lastRequestWasAuthenticationError = false;
1810
- this.authenticationErrorCount = 0;
1811
1903
  } catch (err) {
1812
- // if an error that is not an instance of ClientError is thrown
1813
- // we silently ignore the error when reloading feature flags
1814
1904
  if (err instanceof ClientError) {
1815
1905
  this.onError?.(err);
1816
1906
  }
@@ -3058,16 +3148,20 @@ class PostHog extends PostHogCoreStateless {
3058
3148
  super.debug(enabled);
3059
3149
  this.featureFlagsPoller?.debug(enabled);
3060
3150
  }
3061
- capture({
3062
- distinctId,
3063
- event,
3064
- properties,
3065
- groups,
3066
- sendFeatureFlags,
3067
- timestamp,
3068
- disableGeoip,
3069
- uuid
3070
- }) {
3151
+ capture(props) {
3152
+ if (typeof props === 'string') {
3153
+ this.logMsgIfDebug(() => console.warn('Called capture() with a string as the first argument when an object was expected.'));
3154
+ }
3155
+ const {
3156
+ distinctId,
3157
+ event,
3158
+ properties,
3159
+ groups,
3160
+ sendFeatureFlags,
3161
+ timestamp,
3162
+ disableGeoip,
3163
+ uuid
3164
+ } = props;
3071
3165
  const _capture = props => {
3072
3166
  super.captureStateless(distinctId, event, props, {
3073
3167
  timestamp,
@@ -3397,18 +3491,26 @@ function createEventProcessor(_posthog, {
3397
3491
  const personUrl = new URL(`/project/${_posthog.apiKey}/person/${userId}`, uiHost).toString();
3398
3492
  event.tags['PostHog Person URL'] = personUrl;
3399
3493
  const exceptions = event.exception?.values || [];
3400
- exceptions.map(exception => {
3401
- if (exception.stacktrace) {
3402
- exception.stacktrace.type = 'raw';
3403
- }
3404
- });
3494
+ const exceptionList = exceptions.map(exception => ({
3495
+ ...exception,
3496
+ stacktrace: exception.stacktrace ? {
3497
+ ...exception.stacktrace,
3498
+ type: 'raw',
3499
+ frames: (exception.stacktrace.frames || []).map(frame => {
3500
+ return {
3501
+ ...frame,
3502
+ platform: 'node:javascript'
3503
+ };
3504
+ })
3505
+ } : undefined
3506
+ }));
3405
3507
  const properties = {
3406
3508
  // PostHog Exception Properties,
3407
3509
  $exception_message: exceptions[0]?.value || event.message,
3408
3510
  $exception_type: exceptions[0]?.type,
3409
3511
  $exception_personURL: personUrl,
3410
3512
  $exception_level: event.level,
3411
- $exception_list: exceptions,
3513
+ $exception_list: exceptionList,
3412
3514
  // Sentry Exception Properties
3413
3515
  $sentry_event_id: event.event_id,
3414
3516
  $sentry_exception: event.exception,