posthog-ruby 3.18.0 → 3.20.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 +30 -5
- data/lib/posthog/defaults.rb +1 -0
- data/lib/posthog/feature_flag.rb +4 -1
- data/lib/posthog/feature_flag_evaluations.rb +2 -1
- data/lib/posthog/feature_flags.rb +41 -10
- 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: 36dffe9438cc269541f1642261b27b020bf05993eda1ad9999887428caa1667b
|
|
4
|
+
data.tar.gz: 9575ff454af5b89c0be1eb6abf4c7a1158fdbca88d4c708860339a1710b1393d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9badf7f03c2009d4c643b6e41f4ede382c0bf488eb0ae4d5cf33ec454110102e3fc342b5f3357b58b881f2a513628ba6056f0ccfa82d54ac8cafb4d814ae3811
|
|
7
|
+
data.tar.gz: e7918e45f9645da0052d65d205359e8384709f645f0f1b5201500a5cc97bb092aed771cd7fac7846f5bfc142fcb9e8168f55d9cbc750c6bbb8aab26e60e1138d
|
data/lib/posthog/client.rb
CHANGED
|
@@ -76,6 +76,13 @@ module PostHog
|
|
|
76
76
|
# @option opts [Integer] :feature_flag_request_max_retries How many times to retry a flag request after a
|
|
77
77
|
# transient network error. Each retry sleeps on the calling thread before retrying, so this adds to
|
|
78
78
|
# worst-case latency. Defaults to 1. Set to 0 to disable retrying.
|
|
79
|
+
# @option opts [Boolean] :feature_flags_async_load +true+ to fetch feature flag definitions for local
|
|
80
|
+
# evaluation only on the poller's background thread instead of the calling thread. The constructor
|
|
81
|
+
# returns without waiting for definitions: the poller fetches immediately on boot and, if that fails,
|
|
82
|
+
# keeps retrying on its regular polling interval until a load succeeds. Until then local evaluation
|
|
83
|
+
# treats definitions as absent (check {#feature_flags_loaded?}), so evaluations return nil with
|
|
84
|
+
# +only_evaluate_locally: true+, or fall back to the remote flags endpoint (which still runs on the
|
|
85
|
+
# calling thread) without it. Defaults to +false+.
|
|
79
86
|
# @option opts [Proc] :before_send A callback that receives the event hash and should return either a modified
|
|
80
87
|
# hash to be sent to PostHog or nil to prevent the event from being sent. e.g. `before_send: ->(event) { event }`.
|
|
81
88
|
# @option opts [Boolean] :disable_singleton_warning +true+ to suppress the warning when multiple clients share
|
|
@@ -162,7 +169,8 @@ module PostHog
|
|
|
162
169
|
opts[:feature_flag_request_timeout_seconds] || Defaults::FeatureFlags::FLAG_REQUEST_TIMEOUT_SECONDS,
|
|
163
170
|
opts[:on_error],
|
|
164
171
|
flag_definition_cache_provider: opts[:flag_definition_cache_provider],
|
|
165
|
-
feature_flag_request_max_retries: opts[:feature_flag_request_max_retries]
|
|
172
|
+
feature_flag_request_max_retries: opts[:feature_flag_request_max_retries],
|
|
173
|
+
async_load: opts[:feature_flags_async_load] == true
|
|
166
174
|
)
|
|
167
175
|
end
|
|
168
176
|
|
|
@@ -609,7 +617,8 @@ module PostHog
|
|
|
609
617
|
id: definition[:id],
|
|
610
618
|
version: nil,
|
|
611
619
|
reason: FeatureFlagEvaluations::EVALUATED_LOCALLY_REASON,
|
|
612
|
-
locally_evaluated: true
|
|
620
|
+
locally_evaluated: true,
|
|
621
|
+
has_experiment: definition[:has_experiment]
|
|
613
622
|
)
|
|
614
623
|
locally_evaluated_keys << key.to_s
|
|
615
624
|
end
|
|
@@ -649,7 +658,8 @@ module PostHog
|
|
|
649
658
|
id: metadata ? metadata.id : nil,
|
|
650
659
|
version: metadata ? metadata.version : nil,
|
|
651
660
|
reason: reason ? (reason.description || reason.code) : nil,
|
|
652
|
-
locally_evaluated: false
|
|
661
|
+
locally_evaluated: false,
|
|
662
|
+
has_experiment: metadata&.has_experiment
|
|
653
663
|
)
|
|
654
664
|
end
|
|
655
665
|
rescue StandardError => e
|
|
@@ -766,10 +776,12 @@ module PostHog
|
|
|
766
776
|
# Remove internal information
|
|
767
777
|
response.delete(:requestId)
|
|
768
778
|
response.delete(:evaluatedAt)
|
|
779
|
+
response.delete(:flagDetails)
|
|
769
780
|
response
|
|
770
781
|
end
|
|
771
782
|
|
|
772
|
-
# Reload locally cached feature flag definitions
|
|
783
|
+
# Reload locally cached feature flag definitions synchronously on the
|
|
784
|
+
# calling thread.
|
|
773
785
|
#
|
|
774
786
|
# @return [void]
|
|
775
787
|
def reload_feature_flags
|
|
@@ -784,6 +796,17 @@ module PostHog
|
|
|
784
796
|
@feature_flags_poller.load_feature_flags(true)
|
|
785
797
|
end
|
|
786
798
|
|
|
799
|
+
# Whether feature flag definitions for local evaluation are currently loaded.
|
|
800
|
+
# False until the first successful load, and false again if a quota-limited
|
|
801
|
+
# (402) response discards the definitions.
|
|
802
|
+
#
|
|
803
|
+
# @return [Boolean]
|
|
804
|
+
def feature_flags_loaded?
|
|
805
|
+
return false if @disabled
|
|
806
|
+
|
|
807
|
+
@feature_flags_poller.definitions_loaded?
|
|
808
|
+
end
|
|
809
|
+
|
|
787
810
|
# Whether the client will actually send events. It is disabled when the
|
|
788
811
|
# api_key is missing or blank, in which case every capture call no-ops.
|
|
789
812
|
#
|
|
@@ -921,7 +944,8 @@ module PostHog
|
|
|
921
944
|
person_properties, group_properties = add_local_person_and_group_properties(
|
|
922
945
|
groups, person_properties, group_properties
|
|
923
946
|
)
|
|
924
|
-
feature_flag_response, flag_was_locally_evaluated, request_id, evaluated_at, feature_flag_error, payload
|
|
947
|
+
feature_flag_response, flag_was_locally_evaluated, request_id, evaluated_at, feature_flag_error, payload,
|
|
948
|
+
has_experiment =
|
|
925
949
|
@feature_flags_poller.get_feature_flag(
|
|
926
950
|
key, distinct_id, groups, person_properties, group_properties, only_evaluate_locally
|
|
927
951
|
)
|
|
@@ -931,6 +955,7 @@ module PostHog
|
|
|
931
955
|
'$feature_flag_response' => feature_flag_response,
|
|
932
956
|
'locally_evaluated' => flag_was_locally_evaluated
|
|
933
957
|
}
|
|
958
|
+
properties['$feature_flag_has_experiment'] = has_experiment unless has_experiment.nil?
|
|
934
959
|
properties['$feature_flag_request_id'] = request_id if request_id
|
|
935
960
|
properties['$feature_flag_evaluated_at'] = evaluated_at if evaluated_at
|
|
936
961
|
properties['$feature_flag_error'] = feature_flag_error if feature_flag_error
|
data/lib/posthog/defaults.rb
CHANGED
|
@@ -19,6 +19,7 @@ module PostHog
|
|
|
19
19
|
|
|
20
20
|
module FeatureFlags
|
|
21
21
|
FLAG_REQUEST_TIMEOUT_SECONDS = 3
|
|
22
|
+
POLLING_INTERVAL_SECONDS = 30
|
|
22
23
|
# Number of retries for a flag request after a transient network error.
|
|
23
24
|
# Flag requests are stateless and cause no server-side mutation, so
|
|
24
25
|
# retrying is safe.
|
data/lib/posthog/feature_flag.rb
CHANGED
|
@@ -68,7 +68,7 @@ module PostHog
|
|
|
68
68
|
#
|
|
69
69
|
# @api private
|
|
70
70
|
class FeatureFlagMetadata
|
|
71
|
-
attr_reader :id, :version, :payload, :description
|
|
71
|
+
attr_reader :id, :version, :payload, :description, :has_experiment
|
|
72
72
|
|
|
73
73
|
# @param json [Hash] Raw metadata returned by /flags.
|
|
74
74
|
def initialize(json)
|
|
@@ -77,6 +77,9 @@ module PostHog
|
|
|
77
77
|
@version = json['version']
|
|
78
78
|
@payload = json['payload']
|
|
79
79
|
@description = json['description']
|
|
80
|
+
# Whether the flag is linked to an experiment. nil when the server
|
|
81
|
+
# (an older deployment) does not report the field.
|
|
82
|
+
@has_experiment = json['has_experiment']
|
|
80
83
|
end
|
|
81
84
|
end
|
|
82
85
|
end
|
|
@@ -13,7 +13,7 @@ module PostHog
|
|
|
13
13
|
EVALUATED_LOCALLY_REASON = 'Evaluated locally'
|
|
14
14
|
|
|
15
15
|
EvaluatedFlagRecord = Struct.new(
|
|
16
|
-
:key, :enabled, :variant, :payload, :id, :version, :reason, :locally_evaluated,
|
|
16
|
+
:key, :enabled, :variant, :payload, :id, :version, :reason, :locally_evaluated, :has_experiment,
|
|
17
17
|
keyword_init: true
|
|
18
18
|
)
|
|
19
19
|
|
|
@@ -169,6 +169,7 @@ module PostHog
|
|
|
169
169
|
}
|
|
170
170
|
|
|
171
171
|
if flag
|
|
172
|
+
properties['$feature_flag_has_experiment'] = flag.has_experiment unless flag.has_experiment.nil?
|
|
172
173
|
properties['$feature_flag_payload'] = flag.payload unless flag.payload.nil?
|
|
173
174
|
properties['$feature_flag_id'] = flag.id if flag.id
|
|
174
175
|
properties['$feature_flag_version'] = flag.version if flag.version
|
|
@@ -30,6 +30,7 @@ module PostHog
|
|
|
30
30
|
include PostHog::Utils
|
|
31
31
|
|
|
32
32
|
# @param polling_interval [Integer, nil] Seconds between local feature flag definition polls.
|
|
33
|
+
# Defaults to {Defaults::FeatureFlags::POLLING_INTERVAL_SECONDS}.
|
|
33
34
|
# @param secret_key [String, nil] Credential used to fetch local evaluation definitions. Accepts either a
|
|
34
35
|
# Personal API Key (`phx_...`) or a Project Secret API Key (`phs_...`).
|
|
35
36
|
# @param project_api_key [String] Project API key.
|
|
@@ -40,6 +41,9 @@ module PostHog
|
|
|
40
41
|
# @param feature_flag_request_max_retries [Integer, nil] Retries after a transient network error or retryable
|
|
41
42
|
# HTTP response status on a flag request. Defaults to {Defaults::FeatureFlags::FLAG_REQUEST_MAX_RETRIES}.
|
|
42
43
|
# Set to 0 to disable retrying.
|
|
44
|
+
# @param async_load [Boolean] When true, flag definitions are fetched only on the poller thread: an
|
|
45
|
+
# immediate first tick at construction, then the regular polling cadence, which keeps retrying until a
|
|
46
|
+
# load succeeds.
|
|
43
47
|
def initialize(
|
|
44
48
|
polling_interval,
|
|
45
49
|
secret_key,
|
|
@@ -48,9 +52,10 @@ module PostHog
|
|
|
48
52
|
feature_flag_request_timeout_seconds,
|
|
49
53
|
on_error = nil,
|
|
50
54
|
flag_definition_cache_provider: nil,
|
|
51
|
-
feature_flag_request_max_retries: nil
|
|
55
|
+
feature_flag_request_max_retries: nil,
|
|
56
|
+
async_load: false
|
|
52
57
|
)
|
|
53
|
-
@polling_interval = polling_interval ||
|
|
58
|
+
@polling_interval = polling_interval || Defaults::FeatureFlags::POLLING_INTERVAL_SECONDS
|
|
54
59
|
@secret_key = secret_key
|
|
55
60
|
@project_api_key = project_api_key
|
|
56
61
|
@host = host
|
|
@@ -65,14 +70,16 @@ module PostHog
|
|
|
65
70
|
@on_error = on_error || proc { |status, error| }
|
|
66
71
|
@quota_limited = Concurrent::AtomicBoolean.new(false)
|
|
67
72
|
@flags_etag = Concurrent::AtomicReference.new(nil)
|
|
68
|
-
@flag_definitions_loaded_at = nil
|
|
73
|
+
@flag_definitions_loaded_at = Concurrent::AtomicReference.new(nil)
|
|
74
|
+
@async_load = async_load
|
|
69
75
|
|
|
70
76
|
@flag_definition_cache_provider = flag_definition_cache_provider
|
|
71
77
|
FlagDefinitionCacheProvider.validate!(@flag_definition_cache_provider) if @flag_definition_cache_provider
|
|
72
78
|
|
|
73
79
|
@task =
|
|
74
80
|
Concurrent::TimerTask.new(
|
|
75
|
-
execution_interval: polling_interval
|
|
81
|
+
execution_interval: @polling_interval,
|
|
82
|
+
run_now: @async_load
|
|
76
83
|
) { _load_feature_flags }
|
|
77
84
|
|
|
78
85
|
# If no secret_key, disable local evaluation & thus polling for definitions
|
|
@@ -80,19 +87,31 @@ module PostHog
|
|
|
80
87
|
logger.info 'No secret_key provided, disabling local evaluation'
|
|
81
88
|
@loaded_flags_successfully_once.make_true
|
|
82
89
|
else
|
|
83
|
-
# load once before timer
|
|
84
|
-
load_feature_flags
|
|
90
|
+
# load once synchronously before timer, unless @async_load
|
|
91
|
+
load_feature_flags unless @async_load
|
|
85
92
|
@task.execute
|
|
86
93
|
end
|
|
87
94
|
end
|
|
88
95
|
|
|
89
96
|
def load_feature_flags(force_reload = false)
|
|
90
|
-
return
|
|
97
|
+
return if (@loaded_flags_successfully_once.true? || @async_load) && !force_reload
|
|
91
98
|
|
|
92
99
|
_load_feature_flags
|
|
93
100
|
end
|
|
94
101
|
|
|
95
|
-
|
|
102
|
+
# Whether flag definitions are currently loaded. False until the first
|
|
103
|
+
# successful load, and false again if a quota-limited (402) response
|
|
104
|
+
# discards them.
|
|
105
|
+
def definitions_loaded?
|
|
106
|
+
!@flag_definitions_loaded_at.value.nil?
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Epoch milliseconds of the last successful definitions load, or nil.
|
|
110
|
+
def flag_definitions_loaded_at
|
|
111
|
+
@flag_definitions_loaded_at.value
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
attr_reader :feature_flags_by_key
|
|
96
115
|
|
|
97
116
|
def get_feature_variants(
|
|
98
117
|
distinct_id,
|
|
@@ -221,6 +240,11 @@ module PostHog
|
|
|
221
240
|
end
|
|
222
241
|
|
|
223
242
|
flag_was_locally_evaluated = !response.nil?
|
|
243
|
+
# Whether the flag is linked to an experiment, as reported by the server.
|
|
244
|
+
# Locally-evaluated flags carry it in the stored definition; remotely
|
|
245
|
+
# evaluated flags carry it in the response metadata. nil when the server
|
|
246
|
+
# (an older deployment) does not report it.
|
|
247
|
+
has_experiment = feature_flag[:has_experiment] if flag_was_locally_evaluated
|
|
224
248
|
|
|
225
249
|
request_id = nil
|
|
226
250
|
evaluated_at = nil
|
|
@@ -253,6 +277,9 @@ module PostHog
|
|
|
253
277
|
payload = payloads[key]
|
|
254
278
|
feature_flag_error = errors.join(',') unless errors.empty?
|
|
255
279
|
|
|
280
|
+
flag_detail = flags_data[:flagDetails]&.[](key.to_sym)
|
|
281
|
+
has_experiment = flag_detail&.metadata&.has_experiment
|
|
282
|
+
|
|
256
283
|
logger.debug "Successfully computed flag remotely: #{key} -> #{response}"
|
|
257
284
|
rescue Timeout::Error => e
|
|
258
285
|
@on_error.call(-1, "Timeout while fetching flags remotely: #{e}")
|
|
@@ -266,7 +293,7 @@ module PostHog
|
|
|
266
293
|
end
|
|
267
294
|
end
|
|
268
295
|
|
|
269
|
-
[response, flag_was_locally_evaluated, request_id, evaluated_at, feature_flag_error, payload]
|
|
296
|
+
[response, flag_was_locally_evaluated, request_id, evaluated_at, feature_flag_error, payload, has_experiment]
|
|
270
297
|
end
|
|
271
298
|
|
|
272
299
|
def get_all_flags(
|
|
@@ -324,10 +351,12 @@ module PostHog
|
|
|
324
351
|
errors_while_computing = false
|
|
325
352
|
quota_limited = nil
|
|
326
353
|
status_code = nil
|
|
354
|
+
flag_details = nil
|
|
327
355
|
|
|
328
356
|
if fallback_to_server && !only_evaluate_locally
|
|
329
357
|
begin
|
|
330
358
|
flags_and_payloads = get_flags(distinct_id, groups, person_properties, group_properties)
|
|
359
|
+
flag_details = flags_and_payloads[:flags]
|
|
331
360
|
errors_while_computing = flags_and_payloads[:errorsWhileComputingFlags] || false
|
|
332
361
|
quota_limited = flags_and_payloads[:quotaLimited]
|
|
333
362
|
status_code = flags_and_payloads[:status]
|
|
@@ -368,6 +397,7 @@ module PostHog
|
|
|
368
397
|
{
|
|
369
398
|
featureFlags: flags,
|
|
370
399
|
featureFlagPayloads: payloads,
|
|
400
|
+
flagDetails: flag_details,
|
|
371
401
|
requestId: request_id,
|
|
372
402
|
evaluatedAt: evaluated_at,
|
|
373
403
|
errorsWhileComputingFlags: errors_while_computing,
|
|
@@ -1172,6 +1202,7 @@ module PostHog
|
|
|
1172
1202
|
@feature_flags_by_key = {}
|
|
1173
1203
|
@group_type_mapping = Concurrent::Hash.new
|
|
1174
1204
|
@cohorts = Concurrent::Hash.new
|
|
1205
|
+
@flag_definitions_loaded_at.value = nil
|
|
1175
1206
|
@loaded_flags_successfully_once.make_false
|
|
1176
1207
|
@quota_limited.make_true
|
|
1177
1208
|
return
|
|
@@ -1220,7 +1251,7 @@ module PostHog
|
|
|
1220
1251
|
@cohorts = Concurrent::Hash[deep_symbolize_keys(cohorts)]
|
|
1221
1252
|
|
|
1222
1253
|
logger.debug "Loaded #{@feature_flags.length} feature flags and #{@cohorts.length} cohorts"
|
|
1223
|
-
@flag_definitions_loaded_at = (Time.now.to_f * 1000).to_i
|
|
1254
|
+
@flag_definitions_loaded_at.value = (Time.now.to_f * 1000).to_i
|
|
1224
1255
|
@loaded_flags_successfully_once.make_true if @loaded_flags_successfully_once.false?
|
|
1225
1256
|
end
|
|
1226
1257
|
|
data/lib/posthog/version.rb
CHANGED