statsig 1.24.5 → 1.25.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: 1565b50f9e48bfcd6c456bc8e1681a9adbd523287f656b042c3cce803063d3b6
4
- data.tar.gz: 8dbf5c5879173b3c44e0e155c8e6e649af5e0cb184e1fdd18a47cc8875fb32c0
3
+ metadata.gz: 267f2a16a3bb05a6b046d493bfe98f00b7c083f387eba7862641801ec0579b2b
4
+ data.tar.gz: a20de08fe3b12bcba40795ec1ff53ce5d3c2c27aca3ab41d72541a49e730414c
5
5
  SHA512:
6
- metadata.gz: f8c6e02873f9274b49a42fe94f21d67eec6d2ba720841d107d1b0b3e7f39b3a208fc4bae478337829359e25d109f5e7409dffbd143e802ab5e1f1c2b0d08fcf0
7
- data.tar.gz: f75079d1ba3837d66a8386aa106d2bb4f75e78371dcc53cf09e1921610841d81a958e351e1f1a2f5786ebf0c44c43215a709edcb1e21dd45b563f8b2f4f8004d
6
+ metadata.gz: e7afa3f47ea19f3d759a648f12bd98c8e56c1c757f2c86bc72859ace8ca081e205af90f315cd26c08598fed408a8b4163b452e2312066120d5ead9644f1028ec
7
+ data.tar.gz: d671a37cf7ad21212d71ad90c8fddab13110641a278c475c6f4cec3ff922c0d6b6bceaa1367b902933db481be56601af2ab203c03768a65e9dd959fb900a2438
data/lib/evaluator.rb CHANGED
@@ -6,7 +6,7 @@ require 'evaluation_helpers'
6
6
  require 'client_initialize_helpers'
7
7
  require 'spec_store'
8
8
  require 'time'
9
- require 'user_agent_parser'
9
+ require 'ua_parser'
10
10
  require 'evaluation_details'
11
11
  require 'user_agent_parser/operating_system'
12
12
 
@@ -19,8 +19,8 @@ module Statsig
19
19
 
20
20
  def initialize(network, options, error_callback, init_diagnostics = nil)
21
21
  @spec_store = Statsig::SpecStore.new(network, options, error_callback, init_diagnostics)
22
- @ua_parser = UserAgentParser::Parser.new
23
- CountryLookup.initialize
22
+ UAParser.initialize_async
23
+ CountryLookup.initialize_async
24
24
 
25
25
  @gate_overrides = {}
26
26
  @config_overrides = {}
@@ -447,16 +447,18 @@ module Statsig
447
447
  ua = get_value_from_user(user, 'userAgent')
448
448
  return nil unless ua.is_a?(String)
449
449
 
450
- parsed = @ua_parser.parse ua
451
- os = parsed.os
452
450
  case field.downcase
453
451
  when 'os_name', 'osname'
452
+ os = UAParser.parse_os(ua)
454
453
  return os&.family
455
454
  when 'os_version', 'osversion'
455
+ os = UAParser.parse_os(ua)
456
456
  return os&.version unless os&.version.nil?
457
457
  when 'browser_name', 'browsername'
458
+ parsed = UAParser.parse_ua(ua)
458
459
  return parsed.family
459
460
  when 'browser_version', 'browserversion'
461
+ parsed = UAParser.parse_ua(ua)
460
462
  return parsed.version.to_s
461
463
  else
462
464
  nil
data/lib/layer.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  # typed: false
2
2
 
3
+ require 'sorbet-runtime'
3
4
  ##
4
5
  # Contains the current values from Statsig.
5
6
  # Will contain layer default values for all shared parameters in that layer.
data/lib/network.rb CHANGED
@@ -33,6 +33,8 @@ module Statsig
33
33
  @local_mode = options.local_mode
34
34
  @timeout = options.network_timeout
35
35
  @backoff_multiplier = backoff_mult
36
+ @post_logs_retry_backoff = options.post_logs_retry_backoff
37
+ @post_logs_retry_limit = options.post_logs_retry_limit
36
38
  @session_id = SecureRandom.uuid
37
39
  end
38
40
 
@@ -57,18 +59,26 @@ module Statsig
57
59
  if @timeout
58
60
  http = http.timeout(@timeout)
59
61
  end
62
+ backoff_adjusted = backoff > 10 ? backoff += Random.rand(10) : backoff # to deter overlap
63
+ if @post_logs_retry_backoff
64
+ if @post_logs_retry_backoff.is_a? Integer
65
+ backoff_adjusted = @post_logs_retry_backoff
66
+ else
67
+ backoff_adjusted = @post_logs_retry_backoff.call(retries)
68
+ end
69
+ end
60
70
  begin
61
71
  res = http.post(@api + endpoint, body: body)
62
72
  rescue StandardError => e
63
73
  ## network error retry
64
74
  return nil, e unless retries > 0
65
- sleep backoff
75
+ sleep backoff_adjusted
66
76
  return post_helper(endpoint, body, retries - 1, backoff * @backoff_multiplier)
67
77
  end
68
78
  return res, nil if res.status.success?
69
79
  return nil, NetworkError.new("Got an exception when making request to #{@api + endpoint}: #{res.to_s}", res.status.to_i) unless retries > 0 && $retry_codes.include?(res.code)
70
80
  ## status code retry
71
- sleep backoff
81
+ sleep backoff_adjusted
72
82
  post_helper(endpoint, body, retries - 1, backoff * @backoff_multiplier)
73
83
  end
74
84
 
@@ -97,7 +107,7 @@ module Statsig
97
107
  def post_logs(events)
98
108
  begin
99
109
  json_body = JSON.generate({ 'events' => events, 'statsigMetadata' => Statsig.get_statsig_metadata })
100
- post_helper('log_event', json_body, 5)
110
+ post_helper('log_event', json_body, @post_logs_retry_limit)
101
111
  rescue
102
112
  end
103
113
  end
data/lib/spec_store.rb CHANGED
@@ -32,6 +32,7 @@ module Statsig
32
32
 
33
33
  @id_list_thread_pool = Concurrent::FixedThreadPool.new(
34
34
  options.idlist_threadpool_size,
35
+ name: 'statsig-idlist',
35
36
  max_queue: 100,
36
37
  fallback_policy: :discard,
37
38
  )
@@ -309,6 +310,16 @@ module Statsig
309
310
 
310
311
  init_diagnostics&.mark("get_id_lists", "start", "process", new_id_lists.length)
311
312
 
313
+ delete_lists = []
314
+ local_id_lists.each do |list_name, list|
315
+ unless new_id_lists.key? list_name
316
+ delete_lists.push list_name
317
+ end
318
+ end
319
+ delete_lists.each do |list_name|
320
+ local_id_lists.delete list_name
321
+ end
322
+
312
323
  new_id_lists.each do |list_name, list|
313
324
  new_list = IDList.new(list)
314
325
  local_list = get_id_list(list_name)
@@ -346,21 +357,7 @@ module Statsig
346
357
  end
347
358
 
348
359
  result = Concurrent::Promise.all?(*tasks).execute.wait(@id_lists_sync_interval)
349
- if result.state != :fulfilled
350
- init_diagnostics&.mark("get_id_lists", "end", "process", false)
351
- return # timed out
352
- end
353
-
354
- delete_lists = []
355
- local_id_lists.each do |list_name, list|
356
- unless new_id_lists.key? list_name
357
- delete_lists.push list_name
358
- end
359
- end
360
- delete_lists.each do |list_name|
361
- local_id_lists.delete list_name
362
- end
363
- init_diagnostics&.mark("get_id_lists", "end", "process", true)
360
+ init_diagnostics&.mark("get_id_lists", "end", "process", result.state == :fulfilled)
364
361
  end
365
362
 
366
363
  def get_single_id_list_from_adapter(list)
data/lib/statsig.rb CHANGED
@@ -148,7 +148,7 @@ module Statsig
148
148
  @shared_instance&.get_layer(user, layer_name, StatsigDriver::GetLayerOptions.new(log_exposure: false))
149
149
  end
150
150
 
151
- sig { params(user: StatsigUser, layer_name: String, parameter_name: String).returns(Layer) }
151
+ sig { params(user: StatsigUser, layer_name: String, parameter_name: String).void }
152
152
  ##
153
153
  # Logs an exposure event for the parameter in the given layer
154
154
  #
@@ -227,7 +227,7 @@ module Statsig
227
227
  def self.get_statsig_metadata
228
228
  {
229
229
  'sdkType' => 'ruby-server',
230
- 'sdkVersion' => '1.24.5',
230
+ 'sdkVersion' => '1.24.6',
231
231
  }
232
232
  end
233
233
 
@@ -170,7 +170,6 @@ class StatsigDriver
170
170
  event.user = user
171
171
  event.value = value
172
172
  event.metadata = metadata
173
- event.statsig_metadata = Statsig.get_statsig_metadata
174
173
  @logger.log_event(event)
175
174
  })
176
175
  end
@@ -199,6 +198,7 @@ class StatsigDriver
199
198
  # @return [Hash]
200
199
  def get_client_initialize_response(user)
201
200
  @err_boundary.capture(-> {
201
+ validate_user(user)
202
202
  normalize_user(user)
203
203
  @evaluator.get_client_initialize_response(user)
204
204
  }, -> { nil })
data/lib/statsig_event.rb CHANGED
@@ -1,9 +1,6 @@
1
1
  # typed: true
2
2
  class StatsigEvent
3
- attr_accessor :value
4
- attr_accessor :metadata
5
- attr_accessor :statsig_metadata
6
- attr_accessor :secondary_exposures
3
+ attr_accessor :value, :metadata, :statsig_metadata, :secondary_exposures
7
4
  attr_reader :user
8
5
 
9
6
  def initialize(event_name)
@@ -13,6 +10,7 @@ class StatsigEvent
13
10
  @secondary_exposures = nil
14
11
  @user = nil
15
12
  @time = (Time.now.to_f * 1000).to_i
13
+ @statsig_metadata = Statsig.get_statsig_metadata
16
14
  end
17
15
 
18
16
  def user=(value)
@@ -15,11 +15,12 @@ module Statsig
15
15
  @options = options
16
16
 
17
17
  @logging_pool = Concurrent::ThreadPoolExecutor.new(
18
- min_threads: [2, Concurrent.processor_count].min,
19
- max_threads: [2, Concurrent.processor_count].max,
18
+ name: 'statsig-logger',
19
+ min_threads: @options.logger_threadpool_size,
20
+ max_threads: @options.logger_threadpool_size,
20
21
  # max jobs pending before we start dropping
21
- max_queue: [2, Concurrent.processor_count].max * 5,
22
- fallback_policy: :discard,
22
+ max_queue: 100,
23
+ fallback_policy: :discard
23
24
  )
24
25
 
25
26
  @background_flush = periodic_flush
@@ -44,7 +45,6 @@ module Statsig
44
45
  }
45
46
  return false if not is_unique_exposure(user, $gate_exposure_event, metadata)
46
47
  event.metadata = metadata
47
- event.statsig_metadata = Statsig.get_statsig_metadata
48
48
 
49
49
  event.secondary_exposures = secondary_exposures.is_a?(Array) ? secondary_exposures : []
50
50
 
@@ -62,7 +62,6 @@ module Statsig
62
62
  }
63
63
  return false if not is_unique_exposure(user, $config_exposure_event, metadata)
64
64
  event.metadata = metadata
65
- event.statsig_metadata = Statsig.get_statsig_metadata
66
65
  event.secondary_exposures = secondary_exposures.is_a?(Array) ? secondary_exposures : []
67
66
 
68
67
  safe_add_eval_details(eval_details, event)
@@ -90,7 +89,6 @@ module Statsig
90
89
  }
91
90
  return false if not is_unique_exposure(user, $layer_exposure_event, metadata)
92
91
  event.metadata = metadata
93
- event.statsig_metadata = Statsig.get_statsig_metadata
94
92
  event.secondary_exposures = exposures.is_a?(Array) ? exposures : []
95
93
 
96
94
  safe_add_eval_details(config_evaluation.evaluation_details, event)
@@ -109,7 +107,7 @@ module Statsig
109
107
  Thread.new do
110
108
  loop do
111
109
  sleep @options.logging_interval_seconds
112
- flush
110
+ flush_async
113
111
  @interval += 1
114
112
  @deduper.clear if @interval % 2 == 0
115
113
  end
@@ -64,6 +64,11 @@ class StatsigOptions
64
64
  # default: 3
65
65
  attr_accessor :idlist_threadpool_size
66
66
 
67
+ sig { returns(Integer) }
68
+ # The number of threads allocated to posting event logs.
69
+ # default: 3
70
+ attr_accessor :logger_threadpool_size
71
+
67
72
  sig { returns(T::Boolean) }
68
73
  # Should diagnostics be logged. These include performance metrics for initialize.
69
74
  # default: false
@@ -79,6 +84,15 @@ class StatsigOptions
79
84
  # Number of seconds before a network call is timed out
80
85
  attr_accessor :network_timeout
81
86
 
87
+ sig { returns(Integer) }
88
+ # Number of times to retry sending a batch of failed log events
89
+ attr_accessor :post_logs_retry_limit
90
+
91
+ sig { returns(T.any(Method, Proc, Integer, NilClass)) }
92
+ # The number of seconds, or a function that returns the number of seconds based on the number of retries remaining
93
+ # which overrides the default backoff time between retries
94
+ attr_accessor :post_logs_retry_backoff
95
+
82
96
  sig do
83
97
  params(
84
98
  environment: T.any(T::Hash[String, String], NilClass),
@@ -92,9 +106,12 @@ class StatsigOptions
92
106
  rules_updated_callback: T.any(Method, Proc, NilClass),
93
107
  data_store: T.any(Statsig::Interfaces::IDataStore, NilClass),
94
108
  idlist_threadpool_size: Integer,
109
+ logger_threadpool_size: Integer,
95
110
  disable_diagnostics_logging: T::Boolean,
96
111
  disable_sorbet_logging_handlers: T::Boolean,
97
- network_timeout: T.any(Integer, NilClass)
112
+ network_timeout: T.any(Integer, NilClass),
113
+ post_logs_retry_limit: Integer,
114
+ post_logs_retry_backoff: T.any(Method, Proc, Integer, NilClass)
98
115
  ).void
99
116
  end
100
117
 
@@ -110,9 +127,12 @@ class StatsigOptions
110
127
  rules_updated_callback: nil,
111
128
  data_store: nil,
112
129
  idlist_threadpool_size: 3,
130
+ logger_threadpool_size: 3,
113
131
  disable_diagnostics_logging: false,
114
132
  disable_sorbet_logging_handlers: false,
115
- network_timeout: nil)
133
+ network_timeout: nil,
134
+ post_logs_retry_limit: 3,
135
+ post_logs_retry_backoff: nil)
116
136
  @environment = environment.is_a?(Hash) ? environment : nil
117
137
  @api_url_base = api_url_base
118
138
  @rulesets_sync_interval = rulesets_sync_interval
@@ -124,8 +144,11 @@ class StatsigOptions
124
144
  @rules_updated_callback = rules_updated_callback
125
145
  @data_store = data_store
126
146
  @idlist_threadpool_size = idlist_threadpool_size
147
+ @logger_threadpool_size = logger_threadpool_size
127
148
  @disable_diagnostics_logging = disable_diagnostics_logging
128
149
  @disable_sorbet_logging_handlers = disable_sorbet_logging_handlers
129
150
  @network_timeout = network_timeout
151
+ @post_logs_retry_limit = post_logs_retry_limit
152
+ @post_logs_retry_backoff = post_logs_retry_backoff
130
153
  end
131
154
  end
data/lib/ua_parser.rb ADDED
@@ -0,0 +1,57 @@
1
+ require 'user_agent_parser'
2
+
3
+ module UAParser
4
+ class Parser
5
+ def initialize
6
+ @ua_parser = UserAgentParser::Parser.new
7
+ end
8
+
9
+ def parse_os(*args)
10
+ @ua_parser.parse_os(*args)
11
+ end
12
+
13
+ def parse_ua(*args)
14
+ @ua_parser.parse_ua(*args)
15
+ end
16
+
17
+ def parse_device(*args)
18
+ @ua_parser.parse_device(*args)
19
+ end
20
+ end
21
+
22
+ def self.initialize
23
+ if !@initialize_bg_thread.nil? && @initialize_bg_thread.alive?
24
+ @initialize_bg_thread.kill.join
25
+ end
26
+ @parser = Parser.new
27
+ end
28
+
29
+ def self.initialize_async
30
+ if !@initialize_bg_thread.nil? && @initialize_bg_thread.alive?
31
+ @initialize_bg_thread.kill.join
32
+ end
33
+ @initialize_bg_thread = Thread.new { @parser = Parser.new }
34
+ @initialize_bg_thread
35
+ end
36
+
37
+ def self.parse_os(*args)
38
+ if @parser.nil?
39
+ initialize
40
+ end
41
+ @parser.parse_os(*args)
42
+ end
43
+
44
+ def self.parse_ua(*args)
45
+ if @parser.nil?
46
+ initialize
47
+ end
48
+ @parser.parse_ua(*args)
49
+ end
50
+
51
+ def self.parse_device(*args)
52
+ if @parser.nil?
53
+ initialize
54
+ end
55
+ @parser.parse_device(*args)
56
+ end
57
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: statsig
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.24.5
4
+ version: 1.25.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Statsig, Inc
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-04-03 00:00:00.000000000 Z
11
+ date: 2023-05-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -44,14 +44,14 @@ dependencies:
44
44
  requirements:
45
45
  - - "~>"
46
46
  - !ruby/object:Gem::Version
47
- version: '5.14'
47
+ version: 5.14.0
48
48
  type: :development
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
- version: '5.14'
54
+ version: 5.14.0
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: spy
57
57
  requirement: !ruby/object:Gem::Requirement
@@ -123,19 +123,89 @@ dependencies:
123
123
  - !ruby/object:Gem::Version
124
124
  version: '6.0'
125
125
  - !ruby/object:Gem::Dependency
126
- name: user_agent_parser
126
+ name: rubocop
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: 1.28.2
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: 1.28.2
139
+ - !ruby/object:Gem::Dependency
140
+ name: parallel_tests
127
141
  requirement: !ruby/object:Gem::Requirement
128
142
  requirements:
129
143
  - - "~>"
130
144
  - !ruby/object:Gem::Version
131
145
  version: '2.7'
132
- type: :runtime
146
+ type: :development
133
147
  prerelease: false
134
148
  version_requirements: !ruby/object:Gem::Requirement
135
149
  requirements:
136
150
  - - "~>"
137
151
  - !ruby/object:Gem::Version
138
152
  version: '2.7'
153
+ - !ruby/object:Gem::Dependency
154
+ name: simplecov
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - "~>"
158
+ - !ruby/object:Gem::Version
159
+ version: '0.21'
160
+ type: :development
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - "~>"
165
+ - !ruby/object:Gem::Version
166
+ version: '0.21'
167
+ - !ruby/object:Gem::Dependency
168
+ name: simplecov-lcov
169
+ requirement: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - "~>"
172
+ - !ruby/object:Gem::Version
173
+ version: 0.7.0
174
+ type: :development
175
+ prerelease: false
176
+ version_requirements: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - "~>"
179
+ - !ruby/object:Gem::Version
180
+ version: 0.7.0
181
+ - !ruby/object:Gem::Dependency
182
+ name: simplecov-cobertura
183
+ requirement: !ruby/object:Gem::Requirement
184
+ requirements:
185
+ - - "~>"
186
+ - !ruby/object:Gem::Version
187
+ version: '2.1'
188
+ type: :development
189
+ prerelease: false
190
+ version_requirements: !ruby/object:Gem::Requirement
191
+ requirements:
192
+ - - "~>"
193
+ - !ruby/object:Gem::Version
194
+ version: '2.1'
195
+ - !ruby/object:Gem::Dependency
196
+ name: user_agent_parser
197
+ requirement: !ruby/object:Gem::Requirement
198
+ requirements:
199
+ - - "~>"
200
+ - !ruby/object:Gem::Version
201
+ version: 2.15.0
202
+ type: :runtime
203
+ prerelease: false
204
+ version_requirements: !ruby/object:Gem::Requirement
205
+ requirements:
206
+ - - "~>"
207
+ - !ruby/object:Gem::Version
208
+ version: 2.15.0
139
209
  - !ruby/object:Gem::Dependency
140
210
  name: http
141
211
  requirement: !ruby/object:Gem::Requirement
@@ -160,16 +230,16 @@ dependencies:
160
230
  name: ip3country
161
231
  requirement: !ruby/object:Gem::Requirement
162
232
  requirements:
163
- - - '='
233
+ - - "~>"
164
234
  - !ruby/object:Gem::Version
165
- version: 0.1.1
235
+ version: 0.2.1
166
236
  type: :runtime
167
237
  prerelease: false
168
238
  version_requirements: !ruby/object:Gem::Requirement
169
239
  requirements:
170
- - - '='
240
+ - - "~>"
171
241
  - !ruby/object:Gem::Version
172
- version: 0.1.1
242
+ version: 0.2.1
173
243
  - !ruby/object:Gem::Dependency
174
244
  name: sorbet-runtime
175
245
  requirement: !ruby/object:Gem::Requirement
@@ -224,6 +294,7 @@ files:
224
294
  - lib/statsig_logger.rb
225
295
  - lib/statsig_options.rb
226
296
  - lib/statsig_user.rb
297
+ - lib/ua_parser.rb
227
298
  homepage: https://rubygems.org/gems/statsig
228
299
  licenses:
229
300
  - ISC