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 +4 -4
- data/lib/posthog/client.rb +7 -6
- data/lib/posthog/feature_flags.rb +51 -4
- data/lib/posthog/flag_definition_cache.rb +11 -4
- data/lib/posthog/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 561f4ebad1a6cd75904b800f540c0dbb9352cc081dc57bf5cc0a5dcb82925e89
|
|
4
|
+
data.tar.gz: 5e9ce62f5e48c5380ce71e454507bafa6825a8e7e6db6c05b8896b8c5a665c62
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 41f3fde35a6e73ffa54b09fff2d88990057a3fac677153b2e14e6547fb20dbacebcc7a687bae432a98c62cabad572b5dc64cc57fb0cfbab554d7c72581d84ab8
|
|
7
|
+
data.tar.gz: e596b42169af511879c3397bc6e42aa2410bee4aa4d519b6958ad87d0800da67e6b208670abd809f4bd34d52616db2fd9a1d1ae3b3b8ebdf4c867edfb5ad186f
|
data/lib/posthog/client.rb
CHANGED
|
@@ -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
|
|
89
|
-
# config. Accepts either a Personal API Key (`phx_...`) or a Project Secret API Key (`phs_...`).
|
|
90
|
-
# for local
|
|
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:
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|
#
|
data/lib/posthog/version.rb
CHANGED