posthog-ruby 3.27.0 → 3.28.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9809cd9248724a9b3b032b1f822c7108da7a089330f37bacd1c86d7b614452e2
4
- data.tar.gz: ca0f1dfcaecfbbaa4cf426ca0ab9eda422dede368cf55e899bb2aeb6e4a5b0ab
3
+ metadata.gz: 561f4ebad1a6cd75904b800f540c0dbb9352cc081dc57bf5cc0a5dcb82925e89
4
+ data.tar.gz: 5e9ce62f5e48c5380ce71e454507bafa6825a8e7e6db6c05b8896b8c5a665c62
5
5
  SHA512:
6
- metadata.gz: 0c291a4b34b4d060bd0f85f0fe9622b5ee209b2f888dfff1b049108a63bc8bb8ee0f302218bffb20e71e31d04561b00dede786bb8dc57bced15d52578ca9a2dd
7
- data.tar.gz: 8acdfca32f6df2ee0d9330539715bbc1d29b23a14c7ca6d2a458f6f8857d50ea9318d1f2d4c27354967c91abf5cc0ba87519d3255e5be849c5b3e78fd24d232f
6
+ metadata.gz: 41f3fde35a6e73ffa54b09fff2d88990057a3fac677153b2e14e6547fb20dbacebcc7a687bae432a98c62cabad572b5dc64cc57fb0cfbab554d7c72581d84ab8
7
+ data.tar.gz: e596b42169af511879c3397bc6e42aa2410bee4aa4d519b6958ad87d0800da67e6b208670abd809f4bd34d52616db2fd9a1d1ae3b3b8ebdf4c867edfb5ad186f
@@ -85,9 +85,9 @@ module PostHog
85
85
 
86
86
  # @param opts [Hash] Client configuration.
87
87
  # @option opts [String, nil] :api_key Your project's API key. Missing or blank values disable the client.
88
- # @option opts [String, nil] :secret_key The credential used for local feature flag evaluation and remote
89
- # config. Accepts either a Personal API Key (`phx_...`) or a Project Secret API Key (`phs_...`). Required
90
- # for local feature flag evaluation.
88
+ # @option opts [String, nil] :secret_key The credential used to fetch local feature flag definitions and remote
89
+ # config. Accepts either a Personal API Key (`phx_...`) or a Project Secret API Key (`phs_...`). Not required
90
+ # for local evaluation when definitions are read from +:flag_definition_cache_provider+.
91
91
  # @option opts [String, nil] :personal_api_key
92
92
  # @deprecated Use +:secret_key+ instead. Retained as an alias; when both are supplied, +:secret_key+ wins.
93
93
  # @option opts [String] :host Fully qualified hostname of the PostHog server. Defaults to `https://us.i.posthog.com`.
@@ -180,6 +180,7 @@ module PostHog
180
180
  @feature_flags_poller = nil
181
181
  @secret_key = secret_key
182
182
  @personal_api_key = secret_key
183
+ @flag_definition_cache_provider = opts[:flag_definition_cache_provider]
183
184
 
184
185
  if @disabled && !opts[:silence_disabled_client_error]
185
186
  logger.error('api_key is missing or empty after trimming whitespace; check your project API key')
@@ -214,7 +215,7 @@ module PostHog
214
215
  opts[:host],
215
216
  opts[:feature_flag_request_timeout_seconds] || Defaults::FeatureFlags::FLAG_REQUEST_TIMEOUT_SECONDS,
216
217
  opts[:on_error],
217
- flag_definition_cache_provider: opts[:flag_definition_cache_provider],
218
+ flag_definition_cache_provider: @flag_definition_cache_provider,
218
219
  feature_flag_request_max_retries: opts[:feature_flag_request_max_retries],
219
220
  async_load: opts[:feature_flags_async_load] == true,
220
221
  user_agent: @headers['User-Agent'],
@@ -883,9 +884,9 @@ module PostHog
883
884
  def reload_feature_flags
884
885
  return if @disabled
885
886
 
886
- unless @secret_key
887
+ unless @secret_key || @flag_definition_cache_provider
887
888
  logger.error(
888
- 'You need to specify a secret_key to locally evaluate feature flags'
889
+ 'You need to specify a secret_key or flag_definition_cache_provider to locally evaluate feature flags'
889
890
  )
890
891
  return
891
892
  end
@@ -100,9 +100,9 @@ module PostHog
100
100
  run_now: @async_load
101
101
  ) { _load_feature_flags }
102
102
 
103
- # If no secret_key, disable local evaluation & thus polling for definitions
104
- if @secret_key.nil?
105
- logger.info 'No secret_key provided, disabling local evaluation'
103
+ # Local evaluation can load definitions from either the API or an external cache.
104
+ if @secret_key.nil? && @flag_definition_cache_provider.nil?
105
+ logger.info 'No secret_key or flag_definition_cache_provider provided, disabling local evaluation'
106
106
  @loaded_flags_successfully_once.make_true
107
107
  else
108
108
  # load once synchronously before timer, unless @async_load
@@ -1110,6 +1110,13 @@ module PostHog
1110
1110
  groups: {}, group_properties: {}, snapshot: _evaluation_snapshot)
1111
1111
  flag_filters = flag[:filters] || {}
1112
1112
 
1113
+ # Holdouts precede release conditions: a held-out identifier is excluded from
1114
+ # the flag's targeting entirely rather than bucketed into one of its variants.
1115
+ # The per-condition aggregation handled below never changes holdout membership,
1116
+ # so this uses the flag-level bucketing identity the caller resolved.
1117
+ holdout_variant = holdout_variant_for(flag_filters[:holdout], distinct_id)
1118
+ return holdout_variant unless holdout_variant.nil?
1119
+
1113
1120
  flag_conditions = flag_filters[:groups] || []
1114
1121
  flag_aggregation = flag_filters[:aggregation_group_type_index]
1115
1122
  early_exit = flag_filters[:early_exit] == true
@@ -1233,6 +1240,36 @@ module PostHog
1233
1240
  (Integer(hash_key[0..14], 16).to_f / 0xfffffffffffffff)
1234
1241
  end
1235
1242
 
1243
+ # An experiment holdout excludes an identifier from the flag's release conditions
1244
+ # entirely, resolving it to the synthetic variant "holdout-<id>". That variant does
1245
+ # not need to exist in `filters.multivariate.variants`. Returns nil when the flag has
1246
+ # no usable holdout or the identifier falls outside it, leaving ordinary evaluation
1247
+ # unchanged.
1248
+ def holdout_variant_for(holdout, bucketing_value)
1249
+ return nil unless holdout.is_a?(Hash)
1250
+
1251
+ id = holdout[:id]
1252
+ exclusion_percentage = holdout[:exclusion_percentage]
1253
+ return nil if id.nil? || !exclusion_percentage.is_a?(Numeric)
1254
+
1255
+ # The server clamps an out-of-range percentage rather than rejecting it, and treats
1256
+ # 100 as everyone without hashing, so a full holdout can't miss on a hash boundary.
1257
+ # Fractional percentages are kept as-is.
1258
+ percentage = exclusion_percentage.to_f.clamp(0, 100)
1259
+ return nil if percentage < 100 && _holdout_hash(bucketing_value) > (percentage / 100)
1260
+
1261
+ "holdout-#{id}"
1262
+ end
1263
+
1264
+ # Holdout membership hashes `holdout-<bucketing value>` with no separator, salt, flag
1265
+ # key or holdout id taking part, so one identity lands in the same holdout population
1266
+ # on every flag, the way the server buckets it. The dot-separated flag hash above
1267
+ # selects a different population and must not be reused here.
1268
+ def _holdout_hash(bucketing_value)
1269
+ hash_key = Digest::SHA1.hexdigest "holdout-#{bucketing_value}"
1270
+ (Integer(hash_key[0..14], 16).to_f / 0xfffffffffffffff)
1271
+ end
1272
+
1236
1273
  def get_matching_variant(flag, distinct_id)
1237
1274
  hash_value = _hash(flag[:key], distinct_id, 'variant')
1238
1275
  matching_variant = variant_lookup_table(flag).find do |variant|
@@ -1260,7 +1297,8 @@ module PostHog
1260
1297
 
1261
1298
  if @flag_definition_cache_provider
1262
1299
  begin
1263
- should_fetch = @flag_definition_cache_provider.should_fetch_flag_definitions?
1300
+ # Cache-only readers must not acquire or renew fetch leadership.
1301
+ should_fetch = @secret_key && @flag_definition_cache_provider.should_fetch_flag_definitions?
1264
1302
  rescue StandardError => e
1265
1303
  logger.error("[FEATURE FLAGS] Cache provider should_fetch error: #{e}")
1266
1304
  should_fetch = true
@@ -1271,6 +1309,10 @@ module PostHog
1271
1309
  begin
1272
1310
  cached_data = @flag_definition_cache_provider.flag_definitions
1273
1311
  if cached_data
1312
+ unless cached_data.is_a?(Hash) && get_by_symbol_or_string_key(cached_data, 'flags').is_a?(Array)
1313
+ raise ArgumentError, 'Cached flag definitions must contain a flags array'
1314
+ end
1315
+
1274
1316
  logger.debug '[FEATURE FLAGS] Using cached flag definitions from external cache'
1275
1317
  _apply_flag_definitions(cached_data)
1276
1318
  return
@@ -1288,6 +1330,11 @@ module PostHog
1288
1330
  end
1289
1331
 
1290
1332
  def _fetch_and_apply_flag_definitions
1333
+ unless @secret_key
1334
+ logger.warn '[FEATURE FLAGS] A secret_key is required to fetch flag definitions from PostHog'
1335
+ return
1336
+ end
1337
+
1291
1338
  begin
1292
1339
  res = _request_feature_flag_definitions(etag: @flags_etag.value)
1293
1340
  rescue StandardError => e
@@ -5,7 +5,8 @@ module PostHog
5
5
  #
6
6
  # Enables multi-worker environments (Kubernetes, load-balanced servers,
7
7
  # serverless functions) to share flag definitions via an external cache,
8
- # reducing redundant API calls.
8
+ # reducing redundant API calls. Cache consumers need only the project API key;
9
+ # +:secret_key+ is required only for workers that fetch definitions from PostHog.
9
10
  #
10
11
  # Implement the four required methods on any object and pass it as the
11
12
  # +:flag_definition_cache_provider+ option when creating a {Client}.
@@ -16,7 +17,7 @@ module PostHog
16
17
  # Retrieve cached flag definitions. Return a Hash with +:flags+,
17
18
  # +:group_type_mapping+, +:cohorts+, +:minimal_flag_called_events+, and +:property_matching_version+
18
19
  # keys, or +nil+ if the cache is empty. Returning +nil+ triggers an API
19
- # fetch when no flags are loaded yet (emergency fallback). Providers
20
+ # fetch when no flags are loaded yet and +:secret_key+ is configured (emergency fallback). Providers
20
21
  # written before +:minimal_flag_called_events+ existed continue to work;
21
22
  # a missing key is treated as +false+. Preserve +:property_matching_version+
22
23
  # with the definitions: exactly +2+ selects explicit equality; missing/1
@@ -27,7 +28,9 @@ module PostHog
27
28
  # @!method should_fetch_flag_definitions?
28
29
  # Return +true+ if this instance should fetch new definitions from the
29
30
  # API, +false+ to read from cache instead. Use for distributed lock
30
- # coordination so only one worker fetches at a time.
31
+ # coordination so only one worker fetches at a time. Called only when
32
+ # +:secret_key+ is configured. Without it, the SDK reads +flag_definitions+
33
+ # directly without acquiring or renewing fetch leadership.
31
34
  # @return [Boolean]
32
35
  #
33
36
  # @!method on_flag_definitions_received(data)
@@ -53,12 +56,16 @@ module PostHog
53
56
  # - +on_flag_definitions_received+ errors are logged; flags remain in memory
54
57
  # - +shutdown+ errors are logged; shutdown continues
55
58
  #
59
+ # API fallback requires +:secret_key+. Without it, the SDK logs a warning and
60
+ # keeps any previously loaded definitions. Cache-only readers skip the fetch
61
+ # decision and never publish. Polling and manual reloads continue reading the
62
+ # cache so consumers can pick up later updates.
63
+ #
56
64
  # == Example
57
65
  #
58
66
  # cache = RedisFlagCache.new(redis, service_key: 'my-service')
59
67
  # client = PostHog::Client.new(
60
68
  # api_key: '<project_api_key>',
61
- # secret_key: '<secret_key>',
62
69
  # flag_definition_cache_provider: cache
63
70
  # )
64
71
  #
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PostHog
4
- VERSION = '3.27.0'
4
+ VERSION = '3.28.0'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: posthog-ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.27.0
4
+ version: 3.28.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - ''