posthog-ruby 3.12.0 → 3.12.2

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: b8e287772525236ba5312d91e3a5e9c0f71ba079dbe90e73108dea39b1d02d73
4
- data.tar.gz: d5f52b018111579ab218dbfeb3220b55bb8b4d953273ef4f216d07455578cc82
3
+ metadata.gz: 333bb9ea61d04be7324d0c46eb5dbfa2938a583f8fd88bba845ee6300d219702
4
+ data.tar.gz: eeceaa50cc3defed270dc48ad6863e555d3068a21929c0157f03743b943fdde2
5
5
  SHA512:
6
- metadata.gz: cc598260671fd298d28335308a66f5b4b4472e6c38cfd0697198ef61942f9bd9118897dc6c6b6d0e4e163d568094520f0ce08f40579af7c7b0549e6a83c46369
7
- data.tar.gz: 6227c32f2dd2c10f14c48953b4bc86e7ee7d4ddb0a7b04b28a8d5917e932a11c95f615d8ad21bdc1cb02ba6b01bd36a545c062cefb30b7201f32e55251a76120
6
+ metadata.gz: a35cbbd21df7355144bf1f1c6f25e36fc1ab3341d09a326941f8d4f41b7e40766019a4935e6930f8874bf18fe9bf4b0a265879c8f6e87f5a4eb930ddac33c923
7
+ data.tar.gz: 9269f70dd837716bcbf638e626a6928dcc32f7f9c3e4e49b53777cac2d4347b050b65d1393c70ff750cdee8a25baad5607467b2da4454d272a0fd1ad6f4b311d
@@ -84,10 +84,13 @@ module PostHog
84
84
  opts[:host] = normalize_host_option(opts[:host])
85
85
 
86
86
  @queue = Queue.new
87
+ @queue_mutex = Mutex.new
87
88
  @api_key = opts[:api_key]
88
89
  @disabled = @api_key.nil? || @api_key.empty?
89
90
  @max_queue_size = opts[:max_queue_size] || Defaults::Queue::MAX_SIZE
90
91
  @worker_mutex = Mutex.new
92
+ @shutdown_mutex = Mutex.new
93
+ @shutdown = false
91
94
  @sync_mode = opts[:sync_mode] == true && !opts[:test_mode] && !@disabled
92
95
  @on_error = opts[:on_error] || proc { |status, error| }
93
96
  @worker = if opts[:test_mode] || @disabled
@@ -139,8 +142,9 @@ module PostHog
139
142
  )
140
143
  end
141
144
 
145
+ @distinct_id_has_sent_flag_calls_mutex = Mutex.new
142
146
  @distinct_id_has_sent_flag_calls = SizeLimitedHash.new(Defaults::MAX_HASH_SIZE) do |hash, key|
143
- hash[key] = []
147
+ hash[key] = SizeLimitedArray.new(Defaults::MAX_HASH_SIZE)
144
148
  end
145
149
 
146
150
  @before_send = opts[:before_send]
@@ -173,12 +177,14 @@ module PostHog
173
177
  #
174
178
  # @return [void]
175
179
  def clear
176
- @queue.clear
180
+ @queue_mutex.synchronize { @queue.clear }
177
181
  end
178
182
 
179
183
  # @!macro common_attrs
180
- # @option attrs [String] :message_id ID that uniquely
181
- # identifies a message across the API. (optional)
184
+ # @option attrs [String] :message_id Deprecated. Use `:uuid` instead. If `:uuid` is absent or
185
+ # invalid and `:message_id` is a valid UUID, it is sent as `uuid` for backwards compatibility.
186
+ # If neither value is valid, the SDK generates a `uuid`. SDK metadata is sent as `$lib` and
187
+ # `$lib_version` properties.
182
188
  # @option attrs [Time] :timestamp When the event occurred (optional)
183
189
  # @option attrs [String] :distinct_id The ID for this user in your database
184
190
 
@@ -757,6 +763,16 @@ module PostHog
757
763
  #
758
764
  # @return [void]
759
765
  def shutdown
766
+ already_shutdown = @shutdown_mutex.synchronize do
767
+ if @shutdown
768
+ true
769
+ else
770
+ @shutdown = true
771
+ false
772
+ end
773
+ end
774
+ return if already_shutdown
775
+
760
776
  self.class._decrement_instance_count(@api_key) unless @disabled
761
777
  @feature_flags_poller&.shutdown_poller
762
778
  flush
@@ -764,6 +780,7 @@ module PostHog
764
780
  @sync_lock.synchronize { @transport&.shutdown }
765
781
  else
766
782
  @worker&.shutdown
783
+ @worker_thread&.join(1)
767
784
  end
768
785
  end
769
786
 
@@ -821,7 +838,16 @@ module PostHog
821
838
  ''
822
839
  end
823
840
  reported_key = "#{key}_#{response_repr}#{groups_repr}"
824
- return if @distinct_id_has_sent_flag_calls[distinct_id].include?(reported_key)
841
+ should_capture = @distinct_id_has_sent_flag_calls_mutex.synchronize do
842
+ sent_keys = @distinct_id_has_sent_flag_calls[distinct_id]
843
+ if sent_keys.include?(reported_key)
844
+ false
845
+ else
846
+ sent_keys << reported_key
847
+ true
848
+ end
849
+ end
850
+ return unless should_capture
825
851
 
826
852
  msg = {
827
853
  distinct_id: distinct_id,
@@ -832,7 +858,6 @@ module PostHog
832
858
  msg[:disable_geoip] = disable_geoip unless disable_geoip.nil?
833
859
 
834
860
  capture(msg)
835
- @distinct_id_has_sent_flag_calls[distinct_id] << reported_key
836
861
  end
837
862
 
838
863
  def _feature_flag_evaluations_host
@@ -921,23 +946,27 @@ module PostHog
921
946
  #
922
947
  # returns Boolean of whether the item was added to the queue.
923
948
  def enqueue(action)
924
- return false if @disabled
949
+ return false if @disabled || shutdown?
925
950
 
926
951
  action = process_before_send(action)
927
952
  return false if action.nil? || action.empty?
928
953
 
929
- # add our request id for tracing purposes
930
- action[:messageId] ||= uid
931
-
932
954
  if @sync_mode
933
955
  send_sync(action)
934
956
  return true
935
957
  end
936
958
 
937
- if @queue.length < @max_queue_size
938
- @queue << action
939
- ensure_worker_running
959
+ queued = @queue_mutex.synchronize do
960
+ if @queue.length < @max_queue_size
961
+ @queue << action
962
+ true
963
+ else
964
+ false
965
+ end
966
+ end
940
967
 
968
+ if queued
969
+ ensure_worker_running
941
970
  true
942
971
  else
943
972
  logger.warn(
@@ -995,6 +1024,10 @@ module PostHog
995
1024
  @worker_thread&.alive?
996
1025
  end
997
1026
 
1027
+ def shutdown?
1028
+ @shutdown_mutex.synchronize { @shutdown }
1029
+ end
1030
+
998
1031
  def add_local_person_and_group_properties(distinct_id, groups, person_properties, group_properties)
999
1032
  groups ||= {}
1000
1033
  person_properties ||= {}
@@ -1250,7 +1250,9 @@ module PostHog
1250
1250
  uri.hostname,
1251
1251
  uri.port,
1252
1252
  use_ssl: uri.scheme == 'https',
1253
- read_timeout: request_timeout
1253
+ open_timeout: request_timeout,
1254
+ read_timeout: request_timeout,
1255
+ write_timeout: request_timeout
1254
1256
  ) do |http|
1255
1257
  res = http.request(request_object)
1256
1258
  status_code = res.code.to_i
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'securerandom'
4
+
3
5
  require 'posthog/logging'
4
6
 
5
7
  module PostHog
@@ -25,7 +27,6 @@ module PostHog
25
27
  event = fields[:event]
26
28
  properties = fields[:properties] || {}
27
29
  groups = fields[:groups]
28
- uuid = fields[:uuid]
29
30
  check_presence!(event, 'event')
30
31
  check_is_hash!(properties, 'properties')
31
32
 
@@ -36,8 +37,6 @@ module PostHog
36
37
 
37
38
  isoify_dates! properties
38
39
 
39
- common['uuid'] = uuid if valid_uuid_for_event_props? uuid
40
-
41
40
  common.merge(
42
41
  {
43
42
  type: 'capture',
@@ -130,12 +129,13 @@ module PostHog
130
129
  #
131
130
  # - "timestamp"
132
131
  # - "distinct_id"
133
- # - "message_id"
132
+ # - "message_id" (deprecated; normalized to "uuid" when it is a valid UUID)
134
133
  # - "send_feature_flags"
134
+ #
135
+ # A new "uuid" is generated when neither "uuid" nor "message_id" is valid.
135
136
  def parse_common_fields(fields)
136
137
  timestamp = fields[:timestamp] || Time.new
137
138
  distinct_id = fields[:distinct_id]
138
- message_id = fields[:message_id].to_s if fields[:message_id]
139
139
  send_feature_flags = fields[:send_feature_flags]
140
140
 
141
141
  check_timestamp! timestamp
@@ -151,13 +151,12 @@ module PostHog
151
151
 
152
152
  parsed = {
153
153
  timestamp: datetime_in_iso8601(timestamp),
154
- library: 'posthog-ruby',
155
- library_version: PostHog::VERSION.to_s,
156
- messageId: message_id,
157
154
  distinct_id: distinct_id,
158
155
  properties: properties
159
156
  }
160
157
 
158
+ parsed['uuid'] = normalized_uuid(fields)
159
+
161
160
  if send_feature_flags && fields[:feature_variants]
162
161
  feature_variants = fields[:feature_variants]
163
162
  active_feature_variants = {}
@@ -190,6 +189,16 @@ module PostHog
190
189
  raise ArgumentError, "#{name} must be a Hash" unless obj.is_a? Hash
191
190
  end
192
191
 
192
+ def normalized_uuid(fields)
193
+ uuid = fields[:uuid]
194
+ return uuid if uuid && valid_uuid_for_event_props?(uuid)
195
+
196
+ message_id = fields[:message_id]
197
+ return message_id if message_id && valid_uuid_for_event_props?(message_id)
198
+
199
+ SecureRandom.uuid
200
+ end
201
+
193
202
  # @param [Object] uuid - the UUID to validate, user provided, so we don't know the type
194
203
  # @return [TrueClass, FalseClass] - true if the UUID is valid or absent, false otherwise
195
204
  def valid_uuid_for_event_props?(uuid)
@@ -34,6 +34,8 @@ module PostHog
34
34
  batch_size = options[:batch_size] || Defaults::MessageBatch::MAX_SIZE
35
35
  @batch = MessageBatch.new(batch_size)
36
36
  @lock = Mutex.new
37
+ @shutdown_mutex = Mutex.new
38
+ @shutdown = false
37
39
  @transport = Transport.new api_host: options[:host], skip_ssl_verification: options[:skip_ssl_verification]
38
40
  end
39
41
 
@@ -41,26 +43,32 @@ module PostHog
41
43
  #
42
44
  # @return [void]
43
45
  def run
44
- until Thread.current[:should_exit]
46
+ until shutdown?
45
47
  return if @queue.empty?
46
48
 
47
49
  @lock.synchronize do
48
50
  consume_message_from_queue! until @batch.full? || @queue.empty?
49
51
  end
50
52
 
51
- unless @batch.empty?
52
- res = @transport.send @api_key, @batch
53
- @on_error.call(res.status, res.error) unless res.status == 200
53
+ begin
54
+ unless @batch.empty?
55
+ res = @transport.send @api_key, @batch
56
+ handle_error(res.status, res.error) unless res.status == 200
57
+ end
58
+ ensure
59
+ @lock.synchronize { @batch.clear }
54
60
  end
55
-
56
- @lock.synchronize { @batch.clear }
57
61
  end
58
62
  ensure
63
+ # Worker threads exit when the queue is drained and are restarted for the
64
+ # next burst of events. Close the persistent connection on each exit and
65
+ # let Transport reconnect lazily when a future worker sends another batch.
59
66
  @transport.shutdown
60
67
  end
61
68
 
62
69
  # @return [void]
63
70
  def shutdown
71
+ @shutdown_mutex.synchronize { @shutdown = true }
64
72
  @transport.shutdown
65
73
  end
66
74
 
@@ -74,10 +82,20 @@ module PostHog
74
82
 
75
83
  private
76
84
 
85
+ def shutdown?
86
+ @shutdown_mutex.synchronize { @shutdown }
87
+ end
88
+
77
89
  def consume_message_from_queue!
78
90
  @batch << @queue.pop
79
91
  rescue MessageBatch::JSONGenerationError => e
80
- @on_error.call(-1, e.to_s)
92
+ handle_error(-1, e.to_s)
93
+ end
94
+
95
+ def handle_error(status, error)
96
+ @on_error.call(status, error)
97
+ rescue StandardError => e
98
+ logger.error("Error in on_error callback: #{e.message}")
81
99
  end
82
100
  end
83
101
  end
@@ -52,6 +52,7 @@ module PostHog
52
52
  http.verify_mode = OpenSSL::SSL::VERIFY_NONE if options[:skip_ssl_verification]
53
53
 
54
54
  @http = http
55
+ @http_mutex = Mutex.new
55
56
  end
56
57
 
57
58
  # Sends a batch of messages to the API
@@ -91,7 +92,9 @@ module PostHog
91
92
  #
92
93
  # @return [void]
93
94
  def shutdown
94
- @http.finish if @http.started?
95
+ @http_mutex.synchronize do
96
+ @http.finish if @http.started?
97
+ end
95
98
  end
96
99
 
97
100
  private
@@ -149,9 +152,11 @@ module PostHog
149
152
 
150
153
  [200, '{}']
151
154
  else
152
- @http.start unless @http.started? # Maintain a persistent connection
153
- response = @http.request(request, payload)
154
- [response.code.to_i, response.body]
155
+ @http_mutex.synchronize do
156
+ @http.start unless @http.started? # Maintain a persistent connection
157
+ response = @http.request(request, payload)
158
+ [response.code.to_i, response.body]
159
+ end
155
160
  end
156
161
  end
157
162
 
data/lib/posthog/utils.rb CHANGED
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'securerandom'
4
-
5
3
  module PostHog
6
4
  class InconclusiveMatchError < StandardError
7
5
  end
@@ -60,16 +58,6 @@ module PostHog
60
58
  hash.replace isoify_dates hash
61
59
  end
62
60
 
63
- # public: Returns a uid string
64
- #
65
- def uid
66
- arr = SecureRandom.random_bytes(16).unpack('NnnnnN')
67
- arr[2] = (arr[2] & 0x0fff) | 0x4000
68
- arr[3] = (arr[3] & 0x3fff) | 0x8000
69
-
70
- '%08x-%04x-%04x-%04x-%04x%08x' % arr # rubocop:disable Style/FormatStringToken, Style/FormatString
71
- end
72
-
73
61
  def datetime_in_iso8601(datetime)
74
62
  case datetime
75
63
  when Time
@@ -166,5 +154,24 @@ module PostHog
166
154
  super
167
155
  end
168
156
  end
157
+
158
+ # Array that drops the oldest item when it reaches a maximum length.
159
+ #
160
+ # @api private
161
+ class SizeLimitedArray < Array
162
+ # @param max_length [Integer]
163
+ def initialize(max_length)
164
+ super()
165
+ @max_length = max_length
166
+ end
167
+
168
+ # @param value [Object]
169
+ # @return [Array]
170
+ def <<(value)
171
+ shift if length >= @max_length
172
+ super
173
+ end
174
+ end
175
+ private_constant :SizeLimitedArray
169
176
  end
170
177
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PostHog
4
- VERSION = '3.12.0'
4
+ VERSION = '3.12.2'
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.12.0
4
+ version: 3.12.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - ''