posthog-ruby 3.14.2 → 3.15.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: 5a92261df7b02c5a549f4969422323c5d2c911ec16990fdb4861acc2ca76c3f3
4
- data.tar.gz: bb5d161962343c95e628ddeb5e5bf053ad2d3c74737dbb4ae347e5a37077c82c
3
+ metadata.gz: ab01f90a9fbe05af027e92831b58e906eb38b30914230fd83a35cab28da40fcd
4
+ data.tar.gz: cf06c8facfe324712e096cedfeeef90c755705bcce79384cda2d7b5f4d675cf0
5
5
  SHA512:
6
- metadata.gz: 2c8a69d40d6dda952c2a3ef0d2b1def8ac9c2e6f8acf87e53aae5b62ca9f538628a87c1451475034917f8a003f2f7b901cbd8c41c16c5b2c0d17381a62bdc192
7
- data.tar.gz: a5be03ab5250513092e907238fe0ffcdcf8cdf6124a8cc20cd4eeabdd0123b8e7db9c88c0bf40d861eccfc10f551f0871ba29b075cbaf32172003d8cf4e9f9d6
6
+ metadata.gz: 6825662770b6a96fce094e2c46619fe142144ef8098db49c6a67b4f1faf54d731b65c8767f985413c91bf097767db84648fe2b2ba938ca4f788a0201ffa6002e
7
+ data.tar.gz: 8501399ef589da90e3e7c65629df24d99012240c718874a506fede7833225316a1c97881a8eca1df06a831eace6e9090e3b052686c7656c8cd9a3a1815e3a6b7
@@ -67,12 +67,16 @@ module PostHog
67
67
  # in seconds. Defaults to 30.
68
68
  # @option opts [Integer] :feature_flag_request_timeout_seconds How long to wait for feature flag evaluation,
69
69
  # in seconds. Defaults to 3.
70
+ # @option opts [Integer] :feature_flag_request_max_retries How many times to retry a flag request after a
71
+ # transient network error. Each retry sleeps on the calling thread before retrying, so this adds to
72
+ # worst-case latency. Defaults to 1. Set to 0 to disable retrying.
70
73
  # @option opts [Proc] :before_send A callback that receives the event hash and should return either a modified
71
74
  # hash to be sent to PostHog or nil to prevent the event from being sent. e.g. `before_send: ->(event) { event }`.
72
75
  # @option opts [Boolean] :disable_singleton_warning +true+ to suppress the warning when multiple clients share
73
76
  # the same API key. Use only when you intentionally need multiple clients. Defaults to +false+.
74
77
  # @option opts [Boolean] :skip_ssl_verification +true+ to disable SSL certificate verification for requests.
75
78
  # Intended only for local development or custom deployments.
79
+ # @option opts [Boolean] :compress_request Set to +false+ to disable gzip compression for batch uploads.
76
80
  # @option opts [Object] :flag_definition_cache_provider An object implementing the {FlagDefinitionCacheProvider}
77
81
  # interface for distributed flag definition caching.
78
82
  # @option opts [Boolean] :is_server +true+ to stamp captured events with `$is_server => true` so PostHog
@@ -106,7 +110,8 @@ module PostHog
106
110
  @transport = Transport.new(
107
111
  api_host: opts[:host],
108
112
  skip_ssl_verification: opts[:skip_ssl_verification],
109
- retries: 3
113
+ retries: 3,
114
+ compress_request: opts[:compress_request]
110
115
  )
111
116
  @sync_lock = Mutex.new
112
117
  end
@@ -140,7 +145,8 @@ module PostHog
140
145
  opts[:host],
141
146
  opts[:feature_flag_request_timeout_seconds] || Defaults::FeatureFlags::FLAG_REQUEST_TIMEOUT_SECONDS,
142
147
  opts[:on_error],
143
- flag_definition_cache_provider: opts[:flag_definition_cache_provider]
148
+ flag_definition_cache_provider: opts[:flag_definition_cache_provider],
149
+ feature_flag_request_max_retries: opts[:feature_flag_request_max_retries]
144
150
  )
145
151
  end
146
152
 
@@ -19,6 +19,10 @@ module PostHog
19
19
 
20
20
  module FeatureFlags
21
21
  FLAG_REQUEST_TIMEOUT_SECONDS = 3
22
+ # Number of retries for a flag request after a transient network error.
23
+ # Flag requests are stateless and cause no server-side mutation, so
24
+ # retrying is safe.
25
+ FLAG_REQUEST_MAX_RETRIES = 1
22
26
  end
23
27
 
24
28
  module Queue
@@ -5,6 +5,8 @@ require 'net/http'
5
5
  require 'json'
6
6
  require 'posthog/version'
7
7
  require 'posthog/logging'
8
+ require 'posthog/defaults'
9
+ require 'posthog/backoff_policy'
8
10
  require 'posthog/feature_flag'
9
11
  require 'posthog/flag_definition_cache'
10
12
  require 'digest'
@@ -34,6 +36,8 @@ module PostHog
34
36
  # @param feature_flag_request_timeout_seconds [Integer] Timeout for feature flag requests.
35
37
  # @param on_error [Proc, nil] Callback invoked as `on_error.call(status, error)`.
36
38
  # @param flag_definition_cache_provider [Object, nil] Optional {FlagDefinitionCacheProvider} implementation.
39
+ # @param feature_flag_request_max_retries [Integer, nil] Retries after a transient network error on a flag
40
+ # request. Defaults to {Defaults::FeatureFlags::FLAG_REQUEST_MAX_RETRIES}. Set to 0 to disable retrying.
37
41
  def initialize(
38
42
  polling_interval,
39
43
  personal_api_key,
@@ -41,7 +45,8 @@ module PostHog
41
45
  host,
42
46
  feature_flag_request_timeout_seconds,
43
47
  on_error = nil,
44
- flag_definition_cache_provider: nil
48
+ flag_definition_cache_provider: nil,
49
+ feature_flag_request_max_retries: nil
45
50
  )
46
51
  @polling_interval = polling_interval || 30
47
52
  @personal_api_key = personal_api_key
@@ -53,6 +58,8 @@ module PostHog
53
58
  @loaded_flags_successfully_once = Concurrent::AtomicBoolean.new
54
59
  @feature_flags_by_key = nil
55
60
  @feature_flag_request_timeout_seconds = feature_flag_request_timeout_seconds
61
+ @feature_flag_request_max_retries =
62
+ feature_flag_request_max_retries || Defaults::FeatureFlags::FLAG_REQUEST_MAX_RETRIES
56
63
  @on_error = on_error || proc { |status, error| }
57
64
  @quota_limited = Concurrent::AtomicBoolean.new(false)
58
65
  @flags_etag = Concurrent::AtomicReference.new(nil)
@@ -1240,12 +1247,24 @@ module PostHog
1240
1247
  _request(uri, req, @feature_flag_request_timeout_seconds)
1241
1248
  end
1242
1249
 
1243
- # rubocop:disable Lint/ShadowedException
1250
+ # Transient network errors that are safe to retry. Flag requests are
1251
+ # retry-safe (stateless reads and evaluations, no server-side mutation), so
1252
+ # a one-off blip (TCP retransmit, TLS jitter, an edge/proxy hiccup) should
1253
+ # be absorbed by a retry rather than surfaced to the caller.
1254
+ RETRYABLE_REQUEST_ERRORS = [
1255
+ Timeout::Error, # covers Net::OpenTimeout, Net::ReadTimeout, Net::WriteTimeout
1256
+ Errno::ECONNRESET,
1257
+ EOFError
1258
+ ].freeze
1259
+
1244
1260
  def _request(uri, request_object, timeout = nil, include_etag: false)
1245
1261
  request_object['User-Agent'] = "posthog-ruby/#{PostHog::VERSION}"
1246
1262
  request_timeout = timeout || 10
1263
+ backoff_policy = nil
1264
+ attempts = 0
1247
1265
 
1248
1266
  begin
1267
+ attempts += 1
1249
1268
  Net::HTTP.start(
1250
1269
  uri.hostname,
1251
1270
  uri.port,
@@ -1279,20 +1298,24 @@ module PostHog
1279
1298
  return error_response
1280
1299
  end
1281
1300
  end
1282
- rescue Timeout::Error,
1283
- Errno::EINVAL,
1284
- Errno::ECONNRESET,
1285
- EOFError,
1301
+ rescue *RETRYABLE_REQUEST_ERRORS => e
1302
+ if attempts <= @feature_flag_request_max_retries
1303
+ backoff_policy ||= BackoffPolicy.new
1304
+ interval = backoff_policy.next_interval.to_f / 1000
1305
+ logger.debug("Retrying request to #{_mask_tokens_in_url(uri.to_s)} after #{e.class} (attempt #{attempts})")
1306
+ sleep(interval)
1307
+ retry
1308
+ end
1309
+ logger.debug("Unable to complete request to #{_mask_tokens_in_url(uri.to_s)}")
1310
+ raise
1311
+ rescue Errno::EINVAL,
1286
1312
  Net::HTTPBadResponse,
1287
1313
  Net::HTTPHeaderSyntaxError,
1288
- Net::ReadTimeout,
1289
- Net::WriteTimeout,
1290
1314
  Net::ProtocolError
1291
- logger.debug("Unable to complete request to #{uri}")
1315
+ logger.debug("Unable to complete request to #{_mask_tokens_in_url(uri.to_s)}")
1292
1316
  raise
1293
1317
  end
1294
1318
  end
1295
- # rubocop:enable Lint/ShadowedException
1296
1319
 
1297
1320
  def _mask_tokens_in_url(url)
1298
1321
  url.gsub(/token=([^&]{10})[^&]*/, 'token=\1...')
@@ -27,6 +27,7 @@ module PostHog
27
27
  # @option options [Proc] :on_error Callback invoked as `on_error.call(status, error)`.
28
28
  # @option options [String] :host PostHog API host URL.
29
29
  # @option options [Boolean] :skip_ssl_verification Disable SSL certificate verification.
30
+ # @option options [Boolean] :compress_request Set to +false+ to disable gzip batch request bodies.
30
31
  def initialize(queue, api_key, options = {})
31
32
  symbolize_keys! options
32
33
  @queue = queue
@@ -42,7 +43,11 @@ module PostHog
42
43
  @flush_requested = false
43
44
  @shutdown = false
44
45
  @pid = Process.pid
45
- @transport_options = { api_host: options[:host], skip_ssl_verification: options[:skip_ssl_verification] }
46
+ @transport_options = {
47
+ api_host: options[:host],
48
+ skip_ssl_verification: options[:skip_ssl_verification],
49
+ compress_request: options[:compress_request]
50
+ }
46
51
  @transport = Transport.new(@transport_options)
47
52
  end
48
53
 
@@ -8,6 +8,7 @@ require 'posthog/backoff_policy'
8
8
  require 'net/http'
9
9
  require 'net/https'
10
10
  require 'json'
11
+ require 'zlib'
11
12
 
12
13
  module PostHog
13
14
  # HTTP transport used by the SDK workers.
@@ -28,6 +29,7 @@ module PostHog
28
29
  # @option options [Integer] :retries Number of retry attempts for retryable failures.
29
30
  # @option options [PostHog::BackoffPolicy] :backoff_policy Backoff policy used between retries.
30
31
  # @option options [Boolean] :skip_ssl_verification Disable SSL certificate verification.
32
+ # @option options [Boolean] :compress_request Whether to gzip batch request bodies. Defaults to +true+.
31
33
  def initialize(options = {})
32
34
  if options[:api_host]
33
35
  uri = URI.parse(options[:api_host])
@@ -44,6 +46,7 @@ module PostHog
44
46
  @path = options[:path] || PATH
45
47
  @retries = options[:retries] || RETRIES
46
48
  @backoff_policy = options[:backoff_policy] || PostHog::BackoffPolicy.new
49
+ @compress_request = options[:compress_request] != false
47
50
 
48
51
  http = Net::HTTP.new(options[:host], options[:port])
49
52
  http.use_ssl = options[:ssl]
@@ -144,22 +147,41 @@ module PostHog
144
147
  def send_request(api_key, batch)
145
148
  payload = JSON.generate(api_key: api_key, batch: batch)
146
149
 
147
- request = Net::HTTP::Post.new(@path, @headers)
150
+ request_path, request_headers, request_payload = build_request(@path, @headers, payload)
151
+ request = Net::HTTP::Post.new(request_path, request_headers)
148
152
 
149
153
  if self.class.stub
150
- logger.debug "stubbed request to #{@path}: " \
154
+ logger.debug "stubbed request to #{request_path}: " \
151
155
  "api key = #{api_key}, batch = #{JSON.generate(batch)}"
152
156
 
153
157
  [200, '{}']
154
158
  else
155
159
  @http_mutex.synchronize do
156
160
  @http.start unless @http.started? # Maintain a persistent connection
157
- response = @http.request(request, payload)
161
+ response = @http.request(request, request_payload)
158
162
  [response.code.to_i, response.body]
159
163
  end
160
164
  end
161
165
  end
162
166
 
167
+ def build_request(path, headers, payload)
168
+ return [path, headers, payload] unless @compress_request
169
+
170
+ compressed_payload = gzip_payload(payload)
171
+ return [path, headers, payload] unless compressed_payload
172
+
173
+ compressed_headers = headers.merge('Content-Encoding' => 'gzip')
174
+
175
+ [path, compressed_headers, compressed_payload]
176
+ end
177
+
178
+ def gzip_payload(payload)
179
+ Zlib.gzip(payload)
180
+ rescue Zlib::Error => e
181
+ logger.warn("gzip compression failed; sending uncompressed - #{e.message}")
182
+ nil
183
+ end
184
+
163
185
  class << self
164
186
  attr_writer :stub
165
187
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PostHog
4
- VERSION = '3.14.2'
4
+ VERSION = '3.15.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.14.2
4
+ version: 3.15.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - ''