posthog-ruby 3.3.2 → 3.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.
- checksums.yaml +4 -4
- data/lib/posthog/client.rb +21 -14
- data/lib/posthog/feature_flags.rb +66 -13
- data/lib/posthog/version.rb +1 -1
- metadata +3 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9e99287bd9fe6eb8969c2481c0380c2a6eba12709de5f5189a053cb9ba5ca255
|
|
4
|
+
data.tar.gz: 78b496c5feb9c8cb222393ababa36d0f6fd81c18e603469b0633ac3fcdbfd639
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: dc1c52ff0243af441d895c05199067347931f0bf50a21ba897a3fb75640cb954ec66c79dddb29fb92338479014f3a7f42543a1ef651ebdcbd7fe6845ff9e9002
|
|
7
|
+
data.tar.gz: e46932963b504e54fe4590db73e1956611190b2ab16a1f04ffff5877c01b1e71a2dafc5f9c373361b69bb5f63532d1400c244580ac3c229ced006b350880eef5
|
data/lib/posthog/client.rb
CHANGED
|
@@ -285,26 +285,31 @@ module PostHog
|
|
|
285
285
|
person_properties,
|
|
286
286
|
group_properties
|
|
287
287
|
)
|
|
288
|
-
feature_flag_response, flag_was_locally_evaluated, request_id =
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
288
|
+
feature_flag_response, flag_was_locally_evaluated, request_id, evaluated_at =
|
|
289
|
+
@feature_flags_poller.get_feature_flag(
|
|
290
|
+
key,
|
|
291
|
+
distinct_id,
|
|
292
|
+
groups,
|
|
293
|
+
person_properties,
|
|
294
|
+
group_properties,
|
|
295
|
+
only_evaluate_locally
|
|
296
|
+
)
|
|
296
297
|
|
|
297
298
|
feature_flag_reported_key = "#{key}_#{feature_flag_response}"
|
|
298
299
|
if !@distinct_id_has_sent_flag_calls[distinct_id].include?(feature_flag_reported_key) && send_feature_flag_events
|
|
300
|
+
properties = {
|
|
301
|
+
'$feature_flag' => key,
|
|
302
|
+
'$feature_flag_response' => feature_flag_response,
|
|
303
|
+
'locally_evaluated' => flag_was_locally_evaluated
|
|
304
|
+
}
|
|
305
|
+
properties['$feature_flag_request_id'] = request_id if request_id
|
|
306
|
+
properties['$feature_flag_evaluated_at'] = evaluated_at if evaluated_at
|
|
307
|
+
|
|
299
308
|
capture(
|
|
300
309
|
{
|
|
301
310
|
distinct_id: distinct_id,
|
|
302
311
|
event: '$feature_flag_called',
|
|
303
|
-
properties:
|
|
304
|
-
'$feature_flag' => key,
|
|
305
|
-
'$feature_flag_response' => feature_flag_response,
|
|
306
|
-
'locally_evaluated' => flag_was_locally_evaluated
|
|
307
|
-
}.merge(request_id ? { '$feature_flag_request_id' => request_id } : {}),
|
|
312
|
+
properties: properties,
|
|
308
313
|
groups: groups
|
|
309
314
|
}
|
|
310
315
|
)
|
|
@@ -385,7 +390,9 @@ module PostHog
|
|
|
385
390
|
distinct_id, groups, person_properties, group_properties, only_evaluate_locally
|
|
386
391
|
)
|
|
387
392
|
|
|
388
|
-
|
|
393
|
+
# Remove internal information
|
|
394
|
+
response.delete(:requestId)
|
|
395
|
+
response.delete(:evaluatedAt)
|
|
389
396
|
response
|
|
390
397
|
end
|
|
391
398
|
|
|
@@ -12,6 +12,13 @@ module PostHog
|
|
|
12
12
|
class InconclusiveMatchError < StandardError
|
|
13
13
|
end
|
|
14
14
|
|
|
15
|
+
# RequiresServerEvaluation is raised when feature flag evaluation requires
|
|
16
|
+
# server-side data that is not available locally (e.g., static cohorts,
|
|
17
|
+
# experience continuity). This error should propagate immediately to trigger
|
|
18
|
+
# API fallback, unlike InconclusiveMatchError which allows trying other conditions.
|
|
19
|
+
class RequiresServerEvaluation < StandardError
|
|
20
|
+
end
|
|
21
|
+
|
|
15
22
|
class FeatureFlagsPoller
|
|
16
23
|
include PostHog::Logging
|
|
17
24
|
include PostHog::Utils
|
|
@@ -36,6 +43,7 @@ module PostHog
|
|
|
36
43
|
@feature_flag_request_timeout_seconds = feature_flag_request_timeout_seconds
|
|
37
44
|
@on_error = on_error || proc { |status, error| }
|
|
38
45
|
@quota_limited = Concurrent::AtomicBoolean.new(false)
|
|
46
|
+
@flags_etag = Concurrent::AtomicReference.new(nil)
|
|
39
47
|
@task =
|
|
40
48
|
Concurrent::TimerTask.new(
|
|
41
49
|
execution_interval: polling_interval
|
|
@@ -133,6 +141,7 @@ module PostHog
|
|
|
133
141
|
[key, FeatureFlag.from_value_and_payload(key, value, flags_response[:featureFlagPayloads][key])]
|
|
134
142
|
end
|
|
135
143
|
end
|
|
144
|
+
|
|
136
145
|
flags_response
|
|
137
146
|
end
|
|
138
147
|
|
|
@@ -173,7 +182,7 @@ module PostHog
|
|
|
173
182
|
begin
|
|
174
183
|
response = _compute_flag_locally(feature_flag, distinct_id, groups, person_properties, group_properties)
|
|
175
184
|
logger.debug "Successfully computed flag locally: #{key} -> #{response}"
|
|
176
|
-
rescue InconclusiveMatchError => e
|
|
185
|
+
rescue RequiresServerEvaluation, InconclusiveMatchError => e
|
|
177
186
|
logger.debug "Failed to compute flag #{key} locally: #{e}"
|
|
178
187
|
rescue StandardError => e
|
|
179
188
|
@on_error.call(-1, "Error computing flag locally: #{e}. #{e.backtrace.join("\n")}")
|
|
@@ -183,6 +192,7 @@ module PostHog
|
|
|
183
192
|
flag_was_locally_evaluated = !response.nil?
|
|
184
193
|
|
|
185
194
|
request_id = nil
|
|
195
|
+
evaluated_at = nil
|
|
186
196
|
|
|
187
197
|
if !flag_was_locally_evaluated && !only_evaluate_locally
|
|
188
198
|
begin
|
|
@@ -191,6 +201,7 @@ module PostHog
|
|
|
191
201
|
if flags_data.key?(:featureFlags)
|
|
192
202
|
flags = stringify_keys(flags_data[:featureFlags] || {})
|
|
193
203
|
request_id = flags_data[:requestId]
|
|
204
|
+
evaluated_at = flags_data[:evaluatedAt]
|
|
194
205
|
else
|
|
195
206
|
logger.debug "Missing feature flags key: #{flags_data.to_json}"
|
|
196
207
|
flags = {}
|
|
@@ -204,7 +215,7 @@ module PostHog
|
|
|
204
215
|
end
|
|
205
216
|
end
|
|
206
217
|
|
|
207
|
-
[response, flag_was_locally_evaluated, request_id]
|
|
218
|
+
[response, flag_was_locally_evaluated, request_id, evaluated_at]
|
|
208
219
|
end
|
|
209
220
|
|
|
210
221
|
def get_all_flags(
|
|
@@ -245,13 +256,14 @@ module PostHog
|
|
|
245
256
|
payloads = {}
|
|
246
257
|
fallback_to_server = @feature_flags.empty?
|
|
247
258
|
request_id = nil # Only for /flags requests
|
|
259
|
+
evaluated_at = nil # Only for /flags requests
|
|
248
260
|
|
|
249
261
|
@feature_flags.each do |flag|
|
|
250
262
|
match_value = _compute_flag_locally(flag, distinct_id, groups, person_properties, group_properties)
|
|
251
263
|
flags[flag[:key]] = match_value
|
|
252
264
|
match_payload = _compute_flag_payload_locally(flag[:key], match_value)
|
|
253
265
|
payloads[flag[:key]] = match_payload if match_payload
|
|
254
|
-
rescue InconclusiveMatchError
|
|
266
|
+
rescue RequiresServerEvaluation, InconclusiveMatchError
|
|
255
267
|
fallback_to_server = true
|
|
256
268
|
rescue StandardError => e
|
|
257
269
|
@on_error.call(-1, "Error computing flag locally: #{e}. #{e.backtrace.join("\n")} ")
|
|
@@ -275,6 +287,7 @@ module PostHog
|
|
|
275
287
|
flags = stringify_keys(flags_and_payloads[:featureFlags] || {})
|
|
276
288
|
payloads = stringify_keys(flags_and_payloads[:featureFlagPayloads] || {})
|
|
277
289
|
request_id = flags_and_payloads[:requestId]
|
|
290
|
+
evaluated_at = flags_and_payloads[:evaluatedAt]
|
|
278
291
|
end
|
|
279
292
|
rescue StandardError => e
|
|
280
293
|
@on_error.call(-1, "Error computing flag remotely: #{e}")
|
|
@@ -285,7 +298,8 @@ module PostHog
|
|
|
285
298
|
{
|
|
286
299
|
featureFlags: flags,
|
|
287
300
|
featureFlagPayloads: payloads,
|
|
288
|
-
requestId: request_id
|
|
301
|
+
requestId: request_id,
|
|
302
|
+
evaluatedAt: evaluated_at
|
|
289
303
|
}
|
|
290
304
|
end
|
|
291
305
|
|
|
@@ -461,7 +475,10 @@ module PostHog
|
|
|
461
475
|
cohort_id = extract_value(property, :value).to_s
|
|
462
476
|
property_group = find_cohort_property(cohort_properties, cohort_id)
|
|
463
477
|
|
|
464
|
-
|
|
478
|
+
unless property_group
|
|
479
|
+
raise RequiresServerEvaluation,
|
|
480
|
+
"cohort #{cohort_id} not found in local cohorts - likely a static cohort that requires server evaluation"
|
|
481
|
+
end
|
|
465
482
|
|
|
466
483
|
match_property_group(property_group, property_values, cohort_properties)
|
|
467
484
|
end
|
|
@@ -539,6 +556,9 @@ module PostHog
|
|
|
539
556
|
elsif final_result # group_type == 'OR'
|
|
540
557
|
return true
|
|
541
558
|
end
|
|
559
|
+
rescue RequiresServerEvaluation
|
|
560
|
+
# Immediately propagate - this condition requires server-side data
|
|
561
|
+
raise
|
|
542
562
|
rescue InconclusiveMatchError => e
|
|
543
563
|
PostHog::Logging.logger&.debug("Failed to compute property #{prop} locally: #{e}")
|
|
544
564
|
error_matching_locally = true
|
|
@@ -676,7 +696,7 @@ module PostHog
|
|
|
676
696
|
:match_nested_property_group, :match_regular_property_group
|
|
677
697
|
|
|
678
698
|
def _compute_flag_locally(flag, distinct_id, groups = {}, person_properties = {}, group_properties = {})
|
|
679
|
-
raise
|
|
699
|
+
raise RequiresServerEvaluation, 'Flag has experience continuity enabled' if flag[:ensure_experience_continuity]
|
|
680
700
|
|
|
681
701
|
return false unless flag[:active]
|
|
682
702
|
|
|
@@ -747,7 +767,12 @@ module PostHog
|
|
|
747
767
|
result = variant || true
|
|
748
768
|
break
|
|
749
769
|
end
|
|
770
|
+
rescue RequiresServerEvaluation
|
|
771
|
+
# Static cohort or other missing server-side data - must fallback to API
|
|
772
|
+
raise
|
|
750
773
|
rescue InconclusiveMatchError
|
|
774
|
+
# Evaluation error (bad regex, invalid date, missing property, etc.)
|
|
775
|
+
# Track that we had an inconclusive match, but try other conditions
|
|
751
776
|
is_inconclusive = true
|
|
752
777
|
end
|
|
753
778
|
|
|
@@ -816,12 +841,20 @@ module PostHog
|
|
|
816
841
|
|
|
817
842
|
def _load_feature_flags
|
|
818
843
|
begin
|
|
819
|
-
res = _request_feature_flag_definitions
|
|
844
|
+
res = _request_feature_flag_definitions(etag: @flags_etag.value)
|
|
820
845
|
rescue StandardError => e
|
|
821
846
|
@on_error.call(-1, e.to_s)
|
|
822
847
|
return
|
|
823
848
|
end
|
|
824
849
|
|
|
850
|
+
# Handle 304 Not Modified - flags haven't changed, skip processing
|
|
851
|
+
# Only update ETag if the 304 response includes one
|
|
852
|
+
if res[:not_modified]
|
|
853
|
+
@flags_etag.value = res[:etag] if res[:etag]
|
|
854
|
+
logger.debug '[FEATURE FLAGS] Flags not modified (304), using cached data'
|
|
855
|
+
return
|
|
856
|
+
end
|
|
857
|
+
|
|
825
858
|
# Handle quota limits with 402 status
|
|
826
859
|
if res.is_a?(Hash) && res[:status] == 402
|
|
827
860
|
logger.warn(
|
|
@@ -838,6 +871,9 @@ module PostHog
|
|
|
838
871
|
end
|
|
839
872
|
|
|
840
873
|
if res.key?(:flags)
|
|
874
|
+
# Only update ETag on successful responses with flag data
|
|
875
|
+
@flags_etag.value = res[:etag]
|
|
876
|
+
|
|
841
877
|
@feature_flags = res[:flags] || []
|
|
842
878
|
@feature_flags_by_key = {}
|
|
843
879
|
@feature_flags.each do |flag|
|
|
@@ -853,13 +889,14 @@ module PostHog
|
|
|
853
889
|
end
|
|
854
890
|
end
|
|
855
891
|
|
|
856
|
-
def _request_feature_flag_definitions
|
|
892
|
+
def _request_feature_flag_definitions(etag: nil)
|
|
857
893
|
uri = URI("#{@host}/api/feature_flag/local_evaluation")
|
|
858
894
|
uri.query = URI.encode_www_form([['token', @project_api_key], %w[send_cohorts true]])
|
|
859
895
|
req = Net::HTTP::Get.new(uri)
|
|
860
896
|
req['Authorization'] = "Bearer #{@personal_api_key}"
|
|
897
|
+
req['If-None-Match'] = etag if etag
|
|
861
898
|
|
|
862
|
-
_request(uri, req)
|
|
899
|
+
_request(uri, req, nil, include_etag: true)
|
|
863
900
|
end
|
|
864
901
|
|
|
865
902
|
def _request_feature_flag_evaluation(data = {})
|
|
@@ -883,7 +920,7 @@ module PostHog
|
|
|
883
920
|
end
|
|
884
921
|
|
|
885
922
|
# rubocop:disable Lint/ShadowedException
|
|
886
|
-
def _request(uri, request_object, timeout = nil)
|
|
923
|
+
def _request(uri, request_object, timeout = nil, include_etag: false)
|
|
887
924
|
request_object['User-Agent'] = "posthog-ruby#{PostHog::VERSION}"
|
|
888
925
|
request_timeout = timeout || 10
|
|
889
926
|
|
|
@@ -895,16 +932,28 @@ module PostHog
|
|
|
895
932
|
read_timeout: request_timeout
|
|
896
933
|
) do |http|
|
|
897
934
|
res = http.request(request_object)
|
|
935
|
+
status_code = res.code.to_i
|
|
936
|
+
etag = include_etag ? res['ETag'] : nil
|
|
937
|
+
|
|
938
|
+
# Handle 304 Not Modified - return special response indicating no change
|
|
939
|
+
if status_code == 304
|
|
940
|
+
logger.debug("#{request_object.method} #{_mask_tokens_in_url(uri.to_s)} returned 304 Not Modified")
|
|
941
|
+
return { not_modified: true, etag: etag, status: status_code }
|
|
942
|
+
end
|
|
898
943
|
|
|
899
944
|
# Parse response body to hash
|
|
900
945
|
begin
|
|
901
946
|
response = JSON.parse(res.body, { symbolize_names: true })
|
|
902
|
-
# Only add status if response is a hash
|
|
903
|
-
|
|
947
|
+
# Only add status (and etag if requested) if response is a hash
|
|
948
|
+
extra_fields = { status: status_code }
|
|
949
|
+
extra_fields[:etag] = etag if include_etag
|
|
950
|
+
response = response.merge(extra_fields) if response.is_a?(Hash)
|
|
904
951
|
return response
|
|
905
952
|
rescue JSON::ParserError
|
|
906
953
|
# Handle case when response isn't valid JSON
|
|
907
|
-
|
|
954
|
+
error_response = { error: 'Invalid JSON response', body: res.body, status: status_code }
|
|
955
|
+
error_response[:etag] = etag if include_etag
|
|
956
|
+
return error_response
|
|
908
957
|
end
|
|
909
958
|
end
|
|
910
959
|
rescue Timeout::Error,
|
|
@@ -921,5 +970,9 @@ module PostHog
|
|
|
921
970
|
end
|
|
922
971
|
end
|
|
923
972
|
# rubocop:enable Lint/ShadowedException
|
|
973
|
+
|
|
974
|
+
def _mask_tokens_in_url(url)
|
|
975
|
+
url.gsub(/token=([^&]{10})[^&]*/, 'token=\1...')
|
|
976
|
+
end
|
|
924
977
|
end
|
|
925
978
|
end
|
data/lib/posthog/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: posthog-ruby
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.
|
|
4
|
+
version: 3.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- ''
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: bin
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date: 2025-
|
|
10
|
+
date: 2025-12-04 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: concurrent-ruby
|
|
@@ -57,7 +56,6 @@ licenses:
|
|
|
57
56
|
- MIT
|
|
58
57
|
metadata:
|
|
59
58
|
rubygems_mfa_required: 'true'
|
|
60
|
-
post_install_message:
|
|
61
59
|
rdoc_options: []
|
|
62
60
|
require_paths:
|
|
63
61
|
- lib
|
|
@@ -72,8 +70,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
72
70
|
- !ruby/object:Gem::Version
|
|
73
71
|
version: '0'
|
|
74
72
|
requirements: []
|
|
75
|
-
rubygems_version: 3.
|
|
76
|
-
signing_key:
|
|
73
|
+
rubygems_version: 3.6.6
|
|
77
74
|
specification_version: 4
|
|
78
75
|
summary: PostHog library
|
|
79
76
|
test_files: []
|