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 +4 -4
- data/lib/posthog/client.rb +47 -10
- data/lib/posthog/feature_flags.rb +7 -1
- data/lib/posthog/internal/context.rb +119 -0
- data/lib/posthog/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3f033107e265e546dca78bfbf8f6adb3552e5803ac7dec0bf822afb814e7658d
|
|
4
|
+
data.tar.gz: efe43054d73bfce034973fc12a01301f3b935cfacb1e7e86358332a9879f8911
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e1e3e4e0e5712ab73c8c0c11ce5a5869df0dc95f293b151f81d7fc715844e36867d3aeed393427494789bf8b35f2d96f42c703085d6be405fe1168feaa7daa82
|
|
7
|
+
data.tar.gz: 1347919dcaeda0b7ad237964ae00acc34d4b6e4b9273d3335ce6e4666f09471d8d8fefb53c13d51e345cf3b7c8e467b813b14cbbdad74062dbabbc12fbeadc26
|
data/lib/posthog/client.rb
CHANGED
|
@@ -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
|
|
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
|
data/lib/posthog/version.rb
CHANGED
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.
|
|
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
|