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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f6e3857125c3ca1a120a8ab1680b518c79ca92729186bf0dcc868d5d83126612
4
- data.tar.gz: 5bce014fe04b202359313e36e6adae530dfc77a23be2824eb42c28b29a12fa8b
3
+ metadata.gz: dc3cd53140f36b4f77fb11d60a903858d5053362e7c8fd3d8d4bd55472ed1c4a
4
+ data.tar.gz: f1213ce104a582ae6b9e82315538e31b77fc85dbe7336cdf0e41cbb169b0b47e
5
5
  SHA512:
6
- metadata.gz: 9df1007084659857d4f38c386cedc310a89a0c7adde2681b06bba0a8924b76545160092c2fd7140bc12a2f41038b69baf46ba40db5c3ef7c7b4394da1b30310e
7
- data.tar.gz: 02c14a66f74f619b3da4f2e680f67a186e74f9193ffbb7149cdca12d5bb60d7b554983c8b66a69d46f38002c0caab6facadb5aa92bcd7b690969a6de25ddd038
6
+ metadata.gz: 489f3bd76d70c2034c62c02e217d6e09bd16e6600d9de7b757ae5bda1a0a24d7dafad88a735ddf13ab39fa18cffdab59fcb20e2d080689ff373238fce11f9925
7
+ data.tar.gz: ab7bb81da61fc82e4058355c95bc1d9b97018f0cbcfd5b476f380a058a39e9a0ce6ae3b8ce1fd11aa18acb4d4a1c4fb0cec3546f4729ee5a56dcf4e959e5ee3a
@@ -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, StandardError
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(groups, person_properties,
799
- group_properties)
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(groups, person_properties,
839
- group_properties)
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
- @on_error.call(res.status, res.error) unless res.status.between?(200, 299)
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
- person_properties ||= {}
1247
- group_properties ||= {}
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
- errors << 'flag_missing' if flag.nil?
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