statsig 1.33.2 → 1.33.4

Sign up to get free protection for your applications and to get access to all the features.
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