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 +8 -0
- package/lib/edge/index.cjs +52 -147
- package/lib/edge/index.cjs.map +1 -1
- package/lib/edge/index.mjs +52 -147
- package/lib/edge/index.mjs.map +1 -1
- package/lib/index.d.ts +14 -14
- package/lib/node/index.cjs +52 -147
- package/lib/node/index.cjs.map +1 -1
- package/lib/node/index.mjs +52 -147
- package/lib/node/index.mjs.map +1 -1
- package/package.json +1 -1
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
|
package/lib/edge/index.cjs
CHANGED
|
@@ -935,7 +935,7 @@ function setupExpressErrorHandler(_posthog, app) {
|
|
|
935
935
|
});
|
|
936
936
|
}
|
|
937
937
|
|
|
938
|
-
var version = "5.
|
|
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
|
|
1035
|
-
if ('flags' in
|
|
1036
|
-
// Convert
|
|
1037
|
-
const featureFlags = getFlagValuesFromFlags(
|
|
1038
|
-
const featureFlagPayloads = getPayloadsFromFlags(
|
|
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
|
-
...
|
|
1040
|
+
...flagsResponse,
|
|
1041
1041
|
featureFlags,
|
|
1042
1042
|
featureFlagPayloads,
|
|
1043
1043
|
};
|
|
1044
1044
|
}
|
|
1045
1045
|
else {
|
|
1046
|
-
// Convert
|
|
1047
|
-
const featureFlags =
|
|
1048
|
-
const featureFlagPayloads = Object.fromEntries(Object.entries(
|
|
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
|
-
...
|
|
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
|
|
1487
|
+
async getFlags(distinctId, groups = {}, personProperties = {}, groupProperties = {}, extraPayload = {}) {
|
|
1596
1488
|
await this._initPromise;
|
|
1597
|
-
|
|
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', '
|
|
1615
|
-
// Don't retry /
|
|
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) =>
|
|
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
|
|
1648
|
-
if (
|
|
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 =
|
|
1539
|
+
const featureFlags = flagsResponse.flags;
|
|
1652
1540
|
const flagDetail = featureFlags[key];
|
|
1653
1541
|
return {
|
|
1654
1542
|
response: flagDetail,
|
|
1655
|
-
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
|
|
1706
|
-
if (
|
|
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
|
|
1711
|
-
if (
|
|
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 (
|
|
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:
|
|
1609
|
+
requestId: flagsResponse?.requestId,
|
|
1722
1610
|
};
|
|
1723
1611
|
}
|
|
1724
|
-
return
|
|
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
|
|
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
|
-
|
|
2229
|
+
fallbackToFlags = true;
|
|
2342
2230
|
}
|
|
2343
2231
|
}));
|
|
2344
2232
|
return {
|
|
2345
2233
|
response,
|
|
2346
2234
|
payloads,
|
|
2347
|
-
|
|
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
|
-
|
|
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
|
|
3217
|
+
let fallbackToFlags = true;
|
|
3313
3218
|
if (localEvaluationResult) {
|
|
3314
3219
|
featureFlags = localEvaluationResult.response;
|
|
3315
3220
|
featureFlagPayloads = localEvaluationResult.payloads;
|
|
3316
|
-
|
|
3221
|
+
fallbackToFlags = localEvaluationResult.fallbackToFlags;
|
|
3317
3222
|
}
|
|
3318
|
-
if (
|
|
3223
|
+
if (fallbackToFlags && !onlyEvaluateLocally) {
|
|
3319
3224
|
const remoteEvaluationResult = await super.getFeatureFlagsAndPayloadsStateless(distinctId, groups, personProperties, groupProperties, disableGeoip);
|
|
3320
3225
|
featureFlags = {
|
|
3321
3226
|
...featureFlags,
|