posthog-node 5.3.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,9 @@
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
+
3
7
  # 5.3.1 - 2025-07-07
4
8
 
5
9
  1. feat: decouple feature flag local evaluation from personal API keys; support decrypting remote config payloads without relying on the feature flags poller
@@ -937,7 +937,7 @@ function setupExpressErrorHandler(_posthog, app) {
937
937
  });
938
938
  }
939
939
 
940
- var version = "5.3.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;
@@ -2855,32 +2857,16 @@ class PostHogBackendClient extends PostHogCoreStateless {
2855
2857
  uuid
2856
2858
  });
2857
2859
  };
2858
- const _getFlags = async (distinctId, groups, disableGeoip) => {
2859
- return (await super.getFeatureFlagsStateless(distinctId, groups, undefined, undefined, disableGeoip)).flags;
2860
- };
2861
2860
  // :TRICKY: If we flush, or need to shut down, to not lose events we want this promise to resolve before we flush
2862
2861
  const capturePromise = Promise.resolve().then(async () => {
2863
2862
  if (sendFeatureFlags) {
2864
- // If we are sending feature flags, we need to make sure we have the latest flags
2865
- // return await super.getFeatureFlagsStateless(distinctId, groups, undefined, undefined, disableGeoip)
2866
- 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);
2867
2865
  }
2868
2866
  if (event === '$feature_flag_called') {
2869
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.
2870
2868
  return {};
2871
2869
  }
2872
- if ((this.featureFlagsPoller?.featureFlags?.length || 0) > 0) {
2873
- // Otherwise we may as well check for the flags locally and include them if they are already loaded
2874
- const groupsWithStringValues = {};
2875
- for (const [key, value] of Object.entries(groups || {})) {
2876
- groupsWithStringValues[key] = String(value);
2877
- }
2878
- return await this.getAllFlags(distinctId, {
2879
- groups: groupsWithStringValues,
2880
- disableGeoip,
2881
- onlyEvaluateLocally: true
2882
- });
2883
- }
2884
2870
  return {};
2885
2871
  }).then(flags => {
2886
2872
  // Derive the relevant flag properties to add
@@ -2929,31 +2915,15 @@ class PostHogBackendClient extends PostHogCoreStateless {
2929
2915
  uuid
2930
2916
  });
2931
2917
  };
2932
- const _getFlags = async (distinctId, groups, disableGeoip) => {
2933
- return (await super.getFeatureFlagsStateless(distinctId, groups, undefined, undefined, disableGeoip)).flags;
2934
- };
2935
2918
  const capturePromise = Promise.resolve().then(async () => {
2936
2919
  if (sendFeatureFlags) {
2937
- // If we are sending feature flags, we need to make sure we have the latest flags
2938
- // return await super.getFeatureFlagsStateless(distinctId, groups, undefined, undefined, disableGeoip)
2939
- 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);
2940
2922
  }
2941
2923
  if (event === '$feature_flag_called') {
2942
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.
2943
2925
  return {};
2944
2926
  }
2945
- if ((this.featureFlagsPoller?.featureFlags?.length || 0) > 0) {
2946
- // Otherwise we may as well check for the flags locally and include them if they are already loaded
2947
- const groupsWithStringValues = {};
2948
- for (const [key, value] of Object.entries(groups || {})) {
2949
- groupsWithStringValues[key] = String(value);
2950
- }
2951
- return await this.getAllFlags(distinctId, {
2952
- groups: groupsWithStringValues,
2953
- disableGeoip,
2954
- onlyEvaluateLocally: true
2955
- });
2956
- }
2957
2927
  return {};
2958
2928
  }).then(flags => {
2959
2929
  // Derive the relevant flag properties to add
@@ -3288,6 +3258,56 @@ class PostHogBackendClient extends PostHogCoreStateless {
3288
3258
  }
3289
3259
  }
3290
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
+ }
3291
3311
  addLocalPersonAndGroupProperties(distinctId, groups, personProperties, groupProperties) {
3292
3312
  const allPersonProperties = {
3293
3313
  distinct_id: distinctId,