posthog-node 5.2.1 → 5.4.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,5 +1,13 @@
1
1
  # Next
2
2
 
3
+ # 5.4.0 – 2025-09-07
4
+
5
+ feat: respect local evaluation preferences with `sendFeatureFlags`; add property overrides from the event to those local computations so that the locally evaluated flags can be more accuratee. NB: this change chagnes the default behavior of `capture` and `captureImmediately` – we will now only send feature flag data along with those events if `sendFeatureFlags` is explicitly specified, instead of optimistically sending along locally evaluated flags by default.
6
+
7
+ # 5.3.1 - 2025-07-07
8
+
9
+ 1. feat: decouple feature flag local evaluation from personal API keys; support decrypting remote config payloads without relying on the feature flags poller
10
+
3
11
  # 5.2.1 - 2025-07-07
4
12
 
5
13
  1. feat: add captureExceptionImmediate method on posthog client
@@ -937,7 +937,7 @@ function setupExpressErrorHandler(_posthog, app) {
937
937
  });
938
938
  }
939
939
 
940
- var version = "5.2.1";
940
+ var version = "5.4.0";
941
941
 
942
942
  var PostHogPersistedProperty;
943
943
  (function (PostHogPersistedProperty) {
@@ -2231,7 +2231,9 @@ class FeatureFlagsPoller {
2231
2231
  payloads[flag.key] = matchPayload;
2232
2232
  }
2233
2233
  } catch (e) {
2234
- if (e instanceof InconclusiveMatchError) ; else if (e instanceof Error) {
2234
+ if (e instanceof InconclusiveMatchError) {
2235
+ this.onError?.(new Error(`Unable to compute flag locally: ${flag.key} - ${e.message}`));
2236
+ } else if (e instanceof Error) {
2235
2237
  this.onError?.(new Error(`Error computing flag locally: ${flag.key}: ${e}`));
2236
2238
  }
2237
2239
  fallbackToFlags = true;
@@ -2501,23 +2503,6 @@ class FeatureFlagsPoller {
2501
2503
  stopPoller() {
2502
2504
  clearTimeout(this.poller);
2503
2505
  }
2504
- _requestRemoteConfigPayload(flagKey) {
2505
- const url = `${this.host}/api/projects/@current/feature_flags/${flagKey}/remote_config/`;
2506
- const options = this.getPersonalApiKeyRequestOptions();
2507
- let abortTimeout = null;
2508
- if (this.timeout && typeof this.timeout === 'number') {
2509
- const controller = new AbortController();
2510
- abortTimeout = safeSetTimeout(() => {
2511
- controller.abort();
2512
- }, this.timeout);
2513
- options.signal = controller.signal;
2514
- }
2515
- try {
2516
- return this.fetch(url, options);
2517
- } finally {
2518
- clearTimeout(abortTimeout);
2519
- }
2520
- }
2521
2506
  }
2522
2507
  // # This function takes a distinct_id and a feature flag key and returns a float between 0 and 1.
2523
2508
  // # Given the same distinct_id and key, it'll always return the same float. These floats are
@@ -2802,21 +2787,25 @@ class PostHogBackendClient extends PostHogCoreStateless {
2802
2787
  if (options.personalApiKey.includes('phc_')) {
2803
2788
  throw new Error('Your Personal API key is invalid. These keys are prefixed with "phx_" and can be created in PostHog project settings.');
2804
2789
  }
2805
- this.featureFlagsPoller = new FeatureFlagsPoller({
2806
- pollingInterval: this.options.featureFlagsPollingInterval,
2807
- personalApiKey: options.personalApiKey,
2808
- projectApiKey: apiKey,
2809
- timeout: options.requestTimeout ?? 10000,
2810
- host: this.host,
2811
- fetch: options.fetch,
2812
- onError: err => {
2813
- this._events.emit('error', err);
2814
- },
2815
- onLoad: count => {
2816
- this._events.emit('localEvaluationFlagsLoaded', count);
2817
- },
2818
- customHeaders: this.getCustomHeaders()
2819
- });
2790
+ // Only start the poller if local evaluation is enabled (defaults to true for backward compatibility)
2791
+ const shouldEnableLocalEvaluation = options.enableLocalEvaluation !== false;
2792
+ if (shouldEnableLocalEvaluation) {
2793
+ this.featureFlagsPoller = new FeatureFlagsPoller({
2794
+ pollingInterval: this.options.featureFlagsPollingInterval,
2795
+ personalApiKey: options.personalApiKey,
2796
+ projectApiKey: apiKey,
2797
+ timeout: options.requestTimeout ?? 10000,
2798
+ host: this.host,
2799
+ fetch: options.fetch,
2800
+ onError: err => {
2801
+ this._events.emit('error', err);
2802
+ },
2803
+ onLoad: count => {
2804
+ this._events.emit('localEvaluationFlagsLoaded', count);
2805
+ },
2806
+ customHeaders: this.getCustomHeaders()
2807
+ });
2808
+ }
2820
2809
  }
2821
2810
  this.errorTracking = new ErrorTracking(this, options);
2822
2811
  this.distinctIdHasSentFlagCalls = {};
@@ -2868,32 +2857,16 @@ class PostHogBackendClient extends PostHogCoreStateless {
2868
2857
  uuid
2869
2858
  });
2870
2859
  };
2871
- const _getFlags = async (distinctId, groups, disableGeoip) => {
2872
- return (await super.getFeatureFlagsStateless(distinctId, groups, undefined, undefined, disableGeoip)).flags;
2873
- };
2874
2860
  // :TRICKY: If we flush, or need to shut down, to not lose events we want this promise to resolve before we flush
2875
2861
  const capturePromise = Promise.resolve().then(async () => {
2876
2862
  if (sendFeatureFlags) {
2877
- // If we are sending feature flags, we need to make sure we have the latest flags
2878
- // return await super.getFeatureFlagsStateless(distinctId, groups, undefined, undefined, disableGeoip)
2879
- return await _getFlags(distinctId, groups, disableGeoip);
2863
+ // If we are sending feature flags, we evaluate them locally if the user prefers it, otherwise we fall back to remote evaluation
2864
+ return await this.getFeatureFlagsForEvent(distinctId, groups, properties, disableGeoip);
2880
2865
  }
2881
2866
  if (event === '$feature_flag_called') {
2882
2867
  // If we're capturing a $feature_flag_called event, we don't want to enrich the event with cached flags that may be out of date.
2883
2868
  return {};
2884
2869
  }
2885
- if ((this.featureFlagsPoller?.featureFlags?.length || 0) > 0) {
2886
- // Otherwise we may as well check for the flags locally and include them if they are already loaded
2887
- const groupsWithStringValues = {};
2888
- for (const [key, value] of Object.entries(groups || {})) {
2889
- groupsWithStringValues[key] = String(value);
2890
- }
2891
- return await this.getAllFlags(distinctId, {
2892
- groups: groupsWithStringValues,
2893
- disableGeoip,
2894
- onlyEvaluateLocally: true
2895
- });
2896
- }
2897
2870
  return {};
2898
2871
  }).then(flags => {
2899
2872
  // Derive the relevant flag properties to add
@@ -2942,31 +2915,15 @@ class PostHogBackendClient extends PostHogCoreStateless {
2942
2915
  uuid
2943
2916
  });
2944
2917
  };
2945
- const _getFlags = async (distinctId, groups, disableGeoip) => {
2946
- return (await super.getFeatureFlagsStateless(distinctId, groups, undefined, undefined, disableGeoip)).flags;
2947
- };
2948
2918
  const capturePromise = Promise.resolve().then(async () => {
2949
2919
  if (sendFeatureFlags) {
2950
- // If we are sending feature flags, we need to make sure we have the latest flags
2951
- // return await super.getFeatureFlagsStateless(distinctId, groups, undefined, undefined, disableGeoip)
2952
- return await _getFlags(distinctId, groups, disableGeoip);
2920
+ // If we are sending feature flags, we evaluate them locally if the user prefers it, otherwise we fall back to remote evaluation
2921
+ return await this.getFeatureFlagsForEvent(distinctId, groups, properties, disableGeoip);
2953
2922
  }
2954
2923
  if (event === '$feature_flag_called') {
2955
2924
  // If we're capturing a $feature_flag_called event, we don't want to enrich the event with cached flags that may be out of date.
2956
2925
  return {};
2957
2926
  }
2958
- if ((this.featureFlagsPoller?.featureFlags?.length || 0) > 0) {
2959
- // Otherwise we may as well check for the flags locally and include them if they are already loaded
2960
- const groupsWithStringValues = {};
2961
- for (const [key, value] of Object.entries(groups || {})) {
2962
- groupsWithStringValues[key] = String(value);
2963
- }
2964
- return await this.getAllFlags(distinctId, {
2965
- groups: groupsWithStringValues,
2966
- disableGeoip,
2967
- onlyEvaluateLocally: true
2968
- });
2969
- }
2970
2927
  return {};
2971
2928
  }).then(flags => {
2972
2929
  // Derive the relevant flag properties to add
@@ -3172,7 +3129,10 @@ class PostHogBackendClient extends PostHogCoreStateless {
3172
3129
  return response;
3173
3130
  }
3174
3131
  async getRemoteConfigPayload(flagKey) {
3175
- const response = await this.featureFlagsPoller?._requestRemoteConfigPayload(flagKey);
3132
+ if (!this.options.personalApiKey) {
3133
+ throw new Error('Personal API key is required for remote config payload decryption');
3134
+ }
3135
+ const response = await this._requestRemoteConfigPayload(flagKey);
3176
3136
  if (!response) {
3177
3137
  return undefined;
3178
3138
  }
@@ -3266,6 +3226,88 @@ class PostHogBackendClient extends PostHogCoreStateless {
3266
3226
  this.featureFlagsPoller?.stopPoller();
3267
3227
  return super._shutdown(shutdownTimeoutMs);
3268
3228
  }
3229
+ async _requestRemoteConfigPayload(flagKey) {
3230
+ if (!this.options.personalApiKey) {
3231
+ return undefined;
3232
+ }
3233
+ const url = `${this.host}/api/projects/@current/feature_flags/${flagKey}/remote_config/`;
3234
+ const options = {
3235
+ method: 'GET',
3236
+ headers: {
3237
+ ...this.getCustomHeaders(),
3238
+ 'Content-Type': 'application/json',
3239
+ Authorization: `Bearer ${this.options.personalApiKey}`
3240
+ }
3241
+ };
3242
+ let abortTimeout = null;
3243
+ if (this.options.requestTimeout && typeof this.options.requestTimeout === 'number') {
3244
+ const controller = new AbortController();
3245
+ abortTimeout = safeSetTimeout(() => {
3246
+ controller.abort();
3247
+ }, this.options.requestTimeout);
3248
+ options.signal = controller.signal;
3249
+ }
3250
+ try {
3251
+ return await this.fetch(url, options);
3252
+ } catch (error) {
3253
+ this._events.emit('error', error);
3254
+ return undefined;
3255
+ } finally {
3256
+ if (abortTimeout) {
3257
+ clearTimeout(abortTimeout);
3258
+ }
3259
+ }
3260
+ }
3261
+ extractPropertiesFromEvent(eventProperties, groups) {
3262
+ if (!eventProperties) {
3263
+ return {
3264
+ personProperties: {},
3265
+ groupProperties: {}
3266
+ };
3267
+ }
3268
+ const personProperties = {};
3269
+ const groupProperties = {};
3270
+ for (const [key, value] of Object.entries(eventProperties)) {
3271
+ // If the value is a plain object and the key exists in groups, treat it as group properties
3272
+ if (isPlainObject(value) && groups && key in groups) {
3273
+ const groupProps = {};
3274
+ for (const [groupKey, groupValue] of Object.entries(value)) {
3275
+ groupProps[String(groupKey)] = String(groupValue);
3276
+ }
3277
+ groupProperties[String(key)] = groupProps;
3278
+ } else {
3279
+ // Otherwise treat as person property
3280
+ personProperties[String(key)] = String(value);
3281
+ }
3282
+ }
3283
+ return {
3284
+ personProperties,
3285
+ groupProperties
3286
+ };
3287
+ }
3288
+ async getFeatureFlagsForEvent(distinctId, groups, eventProperties, disableGeoip) {
3289
+ // Extract person and group properties from the event properties
3290
+ const {
3291
+ personProperties: cleanPersonProperties,
3292
+ groupProperties: cleanGroupProperties
3293
+ } = this.extractPropertiesFromEvent(eventProperties, groups);
3294
+ // Prefer local evaluation if available
3295
+ if ((this.featureFlagsPoller?.featureFlags?.length || 0) > 0) {
3296
+ const groupsWithStringValues = {};
3297
+ for (const [key, value] of Object.entries(groups || {})) {
3298
+ groupsWithStringValues[key] = String(value);
3299
+ }
3300
+ return await this.getAllFlags(distinctId, {
3301
+ groups: groupsWithStringValues,
3302
+ personProperties: cleanPersonProperties,
3303
+ groupProperties: cleanGroupProperties,
3304
+ disableGeoip,
3305
+ onlyEvaluateLocally: true
3306
+ });
3307
+ }
3308
+ // Fall back to remote evaluation if local evaluation is not available/is not being used
3309
+ return (await super.getFeatureFlagsStateless(distinctId, groups, cleanPersonProperties, cleanGroupProperties, disableGeoip)).flags;
3310
+ }
3269
3311
  addLocalPersonAndGroupProperties(distinctId, groups, personProperties, groupProperties) {
3270
3312
  const allPersonProperties = {
3271
3313
  distinct_id: distinctId,