statsig 1.33.2 → 1.33.4

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: 7496f0ffb0bb7a06d8377193b93659d31fd2b1f1df55eb81ad061e2e0ef42167
4
- data.tar.gz: 2a0b10e6e7e18a11df14667858957abfead96833ee7a9ca7b7470f22426f3160
3
+ metadata.gz: 6b2947b68bfada4686360dc5aab60a460129a3ca5e4c63d6f2107af68788d2a8
4
+ data.tar.gz: 98e56828d1f31ec18afc0a2f46dbcab90c2a976dc95ef1db9ead5060b240952f
5
5
  SHA512:
6
- metadata.gz: 9bf0bdc595846b6a531d83984e2d75e0649ec6485fe971c3bbb3bc496d9ca6950d227756e88be75f0b33331d0b503248a02dec8fa72c3e398b39eb735d1e4bb2
7
- data.tar.gz: 82f6ee222688547e1d264d337e7bc532ab7ac5801d112c5ecf36f0755ecd3b82e07fd57b71e50c5df0f672899fbab2f3c18e868abf0c4fe80924e7ccabfcee33
6
+ metadata.gz: e1f11434fb5ec030500429cdfd11146fe845568498cded4917e1f412ca50a86beb03f937febcaeceaf8523dacaa550b9024384dc58b0b3c9b2619d37041e0df0
7
+ data.tar.gz: d64fd8980ca648626ecdcd0095d4964aa531d24d7acaa7c07dd70013a32d0f7c5c548602e9578044c0567c0c2ac948f5dd8bd499c2178323b0ae176d0925a82c
data/lib/api_config.rb CHANGED
@@ -87,39 +87,54 @@ module Statsig
87
87
 
88
88
  class APICondition
89
89
 
90
- attr_accessor :type, :target_value, :operator, :field, :additional_values, :id_type
91
-
90
+ attr_accessor :type, :target_value, :operator, :field, :additional_values, :id_type, :hash
92
91
  def self.from_json(json)
93
- operator = json[:operator]
94
- unless operator.nil?
95
- operator = operator&.downcase&.to_sym
96
- unless Const::SUPPORTED_OPERATORS.include?(operator)
97
- raise UnsupportedConfigException
92
+ hash = Statsig::HashUtils.md5(json.to_s).to_sym
93
+ return Statsig::Memo.for_global(:api_condition_from_json, hash) do
94
+ operator = json[:operator]
95
+ unless operator.nil?
96
+ operator = operator&.downcase&.to_sym
97
+ unless Const::SUPPORTED_OPERATORS.include?(operator)
98
+ raise UnsupportedConfigException
99
+ end
98
100
  end
99
- end
100
101
 
101
- type = json[:type]
102
- unless type.nil?
103
- type = type&.downcase&.to_sym
104
- unless Const::SUPPORTED_CONDITION_TYPES.include?(type)
105
- raise UnsupportedConfigException
102
+ type = json[:type]
103
+ unless type.nil?
104
+ type = type&.downcase&.to_sym
105
+ unless Const::SUPPORTED_CONDITION_TYPES.include?(type)
106
+ raise UnsupportedConfigException
107
+ end
106
108
  end
107
- end
108
109
 
109
- new(
110
- type: json[:type],
111
- target_value: json[:targetValue],
112
- operator: json[:operator],
113
- field: json[:field],
114
- additional_values: json[:additionalValues],
115
- id_type: json[:idType]
116
- )
110
+ new(
111
+ type: json[:type],
112
+ target_value: json[:targetValue],
113
+ operator: json[:operator],
114
+ field: json[:field],
115
+ additional_values: json[:additionalValues],
116
+ id_type: json[:idType],
117
+ hash: hash
118
+ )
119
+ end
117
120
  end
118
121
 
119
122
  private
120
123
 
121
- def initialize(type:, target_value:, operator:, field:, additional_values:, id_type:)
124
+ def initialize(type:, target_value:, operator:, field:, additional_values:, id_type:, hash:)
125
+ @hash = hash
126
+
122
127
  @type = type.to_sym unless type.nil?
128
+ if operator == "any_case_sensitive" || operator == "none_case_sensitive"
129
+ if target_value.is_a?(Array)
130
+ target_value = target_value.map { |item| [item.to_s, true] }.to_h
131
+ end
132
+ end
133
+ if operator == "any" || operator == "none"
134
+ if target_value.is_a?(Array)
135
+ target_value = target_value.map { |item| [item.to_s.downcase, true] }.to_h
136
+ end
137
+ end
123
138
  @target_value = target_value
124
139
  @operator = operator.to_sym unless operator.nil?
125
140
  @field = field
@@ -25,6 +25,7 @@ module Statsig
25
25
  end
26
26
 
27
27
  def self.to_response(config_name, config_spec, evaluator, user, client_sdk_key, hash_algo, include_exposures, include_local_overrides)
28
+
28
29
  target_app_id = evaluator.spec_store.get_app_id_for_sdk_key(client_sdk_key)
29
30
  config_target_apps = config_spec.target_app_ids
30
31
 
data/lib/diagnostics.rb CHANGED
@@ -58,7 +58,12 @@ module Statsig
58
58
  rand * 10_000 < rate_over_ten_thousand
59
59
  end
60
60
 
61
- API_CALL_KEYS = %w[check_gate get_config get_experiment get_layer].freeze
61
+ API_CALL_KEYS = {
62
+ :check_gate => true,
63
+ :get_config => true,
64
+ :get_experiment => true,
65
+ :get_layer => true
66
+ }.freeze
62
67
 
63
68
  class Tracker
64
69
  def initialize(diagnostics, context, key, step, tags = {})
@@ -10,9 +10,9 @@ module Statsig
10
10
  @seen = Set.new
11
11
  end
12
12
 
13
- def capture(task:, recover: -> {}, caller: nil)
13
+ def capture(recover: -> {}, caller: nil)
14
14
  begin
15
- res = task.call
15
+ res = yield
16
16
  rescue StandardError, SystemStackError => e
17
17
  if e.is_a?(Statsig::UninitializedError) || e.is_a?(Statsig::ValueError)
18
18
  raise e
@@ -20,7 +20,7 @@ module Statsig
20
20
 
21
21
  puts '[Statsig]: An unexpected exception occurred.'
22
22
  puts e.message
23
- log_exception(e, tag: caller)
23
+ log_exception(e, tag: caller&.to_s)
24
24
  res = recover.call
25
25
  end
26
26
  return res
@@ -10,7 +10,6 @@ module EvaluationHelpers
10
10
  def self.match_string_in_array(array, value, ignore_case, func)
11
11
  str_value = value.to_s
12
12
  str_value_downcased = nil
13
-
14
13
  return false if array.nil?
15
14
 
16
15
  return array.any? do |item|
@@ -26,22 +25,15 @@ module EvaluationHelpers
26
25
  end
27
26
 
28
27
  def self.equal_string_in_array(array, value, ignore_case)
29
- str_value = value.to_s
30
- str_value_downcased = nil
31
-
32
28
  return false if array.nil?
33
29
 
34
- return array.any? do |item|
35
- next false if item.nil?
36
- item_str = item.to_s
37
-
38
- next false unless item_str.length == str_value.length
39
-
40
- return true if item_str == str_value
41
- next false unless ignore_case
30
+ str_value = value.to_s
31
+ str_value_downcased = nil
42
32
 
43
- str_value_downcased ||= str_value.downcase
44
- item_str.downcase == str_value_downcased
33
+ if ignore_case
34
+ return array.has_key?(value.to_s.downcase)
35
+ else
36
+ return array.has_key?(value.to_s)
45
37
  end
46
38
  end
47
39
 
data/lib/evaluator.rb CHANGED
@@ -209,8 +209,8 @@ module Statsig
209
209
 
210
210
  if user.custom_ids.nil? == false
211
211
  evaluated_keys[:customIDs] = user.custom_ids
212
- end
213
-
212
+ end
213
+
214
214
  {
215
215
  feature_gates: Statsig::ResponseFormatter
216
216
  .get_responses(@spec_store.gates, self, user, client_sdk_key, hash_algo, include_local_overrides: include_local_overrides),
@@ -331,8 +331,19 @@ module Statsig
331
331
  def eval_rule(user, rule, end_result)
332
332
  pass = true
333
333
  i = 0
334
+
335
+ memo = user.get_memo
334
336
  until i >= rule.conditions.length
335
- result = eval_condition(user, rule.conditions[i], end_result)
337
+
338
+ condition = rule.conditions[i]
339
+
340
+ if condition.type == :fail_gate || condition.type == :pass_gate
341
+ result = eval_condition(user, condition, end_result)
342
+ else
343
+ result = Memo.for(memo, :eval_rule, condition.hash) do
344
+ eval_condition(user, condition, end_result)
345
+ end
346
+ end
336
347
 
337
348
  pass = false if result != true
338
349
  i += 1
@@ -370,7 +381,6 @@ module Statsig
370
381
  return true
371
382
  when :fail_gate, :pass_gate
372
383
  check_gate(user, target, end_result)
373
-
374
384
  gate_value = end_result.gate_value
375
385
 
376
386
  unless end_result.disable_exposures
@@ -527,8 +537,7 @@ module Statsig
527
537
  def get_value_from_user(user, field)
528
538
  return nil unless field.is_a?(String)
529
539
 
530
- value = get_value_from_user_field(user, field)
531
- value ||= get_value_from_user_field(user, field.downcase)
540
+ value = get_value_from_user_field(user, field.downcase)
532
541
 
533
542
  if value.nil?
534
543
  value = user.custom[field] if user.custom.is_a?(Hash)
@@ -584,8 +593,8 @@ module Statsig
584
593
  return nil unless field.is_a?(String)
585
594
 
586
595
  ua = get_value_from_user(user, Const::USER_AGENT)
587
- return nil unless ua.is_a?(String)
588
596
 
597
+ return nil unless ua.is_a?(String)
589
598
  case field.downcase
590
599
  when Const::OSNAME, Const::OS_NAME
591
600
  os = UAParser.parse_os(ua)
data/lib/hash_utils.rb CHANGED
@@ -19,6 +19,10 @@ module Statsig
19
19
  return Digest::SHA256.base64digest(input_str)
20
20
  end
21
21
 
22
+ def self.md5(input_str)
23
+ return Digest::MD5.base64digest(input_str)
24
+ end
25
+
22
26
  def self.sortHash(input_hash)
23
27
  dictionary = input_hash.clone.sort_by { |key| key }.to_h;
24
28
  input_hash.each do |key, value|
data/lib/memo.rb ADDED
@@ -0,0 +1,23 @@
1
+ module Statsig
2
+ class Memo
3
+
4
+ @global_memo = {}
5
+
6
+ def self.for(hash, method, key)
7
+ method_hash = hash[method]
8
+ unless method_hash
9
+ method_hash = hash[method] = {}
10
+ end
11
+
12
+ return method_hash[key] if method_hash.key?(key)
13
+
14
+ method_hash[key] = yield
15
+ end
16
+
17
+ def self.for_global(method, key)
18
+ return self.for(@global_memo, method, key) do
19
+ yield
20
+ end
21
+ end
22
+ end
23
+ end
data/lib/spec_store.rb CHANGED
@@ -218,12 +218,12 @@ module Statsig
218
218
  end
219
219
 
220
220
  Thread.new do
221
- @error_boundary.capture(task: lambda {
221
+ @error_boundary.capture() do
222
222
  loop do
223
223
  sleep @options.rulesets_sync_interval
224
224
  sync_config_specs
225
225
  end
226
- })
226
+ end
227
227
  end
228
228
  end
229
229
 
@@ -233,12 +233,12 @@ module Statsig
233
233
  end
234
234
 
235
235
  Thread.new do
236
- @error_boundary.capture(task: lambda {
236
+ @error_boundary.capture() do
237
237
  loop do
238
238
  sleep @id_lists_sync_interval
239
239
  sync_id_lists
240
240
  end
241
- })
241
+ end
242
242
  end
243
243
  end
244
244
 
data/lib/statsig.rb CHANGED
@@ -331,6 +331,13 @@ module Statsig
331
331
  @shared_instance&.clear_config_overrides
332
332
  end
333
333
 
334
+ ##
335
+ # @param [HashTable] debug information log with exposure events
336
+ def self.set_debug_info(debug_info)
337
+ ensure_initialized
338
+ @shared_instance&.set_debug_info(debug_info)
339
+ end
340
+
334
341
  ##
335
342
  # Gets all evaluated values for the given user.
336
343
  # These values can then be given to a Statsig Client SDK via bootstrapping.
@@ -356,7 +363,7 @@ module Statsig
356
363
  def self.get_statsig_metadata
357
364
  {
358
365
  'sdkType' => 'ruby-server',
359
- 'sdkVersion' => '1.33.2',
366
+ 'sdkVersion' => '1.33.4',
360
367
  'languageVersion' => RUBY_VERSION
361
368
  }
362
369
  end
@@ -11,7 +11,7 @@ require 'dynamic_config'
11
11
  require 'feature_gate'
12
12
  require 'error_boundary'
13
13
  require 'layer'
14
-
14
+ require 'memo'
15
15
  require 'diagnostics'
16
16
 
17
17
  class StatsigDriver
@@ -26,7 +26,7 @@ class StatsigDriver
26
26
  end
27
27
 
28
28
  @err_boundary = Statsig::ErrorBoundary.new(secret_key)
29
- @err_boundary.capture(task: lambda {
29
+ @err_boundary.capture(caller: __method__) do
30
30
  @diagnostics = Statsig::Diagnostics.new()
31
31
  tracker = @diagnostics.track('initialize', 'overall')
32
32
  @options = options || StatsigOptions.new
@@ -40,7 +40,7 @@ class StatsigDriver
40
40
  tracker.end(success: true)
41
41
 
42
42
  @logger.log_diagnostics_event(@diagnostics, 'initialize')
43
- }, caller: __method__.to_s)
43
+ end
44
44
  end
45
45
 
46
46
  def get_gate_impl(
@@ -56,34 +56,38 @@ class StatsigDriver
56
56
  return FeatureGate.new(gate_name) if gate.nil?
57
57
  return FeatureGate.new(gate.name, target_app_ids: gate.target_app_ids)
58
58
  end
59
+
59
60
  user = verify_inputs(user, gate_name, 'gate_name')
61
+ return Statsig::Memo.for(user.get_memo(), :get_gate_impl, gate_name) do
60
62
 
61
- res = Statsig::ConfigResult.new(name: gate_name, disable_exposures: disable_log_exposure, disable_evaluation_details: disable_evaluation_details)
62
- @evaluator.check_gate(user, gate_name, res, ignore_local_overrides: ignore_local_overrides)
63
+ res = Statsig::ConfigResult.new(name: gate_name, disable_exposures: disable_log_exposure, disable_evaluation_details: disable_evaluation_details)
64
+ @evaluator.check_gate(user, gate_name, res, ignore_local_overrides: ignore_local_overrides)
63
65
 
64
- unless disable_log_exposure
66
+ unless disable_log_exposure
65
67
  @logger.log_gate_exposure(
66
- user, res.name, res.gate_value, res.rule_id, res.secondary_exposures, res.evaluation_details
67
- )
68
+ user, res.name, res.gate_value, res.rule_id, res.secondary_exposures, res.evaluation_details
69
+ )
70
+ end
71
+ FeatureGate.from_config_result(res)
68
72
  end
69
- FeatureGate.from_config_result(res)
70
73
  end
71
74
 
75
+
72
76
  def get_gate(user, gate_name, options = nil)
73
- @err_boundary.capture(task: lambda {
74
- run_with_diagnostics(task: lambda {
77
+ @err_boundary.capture(caller: __method__, recover: -> {false}) do
78
+ run_with_diagnostics(caller: :get_gate) do
75
79
  get_gate_impl(user, gate_name,
76
80
  disable_log_exposure: options&.disable_log_exposure == true,
77
81
  skip_evaluation: options&.skip_evaluation == true,
78
82
  disable_evaluation_details: options&.disable_evaluation_details == true
79
83
  )
80
- }, caller: __method__.to_s)
81
- }, recover: -> { false }, caller: __method__.to_s)
84
+ end
85
+ end
82
86
  end
83
87
 
84
88
  def check_gate(user, gate_name, options = nil)
85
- @err_boundary.capture(task: lambda {
86
- run_with_diagnostics(task: lambda {
89
+ @err_boundary.capture(caller: __method__, recover: -> {false}) do
90
+ run_with_diagnostics(caller: :check_gate) do
87
91
  get_gate_impl(
88
92
  user,
89
93
  gate_name,
@@ -91,22 +95,22 @@ class StatsigDriver
91
95
  disable_evaluation_details: options&.disable_evaluation_details == true,
92
96
  ignore_local_overrides: options&.ignore_local_overrides == true
93
97
  ).value
94
- }, caller: __method__.to_s)
95
- }, recover: -> { false }, caller: __method__.to_s)
98
+ end
99
+ end
96
100
  end
97
101
 
98
102
  def manually_log_gate_exposure(user, gate_name)
99
- @err_boundary.capture(task: lambda {
103
+ @err_boundary.capture(caller: __method__) do
100
104
  res = Statsig::ConfigResult.new(name: gate_name)
101
105
  @evaluator.check_gate(user, gate_name, res)
102
106
  context = { :is_manual_exposure => true }
103
107
  @logger.log_gate_exposure(user, gate_name, res.gate_value, res.rule_id, res.secondary_exposures, res.evaluation_details, context)
104
- })
108
+ end
105
109
  end
106
110
 
107
111
  def get_config(user, dynamic_config_name, options = nil)
108
- @err_boundary.capture(task: lambda {
109
- run_with_diagnostics(task: lambda {
112
+ @err_boundary.capture(caller: __method__, recover: -> { DynamicConfig.new(dynamic_config_name) }) do
113
+ run_with_diagnostics(caller: :get_config) do
110
114
  user = verify_inputs(user, dynamic_config_name, "dynamic_config_name")
111
115
  get_config_impl(
112
116
  user,
@@ -115,13 +119,13 @@ class StatsigDriver
115
119
  disable_evaluation_details: options&.disable_evaluation_details == true,
116
120
  ignore_local_overrides: options&.ignore_local_overrides == true
117
121
  )
118
- }, caller: __method__.to_s)
119
- }, recover: -> { DynamicConfig.new(dynamic_config_name) }, caller: __method__.to_s)
122
+ end
123
+ end
120
124
  end
121
125
 
122
126
  def get_experiment(user, experiment_name, options = nil)
123
- @err_boundary.capture(task: lambda {
124
- run_with_diagnostics(task: lambda {
127
+ @err_boundary.capture(caller: __method__, recover: -> { DynamicConfig.new(experiment_name) }) do
128
+ run_with_diagnostics(caller: :get_experiment) do
125
129
  user = verify_inputs(user, experiment_name, "experiment_name")
126
130
  get_config_impl(
127
131
  user,
@@ -131,63 +135,65 @@ class StatsigDriver
131
135
  disable_evaluation_details: options&.disable_evaluation_details == true,
132
136
  ignore_local_overrides: options&.ignore_local_overrides == true
133
137
  )
134
- }, caller: __method__.to_s)
135
- }, recover: -> { DynamicConfig.new(experiment_name) }, caller: __method__.to_s)
138
+ end
139
+ end
136
140
  end
137
141
 
138
142
  def manually_log_config_exposure(user, config_name)
139
- @err_boundary.capture(task: lambda {
143
+ @err_boundary.capture(caller: __method__) do
140
144
  res = Statsig::ConfigResult.new(name: config_name)
141
145
  @evaluator.get_config(user, config_name, res)
142
146
 
143
147
  context = { :is_manual_exposure => true }
144
148
  @logger.log_config_exposure(user, res.name, res.rule_id, res.secondary_exposures, res.evaluation_details, context)
145
- }, caller: __method__.to_s)
149
+ end
146
150
  end
147
151
 
148
152
  def get_user_persisted_values(user, id_type)
149
- @err_boundary.capture(task: lambda {
153
+ @err_boundary.capture(caller: __method__,) do
150
154
  persisted_values = @persistent_storage_utils.get_user_persisted_values(user, id_type)
151
155
  return {} if persisted_values.nil?
152
156
 
153
157
  persisted_values
154
- }, caller: __method__.to_s)
158
+ end
155
159
  end
156
160
 
157
161
  def get_layer(user, layer_name, options = nil)
158
- @err_boundary.capture(task: lambda {
159
- run_with_diagnostics(task: lambda {
162
+ @err_boundary.capture(caller: __method__, recover: -> { Layer.new(layer_name) }) do
163
+ run_with_diagnostics(caller: :get_layer) do
160
164
  user = verify_inputs(user, layer_name, "layer_name")
161
- exposures_disabled = options&.disable_log_exposure == true
162
- res = Statsig::ConfigResult.new(
163
- name: layer_name,
164
- disable_exposures: exposures_disabled,
165
- disable_evaluation_details: options&.disable_evaluation_details == true
166
- )
167
- @evaluator.get_layer(user, layer_name, res)
168
-
169
- exposure_log_func = !exposures_disabled ? lambda { |layer, parameter_name|
170
- @logger.log_layer_exposure(user, layer, parameter_name, res)
171
- } : nil
172
-
173
- Layer.new(res.name, res.json_value, res.rule_id, res.group_name, res.config_delegate, exposure_log_func)
174
- }, caller: __method__.to_s)
175
- }, recover: lambda { Layer.new(layer_name) }, caller: __method__.to_s)
165
+ Statsig::Memo.for(user.get_memo(), :get_layer, layer_name) do
166
+ exposures_disabled = options&.disable_log_exposure == true
167
+ res = Statsig::ConfigResult.new(
168
+ name: layer_name,
169
+ disable_exposures: exposures_disabled,
170
+ disable_evaluation_details: options&.disable_evaluation_details == true
171
+ )
172
+ @evaluator.get_layer(user, layer_name, res)
173
+
174
+ exposure_log_func = !exposures_disabled ? lambda { |layer, parameter_name|
175
+ @logger.log_layer_exposure(user, layer, parameter_name, res)
176
+ } : nil
177
+
178
+ Layer.new(res.name, res.json_value, res.rule_id, res.group_name, res.config_delegate, exposure_log_func)
179
+ end
180
+ end
181
+ end
176
182
  end
177
183
 
178
184
  def manually_log_layer_parameter_exposure(user, layer_name, parameter_name)
179
- @err_boundary.capture(task: lambda {
185
+ @err_boundary.capture(caller: __method__) do
180
186
  res = Statsig::ConfigResult.new(name: layer_name)
181
187
  @evaluator.get_layer(user, layer_name, res)
182
188
 
183
189
  layer = Layer.new(layer_name, res.json_value, res.rule_id, res.group_name, res.config_delegate)
184
190
  context = { :is_manual_exposure => true }
185
191
  @logger.log_layer_exposure(user, layer, parameter_name, res, context)
186
- }, caller: __method__.to_s)
192
+ end
187
193
  end
188
194
 
189
195
  def log_event(user, event_name, value = nil, metadata = nil)
190
- @err_boundary.capture(task: lambda {
196
+ @err_boundary.capture(caller: __method__) do
191
197
  if !user.nil? && !user.instance_of?(StatsigUser)
192
198
  raise Statsig::ValueError.new('Must provide a valid StatsigUser or nil')
193
199
  end
@@ -200,93 +206,99 @@ class StatsigDriver
200
206
  event.value = value
201
207
  event.metadata = metadata
202
208
  @logger.log_event(event)
203
- }, caller: __method__.to_s)
209
+ end
204
210
  end
205
211
 
206
212
  def manually_sync_rulesets
207
- @err_boundary.capture(task: lambda {
213
+ @err_boundary.capture(caller: __method__) do
208
214
  @evaluator.spec_store.sync_config_specs
209
- }, caller: __method__.to_s)
215
+ end
210
216
  end
211
217
 
212
218
  def manually_sync_idlists
213
- @err_boundary.capture(task: lambda {
219
+ @err_boundary.capture(caller: __method__) do
214
220
  @evaluator.spec_store.sync_id_lists
215
- }, caller: __method__.to_s)
221
+ end
216
222
  end
217
223
 
218
224
  def list_gates
219
- @err_boundary.capture(task: lambda {
225
+ @err_boundary.capture(caller: __method__) do
220
226
  @evaluator.list_gates
221
- }, caller: __method__.to_s)
227
+ end
222
228
  end
223
229
 
224
230
  def list_configs
225
- @err_boundary.capture(task: lambda {
231
+ @err_boundary.capture(caller: __method__) do
226
232
  @evaluator.list_configs
227
- }, caller: __method__.to_s)
233
+ end
228
234
  end
229
235
 
230
236
  def list_experiments
231
- @err_boundary.capture(task: lambda {
237
+ @err_boundary.capture(caller: __method__) do
232
238
  @evaluator.list_experiments
233
- }, caller: __method__.to_s)
239
+ end
234
240
  end
235
241
 
236
242
  def list_autotunes
237
- @err_boundary.capture(task: lambda {
243
+ @err_boundary.capture(caller: __method__) do
238
244
  @evaluator.list_autotunes
239
- }, caller: __method__.to_s)
245
+ end
240
246
  end
241
247
 
242
248
  def list_layers
243
- @err_boundary.capture(task: lambda {
249
+ @err_boundary.capture(caller: __method__) do
244
250
  @evaluator.list_layers
245
- }, caller: __method__.to_s)
251
+ end
246
252
  end
247
253
 
248
254
  def shutdown
249
- @err_boundary.capture(task: lambda {
255
+ @err_boundary.capture(caller: __method__) do
250
256
  @shutdown = true
251
257
  @logger.shutdown
252
258
  @evaluator.shutdown
253
- }, caller: __method__.to_s)
259
+ end
254
260
  end
255
261
 
256
262
  def override_gate(gate_name, gate_value)
257
- @err_boundary.capture(task: lambda {
263
+ @err_boundary.capture(caller: __method__) do
258
264
  @evaluator.override_gate(gate_name, gate_value)
259
- }, caller: __method__.to_s)
265
+ end
260
266
  end
261
267
 
262
268
  def remove_gate_override(gate_name)
263
- @err_boundary.capture(task: lambda {
269
+ @err_boundary.capture(caller: __method__) do
264
270
  @evaluator.remove_gate_override(gate_name)
265
- }, caller: __method__.to_s)
271
+ end
266
272
  end
267
273
 
268
274
  def clear_gate_overrides
269
- @err_boundary.capture(task: lambda {
275
+ @err_boundary.capture(caller: __method__) do
270
276
  @evaluator.clear_gate_overrides
271
- }, caller: __method__.to_s)
277
+ end
272
278
  end
273
279
 
274
280
  def override_config(config_name, config_value)
275
- @err_boundary.capture(task: lambda {
281
+ @err_boundary.capture(caller: __method__) do
276
282
  @evaluator.override_config(config_name, config_value)
277
- }, caller: __method__.to_s)
283
+ end
278
284
  end
279
285
 
280
286
  def remove_config_override(config_name)
281
- @err_boundary.capture(task: lambda {
287
+ @err_boundary.capture(caller: __method__) do
282
288
  @evaluator.remove_config_override(config_name)
283
- }, caller: __method__.to_s)
289
+ end
284
290
  end
285
291
 
286
292
  def clear_config_overrides
287
- @err_boundary.capture(task: lambda {
293
+ @err_boundary.capture(caller: __method__) do
288
294
  @evaluator.clear_config_overrides
289
- }, caller: __method__.to_s)
295
+ end
296
+ end
297
+
298
+ def set_debug_info(debug_info)
299
+ @err_boundary.capture(caller: __method__) do
300
+ @logger.set_debug_info(debug_info)
301
+ end
290
302
  end
291
303
 
292
304
  # @param [StatsigUser] user
@@ -294,11 +306,11 @@ class StatsigDriver
294
306
  # @param [Boolean] include_local_overrides
295
307
  # @return [Hash]
296
308
  def get_client_initialize_response(user, hash, client_sdk_key, include_local_overrides)
297
- @err_boundary.capture(task: lambda {
309
+ @err_boundary.capture(caller: __method__, recover: -> { nil }) do
298
310
  validate_user(user)
299
311
  normalize_user(user)
300
312
  @evaluator.get_client_initialize_response(user, hash, client_sdk_key, include_local_overrides)
301
- }, recover: -> { nil }, caller: __method__.to_s)
313
+ end
302
314
  end
303
315
 
304
316
  def maybe_restart_background_threads
@@ -306,22 +318,24 @@ class StatsigDriver
306
318
  return
307
319
  end
308
320
 
309
- @err_boundary.capture(task: lambda {
321
+ @err_boundary.capture(caller: __method__) do
310
322
  @evaluator.maybe_restart_background_threads
311
323
  @logger.maybe_restart_background_threads
312
- }, caller: __method__.to_s)
324
+ end
313
325
  end
314
326
 
315
327
  private
316
328
 
317
- def run_with_diagnostics(task:, caller:)
318
- diagnostics = nil
319
- if Statsig::Diagnostics::API_CALL_KEYS.include?(caller) && Statsig::Diagnostics.sample(1)
320
- diagnostics = Statsig::Diagnostics.new()
321
- tracker = diagnostics.track('api_call', caller)
329
+ def run_with_diagnostics(caller:)
330
+ if !Statsig::Diagnostics::API_CALL_KEYS[caller] || !Statsig::Diagnostics.sample(1)
331
+ return yield
322
332
  end
333
+
334
+ diagnostics = Statsig::Diagnostics.new()
335
+ tracker = diagnostics.track('api_call', caller.to_s)
336
+
323
337
  begin
324
- res = task.call
338
+ res = yield
325
339
  tracker&.end(success: true)
326
340
  rescue StandardError => e
327
341
  tracker&.end(success: false)
@@ -334,28 +348,35 @@ class StatsigDriver
334
348
 
335
349
  def verify_inputs(user, config_name, variable_name)
336
350
  validate_user(user)
351
+ user = Statsig::Memo.for(user.get_memo(), :verify_inputs, 0) do
352
+ user = normalize_user(user)
353
+ check_shutdown
354
+ maybe_restart_background_threads
355
+ user
356
+ end
357
+
337
358
  if !config_name.is_a?(String) || config_name.empty?
338
359
  raise Statsig::ValueError.new("Invalid #{variable_name} provided")
339
360
  end
340
361
 
341
- check_shutdown
342
- maybe_restart_background_threads
343
- normalize_user(user)
362
+ user
344
363
  end
345
364
 
346
365
  def get_config_impl(user, config_name, disable_log_exposure, user_persisted_values: nil, disable_evaluation_details: false, ignore_local_overrides: false)
347
- res = Statsig::ConfigResult.new(
348
- name: config_name,
349
- disable_exposures: disable_log_exposure,
350
- disable_evaluation_details: disable_evaluation_details
351
- )
352
- @evaluator.get_config(user, config_name, res, user_persisted_values: user_persisted_values, ignore_local_overrides: ignore_local_overrides)
366
+ return Statsig::Memo.for(user.get_memo(), :get_config_impl, config_name) do
367
+ res = Statsig::ConfigResult.new(
368
+ name: config_name,
369
+ disable_exposures: disable_log_exposure,
370
+ disable_evaluation_details: disable_evaluation_details
371
+ )
372
+ @evaluator.get_config(user, config_name, res, user_persisted_values: user_persisted_values, ignore_local_overrides: ignore_local_overrides)
353
373
 
354
- unless disable_log_exposure
355
- @logger.log_config_exposure(user, res.name, res.rule_id, res.secondary_exposures, res.evaluation_details)
356
- end
374
+ unless disable_log_exposure
375
+ @logger.log_config_exposure(user, res.name, res.rule_id, res.secondary_exposures, res.evaluation_details)
376
+ end
357
377
 
358
- DynamicConfig.new(res.name, res.json_value, res.rule_id, res.group_name, res.id_type, res.evaluation_details)
378
+ DynamicConfig.new(res.name, res.json_value, res.rule_id, res.group_name, res.id_type, res.evaluation_details)
379
+ end
359
380
  end
360
381
 
361
382
  def validate_user(user)
data/lib/statsig_event.rb CHANGED
@@ -15,7 +15,9 @@ class StatsigEvent
15
15
 
16
16
  def user=(value)
17
17
  if value.is_a?(StatsigUser)
18
- @user = value.serialize(true)
18
+ @user = Statsig::Memo.for(value.get_memo(), :serialize, 0) do
19
+ value.serialize(true)
20
+ end
19
21
  end
20
22
  end
21
23
 
@@ -28,6 +28,7 @@ module Statsig
28
28
  @deduper = Concurrent::Set.new()
29
29
  @interval = 0
30
30
  @flush_mutex = Mutex.new
31
+ @debug_info = nil
31
32
  end
32
33
 
33
34
  def log_event(event)
@@ -45,6 +46,9 @@ module Statsig
45
46
  gateValue: value.to_s,
46
47
  ruleID: rule_id || Statsig::Const::EMPTY_STR,
47
48
  }
49
+ if @debug_info != nil
50
+ metadata[:debugInfo] = @debug_info
51
+ end
48
52
  return false if not is_unique_exposure(user, $gate_exposure_event, metadata)
49
53
  event.metadata = metadata
50
54
 
@@ -62,6 +66,9 @@ module Statsig
62
66
  config: config_name,
63
67
  ruleID: rule_id || Statsig::Const::EMPTY_STR,
64
68
  }
69
+ if @debug_info != nil
70
+ metadata[:debugInfo] = @debug_info
71
+ end
65
72
  return false if not is_unique_exposure(user, $config_exposure_event, metadata)
66
73
  event.metadata = metadata
67
74
  event.secondary_exposures = secondary_exposures.is_a?(Array) ? secondary_exposures : []
@@ -89,6 +96,9 @@ module Statsig
89
96
  parameterName: parameter_name,
90
97
  isExplicitParameter: String(is_explicit)
91
98
  }
99
+ if @debug_info != nil
100
+ metadata[:debugInfo] = @debug_info
101
+ end
92
102
  return false unless is_unique_exposure(user, $layer_exposure_event, metadata)
93
103
  event.metadata = metadata
94
104
  event.secondary_exposures = exposures.is_a?(Array) ? exposures : []
@@ -117,14 +127,14 @@ module Statsig
117
127
 
118
128
  def periodic_flush
119
129
  Thread.new do
120
- @error_boundary.capture(task: lambda {
130
+ @error_boundary.capture() do
121
131
  loop do
122
132
  sleep @options.logging_interval_seconds
123
133
  flush_async
124
134
  @interval += 1
125
135
  @deduper.clear if @interval % 2 == 0
126
136
  end
127
- })
137
+ end
128
138
  end
129
139
  end
130
140
 
@@ -160,6 +170,10 @@ module Statsig
160
170
  end
161
171
  end
162
172
 
173
+ def set_debug_info(debug_info)
174
+ @debug_info = debug_info
175
+ end
176
+
163
177
  private
164
178
 
165
179
  def safe_add_eval_details(eval_details, event)
@@ -186,21 +200,15 @@ module Statsig
186
200
  def is_unique_exposure(user, event_name, metadata)
187
201
  return true if user.nil?
188
202
  @deduper.clear if @deduper.size > 10000
189
- custom_id_key = ''
190
- if user.custom_ids.is_a?(Hash)
191
- custom_id_key = user.custom_ids.values.join(',')
192
- end
203
+
204
+ user_key = user.user_key
193
205
 
194
206
  metadata_key = ''
195
207
  if metadata.is_a?(Hash)
196
208
  metadata_key = metadata.reject { |key, _| $ignored_metadata_keys.include?(key) }.values.join(',')
197
209
  end
198
-
199
- user_id_key = ''
200
- unless user.user_id.nil?
201
- user_id_key = user.user_id
202
- end
203
- key = [user_id_key, custom_id_key, event_name, metadata_key].join(',')
210
+
211
+ key = [user_key, event_name, metadata_key].join(',')
204
212
 
205
213
  return false if @deduper.include?(key)
206
214
  @deduper.add(key)
data/lib/statsig_user.rb CHANGED
@@ -1,39 +1,78 @@
1
1
  require 'json'
2
2
  require 'constants'
3
-
4
3
  ##
5
4
  # The user object to be evaluated against your Statsig configurations (gates/experiments/dynamic configs).
6
5
  class StatsigUser
7
6
 
8
7
  # An identifier for this user. Evaluated against the User ID criteria. (https://docs.statsig.com/feature-gates/conditions#userid)
9
- attr_accessor :user_id
8
+ attr_reader :user_id
9
+ def user_id=(value)
10
+ value_changed()
11
+ @user_id = value
12
+ end
10
13
 
11
14
  # An identifier for this user. Evaluated against the Email criteria. (https://docs.statsig.com/feature-gates/conditions#email)
12
- attr_accessor :email
15
+ attr_reader :email
16
+ def email=(value)
17
+ value_changed()
18
+ @email = value
19
+ end
13
20
 
14
21
  # An IP address associated with this user. Evaluated against the IP Address criteria. (https://docs.statsig.com/feature-gates/conditions#ip)
15
- attr_accessor :ip
22
+ attr_reader :ip
23
+ def ip=(value)
24
+ value_changed()
25
+ @ip = value
26
+ end
16
27
 
17
28
  # A user agent string associated with this user. Evaluated against Browser Version and Name (https://docs.statsig.com/feature-gates/conditions#browser-version)
18
- attr_accessor :user_agent
29
+ attr_reader :user_agent
30
+ def user_agent=(value)
31
+ value_changed()
32
+ @user_agent = value
33
+ end
19
34
 
20
35
  # The country code associated with this user (e.g New Zealand => NZ). Evaluated against the Country criteria. (https://docs.statsig.com/feature-gates/conditions#country)
21
- attr_accessor :country
36
+ attr_reader :country
37
+ def country=(value)
38
+ value_changed()
39
+ @country = value
40
+ end
22
41
 
23
42
  # An locale for this user.
24
- attr_accessor :locale
43
+ attr_reader :locale
44
+ def locale=(value)
45
+ value_changed()
46
+ @locale = value
47
+ end
25
48
 
26
49
  # The current app version the user is interacting with. Evaluated against the App Version criteria. (https://docs.statsig.com/feature-gates/conditions#app-version)
27
- attr_accessor :app_version
50
+ attr_reader :app_version
51
+ def app_version=(value)
52
+ value_changed()
53
+ @app_version = value
54
+ end
28
55
 
29
56
  # A Hash you can use to set environment variables that apply to this user. e.g. { "tier" => "development" }
30
- attr_accessor :statsig_environment
57
+ attr_reader :statsig_environment
58
+ def statsig_environment=(value)
59
+ value_changed()
60
+ @statsig_environment = value
61
+ end
31
62
 
32
63
  # Any Custom IDs to associated with the user. (See https://docs.statsig.com/guides/experiment-on-custom-id-types)
33
- attr_accessor :custom_ids
64
+ attr_reader :custom_ids
65
+ def custom_ids=(value)
66
+ value_changed()
67
+ @custom_ids = value
68
+ end
34
69
 
35
70
  # Any value you wish to use in evaluation, but do not want logged with events, can be stored in this field.
36
- attr_accessor :private_attributes
71
+ attr_reader :private_attributes
72
+ def private_attributes=(value)
73
+ value_changed()
74
+ @private_attributes = value
75
+ end
37
76
 
38
77
  def custom
39
78
  @custom
@@ -41,9 +80,12 @@ class StatsigUser
41
80
 
42
81
  # Any custom fields for this user. Evaluated against the Custom criteria. (https://docs.statsig.com/feature-gates/conditions#custom)
43
82
  def custom=(value)
83
+ value_changed()
44
84
  @custom = value.is_a?(Hash) ? value : Hash.new
45
85
  end
46
86
 
87
+ attr_accessor :memo_timeout
88
+
47
89
  def initialize(user_hash)
48
90
  the_hash = user_hash
49
91
  begin
@@ -63,6 +105,9 @@ class StatsigUser
63
105
  @private_attributes = from_hash(the_hash, [:private_attributes, :privateAttributes], Hash)
64
106
  @custom_ids = from_hash(the_hash, [:custom_ids, :customIDs], Hash)
65
107
  @statsig_environment = from_hash(the_hash, [:statsig_environment, :statsigEnvironment], Hash)
108
+ @memo = {}
109
+ @dirty = true
110
+ @memo_timeout = 2
66
111
  end
67
112
 
68
113
  def serialize(for_logging)
@@ -138,7 +183,45 @@ class StatsigUser
138
183
  @user_id
139
184
  end
140
185
 
186
+ def get_memo
187
+ current_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
188
+
189
+ if @dirty || current_time - (@memo_access_time ||= current_time) > @memo_timeout
190
+ if @memo.size() > 0
191
+ @memo.clear
192
+ end
193
+ @dirty = false
194
+ @memo_access_time = current_time
195
+ end
196
+
197
+ @memo
198
+ end
199
+
200
+ def clear_memo
201
+ @memo.clear
202
+ @dirty = false
203
+ @memo_access_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
204
+ end
205
+
206
+ def user_key
207
+ unless !@dirty && defined? @_user_key
208
+ custom_id_key = ''
209
+ if self.custom_ids.is_a?(Hash)
210
+ custom_id_key = self.custom_ids.values.join(',')
211
+ end
212
+ user_id_key = ''
213
+ unless self.user_id.nil?
214
+ user_id_key = self.user_id.to_s
215
+ end
216
+ @_user_key = user_id_key + ',' + custom_id_key.to_s
217
+ end
218
+ @_user_key
219
+ end
220
+
141
221
  private
222
+ def value_changed
223
+ @dirty = true
224
+ end
142
225
 
143
226
  # Pulls fields from the user hash via Symbols and Strings
144
227
  def from_hash(user_hash, keys, type)
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.33.2
4
+ version: 1.33.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Statsig, Inc
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-03-05 00:00:00.000000000 Z
11
+ date: 2024-03-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -310,6 +310,7 @@ files:
310
310
  - lib/interfaces/data_store.rb
311
311
  - lib/interfaces/user_persistent_storage.rb
312
312
  - lib/layer.rb
313
+ - lib/memo.rb
313
314
  - lib/network.rb
314
315
  - lib/spec_store.rb
315
316
  - lib/statsig.rb