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 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.11.7";
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
- const url = `${this.host}/decide/?v=4`;
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 error = getError(mechanism, input, hint);
2961
- const exception = await exceptionFromError(stackParser, error);
2962
- exception.value = exception.value || '';
2963
- exception.type = exception.type || 'Error';
2964
- exception.mechanism = mechanism;
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: [exception]
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 instanceof Error) {
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
  }