posthog-ruby 3.8.0 → 3.9.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: a21b0cf7196c00dff9b6ad23c7ee12e54b9984b9e2e7639d0f4371f419de80c2
4
- data.tar.gz: 643244ae98a7d9879e64c905133aafba69e0e5e710e49766bdd38db24f28032d
3
+ metadata.gz: 3f033107e265e546dca78bfbf8f6adb3552e5803ac7dec0bf822afb814e7658d
4
+ data.tar.gz: efe43054d73bfce034973fc12a01301f3b935cfacb1e7e86358332a9879f8911
5
5
  SHA512:
6
- metadata.gz: f3e4329938912befad0b58c463dd5e82ff606e381c4f306e4a1d2e509963a613cff41acc5af853cd7a4f01033af283ee7a5f77324dd31e58c605b3f84a4d9d1b
7
- data.tar.gz: e331c6d025c837c2c2764d4dcccbf61dd0c33e53f55dc9a5fbaf6a2628852c86f290dbf5c8c6c375e178d13ba839126e5c472e3bce1769e118d27b28f8410f17
6
+ metadata.gz: e1e3e4e0e5712ab73c8c0c11ce5a5869df0dc95f293b151f81d7fc715844e36867d3aeed393427494789bf8b35f2d96f42c703085d6be405fe1168feaa7daa82
7
+ data.tar.gz: 1347919dcaeda0b7ad237964ae00acc34d4b6e4b9273d3335ce6e4666f09471d8d8fefb53c13d51e345cf3b7c8e467b813b14cbbdad74062dbabbc12fbeadc26
@@ -14,6 +14,7 @@ require 'posthog/feature_flags'
14
14
  require 'posthog/feature_flag_evaluations'
15
15
  require 'posthog/send_feature_flags_options'
16
16
  require 'posthog/exception_capture'
17
+ require 'posthog/internal/context'
17
18
 
18
19
  module PostHog
19
20
  class Client
@@ -185,9 +186,13 @@ module PostHog
185
186
  # events in PostHog are deduplicated by the
186
187
  # combination of teamId, timestamp date,
187
188
  # event name, distinct id, and UUID
189
+ # @note If `:distinct_id` is omitted, request/context distinct_id is used when
190
+ # available; otherwise a UUID is generated and the event is marked personless
191
+ # with `$process_person_profile: false`.
188
192
  # @macro common_attrs
189
193
  def capture(attrs)
190
194
  symbolize_keys! attrs
195
+ enrich_capture_attrs_with_context(attrs)
191
196
 
192
197
  # Precedence: an explicit `flags` snapshot always wins, regardless of
193
198
  # `send_feature_flags`. The snapshot guarantees the event carries the same
@@ -260,7 +265,8 @@ module PostHog
260
265
  # Captures an exception as an event
261
266
  #
262
267
  # @param [Exception, String, Object] exception The exception to capture, a string message, or exception-like object
263
- # @param [String] distinct_id The ID for the user (optional, defaults to a generated UUID)
268
+ # @param [String] distinct_id The ID for the user (optional, defaults to request/context distinct_id
269
+ # or a generated UUID)
264
270
  # @param [Hash] additional_properties Additional properties to include with the exception event (optional)
265
271
  # @param [PostHog::FeatureFlagEvaluations] flags A snapshot returned by {#evaluate_flags}.
266
272
  # Forwarded to the inner {#capture} call so the captured `$exception` event carries the
@@ -270,12 +276,8 @@ module PostHog
270
276
 
271
277
  return if exception_info.nil?
272
278
 
273
- no_distinct_id_was_provided = distinct_id.nil?
274
- distinct_id ||= SecureRandom.uuid
275
-
276
279
  properties = { '$exception_list' => [exception_info] }
277
280
  properties.merge!(additional_properties) if additional_properties && !additional_properties.empty?
278
- properties['$process_person_profile'] = false if no_distinct_id_was_provided
279
281
 
280
282
  event_data = {
281
283
  distinct_id: distinct_id,
@@ -364,15 +366,15 @@ module PostHog
364
366
  !!response
365
367
  end
366
368
 
367
- # @param [String] flag_key The unique flag key of the feature flag
369
+ # @param [String, Symbol] flag_key The unique flag key of the feature flag
368
370
  # @return [String] The decrypted value of the feature flag payload
369
371
  def get_remote_config_payload(flag_key)
370
- @feature_flags_poller.get_remote_config_payload(flag_key)
372
+ @feature_flags_poller.get_remote_config_payload(flag_key.to_s)
371
373
  end
372
374
 
373
375
  # Returns whether the given feature flag is enabled for the given user or not
374
376
  #
375
- # @param [String] key The key of the feature flag
377
+ # @param [String, Symbol] key The key of the feature flag
376
378
  # @param [String] distinct_id The distinct id of the user
377
379
  # @param [Hash] groups
378
380
  # @param [Hash] person_properties key-value pairs of properties to associate with the user.
@@ -455,7 +457,7 @@ module PostHog
455
457
  # @param [Hash] group_properties
456
458
  # @param [Boolean] only_evaluate_locally Skip the remote /flags call entirely
457
459
  # @param [Boolean] disable_geoip Stamped on captured access events
458
- # @param [Array<String>] flag_keys When set, scopes the underlying /flags
460
+ # @param [Array<String, Symbol>] flag_keys When set, scopes the underlying /flags
459
461
  # request to only these flag keys (sent as `flag_keys_to_evaluate`).
460
462
  # Distinct from {FeatureFlagEvaluations#only}, which filters the
461
463
  # already-fetched snapshot in memory.
@@ -598,7 +600,7 @@ module PostHog
598
600
  # @deprecated Use {#get_feature_flag_result} instead, which returns both the flag value and payload
599
601
  # and properly raises the $feature_flag_called event.
600
602
  #
601
- # @param [String] key The key of the feature flag
603
+ # @param [String, Symbol] key The key of the feature flag
602
604
  # @param [String] distinct_id The distinct id of the user
603
605
  # @option [String or boolean] match_value The value of the feature flag to be matched
604
606
  # @option [Hash] groups
@@ -623,6 +625,7 @@ module PostHog
623
625
  'instead — this consolidates flag evaluation into a single `/flags` request per ' \
624
626
  'incoming request.'
625
627
  )
628
+ key = key.to_s
626
629
  person_properties, group_properties = add_local_person_and_group_properties(distinct_id, groups,
627
630
  person_properties, group_properties)
628
631
  @feature_flags_poller.get_feature_flag_payload(key, distinct_id, match_value, groups, person_properties,
@@ -684,6 +687,39 @@ module PostHog
684
687
 
685
688
  private
686
689
 
690
+ def enrich_capture_attrs_with_context(attrs)
691
+ context = Internal::Context.current
692
+ explicit_properties = attrs[:properties]
693
+ properties_are_hash = explicit_properties.nil? || explicit_properties.is_a?(Hash)
694
+ context_properties = context&.properties || {}
695
+ if properties_are_hash
696
+ attrs[:properties] = Internal::Context.merge_properties(context_properties, explicit_properties || {})
697
+ end
698
+
699
+ return if present_id?(attrs[:distinct_id])
700
+
701
+ if present_id?(context&.distinct_id)
702
+ attrs[:distinct_id] = context.distinct_id
703
+ return
704
+ end
705
+
706
+ attrs[:distinct_id] = SecureRandom.uuid
707
+ return unless properties_are_hash
708
+ return if property_key?(explicit_properties, '$process_person_profile')
709
+
710
+ attrs[:properties]['$process_person_profile'] = false
711
+ end
712
+
713
+ def present_id?(value)
714
+ !(value.nil? || (value.is_a?(String) && value.empty?))
715
+ end
716
+
717
+ def property_key?(properties, key)
718
+ return false unless properties.is_a?(Hash)
719
+
720
+ properties.key?(key) || properties.key?(key.to_sym)
721
+ end
722
+
687
723
  # Shared by the legacy single-flag path ({#get_feature_flag_result}) and the
688
724
  # snapshot's access-recording. Owns dedup-key construction, the
689
725
  # per-distinct_id sent-flags cache, and the `$feature_flag_called` capture call.
@@ -725,6 +761,7 @@ module PostHog
725
761
  only_evaluate_locally: false,
726
762
  send_feature_flag_events: true
727
763
  )
764
+ key = key.to_s
728
765
  person_properties, group_properties = add_local_person_and_group_properties(
729
766
  distinct_id, groups, person_properties, group_properties
730
767
  )
@@ -125,6 +125,7 @@ module PostHog
125
125
 
126
126
  def get_flags(distinct_id, groups = {}, person_properties = {}, group_properties = {}, flag_keys = nil,
127
127
  disable_geoip = nil)
128
+ flag_keys = flag_keys.map(&:to_s) if flag_keys.is_a?(Array)
128
129
  request_data = {
129
130
  distinct_id: distinct_id,
130
131
  groups: groups,
@@ -160,7 +161,7 @@ module PostHog
160
161
  end
161
162
 
162
163
  def get_remote_config_payload(flag_key)
163
- _request_remote_config_payload(flag_key)
164
+ _request_remote_config_payload(flag_key.to_s)
164
165
  end
165
166
 
166
167
  def get_feature_flag(
@@ -171,6 +172,8 @@ module PostHog
171
172
  group_properties = {},
172
173
  only_evaluate_locally = false
173
174
  )
175
+ key = key.to_s
176
+
174
177
  # make sure they're loaded on first run
175
178
  load_feature_flags
176
179
 
@@ -363,6 +366,8 @@ module PostHog
363
366
  group_properties = {},
364
367
  only_evaluate_locally = false
365
368
  )
369
+ key = key.to_s
370
+
366
371
  if match_value.nil?
367
372
  match_value = get_feature_flag(
368
373
  key,
@@ -923,6 +928,7 @@ module PostHog
923
928
  def _compute_flag_payload_locally(key, match_value)
924
929
  return nil if @feature_flags_by_key.nil?
925
930
 
931
+ key = key.to_s
926
932
  response = nil
927
933
  if [true, false].include? match_value
928
934
  response = @feature_flags_by_key.dig(key, :filters, :payloads, match_value.to_s.to_sym)
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PostHog
4
+ module Internal
5
+ # Internal request/fiber-local context applied to capture calls.
6
+ # Uses Rails' isolated execution state when available, otherwise falls back
7
+ # to thread-local storage in the core SDK.
8
+ #
9
+ # This is intentionally not exposed as a public SDK API in Ruby yet. It exists
10
+ # to let framework integrations such as posthog-rails propagate request-scoped
11
+ # tracing headers to regular capture and exception events without making the
12
+ # server-side SDK globally stateful per user.
13
+ class Context
14
+ STORAGE_KEY = :posthog_context
15
+
16
+ attr_reader :distinct_id, :session_id, :properties
17
+
18
+ def initialize(distinct_id: nil, session_id: nil, properties: {})
19
+ @distinct_id = distinct_id
20
+ @session_id = session_id
21
+ @properties = properties ? properties.dup : {}
22
+ apply_session_property!
23
+ end
24
+
25
+ def self.current
26
+ if defined?(ActiveSupport::IsolatedExecutionState)
27
+ ActiveSupport::IsolatedExecutionState[STORAGE_KEY]
28
+ else
29
+ Thread.current[STORAGE_KEY]
30
+ end
31
+ end
32
+
33
+ def self.current=(context)
34
+ if defined?(ActiveSupport::IsolatedExecutionState)
35
+ ActiveSupport::IsolatedExecutionState[STORAGE_KEY] = context
36
+ else
37
+ Thread.current[STORAGE_KEY] = context
38
+ end
39
+ end
40
+
41
+ def self.with_context(data = nil, fresh: false, **kwargs)
42
+ previous_context = current
43
+ raise ArgumentError, 'with_context requires a block' unless block_given?
44
+
45
+ self.current = resolve(merge_data_and_kwargs(data, kwargs), previous_context, fresh: fresh)
46
+ yield
47
+ ensure
48
+ self.current = previous_context
49
+ end
50
+
51
+ def self.resolve(data, parent, fresh: false)
52
+ data = normalize_data(data)
53
+
54
+ parent_properties = fresh || parent.nil? ? {} : parent.properties
55
+ properties = merge_properties(parent_properties, data[:properties] || {})
56
+ if data[:session_id] && !session_property_key?(data[:properties])
57
+ properties.delete('$session_id')
58
+ properties.delete(:$session_id)
59
+ end
60
+
61
+ new(
62
+ distinct_id: data[:distinct_id] || (fresh || parent.nil? ? nil : parent.distinct_id),
63
+ session_id: data[:session_id] || (fresh || parent.nil? ? nil : parent.session_id),
64
+ properties: properties
65
+ )
66
+ end
67
+ private_class_method :resolve
68
+
69
+ def self.merge_data_and_kwargs(data, kwargs)
70
+ data ||= {}
71
+ raise ArgumentError, 'context data must be a Hash' unless data.is_a?(Hash)
72
+
73
+ data.merge(kwargs)
74
+ end
75
+ private_class_method :merge_data_and_kwargs
76
+
77
+ def self.merge_properties(base, overrides)
78
+ merged = (base || {}).dup
79
+ (overrides || {}).each do |key, value|
80
+ merged.delete(key.to_s) if key.is_a?(Symbol)
81
+ merged.delete(key.to_sym) if key.is_a?(String)
82
+ merged[key] = value
83
+ end
84
+ merged
85
+ end
86
+
87
+ def self.normalize_data(data)
88
+ data ||= {}
89
+ raise ArgumentError, 'context data must be a Hash' unless data.is_a?(Hash)
90
+
91
+ properties = data[:properties] || data['properties'] || {}
92
+ raise ArgumentError, 'context properties must be a Hash' unless properties.is_a?(Hash)
93
+
94
+ {
95
+ distinct_id: data[:distinct_id] || data['distinct_id'] || data[:distinctId] || data['distinctId'],
96
+ session_id: data[:session_id] || data['session_id'] || data[:sessionId] || data['sessionId'],
97
+ properties: properties
98
+ }
99
+ end
100
+ private_class_method :normalize_data
101
+
102
+ def self.session_property_key?(properties)
103
+ return false unless properties.is_a?(Hash)
104
+
105
+ properties.key?('$session_id') || properties.key?(:$session_id)
106
+ end
107
+ private_class_method :session_property_key?
108
+
109
+ def apply_session_property!
110
+ return if session_id.nil? || properties.key?('$session_id') || properties.key?(:$session_id)
111
+
112
+ properties['$session_id'] = session_id
113
+ end
114
+ private :apply_session_property!
115
+ end
116
+ end
117
+
118
+ private_constant :Internal
119
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PostHog
4
- VERSION = '3.8.0'
4
+ VERSION = '3.9.0'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: posthog-ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.8.0
4
+ version: 3.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - ''
@@ -46,6 +46,7 @@ files:
46
46
  - lib/posthog/feature_flags.rb
47
47
  - lib/posthog/field_parser.rb
48
48
  - lib/posthog/flag_definition_cache.rb
49
+ - lib/posthog/internal/context.rb
49
50
  - lib/posthog/logging.rb
50
51
  - lib/posthog/message_batch.rb
51
52
  - lib/posthog/noop_worker.rb