statsig 1.25.2 → 1.26.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: c7f3c8522d7d8062daf248fd667fd0d8152a4194339fde799814a5da44812eda
4
- data.tar.gz: 1fd5e67d4a75ab3b5b6c92c780841252a4610e18bc13310f9b11dbb583aaf16a
3
+ metadata.gz: 42f5328314de8727a2baaf89da54999627a5505ad95cd5ab816877741e02dd55
4
+ data.tar.gz: f6af1d20f540ceae4e24c70bdc702fbd9db3acc38776b71ecc50ded6ef40f67d
5
5
  SHA512:
6
- metadata.gz: 8f516ca6d221c9d789f1f358b550edeedf4203c01ead45374b97a3b81f4205d09f1c7c3e0214516cdcad314bacbdbde39a5df2053779679fe4df593c974c8300
7
- data.tar.gz: a90846ef3c050c7c0b8bddbabd19ed7bff6236a15ed941f55d1a305f4c9cb5ff35f595a3d7ea4ab1560d7f7bae11c669c29d17f8041da9125524669703dc5370
6
+ metadata.gz: 8c4375a325bf7dd1afaa584951b31eca2fd19422301b72397729fdca89e1ef8ae9b7a979aff562889cf49fc98fcd7db3edfc022ca66eb063b2680645c90df2db
7
+ data.tar.gz: ab23702707825b1ed5ada0df1982d765b97b75e8d813aeeaa9f24ae31974bb0cafa58249e82cc041eaa845bea9ee7639189c8778f8e21e0908b7ec3174d41661
@@ -1,4 +1,7 @@
1
1
  # typed: true
2
+
3
+ require_relative 'hash_utils'
4
+
2
5
  $empty_eval_result = {
3
6
  :gate_value => false,
4
7
  :json_value => {},
@@ -9,10 +12,12 @@ $empty_eval_result = {
9
12
 
10
13
  module ClientInitializeHelpers
11
14
  class ResponseFormatter
12
- def initialize(evaluator, user)
15
+ def initialize(evaluator, user, hash, client_sdk_key)
13
16
  @evaluator = evaluator
14
17
  @user = user
15
18
  @specs = evaluator.spec_store.get_raw_specs
19
+ @hash = hash
20
+ @client_sdk_key = client_sdk_key
16
21
  end
17
22
 
18
23
  def get_responses(key)
@@ -24,6 +29,13 @@ module ClientInitializeHelpers
24
29
  private
25
30
 
26
31
  def to_response(config_name, config_spec)
32
+ target_app_id = @evaluator.spec_store.get_app_id_for_sdk_key(@client_sdk_key)
33
+ config_target_apps = config_spec['targetAppIDs']
34
+
35
+ unless target_app_id.nil? || config_target_apps.nil? || config_target_apps.include?(target_app_id)
36
+ return nil
37
+ end
38
+
27
39
  eval_result = @evaluator.eval_spec(@user, config_spec)
28
40
  if eval_result.nil?
29
41
  return nil
@@ -33,6 +45,8 @@ module ClientInitializeHelpers
33
45
  :gate_value => eval_result.gate_value,
34
46
  :json_value => eval_result.json_value,
35
47
  :rule_id => eval_result.rule_id,
48
+ :group_name => eval_result.group_name,
49
+ :id_type => eval_result.id_type,
36
50
  :config_delegate => eval_result.config_delegate,
37
51
  :is_experiment_group => eval_result.is_experiment_group,
38
52
  :secondary_exposures => eval_result.secondary_exposures,
@@ -52,10 +66,14 @@ module ClientInitializeHelpers
52
66
  end
53
67
 
54
68
  result['value'] = safe_eval_result[:gate_value]
69
+ result["group_name"] = safe_eval_result[:group_name]
70
+ result["id_type"] = safe_eval_result[:id_type]
55
71
  when 'dynamic_config'
56
72
  id_type = config_spec['idType']
57
73
  result['value'] = safe_eval_result[:json_value]
58
74
  result["group"] = safe_eval_result[:rule_id]
75
+ result["group_name"] = safe_eval_result[:group_name]
76
+ result["id_type"] = safe_eval_result[:id_type]
59
77
  result["is_device_based"] = id_type.is_a?(String) && id_type.downcase == 'stableid'
60
78
  else
61
79
  return nil
@@ -67,6 +85,7 @@ module ClientInitializeHelpers
67
85
 
68
86
  if entity_type == 'layer'
69
87
  populate_layer_fields(config_spec, safe_eval_result, result)
88
+ result.delete('id_type') # not exposed for layer configs in /initialize
70
89
  end
71
90
 
72
91
  hashed_name = hash_name(config_name)
@@ -126,7 +145,14 @@ module ClientInitializeHelpers
126
145
  end
127
146
 
128
147
  def hash_name(name)
129
- Digest::SHA256.base64digest(name)
148
+ case @hash
149
+ when 'none'
150
+ return name
151
+ when 'sha256'
152
+ return Statsig::HashUtils.sha256(name)
153
+ when 'djb2'
154
+ return Statsig::HashUtils.djb2(name)
155
+ end
130
156
  end
131
157
  end
132
- end
158
+ end
data/lib/diagnostics.rb CHANGED
@@ -22,33 +22,37 @@ module Statsig
22
22
  key: String,
23
23
  action: String,
24
24
  step: T.any(String, NilClass),
25
- value: T.any(String, Integer, T::Boolean, NilClass),
26
- metadata: T.any(T::Hash[Symbol, T.untyped], NilClass)
25
+ tags: T::Hash[Symbol, T.untyped]
27
26
  ).void
28
27
  end
29
28
 
30
- def mark(key, action, step = nil, value = nil, metadata = nil)
31
- @markers.push({
32
- key: key,
33
- step: step,
34
- action: action,
35
- value: value,
36
- metadata: metadata,
37
- timestamp: (Time.now.to_f * 1000).to_i
38
- })
29
+ def mark(key, action, step, tags)
30
+ marker = {
31
+ key: key,
32
+ action: action,
33
+ timestamp: (Time.now.to_f * 1000).to_i
34
+ }
35
+ if !step.nil?
36
+ marker[:step] = step
37
+ end
38
+ tags.each do |key, val|
39
+ unless val.nil?
40
+ marker[key] = val
41
+ end
42
+ end
43
+ @markers.push(marker)
39
44
  end
40
45
 
41
46
  sig do
42
47
  params(
43
48
  key: String,
44
49
  step: T.any(String, NilClass),
45
- value: T.any(String, Integer, T::Boolean, NilClass),
46
- metadata: T.any(T::Hash[Symbol, T.untyped], NilClass)
50
+ tags: T::Hash[Symbol, T.untyped]
47
51
  ).returns(Tracker)
48
52
  end
49
- def track(key, step = nil, value = nil, metadata = nil)
50
- tracker = Tracker.new(self, key, step, metadata)
51
- tracker.start(value)
53
+ def track(key, step = nil, tags = {})
54
+ tracker = Tracker.new(self, key, step, tags)
55
+ tracker.start(**tags)
52
56
  tracker
53
57
  end
54
58
 
@@ -65,6 +69,10 @@ module Statsig
65
69
  @markers.clear
66
70
  end
67
71
 
72
+ def self.sample(rate)
73
+ rand(rate).zero?
74
+ end
75
+
68
76
  class Context
69
77
  INITIALIZE = 'initialize'.freeze
70
78
  CONFIG_SYNC = 'config_sync'.freeze
@@ -81,22 +89,22 @@ module Statsig
81
89
  diagnostics: Diagnostics,
82
90
  key: String,
83
91
  step: T.any(String, NilClass),
84
- metadata: T.any(T::Hash[Symbol, T.untyped], NilClass)
92
+ tags: T::Hash[Symbol, T.untyped]
85
93
  ).void
86
94
  end
87
- def initialize(diagnostics, key, step, metadata)
95
+ def initialize(diagnostics, key, step, tags = {})
88
96
  @diagnostics = diagnostics
89
97
  @key = key
90
98
  @step = step
91
- @metadata = metadata
99
+ @tags = tags
92
100
  end
93
101
 
94
- def start(value = nil)
95
- @diagnostics.mark(@key, 'start', @step, value, @metadata)
102
+ def start(**tags)
103
+ @diagnostics.mark(@key, 'start', @step, tags.nil? ? {} : tags.merge(@tags))
96
104
  end
97
105
 
98
- def end(value = nil)
99
- @diagnostics.mark(@key, 'end', @step, value, @metadata)
106
+ def end(**tags)
107
+ @diagnostics.mark(@key, 'end', @step, tags.nil? ? {} : tags.merge(@tags))
100
108
  end
101
109
  end
102
110
  end
@@ -9,70 +9,56 @@ module Statsig
9
9
  class ErrorBoundary
10
10
  extend T::Sig
11
11
 
12
- sig { returns(T.any(StatsigLogger, NilClass)) }
13
- attr_accessor :logger
14
-
15
12
  sig { params(sdk_key: String).void }
16
13
  def initialize(sdk_key)
17
14
  @sdk_key = sdk_key
18
15
  @seen = Set.new
19
16
  end
20
17
 
21
- def sample_diagnostics
22
- rand(10_000).zero?
23
- end
24
-
25
18
  def capture(task:, recover: -> {}, caller: nil)
26
- if !caller.nil? && Diagnostics::API_CALL_KEYS.include?(caller) && sample_diagnostics
27
- diagnostics = Diagnostics.new('api_call')
28
- tracker = diagnostics.track(caller)
29
- end
30
19
  begin
31
20
  res = task.call
32
- tracker&.end(true)
33
21
  rescue StandardError => e
34
- tracker&.end(false)
35
- if e.is_a?(Statsig::UninitializedError) or e.is_a?(Statsig::ValueError)
22
+ if e.is_a?(Statsig::UninitializedError) || e.is_a?(Statsig::ValueError)
36
23
  raise e
37
24
  end
25
+
38
26
  puts '[Statsig]: An unexpected exception occurred.'
39
- log_exception(e)
27
+ log_exception(e, tag: caller)
40
28
  res = recover.call
41
29
  end
42
- @logger&.log_diagnostics_event(diagnostics)
43
30
  return res
44
31
  end
45
32
 
46
33
  private
47
34
 
48
- def log_exception(exception)
49
- begin
50
- name = exception.class.name
51
- if @seen.include?(name)
52
- return
53
- end
54
-
55
- @seen << name
56
- meta = Statsig.get_statsig_metadata
57
- http = HTTP.headers(
58
- {
59
- 'STATSIG-API-KEY' => @sdk_key,
60
- 'STATSIG-SDK-TYPE' => meta['sdkType'],
61
- 'STATSIG-SDK-VERSION' => meta['sdkVersion'],
62
- 'Content-Type' => 'application/json; charset=UTF-8'
63
- }).accept(:json)
64
- body = {
65
- 'exception' => name,
66
- 'info' => {
67
- 'trace' => exception.backtrace.to_s,
68
- 'message' => exception.message
69
- }.to_s,
70
- 'statsigMetadata' => meta
71
- }
72
- http.post($endpoint, body: JSON.generate(body))
73
- rescue
35
+ def log_exception(exception, tag: nil)
36
+ name = exception.class.name
37
+ if @seen.include?(name)
74
38
  return
75
39
  end
40
+
41
+ @seen << name
42
+ meta = Statsig.get_statsig_metadata
43
+ http = HTTP.headers(
44
+ {
45
+ 'STATSIG-API-KEY' => @sdk_key,
46
+ 'STATSIG-SDK-TYPE' => meta['sdkType'],
47
+ 'STATSIG-SDK-VERSION' => meta['sdkVersion'],
48
+ 'Content-Type' => 'application/json; charset=UTF-8'
49
+ }).accept(:json)
50
+ body = {
51
+ 'exception' => name,
52
+ 'info' => {
53
+ 'trace' => exception.backtrace.to_s,
54
+ 'message' => exception.message
55
+ }.to_s,
56
+ 'statsigMetadata' => meta,
57
+ 'tag' => tag
58
+ }
59
+ http.post($endpoint, body: JSON.generate(body))
60
+ rescue StandardError
61
+ return
76
62
  end
77
63
  end
78
- end
64
+ end
data/lib/evaluator.rb CHANGED
@@ -17,8 +17,8 @@ module Statsig
17
17
  class Evaluator
18
18
  attr_accessor :spec_store
19
19
 
20
- def initialize(network, options, error_callback, diagnostics)
21
- @spec_store = Statsig::SpecStore.new(network, options, error_callback, diagnostics)
20
+ def initialize(network, options, error_callback, diagnostics, error_boundary, logger)
21
+ @spec_store = Statsig::SpecStore.new(network, options, error_callback, diagnostics, error_boundary, logger)
22
22
  UAParser.initialize_async
23
23
  CountryLookup.initialize_async
24
24
 
@@ -98,12 +98,12 @@ module Statsig
98
98
  eval_spec(user, @spec_store.get_layer(layer_name))
99
99
  end
100
100
 
101
- def get_client_initialize_response(user)
101
+ def get_client_initialize_response(user, hash, client_sdk_key)
102
102
  if @spec_store.is_ready_for_checks == false
103
103
  return nil
104
104
  end
105
105
 
106
- formatter = ClientInitializeHelpers::ResponseFormatter.new(self, user)
106
+ formatter = ClientInitializeHelpers::ResponseFormatter.new(self, user, hash, client_sdk_key)
107
107
 
108
108
  evaluated_keys = {}
109
109
  if user.user_id.nil? == false
@@ -123,6 +123,7 @@ module Statsig
123
123
  "generator" => "statsig-ruby-sdk",
124
124
  "evaluated_keys" => evaluated_keys,
125
125
  "time" => 0,
126
+ "hash_used" => hash
126
127
  }
127
128
  end
128
129
 
@@ -199,7 +200,7 @@ module Statsig
199
200
  @spec_store.initial_config_sync_time,
200
201
  @spec_store.init_reason
201
202
  ),
202
- group_name: nil,
203
+ group_name: 'default',
203
204
  id_type: config['idType']
204
205
  )
205
206
  end
data/lib/hash_utils.rb ADDED
@@ -0,0 +1,17 @@
1
+ module Statsig
2
+ class HashUtils
3
+ def self.djb2(input_str)
4
+ hash = 0
5
+ input_str.each_char.each do |c|
6
+ hash = (hash << 5) - hash + c.ord
7
+ hash &= hash
8
+ end
9
+ hash &= 0xFFFFFFFF # Convert to unsigned 32-bit integer
10
+ return hash.to_s
11
+ end
12
+
13
+ def self.sha256(input_str)
14
+ return Digest::SHA256.base64digest(input_str)
15
+ end
16
+ end
17
+ end
data/lib/network.rb CHANGED
@@ -5,8 +5,9 @@ require 'json'
5
5
  require 'securerandom'
6
6
  require 'sorbet-runtime'
7
7
  require 'uri_helper'
8
+ require 'connection_pool'
8
9
 
9
- $retry_codes = [408, 500, 502, 503, 504, 522, 524, 599]
10
+ RETRY_CODES = [408, 500, 502, 503, 504, 522, 524, 599].freeze
10
11
 
11
12
  module Statsig
12
13
  class NetworkError < StandardError
@@ -33,29 +34,36 @@ module Statsig
33
34
  @post_logs_retry_backoff = options.post_logs_retry_backoff
34
35
  @post_logs_retry_limit = options.post_logs_retry_limit
35
36
  @session_id = SecureRandom.uuid
37
+ @connection_pool = ConnectionPool.new(size: 3) do
38
+ meta = Statsig.get_statsig_metadata
39
+ client = HTTP.headers(
40
+ {
41
+ 'STATSIG-API-KEY' => @server_secret,
42
+ 'STATSIG-CLIENT-TIME' => (Time.now.to_f * 1000).to_i.to_s,
43
+ 'STATSIG-SERVER-SESSION-ID' => @session_id,
44
+ 'Content-Type' => 'application/json; charset=UTF-8',
45
+ 'STATSIG-SDK-TYPE' => meta['sdkType'],
46
+ 'STATSIG-SDK-VERSION' => meta['sdkVersion']
47
+ }
48
+ ).accept(:json)
49
+ if @timeout
50
+ client = client.timeout(@timeout)
51
+ end
52
+
53
+ client
54
+ end
36
55
  end
37
56
 
38
- sig { params(endpoint: String, body: String, retries: Integer, backoff: Integer)
39
- .returns([T.any(HTTP::Response, NilClass), T.any(StandardError, NilClass)]) }
57
+ sig do
58
+ params(endpoint: String, body: String, retries: Integer, backoff: Integer)
59
+ .returns([T.any(HTTP::Response, NilClass), T.any(StandardError, NilClass)])
60
+ end
40
61
 
41
62
  def post_helper(endpoint, body, retries = 0, backoff = 1)
42
63
  if @local_mode
43
64
  return nil, nil
44
65
  end
45
66
 
46
- meta = Statsig.get_statsig_metadata
47
- http = HTTP.headers(
48
- {
49
- "STATSIG-API-KEY" => @server_secret,
50
- "STATSIG-CLIENT-TIME" => (Time.now.to_f * 1000).to_i.to_s,
51
- "STATSIG-SERVER-SESSION-ID" => @session_id,
52
- "Content-Type" => "application/json; charset=UTF-8",
53
- "STATSIG-SDK-TYPE" => meta['sdkType'],
54
- "STATSIG-SDK-VERSION" => meta['sdkVersion'],
55
- }).accept(:json)
56
- if @timeout
57
- http = http.timeout(@timeout)
58
- end
59
67
  backoff_adjusted = backoff > 10 ? backoff += Random.rand(10) : backoff # to deter overlap
60
68
  if @post_logs_retry_backoff
61
69
  if @post_logs_retry_backoff.is_a? Integer
@@ -66,48 +74,53 @@ module Statsig
66
74
  end
67
75
  url = URIHelper.build_url(endpoint)
68
76
  begin
69
- res = http.post(url, body: body)
77
+ res = @connection_pool.with do |conn|
78
+ conn.headers('STATSIG-CLIENT-TIME' => (Time.now.to_f * 1000).to_i.to_s).post(url, body: body)
79
+ end
70
80
  rescue StandardError => e
71
81
  ## network error retry
72
- return nil, e unless retries > 0
82
+ return nil, e unless retries.positive?
83
+
73
84
  sleep backoff_adjusted
74
85
  return post_helper(endpoint, body, retries - 1, backoff * @backoff_multiplier)
75
86
  end
76
87
  return res, nil if res.status.success?
77
- return nil, NetworkError.new("Got an exception when making request to #{url}: #{res.to_s}", res.status.to_i) unless retries > 0 && $retry_codes.include?(res.code)
88
+
89
+ unless retries.positive? && RETRY_CODES.include?(res.code)
90
+ return res, NetworkError.new("Got an exception when making request to #{url}: #{res.to_s}",
91
+ res.status.to_i)
92
+ end
93
+
78
94
  ## status code retry
79
95
  sleep backoff_adjusted
80
96
  post_helper(endpoint, body, retries - 1, backoff * @backoff_multiplier)
81
97
  end
82
98
 
83
99
  def check_gate(user, gate_name)
84
- begin
85
- request_body = JSON.generate({ 'user' => user&.serialize(false), 'gateName' => gate_name })
86
- response, _ = post_helper('check_gate', request_body)
87
- return JSON.parse(response.body) unless response.nil?
88
- false
89
- rescue
90
- return false
91
- end
100
+ request_body = JSON.generate({ 'user' => user&.serialize(false), 'gateName' => gate_name })
101
+ response, = post_helper('check_gate', request_body)
102
+ return JSON.parse(response.body) unless response.nil?
103
+
104
+ false
105
+ rescue StandardError
106
+ false
92
107
  end
93
108
 
94
109
  def get_config(user, dynamic_config_name)
95
- begin
96
- request_body = JSON.generate({ 'user' => user&.serialize(false), 'configName' => dynamic_config_name })
97
- response, _ = post_helper('get_config', request_body)
98
- return JSON.parse(response.body) unless response.nil?
99
- nil
100
- rescue
101
- return nil
102
- end
110
+ request_body = JSON.generate({ 'user' => user&.serialize(false), 'configName' => dynamic_config_name })
111
+ response, = post_helper('get_config', request_body)
112
+ return JSON.parse(response.body) unless response.nil?
113
+
114
+ nil
115
+ rescue StandardError
116
+ nil
103
117
  end
104
118
 
105
119
  def post_logs(events)
106
- begin
107
- json_body = JSON.generate({ 'events' => events, 'statsigMetadata' => Statsig.get_statsig_metadata })
108
- post_helper('log_event', json_body, @post_logs_retry_limit)
109
- rescue
110
- end
120
+ json_body = JSON.generate({ 'events' => events, 'statsigMetadata' => Statsig.get_statsig_metadata })
121
+ post_helper('log_event', json_body, @post_logs_retry_limit)
122
+ rescue StandardError
123
+
111
124
  end
112
125
  end
113
- end
126
+ end
data/lib/spec_store.rb CHANGED
@@ -12,7 +12,7 @@ module Statsig
12
12
  attr_accessor :initial_config_sync_time
13
13
  attr_accessor :init_reason
14
14
 
15
- def initialize(network, options, error_callback, diagnostics)
15
+ def initialize(network, options, error_callback, diagnostics, error_boundary, logger)
16
16
  @init_reason = EvaluationReason::UNINITIALIZED
17
17
  @network = network
18
18
  @options = options
@@ -27,9 +27,12 @@ module Statsig
27
27
  :configs => {},
28
28
  :layers => {},
29
29
  :id_lists => {},
30
- :experiment_to_layer => {}
30
+ :experiment_to_layer => {},
31
+ :sdk_keys_to_app_ids => {}
31
32
  }
32
33
  @diagnostics = diagnostics
34
+ @error_boundary = error_boundary
35
+ @logger = logger
33
36
 
34
37
  @id_list_thread_pool = Concurrent::FixedThreadPool.new(
35
38
  options.idlist_threadpool_size,
@@ -39,18 +42,19 @@ module Statsig
39
42
  )
40
43
 
41
44
  unless @options.bootstrap_values.nil?
42
- begin
43
- if !@options.data_store.nil?
44
- puts 'data_store gets priority over bootstrap_values. bootstrap_values will be ignored'
45
- else
46
- tracker = @diagnostics.track('bootstrap', 'process')
45
+ if !@options.data_store.nil?
46
+ puts 'data_store gets priority over bootstrap_values. bootstrap_values will be ignored'
47
+ else
48
+ tracker = @diagnostics.track('bootstrap', 'process')
49
+ begin
47
50
  if process_specs(options.bootstrap_values)
48
51
  @init_reason = EvaluationReason::BOOTSTRAP
49
52
  end
50
- tracker.end(@init_reason == EvaluationReason::BOOTSTRAP)
53
+ rescue
54
+ puts 'the provided bootstrapValues is not a valid JSON string'
55
+ ensure
56
+ tracker.end(success: @init_reason == EvaluationReason::BOOTSTRAP)
51
57
  end
52
- rescue
53
- puts 'the provided bootstrapValues is not a valid JSON string'
54
58
  end
55
59
  end
56
60
 
@@ -119,15 +123,27 @@ module Statsig
119
123
  @specs[:id_lists][list_name]
120
124
  end
121
125
 
126
+ def has_sdk_key?(sdk_key)
127
+ @specs[:sdk_keys_to_app_ids].key?(sdk_key)
128
+ end
129
+
130
+ def get_app_id_for_sdk_key(sdk_key)
131
+ if sdk_key.nil?
132
+ return nil
133
+ end
134
+ return nil unless has_sdk_key?(sdk_key)
135
+ @specs[:sdk_keys_to_app_ids][sdk_key]
136
+ end
137
+
122
138
  def get_raw_specs
123
139
  @specs
124
140
  end
125
141
 
126
142
  def maybe_restart_background_threads
127
- if @config_sync_thread.nil? or !@config_sync_thread.alive?
143
+ if @config_sync_thread.nil? || !@config_sync_thread.alive?
128
144
  @config_sync_thread = sync_config_specs
129
145
  end
130
- if @id_lists_sync_thread.nil? or !@id_lists_sync_thread.alive?
146
+ if @id_lists_sync_thread.nil? || !@id_lists_sync_thread.alive?
131
147
  @id_lists_sync_thread = sync_id_lists
132
148
  end
133
149
  end
@@ -137,16 +153,16 @@ module Statsig
137
153
  def load_config_specs_from_storage_adapter
138
154
  tracker = @diagnostics.track('data_store_config_specs', 'fetch')
139
155
  cached_values = @options.data_store.get(Interfaces::IDataStore::CONFIG_SPECS_KEY)
140
- tracker.end(true)
156
+ tracker.end(success: true)
141
157
  return if cached_values.nil?
142
158
 
143
159
  tracker = @diagnostics.track('data_store_config_specs', 'process')
144
160
  process_specs(cached_values, from_adapter: true)
145
161
  @init_reason = EvaluationReason::DATA_ADAPTER
146
- tracker.end(true)
162
+ tracker.end(success: true)
147
163
  rescue StandardError
148
164
  # Fallback to network
149
- tracker.end(false)
165
+ tracker.end(success: false)
150
166
  download_config_specs
151
167
  end
152
168
 
@@ -159,29 +175,35 @@ module Statsig
159
175
 
160
176
  def sync_config_specs
161
177
  Thread.new do
162
- @diagnostics = Diagnostics.new('config_sync')
163
- loop do
164
- sleep @options.rulesets_sync_interval
165
- if @options.data_store&.should_be_used_for_querying_updates(Interfaces::IDataStore::CONFIG_SPECS_KEY)
166
- load_config_specs_from_storage_adapter
167
- else
168
- download_config_specs
178
+ @error_boundary.capture(task: lambda {
179
+ loop do
180
+ @diagnostics = Diagnostics.new('config_sync')
181
+ sleep @options.rulesets_sync_interval
182
+ if @options.data_store&.should_be_used_for_querying_updates(Interfaces::IDataStore::CONFIG_SPECS_KEY)
183
+ load_config_specs_from_storage_adapter
184
+ else
185
+ download_config_specs
186
+ end
187
+ @logger.log_diagnostics_event(@diagnostics)
169
188
  end
170
- end
189
+ })
171
190
  end
172
191
  end
173
192
 
174
193
  def sync_id_lists
175
194
  Thread.new do
176
- @diagnostics = Diagnostics.new('config_sync')
177
- loop do
178
- sleep @id_lists_sync_interval
179
- if @options.data_store&.should_be_used_for_querying_updates(Interfaces::IDataStore::ID_LISTS_KEY)
180
- get_id_lists_from_adapter
181
- else
182
- get_id_lists_from_network
195
+ @error_boundary.capture(task: lambda {
196
+ loop do
197
+ @diagnostics = Diagnostics.new('config_sync')
198
+ sleep @id_lists_sync_interval
199
+ if @options.data_store&.should_be_used_for_querying_updates(Interfaces::IDataStore::ID_LISTS_KEY)
200
+ get_id_lists_from_adapter
201
+ else
202
+ get_id_lists_from_network
203
+ end
204
+ @logger.log_diagnostics_event(@diagnostics)
183
205
  end
184
- end
206
+ })
185
207
  end
186
208
  end
187
209
 
@@ -195,7 +217,7 @@ module Statsig
195
217
  if e.is_a? NetworkError
196
218
  code = e.http_code
197
219
  end
198
- tracker.end(code)
220
+ tracker.end(statusCode: code, success: e.nil?, sdkRegion: response&.headers&.[]('X-Statsig-Region'))
199
221
 
200
222
  if e.nil?
201
223
  unless response.nil?
@@ -203,7 +225,7 @@ module Statsig
203
225
  if process_specs(response.body.to_s)
204
226
  @init_reason = EvaluationReason::NETWORK
205
227
  end
206
- tracker.end(@init_reason == EvaluationReason::NETWORK)
228
+ tracker.end(success: @init_reason == EvaluationReason::NETWORK)
207
229
 
208
230
  @rules_updated_callback.call(response.body.to_s, @last_config_sync_time) unless response.body.nil? or @rules_updated_callback.nil?
209
231
  end
@@ -252,6 +274,9 @@ module Statsig
252
274
  @specs[:configs] = new_configs
253
275
  @specs[:layers] = new_layers
254
276
  @specs[:experiment_to_layer] = new_exp_to_layer
277
+ @specs[:sdk_keys_to_app_ids] = specs_json['sdk_keys_to_app_ids'] || {}
278
+
279
+ specs_json['diagnostics']
255
280
 
256
281
  unless from_adapter
257
282
  save_config_specs_to_storage_adapter(specs_string)
@@ -264,12 +289,12 @@ module Statsig
264
289
  cached_values = @options.data_store.get(Interfaces::IDataStore::ID_LISTS_KEY)
265
290
  return if cached_values.nil?
266
291
 
267
- tracker.end(true)
292
+ tracker.end(success: true)
268
293
  id_lists = JSON.parse(cached_values)
269
294
  process_id_lists(id_lists, from_adapter: true)
270
295
  rescue StandardError
271
296
  # Fallback to network
272
- tracker.end(false)
297
+ tracker.end(success: false)
273
298
  get_id_lists_from_network
274
299
  end
275
300
 
@@ -287,8 +312,9 @@ module Statsig
287
312
  if e.is_a? NetworkError
288
313
  code = e.http_code
289
314
  end
290
- tracker.end(code)
291
- if !e.nil? || response.nil?
315
+ success = e.nil? && !response.nil?
316
+ tracker.end(statusCode: code, success: success, sdkRegion: response&.headers&.[]('X-Statsig-Region'))
317
+ if !success
292
318
  return
293
319
  end
294
320
 
@@ -311,11 +337,11 @@ module Statsig
311
337
  tracker = @diagnostics.track(
312
338
  from_adapter ? 'data_store_id_lists' : 'get_id_list_sources',
313
339
  'process',
314
- new_id_lists.length
340
+ { idListCount: new_id_lists.length }
315
341
  )
316
342
 
317
343
  if new_id_lists.empty?
318
- tracker.end
344
+ tracker.end(success: true)
319
345
  return
320
346
  end
321
347
 
@@ -366,17 +392,17 @@ module Statsig
366
392
  end
367
393
 
368
394
  result = Concurrent::Promise.all?(*tasks).execute.wait(@id_lists_sync_interval)
369
- tracker.end(result.state == :fulfilled)
395
+ tracker.end(success: result.state == :fulfilled)
370
396
  end
371
397
 
372
398
  def get_single_id_list_from_adapter(list)
373
- tracker = @diagnostics.track('data_store_id_list', 'fetch', nil, { url: list.url })
399
+ tracker = @diagnostics.track('data_store_id_list', 'fetch', { url: list.url })
374
400
  cached_values = @options.data_store.get("#{Interfaces::IDataStore::ID_LISTS_KEY}::#{list.name}")
375
- tracker.end(true)
401
+ tracker.end(success: true)
376
402
  content = cached_values.to_s
377
403
  process_single_id_list(list, content, from_adapter: true)
378
404
  rescue StandardError
379
- tracker.end(false)
405
+ tracker.end(success: false)
380
406
  nil
381
407
  end
382
408
 
@@ -389,10 +415,10 @@ module Statsig
389
415
  def download_single_id_list(list)
390
416
  nil unless list.is_a? IDList
391
417
  http = HTTP.headers({ 'Range' => "bytes=#{list&.size || 0}-" }).accept(:json)
418
+ tracker = @diagnostics.track('get_id_list', 'network_request', { url: list.url })
392
419
  begin
393
- tracker = @diagnostics.track('get_id_list', 'network_request', nil, { url: list.url })
394
420
  res = http.get(list.url)
395
- tracker.end(res.status.code)
421
+ tracker.end(statusCode: res.status.code, success: res.status.success?)
396
422
  nil unless res.status.success?
397
423
  content_length = Integer(res['content-length'])
398
424
  nil if content_length.nil? || content_length <= 0
@@ -400,6 +426,7 @@ module Statsig
400
426
  success = process_single_id_list(list, content, content_length)
401
427
  save_single_id_list_to_adapter(list.name, content) unless success.nil? || !success
402
428
  rescue
429
+ tracker.end(success: false)
403
430
  nil
404
431
  end
405
432
  end
@@ -407,10 +434,10 @@ module Statsig
407
434
  def process_single_id_list(list, content, content_length = nil, from_adapter: false)
408
435
  false unless list.is_a? IDList
409
436
  begin
410
- tracker = @diagnostics.track(from_adapter ? 'data_store_id_list' : 'get_id_list', 'process', nil, { url: list.url })
437
+ tracker = @diagnostics.track(from_adapter ? 'data_store_id_list' : 'get_id_list', 'process', { url: list.url })
411
438
  unless content.is_a?(String) && (content[0] == '-' || content[0] == '+')
412
439
  @specs[:id_lists].delete(list.name)
413
- tracker.end(false)
440
+ tracker.end(success: false)
414
441
  return false
415
442
  end
416
443
  ids_clone = list.ids # clone the list, operate on the new list, and swap out the old list, so the operation is thread-safe
@@ -432,10 +459,10 @@ module Statsig
432
459
  else
433
460
  list.size + content_length
434
461
  end
435
- tracker.end(true)
462
+ tracker.end(success: true)
436
463
  return true
437
464
  rescue
438
- tracker.end(false)
465
+ tracker.end(success: false)
439
466
  return false
440
467
  end
441
468
  end
data/lib/statsig.rb CHANGED
@@ -208,17 +208,19 @@ module Statsig
208
208
  @shared_instance&.override_config(config_name, config_value)
209
209
  end
210
210
 
211
- sig { params(user: StatsigUser).returns(T.any(T::Hash[String, T.untyped], NilClass)) }
211
+ sig { params(user: StatsigUser, hash: String, client_sdk_key: T.any(String, NilClass)).returns(T.any(T::Hash[String, T.untyped], NilClass)) }
212
212
  ##
213
213
  # Gets all evaluated values for the given user.
214
214
  # These values can then be given to a Statsig Client SDK via bootstrapping.
215
215
  #
216
216
  # @param user A StatsigUser object used for the evaluation
217
+ # @param hash The type of hashing algorithm to use ('sha256', 'djb2', 'none')
218
+ # @param client_sdk_key A optional client sdk key to be used for the evaluation
217
219
  #
218
220
  # @note See Ruby Documentation: https://docs.statsig.com/server/rubySDK)
219
- def self.get_client_initialize_response(user)
221
+ def self.get_client_initialize_response(user, hash: 'sha256', client_sdk_key: nil)
220
222
  ensure_initialized
221
- @shared_instance&.get_client_initialize_response(user)
223
+ @shared_instance&.get_client_initialize_response(user, hash, client_sdk_key)
222
224
  end
223
225
 
224
226
  sig { returns(T::Hash[String, String]) }
@@ -227,7 +229,7 @@ module Statsig
227
229
  def self.get_statsig_metadata
228
230
  {
229
231
  'sdkType' => 'ruby-server',
230
- 'sdkVersion' => '1.25.2',
232
+ 'sdkVersion' => '1.26.0',
231
233
  }
232
234
  end
233
235
 
@@ -37,13 +37,12 @@ class StatsigDriver
37
37
  @shutdown = false
38
38
  @secret_key = secret_key
39
39
  @net = Statsig::Network.new(secret_key, @options)
40
- @logger = Statsig::StatsigLogger.new(@net, @options)
41
- @evaluator = Statsig::Evaluator.new(@net, @options, error_callback, @diagnostics)
42
- tracker.end('success')
40
+ @logger = Statsig::StatsigLogger.new(@net, @options, @err_boundary)
41
+ @evaluator = Statsig::Evaluator.new(@net, @options, error_callback, @diagnostics, @err_boundary, @logger)
42
+ tracker.end(success: true)
43
43
 
44
44
  @logger.log_diagnostics_event(@diagnostics)
45
- })
46
- @err_boundary.logger = @logger
45
+ }, caller: __method__.to_s)
47
46
  end
48
47
 
49
48
  class CheckGateOptions < T::Struct
@@ -54,32 +53,36 @@ class StatsigDriver
54
53
 
55
54
  def check_gate(user, gate_name, options = CheckGateOptions.new)
56
55
  @err_boundary.capture(task: lambda {
57
- user = verify_inputs(user, gate_name, "gate_name")
56
+ run_with_diagnostics(task: lambda {
57
+ user = verify_inputs(user, gate_name, "gate_name")
58
58
 
59
- res = @evaluator.check_gate(user, gate_name)
60
- if res.nil?
61
- res = Statsig::ConfigResult.new(gate_name)
62
- end
59
+ res = @evaluator.check_gate(user, gate_name)
60
+ if res.nil?
61
+ res = Statsig::ConfigResult.new(gate_name)
62
+ end
63
63
 
64
- if res == $fetch_from_server
65
- res = check_gate_fallback(user, gate_name)
66
- # exposure logged by the server
67
- else
68
- if options.log_exposure
69
- @logger.log_gate_exposure(user, res.name, res.gate_value, res.rule_id, res.secondary_exposures, res.evaluation_details)
64
+ if res == $fetch_from_server
65
+ res = check_gate_fallback(user, gate_name)
66
+ # exposure logged by the server
67
+ else
68
+ if options.log_exposure
69
+ @logger.log_gate_exposure(user, res.name, res.gate_value, res.rule_id, res.secondary_exposures, res.evaluation_details)
70
+ end
70
71
  end
71
- end
72
72
 
73
- res.gate_value
73
+ res.gate_value
74
+ }, caller: __method__.to_s)
74
75
  }, recover: -> { false }, caller: __method__.to_s)
75
76
  end
76
77
 
77
78
  sig { params(user: StatsigUser, gate_name: String).void }
78
79
 
79
80
  def manually_log_gate_exposure(user, gate_name)
80
- res = @evaluator.check_gate(user, gate_name)
81
- context = {'is_manual_exposure' => true}
82
- @logger.log_gate_exposure(user, gate_name, res.gate_value, res.rule_id, res.secondary_exposures, res.evaluation_details, context)
81
+ @err_boundary.capture(task: lambda {
82
+ res = @evaluator.check_gate(user, gate_name)
83
+ context = { 'is_manual_exposure' => true }
84
+ @logger.log_gate_exposure(user, gate_name, res.gate_value, res.rule_id, res.secondary_exposures, res.evaluation_details, context)
85
+ })
83
86
  end
84
87
 
85
88
  class GetConfigOptions < T::Struct
@@ -90,8 +93,10 @@ class StatsigDriver
90
93
 
91
94
  def get_config(user, dynamic_config_name, options = GetConfigOptions.new)
92
95
  @err_boundary.capture(task: lambda {
93
- user = verify_inputs(user, dynamic_config_name, "dynamic_config_name")
94
- get_config_impl(user, dynamic_config_name, options)
96
+ run_with_diagnostics(task: lambda {
97
+ user = verify_inputs(user, dynamic_config_name, "dynamic_config_name")
98
+ get_config_impl(user, dynamic_config_name, options)
99
+ }, caller: __method__.to_s)
95
100
  }, recover: -> { DynamicConfig.new(dynamic_config_name) }, caller: __method__.to_s)
96
101
  end
97
102
 
@@ -103,17 +108,21 @@ class StatsigDriver
103
108
 
104
109
  def get_experiment(user, experiment_name, options = GetExperimentOptions.new)
105
110
  @err_boundary.capture(task: lambda {
106
- user = verify_inputs(user, experiment_name, "experiment_name")
107
- get_config_impl(user, experiment_name, options)
111
+ run_with_diagnostics(task: lambda {
112
+ user = verify_inputs(user, experiment_name, "experiment_name")
113
+ get_config_impl(user, experiment_name, options)
114
+ }, caller: __method__.to_s)
108
115
  }, recover: -> { DynamicConfig.new(experiment_name) }, caller: __method__.to_s)
109
116
  end
110
117
 
111
118
  sig { params(user: StatsigUser, config_name: String).void }
112
119
 
113
120
  def manually_log_config_exposure(user, config_name)
114
- res = @evaluator.get_config(user, config_name)
115
- context = {'is_manual_exposure' => true}
116
- @logger.log_config_exposure(user, res.name, res.rule_id, res.secondary_exposures, res.evaluation_details, context)
121
+ @err_boundary.capture(task: lambda {
122
+ res = @evaluator.get_config(user, config_name)
123
+ context = { 'is_manual_exposure' => true }
124
+ @logger.log_config_exposure(user, res.name, res.rule_id, res.secondary_exposures, res.evaluation_details, context)
125
+ }, caller: __method__.to_s)
117
126
  end
118
127
 
119
128
  class GetLayerOptions < T::Struct
@@ -124,37 +133,39 @@ class StatsigDriver
124
133
 
125
134
  def get_layer(user, layer_name, options = GetLayerOptions.new)
126
135
  @err_boundary.capture(task: lambda {
127
- user = verify_inputs(user, layer_name, "layer_name")
136
+ run_with_diagnostics(task: lambda {
137
+ user = verify_inputs(user, layer_name, "layer_name")
128
138
 
129
- res = @evaluator.get_layer(user, layer_name)
130
- if res.nil?
131
- res = Statsig::ConfigResult.new(layer_name)
132
- end
139
+ res = @evaluator.get_layer(user, layer_name)
140
+ if res.nil?
141
+ res = Statsig::ConfigResult.new(layer_name)
142
+ end
133
143
 
134
- if res == $fetch_from_server
135
- if res.config_delegate.empty?
136
- return Layer.new(layer_name)
144
+ if res == $fetch_from_server
145
+ if res.config_delegate.empty?
146
+ return Layer.new(layer_name)
147
+ end
148
+ res = get_config_fallback(user, res.config_delegate)
149
+ # exposure logged by the server
137
150
  end
138
- res = get_config_fallback(user, res.config_delegate)
139
- # exposure logged by the server
140
- end
141
-
142
- exposure_log_func = options.log_exposure ? lambda { |layer, parameter_name|
143
- @logger.log_layer_exposure(user, layer, parameter_name, res)
144
- } : nil
145
- Layer.new(res.name, res.json_value, res.rule_id, exposure_log_func)
146
- }, recover: lambda {
147
- Layer.new(layer_name)
148
- }, caller: __method__.to_s)
151
+
152
+ exposure_log_func = options.log_exposure ? lambda { |layer, parameter_name|
153
+ @logger.log_layer_exposure(user, layer, parameter_name, res)
154
+ } : nil
155
+ Layer.new(res.name, res.json_value, res.rule_id, exposure_log_func)
156
+ }, caller: __method__.to_s)
157
+ }, recover: lambda { Layer.new(layer_name) }, caller: __method__.to_s)
149
158
  end
150
159
 
151
160
  sig { params(user: StatsigUser, layer_name: String, parameter_name: String).void }
152
161
 
153
162
  def manually_log_layer_parameter_exposure(user, layer_name, parameter_name)
154
- res = @evaluator.get_layer(user, layer_name)
155
- layer = Layer.new(layer_name, res.json_value, res.rule_id)
156
- context = {'is_manual_exposure' => true}
157
- @logger.log_layer_exposure(user, layer, parameter_name, res, context)
163
+ @err_boundary.capture(task: lambda {
164
+ res = @evaluator.get_layer(user, layer_name)
165
+ layer = Layer.new(layer_name, res.json_value, res.rule_id)
166
+ context = { 'is_manual_exposure' => true }
167
+ @logger.log_layer_exposure(user, layer, parameter_name, res, context)
168
+ }, caller: __method__.to_s)
158
169
  end
159
170
 
160
171
  def log_event(user, event_name, value = nil, metadata = nil)
@@ -171,7 +182,7 @@ class StatsigDriver
171
182
  event.value = value
172
183
  event.metadata = metadata
173
184
  @logger.log_event(event)
174
- })
185
+ }, caller: __method__.to_s)
175
186
  end
176
187
 
177
188
  def shutdown
@@ -179,29 +190,30 @@ class StatsigDriver
179
190
  @shutdown = true
180
191
  @logger.shutdown
181
192
  @evaluator.shutdown
182
- })
193
+ }, caller: __method__.to_s)
183
194
  end
184
195
 
185
196
  def override_gate(gate_name, gate_value)
186
197
  @err_boundary.capture(task: lambda {
187
198
  @evaluator.override_gate(gate_name, gate_value)
188
- })
199
+ }, caller: __method__.to_s)
189
200
  end
190
201
 
191
202
  def override_config(config_name, config_value)
192
203
  @err_boundary.capture(task: lambda {
193
204
  @evaluator.override_config(config_name, config_value)
194
- })
205
+ }, caller: __method__.to_s)
195
206
  end
196
207
 
197
208
  # @param [StatsigUser] user
209
+ # @param [String | nil] client_sdk_key
198
210
  # @return [Hash]
199
- def get_client_initialize_response(user)
211
+ def get_client_initialize_response(user, hash, client_sdk_key)
200
212
  @err_boundary.capture(task: lambda {
201
213
  validate_user(user)
202
214
  normalize_user(user)
203
- @evaluator.get_client_initialize_response(user)
204
- }, recover: -> { nil })
215
+ @evaluator.get_client_initialize_response(user, hash, client_sdk_key)
216
+ }, recover: -> { nil }, caller: __method__.to_s)
205
217
  end
206
218
 
207
219
  def maybe_restart_background_threads
@@ -212,11 +224,29 @@ class StatsigDriver
212
224
  @err_boundary.capture(task: lambda {
213
225
  @evaluator.maybe_restart_background_threads
214
226
  @logger.maybe_restart_background_threads
215
- })
227
+ }, caller: __method__.to_s)
216
228
  end
217
229
 
218
230
  private
219
231
 
232
+ def run_with_diagnostics(task:, caller:)
233
+ diagnostics = nil
234
+ if Statsig::Diagnostics::API_CALL_KEYS.include?(caller) && Statsig::Diagnostics.sample(10_000)
235
+ diagnostics = Statsig::Diagnostics.new('api_call')
236
+ tracker = diagnostics.track(caller)
237
+ end
238
+ begin
239
+ res = task.call
240
+ tracker&.end(success: true)
241
+ rescue StandardError => e
242
+ tracker&.end(success: false)
243
+ raise e
244
+ ensure
245
+ @logger.log_diagnostics_event(diagnostics)
246
+ end
247
+ return res
248
+ end
249
+
220
250
  sig { params(user: StatsigUser, config_name: String, variable_name: String).returns(StatsigUser) }
221
251
 
222
252
  def verify_inputs(user, config_name, variable_name)
@@ -1,3 +1,4 @@
1
+ # typed: true
1
2
  module Statsig
2
3
  class UninitializedError < StandardError
3
4
  def initialize(msg="Must call initialize first.")
@@ -9,7 +9,7 @@ $diagnostics_event = 'statsig::diagnostics'
9
9
  $ignored_metadata_keys = ['serverTime', 'configSyncTime', 'initTime', 'reason']
10
10
  module Statsig
11
11
  class StatsigLogger
12
- def initialize(network, options)
12
+ def initialize(network, options, error_boundary)
13
13
  @network = network
14
14
  @events = []
15
15
  @options = options
@@ -23,9 +23,11 @@ module Statsig
23
23
  fallback_policy: :discard
24
24
  )
25
25
 
26
+ @error_boundary = error_boundary
26
27
  @background_flush = periodic_flush
27
28
  @deduper = Concurrent::Set.new()
28
29
  @interval = 0
30
+ @flush_mutex = Mutex.new
29
31
  end
30
32
 
31
33
  def log_event(event)
@@ -109,12 +111,14 @@ module Statsig
109
111
 
110
112
  def periodic_flush
111
113
  Thread.new do
112
- loop do
113
- sleep @options.logging_interval_seconds
114
- flush_async
115
- @interval += 1
116
- @deduper.clear if @interval % 2 == 0
117
- end
114
+ @error_boundary.capture(task: lambda {
115
+ loop do
116
+ sleep @options.logging_interval_seconds
117
+ flush_async
118
+ @interval += 1
119
+ @deduper.clear if @interval % 2 == 0
120
+ end
121
+ })
118
122
  end
119
123
  end
120
124
 
@@ -132,18 +136,20 @@ module Statsig
132
136
  end
133
137
 
134
138
  def flush
135
- if @events.length == 0
136
- return
137
- end
138
- events_clone = @events
139
- @events = []
140
- flush_events = events_clone.map { |e| e.serialize }
139
+ @flush_mutex.synchronize do
140
+ if @events.length.zero?
141
+ return
142
+ end
141
143
 
142
- @network.post_logs(flush_events)
144
+ events_clone = @events
145
+ @events = []
146
+ flush_events = events_clone.map { |e| e.serialize }
147
+ @network.post_logs(flush_events)
148
+ end
143
149
  end
144
150
 
145
151
  def maybe_restart_background_threads
146
- if @background_flush.nil? or !@background_flush.alive?
152
+ if @background_flush.nil? || !@background_flush.alive?
147
153
  @background_flush = periodic_flush
148
154
  end
149
155
  end
data/lib/ua_parser.rb CHANGED
@@ -1,3 +1,4 @@
1
+ # typed: true
1
2
  require 'user_agent_parser'
2
3
 
3
4
  module UAParser
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.25.2
4
+ version: 1.26.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-06-20 00:00:00.000000000 Z
11
+ date: 2023-07-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -52,6 +52,34 @@ dependencies:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
54
  version: 5.14.0
55
+ - !ruby/object:Gem::Dependency
56
+ name: minitest-reporters
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.6'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1.6'
69
+ - !ruby/object:Gem::Dependency
70
+ name: minitest-suite
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: 0.0.3
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: 0.0.3
55
83
  - !ruby/object:Gem::Dependency
56
84
  name: spy
57
85
  requirement: !ruby/object:Gem::Requirement
@@ -226,6 +254,26 @@ dependencies:
226
254
  - - "<"
227
255
  - !ruby/object:Gem::Version
228
256
  version: '6.0'
257
+ - !ruby/object:Gem::Dependency
258
+ name: connection_pool
259
+ requirement: !ruby/object:Gem::Requirement
260
+ requirements:
261
+ - - "~>"
262
+ - !ruby/object:Gem::Version
263
+ version: '2.4'
264
+ - - ">="
265
+ - !ruby/object:Gem::Version
266
+ version: 2.4.1
267
+ type: :runtime
268
+ prerelease: false
269
+ version_requirements: !ruby/object:Gem::Requirement
270
+ requirements:
271
+ - - "~>"
272
+ - !ruby/object:Gem::Version
273
+ version: '2.4'
274
+ - - ">="
275
+ - !ruby/object:Gem::Version
276
+ version: 2.4.1
229
277
  - !ruby/object:Gem::Dependency
230
278
  name: ip3country
231
279
  requirement: !ruby/object:Gem::Requirement
@@ -282,6 +330,7 @@ files:
282
330
  - lib/evaluation_details.rb
283
331
  - lib/evaluation_helpers.rb
284
332
  - lib/evaluator.rb
333
+ - lib/hash_utils.rb
285
334
  - lib/id_list.rb
286
335
  - lib/interfaces/data_store.rb
287
336
  - lib/layer.rb