posthog-node 4.10.1 → 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 +10 -4
- package/lib/index.cjs.js +148 -54
- package/lib/index.cjs.js.map +1 -1
- package/lib/index.d.ts +250 -13
- package/lib/index.esm.js +148 -54
- package/lib/index.esm.js.map +1 -1
- package/lib/posthog-core/src/index.d.ts +15 -1
- package/lib/posthog-core/src/types.d.ts +35 -12
- package/lib/posthog-node/src/feature-flags.d.ts +2 -2
- package/lib/posthog-node/src/posthog-node.d.ts +1 -1
- package/lib/posthog-node/test/test-utils.d.ts +2 -1
- package/package.json +1 -1
- package/src/feature-flags.ts +77 -50
- package/src/posthog-node.ts +8 -10
- package/test/posthog-node.spec.ts +31 -0
- package/test/test-utils.ts +3 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,18 +1,24 @@
|
|
|
1
1
|
# Next
|
|
2
2
|
|
|
3
|
-
# 4.10.
|
|
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
|
|
4
8
|
|
|
5
9
|
1. Fix: only set `platform` on PostHog exception frame properties
|
|
10
|
+
1. Fix: prevent fetch floods when rate-limited.
|
|
11
|
+
|
|
6
12
|
|
|
7
|
-
# 4.10.0 –
|
|
13
|
+
# 4.10.0 – 2025-03-06
|
|
8
14
|
|
|
9
15
|
1. Attach requestId to $feature_flag_called if present in /decide response
|
|
10
16
|
|
|
11
|
-
# 4.9.0 –
|
|
17
|
+
# 4.9.0 – 2025-03-04
|
|
12
18
|
|
|
13
19
|
1. Allow feature flags to be evaluated individually when local evaluation is not being used
|
|
14
20
|
|
|
15
|
-
# 4.8.1 –
|
|
21
|
+
# 4.8.1 – 2025-02-26
|
|
16
22
|
|
|
17
23
|
1. Supports gracefully handling quotaLimited responses from the PostHog API for feature flag evaluation
|
|
18
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.
|
|
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.
|
|
1524
|
-
this.
|
|
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.
|
|
1828
|
+
if (!this.shouldBeginExponentialBackoff) {
|
|
1763
1829
|
return this.pollingInterval;
|
|
1764
1830
|
}
|
|
1765
|
-
return Math.min(SIXTY_SECONDS, this.pollingInterval * 2 ** this.
|
|
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
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
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;
|
|
1793
|
-
return;
|
|
1794
|
-
}
|
|
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
|
|
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
|
|
1798
1844
|
return;
|
|
1799
1845
|
}
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
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
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
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,
|