posthog-ruby 3.8.1 → 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: 4400cef0c43dd5e79c18f38712b6f0033271f798ad8f97362338516ab491185a
4
- data.tar.gz: c789f72bcd04df210a74e79d9069e20a5a7f02b0b9b48eae344f58a33c348c8e
3
+ metadata.gz: 3f033107e265e546dca78bfbf8f6adb3552e5803ac7dec0bf822afb814e7658d
4
+ data.tar.gz: efe43054d73bfce034973fc12a01301f3b935cfacb1e7e86358332a9879f8911
5
5
  SHA512:
6
- metadata.gz: fc6f2f8cfd188881c9aec1ab5867bf555ebfc8fdf12f87e2fb086884e75968e268df8346b1463a563bb48529f6f7c6326366718885c6e6584aa140ff5aa15f87
7
- data.tar.gz: 7b6c516b339b0beaa8a0980d5e1d02027e826065f6bb28c85817344227cced2500b95e359bfe8955af952f553dc9b6cee958be8ffe2f52629d1bd1cf3df5e6f6
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,
@@ -685,6 +687,39 @@ module PostHog
685
687
 
686
688
  private
687
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
+
688
723
  # Shared by the legacy single-flag path ({#get_feature_flag_result}) and the
689
724
  # snapshot's access-recording. Owns dedup-key construction, the
690
725
  # per-distinct_id sent-flags cache, and the `$feature_flag_called` capture call.
@@ -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.1'
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.1
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