posthog-ruby 3.28.4 → 3.29.1
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 +74 -32
- data/lib/posthog/feature_flag_error.rb +2 -0
- data/lib/posthog/feature_flag_evaluations.rb +20 -2
- data/lib/posthog/feature_flags.rb +556 -168
- data/lib/posthog/flag_evaluation_errors.rb +35 -0
- data/lib/posthog/internal/context.rb +12 -6
- data/lib/posthog/internal/span_context.rb +34 -0
- data/lib/posthog/mcp/instrumentation.rb +3 -1
- data/lib/posthog/mcp/request_scope.rb +26 -15
- data/lib/posthog/otel/context_span_processor.rb +21 -0
- data/lib/posthog/unresolved_flag_reason.rb +20 -0
- data/lib/posthog/utils.rb +13 -6
- data/lib/posthog/version.rb +1 -1
- data/lib/posthog.rb +1 -0
- metadata +5 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: dc3cd53140f36b4f77fb11d60a903858d5053362e7c8fd3d8d4bd55472ed1c4a
|
|
4
|
+
data.tar.gz: f1213ce104a582ae6b9e82315538e31b77fc85dbe7336cdf0e41cbb169b0b47e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 489f3bd76d70c2034c62c02e217d6e09bd16e6600d9de7b757ae5bda1a0a24d7dafad88a735ddf13ab39fa18cffdab59fcb20e2d080689ff373238fce11f9925
|
|
7
|
+
data.tar.gz: ab7bb81da61fc82e4058355c95bc1d9b97018f0cbcfd5b476f380a058a39e9a0ce6ae3b8ce1fd11aa18acb4d4a1c4fb0cec3546f4729ee5a56dcf4e959e5ee3a
|
data/lib/posthog/client.rb
CHANGED
|
@@ -54,6 +54,9 @@ module PostHog
|
|
|
54
54
|
RELEASE_ID_ENV_VAR = 'POSTHOG_RELEASE_ID'
|
|
55
55
|
private_constant :RELEASE_ID_ENV_VAR
|
|
56
56
|
|
|
57
|
+
CAPTURED_EXCEPTION_IVAR = :@posthog_exception_captured
|
|
58
|
+
private_constant :CAPTURED_EXCEPTION_IVAR
|
|
59
|
+
|
|
57
60
|
# Thread-safe tracking of client instances per API key for singleton warnings
|
|
58
61
|
@instances_by_api_key = {}
|
|
59
62
|
@instances_mutex = Mutex.new
|
|
@@ -410,13 +413,13 @@ module PostHog
|
|
|
410
413
|
# Defaults to `{ 'type' => 'generic', 'handled' => true }` for manual captures.
|
|
411
414
|
# @return [Boolean, nil] Whether the exception event was queued or sent, or nil if the input could not be parsed.
|
|
412
415
|
def capture_exception(exception, distinct_id = nil, additional_properties = {}, flags: nil, mechanism: nil)
|
|
413
|
-
return false if @disabled
|
|
416
|
+
return false if @disabled || exception_captured?(exception)
|
|
414
417
|
|
|
415
418
|
exception_list = ExceptionCapture.build_exception_list(exception, mechanism: mechanism)
|
|
416
419
|
|
|
417
420
|
return if exception_list.nil?
|
|
418
421
|
|
|
419
|
-
properties = { '$exception_list' => exception_list }
|
|
422
|
+
properties = { '$exception_list' => exception_list }.merge(active_trace_properties)
|
|
420
423
|
properties.merge!(additional_properties) if additional_properties && !additional_properties.empty?
|
|
421
424
|
|
|
422
425
|
event_data = {
|
|
@@ -427,7 +430,9 @@ module PostHog
|
|
|
427
430
|
}
|
|
428
431
|
event_data[:flags] = flags if flags
|
|
429
432
|
|
|
430
|
-
capture(event_data)
|
|
433
|
+
capture(event_data).tap do |captured|
|
|
434
|
+
mark_exception_captured(exception) if captured
|
|
435
|
+
end
|
|
431
436
|
end
|
|
432
437
|
|
|
433
438
|
# Identifies a user
|
|
@@ -635,7 +640,8 @@ module PostHog
|
|
|
635
640
|
# @param [Hash] groups
|
|
636
641
|
# @param [Hash] person_properties key-value pairs of properties to associate with the user
|
|
637
642
|
# @param [Hash] group_properties
|
|
638
|
-
# @param [Boolean] only_evaluate_locally Skip the remote /flags call entirely
|
|
643
|
+
# @param [Boolean] only_evaluate_locally Skip the remote /flags call entirely. Flags that local evaluation
|
|
644
|
+
# cannot resolve are left out of the snapshot and listed in {FeatureFlagEvaluations#unresolved_flags}.
|
|
639
645
|
# @param [Boolean, nil] disable_geoip When true, disables GeoIP lookup for remote evaluation and stamps captured
|
|
640
646
|
# access events.
|
|
641
647
|
# @param [Array<String, Symbol>, nil] flag_keys When set, scopes evaluation to only these flag keys.
|
|
@@ -665,12 +671,13 @@ module PostHog
|
|
|
665
671
|
)
|
|
666
672
|
end
|
|
667
673
|
|
|
668
|
-
person_properties, group_properties = add_local_person_and_group_properties(
|
|
674
|
+
groups, person_properties, group_properties = add_local_person_and_group_properties(
|
|
669
675
|
groups, person_properties, group_properties
|
|
670
676
|
)
|
|
671
677
|
|
|
672
678
|
records = {}
|
|
673
679
|
locally_evaluated_keys = Set.new
|
|
680
|
+
unresolved_flags = {}
|
|
674
681
|
flag_keys_set = flag_keys&.to_set(&:to_s)
|
|
675
682
|
|
|
676
683
|
@feature_flags_poller.load_feature_flags
|
|
@@ -685,7 +692,10 @@ module PostHog
|
|
|
685
692
|
:_compute_flag_locally,
|
|
686
693
|
definition, distinct_id, groups, person_properties, group_properties, snapshot: definition_snapshot
|
|
687
694
|
)
|
|
688
|
-
rescue PostHog::RequiresServerEvaluation, PostHog::InconclusiveMatchError
|
|
695
|
+
rescue PostHog::RequiresServerEvaluation, PostHog::InconclusiveMatchError => e
|
|
696
|
+
unresolved_flags[key.to_s] = e.reason
|
|
697
|
+
next
|
|
698
|
+
rescue StandardError
|
|
689
699
|
next
|
|
690
700
|
end
|
|
691
701
|
|
|
@@ -773,7 +783,8 @@ module PostHog
|
|
|
773
783
|
flag_definitions_loaded_at: @feature_flags_poller.flag_definitions_loaded_at,
|
|
774
784
|
errors_while_computing: errors_while_computing,
|
|
775
785
|
quota_limited: quota_limited,
|
|
776
|
-
minimal_flag_called_events: minimal_flag_called_events
|
|
786
|
+
minimal_flag_called_events: minimal_flag_called_events,
|
|
787
|
+
unresolved_flags: unresolved_flags.reject { |key, _reason| records.key?(key) }
|
|
777
788
|
)
|
|
778
789
|
end
|
|
779
790
|
|
|
@@ -795,8 +806,9 @@ module PostHog
|
|
|
795
806
|
)
|
|
796
807
|
return {} if @disabled
|
|
797
808
|
|
|
798
|
-
person_properties, group_properties = add_local_person_and_group_properties(
|
|
799
|
-
|
|
809
|
+
groups, person_properties, group_properties = add_local_person_and_group_properties(
|
|
810
|
+
groups, person_properties, group_properties
|
|
811
|
+
)
|
|
800
812
|
@feature_flags_poller.get_all_flags(distinct_id, groups, person_properties, group_properties,
|
|
801
813
|
only_evaluate_locally)
|
|
802
814
|
end
|
|
@@ -835,8 +847,9 @@ module PostHog
|
|
|
835
847
|
return nil if @disabled
|
|
836
848
|
|
|
837
849
|
key = key.to_s
|
|
838
|
-
person_properties, group_properties = add_local_person_and_group_properties(
|
|
839
|
-
|
|
850
|
+
groups, person_properties, group_properties = add_local_person_and_group_properties(
|
|
851
|
+
groups, person_properties, group_properties
|
|
852
|
+
)
|
|
840
853
|
@feature_flags_poller.get_feature_flag_payload(key, distinct_id, match_value, groups, person_properties,
|
|
841
854
|
group_properties, only_evaluate_locally)
|
|
842
855
|
end
|
|
@@ -862,7 +875,7 @@ module PostHog
|
|
|
862
875
|
)
|
|
863
876
|
return { featureFlags: {}, featureFlagPayloads: {} } if @disabled
|
|
864
877
|
|
|
865
|
-
person_properties, group_properties = add_local_person_and_group_properties(
|
|
878
|
+
groups, person_properties, group_properties = add_local_person_and_group_properties(
|
|
866
879
|
groups, person_properties, group_properties
|
|
867
880
|
)
|
|
868
881
|
response = @feature_flags_poller.get_all_flags_and_payloads(
|
|
@@ -978,6 +991,44 @@ module PostHog
|
|
|
978
991
|
|
|
979
992
|
private
|
|
980
993
|
|
|
994
|
+
def active_trace_properties
|
|
995
|
+
return {} unless defined?(::OpenTelemetry::Trace)
|
|
996
|
+
|
|
997
|
+
context = ::OpenTelemetry::Trace.current_span.context
|
|
998
|
+
return {} unless context.valid?
|
|
999
|
+
|
|
1000
|
+
trace_id = context.hex_trace_id
|
|
1001
|
+
span_id = context.hex_span_id
|
|
1002
|
+
return {} unless trace_id.match?(/\A[0-9a-f]{32}\z/) && trace_id != '0' * 32
|
|
1003
|
+
return {} unless span_id.match?(/\A[0-9a-f]{16}\z/) && span_id != '0' * 16
|
|
1004
|
+
|
|
1005
|
+
context_properties = Internal::Context.current&.properties
|
|
1006
|
+
{ '$trace_id' => trace_id, '$span_id' => span_id }.reject do |key, _|
|
|
1007
|
+
property_key?(context_properties, key)
|
|
1008
|
+
end
|
|
1009
|
+
rescue StandardError, LoadError
|
|
1010
|
+
{}
|
|
1011
|
+
end
|
|
1012
|
+
|
|
1013
|
+
def exception_captured?(exception)
|
|
1014
|
+
exception.is_a?(Exception) && exception.instance_variable_get(CAPTURED_EXCEPTION_IVAR).equal?(exception)
|
|
1015
|
+
rescue StandardError
|
|
1016
|
+
false
|
|
1017
|
+
end
|
|
1018
|
+
|
|
1019
|
+
# Rails can unwrap template errors, so mark the causes included in the event too.
|
|
1020
|
+
def mark_exception_captured(exception)
|
|
1021
|
+
ExceptionCapture::MAX_CHAINED_EXCEPTIONS.times do
|
|
1022
|
+
break unless exception.is_a?(Exception)
|
|
1023
|
+
break if exception_captured?(exception)
|
|
1024
|
+
|
|
1025
|
+
exception.instance_variable_set(CAPTURED_EXCEPTION_IVAR, exception) unless exception.frozen?
|
|
1026
|
+
exception = exception.cause
|
|
1027
|
+
end
|
|
1028
|
+
rescue StandardError
|
|
1029
|
+
nil
|
|
1030
|
+
end
|
|
1031
|
+
|
|
981
1032
|
def enrich_capture_attrs_with_context(attrs)
|
|
982
1033
|
context = Internal::Context.current
|
|
983
1034
|
explicit_properties = attrs[:properties]
|
|
@@ -1075,7 +1126,7 @@ module PostHog
|
|
|
1075
1126
|
return nil if @disabled
|
|
1076
1127
|
|
|
1077
1128
|
key = key.to_s
|
|
1078
|
-
person_properties, group_properties = add_local_person_and_group_properties(
|
|
1129
|
+
groups, person_properties, group_properties = add_local_person_and_group_properties(
|
|
1079
1130
|
groups, person_properties, group_properties
|
|
1080
1131
|
)
|
|
1081
1132
|
feature_flag_response, flag_was_locally_evaluated, request_id, evaluated_at, feature_flag_error, payload,
|
|
@@ -1152,10 +1203,7 @@ module PostHog
|
|
|
1152
1203
|
action = process_before_send(action)
|
|
1153
1204
|
return false if action.nil? || action.empty?
|
|
1154
1205
|
|
|
1155
|
-
if @sync_mode
|
|
1156
|
-
send_sync(action)
|
|
1157
|
-
return true
|
|
1158
|
-
end
|
|
1206
|
+
return send_sync(action) if @sync_mode
|
|
1159
1207
|
|
|
1160
1208
|
queued = @queue_mutex.synchronize do
|
|
1161
1209
|
if @queue.length < @max_queue_size
|
|
@@ -1223,13 +1271,15 @@ module PostHog
|
|
|
1223
1271
|
batch << action
|
|
1224
1272
|
rescue MessageBatch::JSONGenerationError => e
|
|
1225
1273
|
@on_error.call(-1, e.to_s)
|
|
1226
|
-
return
|
|
1274
|
+
return false
|
|
1227
1275
|
end
|
|
1228
|
-
return if batch.empty?
|
|
1276
|
+
return false if batch.empty?
|
|
1229
1277
|
|
|
1230
1278
|
@sync_lock.synchronize do
|
|
1231
1279
|
res = @transport.send(@api_key, batch)
|
|
1232
|
-
|
|
1280
|
+
success = res.status.between?(200, 299)
|
|
1281
|
+
@on_error.call(res.status, res.error) unless success
|
|
1282
|
+
success
|
|
1233
1283
|
end
|
|
1234
1284
|
end
|
|
1235
1285
|
|
|
@@ -1242,17 +1292,9 @@ module PostHog
|
|
|
1242
1292
|
end
|
|
1243
1293
|
|
|
1244
1294
|
def add_local_person_and_group_properties(groups, person_properties, group_properties)
|
|
1245
|
-
groups
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
symbolize_keys! groups
|
|
1250
|
-
symbolize_keys! person_properties
|
|
1251
|
-
symbolize_keys! group_properties
|
|
1252
|
-
|
|
1253
|
-
group_properties.each_value do |value|
|
|
1254
|
-
symbolize_keys! value
|
|
1255
|
-
end
|
|
1295
|
+
groups, person_properties, group_properties = normalize_flag_evaluation_inputs(
|
|
1296
|
+
groups, person_properties, group_properties
|
|
1297
|
+
)
|
|
1256
1298
|
|
|
1257
1299
|
all_group_properties = {}
|
|
1258
1300
|
groups&.each do |group_name, group_key|
|
|
@@ -1261,7 +1303,7 @@ module PostHog
|
|
|
1261
1303
|
}.merge((group_properties && group_properties[group_name]) || {})
|
|
1262
1304
|
end
|
|
1263
1305
|
|
|
1264
|
-
[person_properties, all_group_properties]
|
|
1306
|
+
[groups, person_properties, all_group_properties]
|
|
1265
1307
|
end
|
|
1266
1308
|
end
|
|
1267
1309
|
end
|
|
@@ -10,6 +10,7 @@ module PostHog
|
|
|
10
10
|
# Error values:
|
|
11
11
|
# ERRORS_WHILE_COMPUTING: Server returned errorsWhileComputingFlags=true
|
|
12
12
|
# FLAG_MISSING: Requested flag not in API response
|
|
13
|
+
# LOCAL_EVALUATION_INCONCLUSIVE: Flag has a loaded local definition but local evaluation could not resolve it
|
|
13
14
|
# QUOTA_LIMITED: Rate/quota limit exceeded
|
|
14
15
|
# TIMEOUT: Request timed out
|
|
15
16
|
# CONNECTION_ERROR: Network connectivity issue
|
|
@@ -21,6 +22,7 @@ module PostHog
|
|
|
21
22
|
class FeatureFlagError
|
|
22
23
|
ERRORS_WHILE_COMPUTING = 'errors_while_computing_flags'
|
|
23
24
|
FLAG_MISSING = 'flag_missing'
|
|
25
|
+
LOCAL_EVALUATION_INCONCLUSIVE = 'local_evaluation_inconclusive'
|
|
24
26
|
QUOTA_LIMITED = 'quota_limited'
|
|
25
27
|
TIMEOUT = 'timeout'
|
|
26
28
|
CONNECTION_ERROR = 'connection_error'
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'set'
|
|
4
|
+
require 'posthog/feature_flag_error'
|
|
4
5
|
|
|
5
6
|
module PostHog
|
|
6
7
|
# A snapshot of feature flag evaluations for one distinct_id, returned by
|
|
@@ -34,6 +35,15 @@ module PostHog
|
|
|
34
35
|
# @return [Time, nil] When local flag definitions were loaded.
|
|
35
36
|
attr_reader :flag_definitions_loaded_at
|
|
36
37
|
|
|
38
|
+
# Flags that have a loaded local definition but that neither local evaluation nor a remote
|
|
39
|
+
# fallback resolved, mapped to the reason local evaluation was inconclusive. These flags are not
|
|
40
|
+
# in {#keys}, and their accessors follow the missing-flag contract. Reading this does not count
|
|
41
|
+
# as an access and does not send any event. Always empty on snapshots returned by {#only} and
|
|
42
|
+
# {#only_accessed}.
|
|
43
|
+
#
|
|
44
|
+
# @return [Hash{String => String}] Frozen hash of flag key to an {UnresolvedFlagReason} value.
|
|
45
|
+
attr_reader :unresolved_flags
|
|
46
|
+
|
|
37
47
|
# @param host [Host, nil] Internal host callbacks used to record flag access events.
|
|
38
48
|
# @param distinct_id [String, nil] The distinct id these evaluations belong to.
|
|
39
49
|
# @param flags [Hash] Evaluated flags keyed by flag key.
|
|
@@ -47,6 +57,8 @@ module PostHog
|
|
|
47
57
|
# @param minimal_flag_called_events [Boolean] Server-controlled gate for minimal
|
|
48
58
|
# `$feature_flag_called` events.
|
|
49
59
|
# @param accessed [Array<String>, Set<String>, nil] Flag keys already accessed by this snapshot.
|
|
60
|
+
# @param unresolved_flags [Hash{String => String}, nil] Unresolved flag keys mapped to an
|
|
61
|
+
# {UnresolvedFlagReason} value.
|
|
50
62
|
def initialize(
|
|
51
63
|
host: nil,
|
|
52
64
|
distinct_id: nil,
|
|
@@ -59,7 +71,8 @@ module PostHog
|
|
|
59
71
|
errors_while_computing: false,
|
|
60
72
|
quota_limited: false,
|
|
61
73
|
minimal_flag_called_events: false,
|
|
62
|
-
accessed: nil
|
|
74
|
+
accessed: nil,
|
|
75
|
+
unresolved_flags: nil
|
|
63
76
|
)
|
|
64
77
|
@host = host
|
|
65
78
|
@distinct_id = distinct_id || ''
|
|
@@ -73,6 +86,7 @@ module PostHog
|
|
|
73
86
|
@quota_limited = quota_limited
|
|
74
87
|
@minimal_flag_called_events = minimal_flag_called_events == true
|
|
75
88
|
@accessed = Set.new(accessed || [])
|
|
89
|
+
@unresolved_flags = (unresolved_flags || {}).dup.freeze
|
|
76
90
|
end
|
|
77
91
|
|
|
78
92
|
# @return [Array<String>] The evaluated flag keys in this snapshot.
|
|
@@ -198,7 +212,11 @@ module PostHog
|
|
|
198
212
|
errors = []
|
|
199
213
|
errors << 'errors_while_computing_flags' if @errors_while_computing
|
|
200
214
|
errors << 'quota_limited' if @quota_limited
|
|
201
|
-
|
|
215
|
+
if flag.nil? && @unresolved_flags.key?(key)
|
|
216
|
+
errors << FeatureFlagError::LOCAL_EVALUATION_INCONCLUSIVE
|
|
217
|
+
elsif flag.nil?
|
|
218
|
+
errors << FeatureFlagError::FLAG_MISSING
|
|
219
|
+
end
|
|
202
220
|
properties['$feature_flag_error'] = errors.join(',') unless errors.empty?
|
|
203
221
|
|
|
204
222
|
# Emit a minimal event only when the server gate is on and the flag is
|