posthog-node 4.11.7 → 4.13.0
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/index.cjs.js +171 -9
- package/lib/index.cjs.js.map +1 -1
- package/lib/index.d.ts +17 -0
- package/lib/index.esm.js +171 -9
- package/lib/index.esm.js.map +1 -1
- package/lib/posthog-core/src/utils.d.ts +3 -0
- package/lib/posthog-node/src/feature-flags.d.ts +7 -0
- package/lib/posthog-node/src/posthog-node.d.ts +6 -0
- package/lib/posthog-node/src/types.d.ts +11 -0
- package/package.json +2 -2
- package/src/crypto-helpers.ts +1 -1
- package/src/extensions/error-tracking/error-conversion.ts +23 -10
- package/src/feature-flags.ts +13 -2
- package/src/posthog-node.ts +34 -0
- package/src/types.ts +13 -0
- package/test/extensions/error-conversion.spec.ts +44 -0
- package/test/feature-flags.decide.spec.ts +5 -5
- package/test/feature-flags.spec.ts +224 -6
- package/test/posthog-node.spec.ts +16 -16
- package/test/test-utils.ts +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
# 4.13.0 - 2025-04-21
|
|
2
|
+
|
|
3
|
+
1. feat: Add method to wait for local evaluation feature flag definitions to be loaded
|
|
4
|
+
|
|
5
|
+
# 4.12.0 – 2025-04-17
|
|
6
|
+
|
|
7
|
+
1. chore: roll out new feature flag evaluation backend to majority of customers
|
|
8
|
+
|
|
1
9
|
# 4.11.7 - 2025-04-16
|
|
2
10
|
|
|
3
11
|
1. fix: do not reference `node:` prefix as it is not supported by Next.js edge runtime
|
package/lib/index.cjs.js
CHANGED
|
@@ -22,7 +22,7 @@ function _interopNamespace(e) {
|
|
|
22
22
|
return Object.freeze(n);
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
var version = "4.
|
|
25
|
+
var version = "4.13.0";
|
|
26
26
|
|
|
27
27
|
var PostHogPersistedProperty;
|
|
28
28
|
(function (PostHogPersistedProperty) {
|
|
@@ -194,6 +194,90 @@ const parsePayload = (response) => {
|
|
|
194
194
|
}
|
|
195
195
|
};
|
|
196
196
|
|
|
197
|
+
// Rollout constants
|
|
198
|
+
const NEW_FLAGS_ROLLOUT_PERCENTAGE = 1;
|
|
199
|
+
// The fnv1a hashes of the tokens that are explicitly excluded from the rollout
|
|
200
|
+
// see https://github.com/PostHog/posthog-js-lite/blob/main/posthog-core/src/utils.ts#L84
|
|
201
|
+
// are hashed API tokens from our top 10 for each category supported by this SDK.
|
|
202
|
+
const NEW_FLAGS_EXCLUDED_HASHES = new Set([
|
|
203
|
+
// Node
|
|
204
|
+
'61be3dd8',
|
|
205
|
+
'96f6df5f',
|
|
206
|
+
'8cfdba9b',
|
|
207
|
+
'bf027177',
|
|
208
|
+
'e59430a8',
|
|
209
|
+
'7fa5500b',
|
|
210
|
+
'569798e9',
|
|
211
|
+
'04809ff7',
|
|
212
|
+
'0ebc61a5',
|
|
213
|
+
'32de7f98',
|
|
214
|
+
'3beeb69a',
|
|
215
|
+
'12d34ad9',
|
|
216
|
+
'733853ec',
|
|
217
|
+
'0645bb64',
|
|
218
|
+
'5dcbee21',
|
|
219
|
+
'b1f95fa3',
|
|
220
|
+
'2189e408',
|
|
221
|
+
'82b460c2',
|
|
222
|
+
'3a8cc979',
|
|
223
|
+
'29ef8843',
|
|
224
|
+
'2cdbf767',
|
|
225
|
+
'38084b54',
|
|
226
|
+
// React Native
|
|
227
|
+
'50f9f8de',
|
|
228
|
+
'41d0df91',
|
|
229
|
+
'5c236689',
|
|
230
|
+
'c11aedd3',
|
|
231
|
+
'ada46672',
|
|
232
|
+
'f4331ee1',
|
|
233
|
+
'42fed62a',
|
|
234
|
+
'c957462c',
|
|
235
|
+
'd62f705a',
|
|
236
|
+
// Web (lots of teams per org, hence lots of API tokens)
|
|
237
|
+
'e0162666',
|
|
238
|
+
'01b3e5cf',
|
|
239
|
+
'441cef7f',
|
|
240
|
+
'bb9cafee',
|
|
241
|
+
'8f348eb0',
|
|
242
|
+
'b2553f3a',
|
|
243
|
+
'97469d7d',
|
|
244
|
+
'39f21a76',
|
|
245
|
+
'03706dcc',
|
|
246
|
+
'27d50569',
|
|
247
|
+
'307584a7',
|
|
248
|
+
'6433e92e',
|
|
249
|
+
'150c7fbb',
|
|
250
|
+
'49f57f22',
|
|
251
|
+
'3772f65b',
|
|
252
|
+
'01eb8256',
|
|
253
|
+
'3c9e9234',
|
|
254
|
+
'f853c7f7',
|
|
255
|
+
'c0ac4b67',
|
|
256
|
+
'cd609d40',
|
|
257
|
+
'10ca9b1a',
|
|
258
|
+
'8a87f11b',
|
|
259
|
+
'8e8e5216',
|
|
260
|
+
'1f6b63b3',
|
|
261
|
+
'db7943dd',
|
|
262
|
+
'79b7164c',
|
|
263
|
+
'07f78e33',
|
|
264
|
+
'2d21b6fd',
|
|
265
|
+
'952db5ee',
|
|
266
|
+
'a7d3b43f',
|
|
267
|
+
'1924dd9c',
|
|
268
|
+
'84e1b8f6',
|
|
269
|
+
'dff631b6',
|
|
270
|
+
'c5aa8a79',
|
|
271
|
+
'fa133a95',
|
|
272
|
+
'498a4508',
|
|
273
|
+
'24748755',
|
|
274
|
+
'98f3d658',
|
|
275
|
+
'21bbda67',
|
|
276
|
+
'7dbfed69',
|
|
277
|
+
'be3ec24c',
|
|
278
|
+
'fc80b8e2',
|
|
279
|
+
'75cc0998',
|
|
280
|
+
]);
|
|
197
281
|
function assert(truthyValue, message) {
|
|
198
282
|
if (!truthyValue || typeof truthyValue !== 'string' || isEmpty(truthyValue)) {
|
|
199
283
|
throw new Error(message);
|
|
@@ -244,6 +328,30 @@ function safeSetTimeout(fn, timeout) {
|
|
|
244
328
|
}
|
|
245
329
|
function getFetch() {
|
|
246
330
|
return typeof fetch !== 'undefined' ? fetch : typeof global.fetch !== 'undefined' ? global.fetch : undefined;
|
|
331
|
+
}
|
|
332
|
+
// FNV-1a hash function
|
|
333
|
+
// https://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function
|
|
334
|
+
// I know, I know, I'm rolling my own hash function, but I didn't want to take on
|
|
335
|
+
// a crypto dependency and this is just temporary anyway
|
|
336
|
+
function fnv1a(str) {
|
|
337
|
+
let hash = 0x811c9dc5; // FNV offset basis
|
|
338
|
+
for (let i = 0; i < str.length; i++) {
|
|
339
|
+
hash ^= str.charCodeAt(i);
|
|
340
|
+
hash += (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24);
|
|
341
|
+
}
|
|
342
|
+
// Convert to hex string, padding to 8 chars
|
|
343
|
+
return (hash >>> 0).toString(16).padStart(8, '0');
|
|
344
|
+
}
|
|
345
|
+
function isTokenInRollout(token, percentage = 0, excludedHashes) {
|
|
346
|
+
const tokenHash = fnv1a(token);
|
|
347
|
+
// Check excluded hashes (we're explicitly including these tokens from the rollout)
|
|
348
|
+
if (excludedHashes?.has(tokenHash)) {
|
|
349
|
+
return false;
|
|
350
|
+
}
|
|
351
|
+
// Convert hash to int and divide by max value to get number between 0-1
|
|
352
|
+
const hashInt = parseInt(tokenHash, 16);
|
|
353
|
+
const hashFloat = hashInt / 0xffffffff;
|
|
354
|
+
return hashFloat < percentage;
|
|
247
355
|
}
|
|
248
356
|
|
|
249
357
|
// Copyright (c) 2013 Pieroxy <pieroxy@pieroxy.net>
|
|
@@ -1323,7 +1431,11 @@ class PostHogCoreStateless {
|
|
|
1323
1431
|
***/
|
|
1324
1432
|
async getDecide(distinctId, groups = {}, personProperties = {}, groupProperties = {}, extraPayload = {}) {
|
|
1325
1433
|
await this._initPromise;
|
|
1326
|
-
|
|
1434
|
+
// Check if the API token is in the new flags rollout
|
|
1435
|
+
// This is a temporary measure to ensure that we can still use the old flags API
|
|
1436
|
+
// while we migrate to the new flags API
|
|
1437
|
+
const useFlags = isTokenInRollout(this.apiKey, NEW_FLAGS_ROLLOUT_PERCENTAGE, NEW_FLAGS_EXCLUDED_HASHES);
|
|
1438
|
+
const url = useFlags ? `${this.host}/flags/?v=2` : `${this.host}/decide/?v=4`;
|
|
1327
1439
|
const fetchOptions = {
|
|
1328
1440
|
method: 'POST',
|
|
1329
1441
|
headers: { ...this.getCustomHeaders(), 'Content-Type': 'application/json' },
|
|
@@ -1876,6 +1988,7 @@ class FeatureFlagsPoller {
|
|
|
1876
1988
|
this.fetch = options.fetch || fetch$1;
|
|
1877
1989
|
this.onError = options.onError;
|
|
1878
1990
|
this.customHeaders = customHeaders;
|
|
1991
|
+
this.onLoad = options.onLoad;
|
|
1879
1992
|
void this.loadFeatureFlags();
|
|
1880
1993
|
}
|
|
1881
1994
|
debug(enabled = true) {
|
|
@@ -2092,6 +2205,13 @@ class FeatureFlagsPoller {
|
|
|
2092
2205
|
await this._loadFeatureFlags();
|
|
2093
2206
|
}
|
|
2094
2207
|
}
|
|
2208
|
+
/**
|
|
2209
|
+
* Returns true if the feature flags poller has loaded successfully at least once and has more than 0 feature flags.
|
|
2210
|
+
* This is useful to check if local evaluation is ready before calling getFeatureFlag.
|
|
2211
|
+
*/
|
|
2212
|
+
isLocalEvaluationReady() {
|
|
2213
|
+
return (this.loadedSuccessfullyOnce ?? false) && (this.featureFlags?.length ?? 0) > 0;
|
|
2214
|
+
}
|
|
2095
2215
|
/**
|
|
2096
2216
|
* If a client is misconfigured with an invalid or improper API key, the polling interval is doubled each time
|
|
2097
2217
|
* until a successful request is made, up to a maximum of 60 seconds.
|
|
@@ -2167,6 +2287,7 @@ class FeatureFlagsPoller {
|
|
|
2167
2287
|
this.loadedSuccessfullyOnce = true;
|
|
2168
2288
|
this.shouldBeginExponentialBackoff = false;
|
|
2169
2289
|
this.backOffCount = 0;
|
|
2290
|
+
this.onLoad?.(this.featureFlags.length);
|
|
2170
2291
|
break;
|
|
2171
2292
|
}
|
|
2172
2293
|
default:
|
|
@@ -2957,16 +3078,28 @@ async function propertiesFromUnknownInput(stackParser, input, hint) {
|
|
|
2957
3078
|
handled: true,
|
|
2958
3079
|
type: 'generic'
|
|
2959
3080
|
};
|
|
2960
|
-
const
|
|
2961
|
-
const
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
3081
|
+
const errorList = getErrorList(mechanism, input, hint);
|
|
3082
|
+
const exceptionList = await Promise.all(errorList.map(async error => {
|
|
3083
|
+
const exception = await exceptionFromError(stackParser, error);
|
|
3084
|
+
exception.value = exception.value || '';
|
|
3085
|
+
exception.type = exception.type || 'Error';
|
|
3086
|
+
exception.mechanism = mechanism;
|
|
3087
|
+
return exception;
|
|
3088
|
+
}));
|
|
2965
3089
|
const properties = {
|
|
2966
|
-
$exception_list:
|
|
3090
|
+
$exception_list: exceptionList
|
|
2967
3091
|
};
|
|
2968
3092
|
return properties;
|
|
2969
3093
|
}
|
|
3094
|
+
// Flatten error causes into a list of errors
|
|
3095
|
+
// See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/cause
|
|
3096
|
+
function getErrorList(mechanism, input, hint) {
|
|
3097
|
+
const error = getError(mechanism, input, hint);
|
|
3098
|
+
if (error.cause) {
|
|
3099
|
+
return [error, ...getErrorList(mechanism, error.cause, hint)];
|
|
3100
|
+
}
|
|
3101
|
+
return [error];
|
|
3102
|
+
}
|
|
2970
3103
|
function getError(mechanism, exception, hint) {
|
|
2971
3104
|
if (isError(exception)) {
|
|
2972
3105
|
return exception;
|
|
@@ -2993,7 +3126,7 @@ function getErrorPropertyFromObject(obj) {
|
|
|
2993
3126
|
for (const prop in obj) {
|
|
2994
3127
|
if (Object.prototype.hasOwnProperty.call(obj, prop)) {
|
|
2995
3128
|
const value = obj[prop];
|
|
2996
|
-
if (value
|
|
3129
|
+
if (isError(value)) {
|
|
2997
3130
|
return value;
|
|
2998
3131
|
}
|
|
2999
3132
|
}
|
|
@@ -3420,6 +3553,9 @@ class PostHog extends PostHogCoreStateless {
|
|
|
3420
3553
|
onError: err => {
|
|
3421
3554
|
this._events.emit('error', err);
|
|
3422
3555
|
},
|
|
3556
|
+
onLoad: count => {
|
|
3557
|
+
this._events.emit('localEvaluationFlagsLoaded', count);
|
|
3558
|
+
},
|
|
3423
3559
|
customHeaders: this.getCustomHeaders()
|
|
3424
3560
|
});
|
|
3425
3561
|
}
|
|
@@ -3552,6 +3688,28 @@ class PostHog extends PostHogCoreStateless {
|
|
|
3552
3688
|
disableGeoip: data.disableGeoip
|
|
3553
3689
|
});
|
|
3554
3690
|
}
|
|
3691
|
+
isLocalEvaluationReady() {
|
|
3692
|
+
return this.featureFlagsPoller?.isLocalEvaluationReady() ?? false;
|
|
3693
|
+
}
|
|
3694
|
+
async waitForLocalEvaluationReady(timeoutMs = THIRTY_SECONDS) {
|
|
3695
|
+
if (this.isLocalEvaluationReady()) {
|
|
3696
|
+
return true;
|
|
3697
|
+
}
|
|
3698
|
+
if (this.featureFlagsPoller === undefined) {
|
|
3699
|
+
return false;
|
|
3700
|
+
}
|
|
3701
|
+
return new Promise(resolve => {
|
|
3702
|
+
const timeout = setTimeout(() => {
|
|
3703
|
+
cleanup();
|
|
3704
|
+
resolve(false);
|
|
3705
|
+
}, timeoutMs);
|
|
3706
|
+
const cleanup = this._events.on('localEvaluationFlagsLoaded', count => {
|
|
3707
|
+
clearTimeout(timeout);
|
|
3708
|
+
cleanup();
|
|
3709
|
+
resolve(count > 0);
|
|
3710
|
+
});
|
|
3711
|
+
});
|
|
3712
|
+
}
|
|
3555
3713
|
async getFeatureFlag(key, distinctId, options) {
|
|
3556
3714
|
const {
|
|
3557
3715
|
groups,
|
|
@@ -3729,6 +3887,10 @@ class PostHog extends PostHogCoreStateless {
|
|
|
3729
3887
|
disableGeoip
|
|
3730
3888
|
}, distinctId);
|
|
3731
3889
|
}
|
|
3890
|
+
/**
|
|
3891
|
+
* Reloads the feature flag definitions from the server for local evaluation.
|
|
3892
|
+
* This is useful to call if you want to ensure that the feature flags are up to date before calling getFeatureFlag.
|
|
3893
|
+
*/
|
|
3732
3894
|
async reloadFeatureFlags() {
|
|
3733
3895
|
await this.featureFlagsPoller?.loadFeatureFlags(true);
|
|
3734
3896
|
}
|