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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f6e3857125c3ca1a120a8ab1680b518c79ca92729186bf0dcc868d5d83126612
4
- data.tar.gz: 5bce014fe04b202359313e36e6adae530dfc77a23be2824eb42c28b29a12fa8b
3
+ metadata.gz: 9a3e8eecf2cef5e82e5cc8ba7f7ddefc8c9f372b86039c9e834ad6308b083a3e
4
+ data.tar.gz: 7ea54f05eaf248de3f5106729d8f5d02c919b49c1e32db7030704288c8e6c8b2
5
5
  SHA512:
6
- metadata.gz: 9df1007084659857d4f38c386cedc310a89a0c7adde2681b06bba0a8924b76545160092c2fd7140bc12a2f41038b69baf46ba40db5c3ef7c7b4394da1b30310e
7
- data.tar.gz: 02c14a66f74f619b3da4f2e680f67a186e74f9193ffbb7149cdca12d5bb60d7b554983c8b66a69d46f38002c0caab6facadb5aa92bcd7b690969a6de25ddd038
6
+ metadata.gz: 6a72a75666f8c77ab2aa9abfb863404ca9076de5f205da215f7fc24271999c75e144d8ded893d2e389218c0068383dfdcdbcbead6ce35e643947adc476a76602
7
+ data.tar.gz: 6be683bbd6b6831913a146ac1cea6ac348c5dc929af6443131459febedf1e1890c32fb7b7482ba8f42402d7cdd1c9ebab7934b720be915bc432d376caa3389b7
@@ -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, 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
 
@@ -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
- @on_error.call(res.status, res.error) unless res.status.between?(200, 299)
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
- 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