posthog-ruby 3.28.4 → 3.29.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 +61 -13
- data/lib/posthog/feature_flag_error.rb +2 -0
- data/lib/posthog/feature_flag_evaluations.rb +20 -2
- data/lib/posthog/feature_flags.rb +546 -148
- 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 +5 -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: 9a3e8eecf2cef5e82e5cc8ba7f7ddefc8c9f372b86039c9e834ad6308b083a3e
|
|
4
|
+
data.tar.gz: 7ea54f05eaf248de3f5106729d8f5d02c919b49c1e32db7030704288c8e6c8b2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6a72a75666f8c77ab2aa9abfb863404ca9076de5f205da215f7fc24271999c75e144d8ded893d2e389218c0068383dfdcdbcbead6ce35e643947adc476a76602
|
|
7
|
+
data.tar.gz: 6be683bbd6b6831913a146ac1cea6ac348c5dc929af6443131459febedf1e1890c32fb7b7482ba8f42402d7cdd1c9ebab7934b720be915bc432d376caa3389b7
|
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.
|
|
@@ -671,6 +677,7 @@ module PostHog
|
|
|
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
|
|
|
@@ -978,6 +989,44 @@ module PostHog
|
|
|
978
989
|
|
|
979
990
|
private
|
|
980
991
|
|
|
992
|
+
def active_trace_properties
|
|
993
|
+
return {} unless defined?(::OpenTelemetry::Trace)
|
|
994
|
+
|
|
995
|
+
context = ::OpenTelemetry::Trace.current_span.context
|
|
996
|
+
return {} unless context.valid?
|
|
997
|
+
|
|
998
|
+
trace_id = context.hex_trace_id
|
|
999
|
+
span_id = context.hex_span_id
|
|
1000
|
+
return {} unless trace_id.match?(/\A[0-9a-f]{32}\z/) && trace_id != '0' * 32
|
|
1001
|
+
return {} unless span_id.match?(/\A[0-9a-f]{16}\z/) && span_id != '0' * 16
|
|
1002
|
+
|
|
1003
|
+
context_properties = Internal::Context.current&.properties
|
|
1004
|
+
{ '$trace_id' => trace_id, '$span_id' => span_id }.reject do |key, _|
|
|
1005
|
+
property_key?(context_properties, key)
|
|
1006
|
+
end
|
|
1007
|
+
rescue StandardError, LoadError
|
|
1008
|
+
{}
|
|
1009
|
+
end
|
|
1010
|
+
|
|
1011
|
+
def exception_captured?(exception)
|
|
1012
|
+
exception.is_a?(Exception) && exception.instance_variable_get(CAPTURED_EXCEPTION_IVAR).equal?(exception)
|
|
1013
|
+
rescue StandardError
|
|
1014
|
+
false
|
|
1015
|
+
end
|
|
1016
|
+
|
|
1017
|
+
# Rails can unwrap template errors, so mark the causes included in the event too.
|
|
1018
|
+
def mark_exception_captured(exception)
|
|
1019
|
+
ExceptionCapture::MAX_CHAINED_EXCEPTIONS.times do
|
|
1020
|
+
break unless exception.is_a?(Exception)
|
|
1021
|
+
break if exception_captured?(exception)
|
|
1022
|
+
|
|
1023
|
+
exception.instance_variable_set(CAPTURED_EXCEPTION_IVAR, exception) unless exception.frozen?
|
|
1024
|
+
exception = exception.cause
|
|
1025
|
+
end
|
|
1026
|
+
rescue StandardError
|
|
1027
|
+
nil
|
|
1028
|
+
end
|
|
1029
|
+
|
|
981
1030
|
def enrich_capture_attrs_with_context(attrs)
|
|
982
1031
|
context = Internal::Context.current
|
|
983
1032
|
explicit_properties = attrs[:properties]
|
|
@@ -1152,10 +1201,7 @@ module PostHog
|
|
|
1152
1201
|
action = process_before_send(action)
|
|
1153
1202
|
return false if action.nil? || action.empty?
|
|
1154
1203
|
|
|
1155
|
-
if @sync_mode
|
|
1156
|
-
send_sync(action)
|
|
1157
|
-
return true
|
|
1158
|
-
end
|
|
1204
|
+
return send_sync(action) if @sync_mode
|
|
1159
1205
|
|
|
1160
1206
|
queued = @queue_mutex.synchronize do
|
|
1161
1207
|
if @queue.length < @max_queue_size
|
|
@@ -1223,13 +1269,15 @@ module PostHog
|
|
|
1223
1269
|
batch << action
|
|
1224
1270
|
rescue MessageBatch::JSONGenerationError => e
|
|
1225
1271
|
@on_error.call(-1, e.to_s)
|
|
1226
|
-
return
|
|
1272
|
+
return false
|
|
1227
1273
|
end
|
|
1228
|
-
return if batch.empty?
|
|
1274
|
+
return false if batch.empty?
|
|
1229
1275
|
|
|
1230
1276
|
@sync_lock.synchronize do
|
|
1231
1277
|
res = @transport.send(@api_key, batch)
|
|
1232
|
-
|
|
1278
|
+
success = res.status.between?(200, 299)
|
|
1279
|
+
@on_error.call(res.status, res.error) unless success
|
|
1280
|
+
success
|
|
1233
1281
|
end
|
|
1234
1282
|
end
|
|
1235
1283
|
|
|
@@ -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
|