statsig 1.29.0 → 1.31.0

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: '093cc05e0b8f3bb7e2f0f019cf36ad33e5abc4c4349d2fb1faa056c1ff5aa341'
4
- data.tar.gz: d66640f8b0c317ca018ff4ccc77d1d370af89b1c84416508631deb610236d519
3
+ metadata.gz: 1442642777f1e73e38aa9944a88f9eab27a88df0c856ed0bc708555fe51a7b91
4
+ data.tar.gz: '07159c53071c5650f01016e485aca359568025f96dbeea39e3b99d39d1286662'
5
5
  SHA512:
6
- metadata.gz: f634575320cd1ababfa25816ee8ac5fe27e34c761895b4ba6b20957363e487c295767d65e123ab7d306aa1ce2f535f55cc58570f8767790948d2647ab321150b
7
- data.tar.gz: d563727b175a23afa50cb68cbaf91eb49435bae46b1a48b72d9bf0ff38febeefa86b886c34cc2632f37e357c44f96a1e2586050b41e9ac156697f4105c4f9f0b
6
+ metadata.gz: 1454423d5e182969c8abce5ab37a7aef9ed9ff854398725b13d3743dc85f49dffb2a7e9176ffdda6e18bef2cdbc4c191aac0716ac204b5a054e1e11e359bde6c
7
+ data.tar.gz: 1a07714268ca3bec58974e3059c34dcc6a7e7f0a8617fd22874b0c49342546cc475ea9b6ec24569e4d07109aca7c0afdd5ead59f809512d5a9ba83ca0894e4e7
@@ -3,13 +3,6 @@
3
3
  require_relative 'hash_utils'
4
4
  require 'sorbet-runtime'
5
5
 
6
- $empty_eval_result = {
7
- :gate_value => false,
8
- :json_value => {},
9
- :rule_id => "",
10
- :is_experiment_group => false,
11
- :secondary_exposures => []
12
- }
13
6
 
14
7
  module ClientInitializeHelpers
15
8
  class ResponseFormatter
@@ -51,18 +44,6 @@ module ClientInitializeHelpers
51
44
  return nil
52
45
  end
53
46
 
54
- safe_eval_result = eval_result == $fetch_from_server ? $empty_eval_result : {
55
- :gate_value => eval_result.gate_value,
56
- :json_value => eval_result.json_value,
57
- :rule_id => eval_result.rule_id,
58
- :group_name => eval_result.group_name,
59
- :id_type => eval_result.id_type,
60
- :config_delegate => eval_result.config_delegate,
61
- :is_experiment_group => eval_result.is_experiment_group,
62
- :secondary_exposures => filter_segments_from_secondary_exposures(eval_result.secondary_exposures),
63
- :undelegated_sec_exps => eval_result.undelegated_sec_exps
64
- }
65
-
66
47
  category = config_spec['type']
67
48
  entity_type = config_spec['entity']
68
49
 
@@ -75,26 +56,26 @@ module ClientInitializeHelpers
75
56
  return nil
76
57
  end
77
58
 
78
- result['value'] = safe_eval_result[:gate_value]
79
- result["group_name"] = safe_eval_result[:group_name]
80
- result["id_type"] = safe_eval_result[:id_type]
59
+ result['value'] = eval_result.gate_value
60
+ result["group_name"] = eval_result.group_name
61
+ result["id_type"] = eval_result.id_type
81
62
  when 'dynamic_config'
82
63
  id_type = config_spec['idType']
83
- result['value'] = safe_eval_result[:json_value]
84
- result["group"] = safe_eval_result[:rule_id]
85
- result["group_name"] = safe_eval_result[:group_name]
86
- result["id_type"] = safe_eval_result[:id_type]
64
+ result['value'] = eval_result.json_value
65
+ result["group"] = eval_result.rule_id
66
+ result["group_name"] = eval_result.group_name
67
+ result["id_type"] = eval_result.id_type
87
68
  result["is_device_based"] = id_type.is_a?(String) && id_type.downcase == 'stableid'
88
69
  else
89
70
  return nil
90
71
  end
91
72
 
92
73
  if entity_type == 'experiment'
93
- populate_experiment_fields(config_name, config_spec, safe_eval_result, result)
74
+ populate_experiment_fields(config_name, config_spec, eval_result, result)
94
75
  end
95
76
 
96
77
  if entity_type == 'layer'
97
- populate_layer_fields(config_spec, safe_eval_result, result)
78
+ populate_layer_fields(config_spec, eval_result, result)
98
79
  result.delete('id_type') # not exposed for layer configs in /initialize
99
80
  end
100
81
 
@@ -102,8 +83,8 @@ module ClientInitializeHelpers
102
83
  [hashed_name, result.merge(
103
84
  {
104
85
  "name" => hashed_name,
105
- "rule_id" => safe_eval_result[:rule_id],
106
- "secondary_exposures" => clean_exposures(safe_eval_result[:secondary_exposures])
86
+ "rule_id" => eval_result.rule_id,
87
+ "secondary_exposures" => clean_exposures(eval_result.secondary_exposures)
107
88
  }).compact]
108
89
  end
109
90
 
@@ -118,7 +99,7 @@ module ClientInitializeHelpers
118
99
  end
119
100
 
120
101
  def populate_experiment_fields(config_name, config_spec, eval_result, result)
121
- result["is_user_in_experiment"] = eval_result[:is_experiment_group]
102
+ result["is_user_in_experiment"] = eval_result.is_experiment_group
122
103
  result["is_experiment_active"] = config_spec['isActive'] == true
123
104
 
124
105
  if config_spec['hasSharedParams'] != true
@@ -138,7 +119,7 @@ module ClientInitializeHelpers
138
119
  end
139
120
 
140
121
  def populate_layer_fields(config_spec, eval_result, result)
141
- delegate = eval_result[:config_delegate]
122
+ delegate = eval_result.config_delegate
142
123
  result["explicit_parameters"] = config_spec["explicitParameters"] || []
143
124
 
144
125
  if delegate.nil? == false && delegate.empty? == false
@@ -151,7 +132,7 @@ module ClientInitializeHelpers
151
132
  result["explicit_parameters"] = delegate_spec["explicitParameters"] || []
152
133
  end
153
134
 
154
- result["undelegated_secondary_exposures"] = clean_exposures(eval_result[:undelegated_sec_exps] || [])
135
+ result["undelegated_secondary_exposures"] = clean_exposures(eval_result.undelegated_sec_exps || [])
155
136
  end
156
137
 
157
138
  def hash_name(name)
data/lib/config_result.rb CHANGED
@@ -18,6 +18,7 @@ module Statsig
18
18
  attr_accessor :evaluation_details
19
19
  attr_accessor :group_name
20
20
  attr_accessor :id_type
21
+ attr_accessor :target_app_ids
21
22
 
22
23
  def initialize(
23
24
  name,
@@ -30,7 +31,8 @@ module Statsig
30
31
  is_experiment_group: false,
31
32
  evaluation_details: nil,
32
33
  group_name: nil,
33
- id_type: '')
34
+ id_type: '',
35
+ target_app_ids: nil)
34
36
  @name = name
35
37
  @gate_value = gate_value
36
38
  @json_value = json_value
@@ -43,6 +45,7 @@ module Statsig
43
45
  @evaluation_details = evaluation_details
44
46
  @group_name = group_name
45
47
  @id_type = id_type
48
+ @target_app_ids = target_app_ids
46
49
  end
47
50
 
48
51
  sig { params(config_name: String, user_persisted_values: UserPersistedValues).returns(T.nilable(ConfigResult)) }
@@ -62,7 +65,9 @@ module Statsig
62
65
  hash['rule_id'],
63
66
  hash['secondary_exposures'],
64
67
  evaluation_details: EvaluationDetails.persisted(hash['config_sync_time'], hash['init_time']),
65
- group_name: hash['group_name']
68
+ group_name: hash['group_name'],
69
+ id_type: hash['id_type'],
70
+ target_app_ids: hash['target_app_ids']
66
71
  )
67
72
  end
68
73
 
@@ -75,7 +80,9 @@ module Statsig
75
80
  secondary_exposures: @secondary_exposures,
76
81
  config_sync_time: @evaluation_details.config_sync_time,
77
82
  init_time: @init_time,
78
- group_name: @group_name
83
+ group_name: @group_name,
84
+ id_type: @id_type,
85
+ target_app_ids: @target_app_ids
79
86
  }
80
87
  end
81
88
  end
data/lib/diagnostics.rb CHANGED
@@ -6,18 +6,14 @@ module Statsig
6
6
  class Diagnostics
7
7
  extend T::Sig
8
8
 
9
- sig { returns(String) }
10
- attr_accessor :context
11
-
12
- sig { returns(T::Array[T::Hash[Symbol, T.untyped]]) }
9
+ sig { returns(T::Hash[String, T::Array[T::Hash[Symbol, T.untyped]]]) }
13
10
  attr_reader :markers
14
11
 
15
12
  sig { returns(T::Hash[String, Numeric]) }
16
13
  attr_accessor :sample_rates
17
14
 
18
- def initialize(context)
19
- @context = context
20
- @markers = []
15
+ def initialize()
16
+ @markers = {:initialize => [], :api_call => [], :config_sync => []}
21
17
  @sample_rates = {}
22
18
  end
23
19
 
@@ -26,11 +22,12 @@ module Statsig
26
22
  key: String,
27
23
  action: String,
28
24
  step: T.any(String, NilClass),
29
- tags: T::Hash[Symbol, T.untyped]
25
+ tags: T::Hash[Symbol, T.untyped],
26
+ context: String
30
27
  ).void
31
28
  end
32
29
 
33
- def mark(key, action, step, tags)
30
+ def mark(key, action, step, tags, context)
34
31
  marker = {
35
32
  key: key,
36
33
  action: action,
@@ -44,60 +41,49 @@ module Statsig
44
41
  marker[key] = val
45
42
  end
46
43
  end
47
- @markers.push(marker)
44
+ if @markers[context].nil?
45
+ @markers[context] = []
46
+ end
47
+ @markers[context].push(marker)
48
48
  end
49
49
 
50
50
  sig do
51
51
  params(
52
+ context: String,
52
53
  key: String,
53
54
  step: T.any(String, NilClass),
54
55
  tags: T::Hash[Symbol, T.untyped]
55
56
  ).returns(Tracker)
56
57
  end
57
- def track(key, step = nil, tags = {})
58
- tracker = Tracker.new(self, key, step, tags)
58
+ def track(context, key, step = nil, tags = {})
59
+ tracker = Tracker.new(self, context, key, step, tags)
59
60
  tracker.start(**tags)
60
61
  tracker
61
62
  end
62
63
 
63
- sig { returns(T::Hash[Symbol, T.untyped]) }
64
-
65
- def serialize
66
- {
67
- context: @context.clone,
68
- markers: @markers.clone
69
- }
70
- end
71
-
72
- def serialize_with_sampling
73
- marker_keys = @markers.map { |e| e[:key] }
64
+ def serialize_with_sampling(context)
65
+ marker_keys = @markers[context].map { |e| e[:key] }
74
66
  unique_marker_keys = marker_keys.uniq { |e| e }
75
67
  sampled_marker_keys = unique_marker_keys.select do |key|
76
68
  @sample_rates.key?(key) && !self.class.sample(@sample_rates[key])
77
69
  end
78
- final_markers = @markers.select do |marker|
70
+ final_markers = @markers[context].select do |marker|
79
71
  !sampled_marker_keys.include?(marker[:key])
80
72
  end
81
73
  {
82
- context: @context.clone,
83
- markers: final_markers
74
+ context: context.clone,
75
+ markers: final_markers.clone
84
76
  }
85
77
  end
86
78
 
87
- def clear_markers
88
- @markers.clear
79
+ def clear_markers(context)
80
+ @markers[context].clear
89
81
  end
90
82
 
91
83
  def self.sample(rate_over_ten_thousand)
92
84
  rand * 10_000 < rate_over_ten_thousand
93
85
  end
94
86
 
95
- class Context
96
- INITIALIZE = 'initialize'.freeze
97
- CONFIG_SYNC = 'config_sync'.freeze
98
- API_CALL = 'api_call'.freeze
99
- end
100
-
101
87
  API_CALL_KEYS = %w[check_gate get_config get_experiment get_layer].freeze
102
88
 
103
89
  class Tracker
@@ -106,24 +92,26 @@ module Statsig
106
92
  sig do
107
93
  params(
108
94
  diagnostics: Diagnostics,
95
+ context: String,
109
96
  key: String,
110
97
  step: T.any(String, NilClass),
111
98
  tags: T::Hash[Symbol, T.untyped]
112
99
  ).void
113
100
  end
114
- def initialize(diagnostics, key, step, tags = {})
101
+ def initialize(diagnostics, context, key, step, tags = {})
115
102
  @diagnostics = diagnostics
103
+ @context = context
116
104
  @key = key
117
105
  @step = step
118
106
  @tags = tags
119
107
  end
120
108
 
121
109
  def start(**tags)
122
- @diagnostics.mark(@key, 'start', @step, tags.nil? ? {} : tags.merge(@tags))
110
+ @diagnostics.mark(@key, 'start', @step, tags.nil? ? {} : tags.merge(@tags), @context)
123
111
  end
124
112
 
125
113
  def end(**tags)
126
- @diagnostics.mark(@key, 'end', @step, tags.nil? ? {} : tags.merge(@tags))
114
+ @diagnostics.mark(@key, 'end', @step, tags.nil? ? {} : tags.merge(@tags), @context)
127
115
  end
128
116
  end
129
117
  end
@@ -24,6 +24,7 @@ module Statsig
24
24
  end
25
25
 
26
26
  puts '[Statsig]: An unexpected exception occurred.'
27
+ puts e.message
27
28
  log_exception(e, tag: caller)
28
29
  res = recover.call
29
30
  end
@@ -45,6 +46,7 @@ module Statsig
45
46
  'STATSIG-API-KEY' => @sdk_key,
46
47
  'STATSIG-SDK-TYPE' => meta['sdkType'],
47
48
  'STATSIG-SDK-VERSION' => meta['sdkVersion'],
49
+ 'STATSIG-SDK-LANGUAGE-VERSION' => meta['languageVersion'],
48
50
  'Content-Type' => 'application/json; charset=UTF-8'
49
51
  }).accept(:json)
50
52
  body = {
@@ -9,6 +9,7 @@ module Statsig
9
9
  BOOTSTRAP = 'Bootstrap'.freeze
10
10
  DATA_ADAPTER = 'DataAdapter'.freeze
11
11
  PERSISTED = 'Persisted'.freeze
12
+ UNSUPPORTED = 'Unsupported'.freeze
12
13
  end
13
14
 
14
15
  class EvaluationDetails
data/lib/evaluator.rb CHANGED
@@ -13,13 +13,12 @@ require 'evaluation_details'
13
13
  require 'user_agent_parser/operating_system'
14
14
  require 'user_persistent_storage_utils'
15
15
 
16
- $fetch_from_server = 'fetch_from_server'
17
- $type_dynamic_config = 'dynamic_config'
18
-
19
16
  module Statsig
20
17
  class Evaluator
21
18
  extend T::Sig
22
19
 
20
+ UNSUPPORTED_EVALUATION = :unsupported_eval
21
+
23
22
  sig { returns(SpecStore) }
24
23
  attr_accessor :spec_store
25
24
 
@@ -31,20 +30,16 @@ module Statsig
31
30
 
32
31
  sig do
33
32
  params(
34
- network: Network,
33
+ store: SpecStore,
35
34
  options: StatsigOptions,
36
- error_callback: T.any(Method, Proc, NilClass),
37
- diagnostics: Diagnostics,
38
- error_boundary: ErrorBoundary,
39
- logger: StatsigLogger,
40
35
  persistent_storage_utils: UserPersistentStorageUtils,
41
36
  ).void
42
37
  end
43
- def initialize(network, options, error_callback, diagnostics, error_boundary, logger, persistent_storage_utils)
44
- @spec_store = Statsig::SpecStore.new(network, options, error_callback, diagnostics, error_boundary, logger)
38
+ def initialize(store, options, persistent_storage_utils)
45
39
  UAParser.initialize_async
46
40
  CountryLookup.initialize_async
47
41
 
42
+ @spec_store = store
48
43
  @gate_overrides = {}
49
44
  @config_overrides = {}
50
45
  @options = options
@@ -122,7 +117,7 @@ module Statsig
122
117
  @persistent_storage_utils.add_evaluation_to_user_persisted_values(user_persisted_values, config_name, evaluation)
123
118
  @persistent_storage_utils.save_to_storage(user, config['idType'], user_persisted_values)
124
119
  end
125
- # Otherwise, remove from persisted storage
120
+ # Otherwise, remove from persisted storage
126
121
  else
127
122
  @persistent_storage_utils.remove_experiment_from_storage(user, config['idType'], config_name)
128
123
  evaluation = eval_spec(user, config)
@@ -143,6 +138,26 @@ module Statsig
143
138
  eval_spec(user, @spec_store.get_layer(layer_name))
144
139
  end
145
140
 
141
+ def list_gates
142
+ @spec_store.gates.map { |name, _| name }
143
+ end
144
+
145
+ def list_configs
146
+ @spec_store.configs.map { |name, config| name if config['entity'] == 'dynamic_config' }.compact
147
+ end
148
+
149
+ def list_experiments
150
+ @spec_store.configs.map { |name, config| name if config['entity'] == 'experiment' }.compact
151
+ end
152
+
153
+ def list_autotunes
154
+ @spec_store.configs.map { |name, config| name if config['entity'] == 'autotune' }.compact
155
+ end
156
+
157
+ def list_layers
158
+ @spec_store.layers.map { |name, _| name }
159
+ end
160
+
146
161
  def get_client_initialize_response(user, hash, client_sdk_key)
147
162
  if @spec_store.is_ready_for_checks == false
148
163
  return nil
@@ -204,7 +219,21 @@ module Statsig
204
219
  until i >= config['rules'].length do
205
220
  rule = config['rules'][i]
206
221
  result = eval_rule(user, rule)
207
- return $fetch_from_server if result.to_s == $fetch_from_server
222
+ return Statsig::ConfigResult.new(
223
+ config['name'],
224
+ false,
225
+ config['defaultValue'],
226
+ '',
227
+ exposures,
228
+ evaluation_details: EvaluationDetails.new(
229
+ @spec_store.last_config_sync_time,
230
+ @spec_store.initial_config_sync_time,
231
+ EvaluationReason::UNSUPPORTED,
232
+ ),
233
+ group_name: nil,
234
+ id_type: config['idType'],
235
+ target_app_ids: config['targetAppIDs']
236
+ ) if result == UNSUPPORTED_EVALUATION
208
237
  exposures = exposures + result.secondary_exposures
209
238
  if result.gate_value
210
239
 
@@ -226,7 +255,8 @@ module Statsig
226
255
  ),
227
256
  is_experiment_group: result.is_experiment_group,
228
257
  group_name: result.group_name,
229
- id_type: config['idType']
258
+ id_type: config['idType'],
259
+ target_app_ids: config['targetAppIDs']
230
260
  )
231
261
  end
232
262
 
@@ -248,7 +278,8 @@ module Statsig
248
278
  @spec_store.init_reason
249
279
  ),
250
280
  group_name: nil,
251
- id_type: config['idType']
281
+ id_type: config['idType'],
282
+ target_app_ids: config['targetAppIDs']
252
283
  )
253
284
  end
254
285
 
@@ -260,8 +291,8 @@ module Statsig
260
291
  i = 0
261
292
  until i >= rule['conditions'].length do
262
293
  result = eval_condition(user, rule['conditions'][i])
263
- if result.to_s == $fetch_from_server
264
- return $fetch_from_server
294
+ if result == UNSUPPORTED_EVALUATION
295
+ return UNSUPPORTED_EVALUATION
265
296
  end
266
297
 
267
298
  if result.is_a?(Hash)
@@ -294,7 +325,7 @@ module Statsig
294
325
  return nil unless (config = @spec_store.get_config(delegate))
295
326
 
296
327
  delegated_result = self.eval_spec(user, config)
297
- return $fetch_from_server if delegated_result.to_s == $fetch_from_server
328
+ return UNSUPPORTED_EVALUATION if delegated_result == UNSUPPORTED_EVALUATION
298
329
 
299
330
  delegated_result.name = name
300
331
  delegated_result.config_delegate = delegate
@@ -314,7 +345,7 @@ module Statsig
314
345
  additional_values = Hash.new unless additional_values.is_a? Hash
315
346
  id_type = condition['idType']
316
347
 
317
- return $fetch_from_server unless type.is_a? String
348
+ return UNSUPPORTED_EVALUATION unless type.is_a? String
318
349
  type = type.downcase
319
350
 
320
351
  case type
@@ -322,7 +353,7 @@ module Statsig
322
353
  return true
323
354
  when 'fail_gate', 'pass_gate'
324
355
  other_gate_result = check_gate(user, target)
325
- return $fetch_from_server if other_gate_result.to_s == $fetch_from_server
356
+ return UNSUPPORTED_EVALUATION if other_gate_result == UNSUPPORTED_EVALUATION
326
357
 
327
358
  gate_value = other_gate_result&.gate_value == true
328
359
  new_exposure = {
@@ -337,10 +368,8 @@ module Statsig
337
368
  }
338
369
  when 'ip_based'
339
370
  value = get_value_from_user(user, field) || get_value_from_ip(user, field)
340
- return $fetch_from_server if value.to_s == $fetch_from_server
341
371
  when 'ua_based'
342
372
  value = get_value_from_user(user, field) || get_value_from_ua(user, field)
343
- return $fetch_from_server if value.to_s == $fetch_from_server
344
373
  when 'user_field'
345
374
  value = get_value_from_user(user, field)
346
375
  when 'environment_field'
@@ -359,10 +388,10 @@ module Statsig
359
388
  when 'unit_id'
360
389
  value = user.get_unit_id(id_type)
361
390
  else
362
- return $fetch_from_server
391
+ return UNSUPPORTED_EVALUATION
363
392
  end
364
393
 
365
- return $fetch_from_server if value.to_s == $fetch_from_server || !operator.is_a?(String)
394
+ return UNSUPPORTED_EVALUATION if !operator.is_a?(String)
366
395
  operator = operator.downcase
367
396
 
368
397
  case operator
@@ -444,7 +473,7 @@ module Statsig
444
473
  return false
445
474
  end
446
475
  else
447
- return $fetch_from_server
476
+ return UNSUPPORTED_EVALUATION
448
477
  end
449
478
  end
450
479
 
@@ -0,0 +1,70 @@
1
+ # typed: false
2
+
3
+ require 'sorbet-runtime'
4
+
5
+ class FeatureGate
6
+ extend T::Sig
7
+
8
+ sig { returns(String) }
9
+ attr_accessor :name
10
+
11
+ sig { returns(T::Boolean) }
12
+ attr_accessor :value
13
+
14
+ sig { returns(String) }
15
+ attr_accessor :rule_id
16
+
17
+ sig { returns(T.nilable(String)) }
18
+ attr_accessor :group_name
19
+
20
+ sig { returns(String) }
21
+ attr_accessor :id_type
22
+
23
+ sig { returns(T.nilable(Statsig::EvaluationDetails)) }
24
+ attr_accessor :evaluation_details
25
+
26
+ sig { returns(T.nilable(T::Array[String])) }
27
+ attr_accessor :target_app_ids
28
+
29
+ sig do
30
+ params(
31
+ name: String,
32
+ value: T::Boolean,
33
+ rule_id: String,
34
+ group_name: T.nilable(String),
35
+ id_type: String,
36
+ evaluation_details: T.nilable(Statsig::EvaluationDetails),
37
+ target_app_ids: T.nilable(T::Array[String])
38
+ ).void
39
+ end
40
+ def initialize(
41
+ name,
42
+ value: false,
43
+ rule_id: '',
44
+ group_name: nil,
45
+ id_type: '',
46
+ evaluation_details: nil,
47
+ target_app_ids: nil
48
+ )
49
+ @name = name
50
+ @value = value
51
+ @rule_id = rule_id
52
+ @group_name = group_name
53
+ @id_type = id_type
54
+ @evaluation_details = evaluation_details
55
+ @target_app_ids = target_app_ids
56
+ end
57
+
58
+ sig { params(res: Statsig::ConfigResult).returns(FeatureGate) }
59
+ def self.from_config_result(res)
60
+ new(
61
+ res.name,
62
+ value: res.gate_value,
63
+ rule_id: res.rule_id,
64
+ group_name: res.group_name,
65
+ id_type: res.id_type,
66
+ evaluation_details: res.evaluation_details,
67
+ target_app_ids: res.target_app_ids
68
+ )
69
+ end
70
+ end
data/lib/network.rb CHANGED
@@ -42,6 +42,7 @@ module Statsig
42
42
  'Content-Type' => 'application/json; charset=UTF-8',
43
43
  'STATSIG-SDK-TYPE' => meta['sdkType'],
44
44
  'STATSIG-SDK-VERSION' => meta['sdkVersion'],
45
+ 'STATSIG-SDK-LANGUAGE-VERSION' => meta['languageVersion'],
45
46
  'Accept-Encoding' => 'gzip'
46
47
  }
47
48
  ).accept(:json)
@@ -131,26 +132,6 @@ module Statsig
131
132
  request(method, endpoint, body, retries - 1, backoff * @backoff_multiplier)
132
133
  end
133
134
 
134
- def check_gate(user, gate_name)
135
- request_body = JSON.generate({ 'user' => user&.serialize(false), 'gateName' => gate_name })
136
- response, = post('check_gate', request_body)
137
- return JSON.parse(response.body) unless response.nil?
138
-
139
- false
140
- rescue StandardError
141
- false
142
- end
143
-
144
- def get_config(user, dynamic_config_name)
145
- request_body = JSON.generate({ 'user' => user&.serialize(false), 'configName' => dynamic_config_name })
146
- response, = post('get_config', request_body)
147
- return JSON.parse(response.body) unless response.nil?
148
-
149
- nil
150
- rescue StandardError
151
- nil
152
- end
153
-
154
135
  def post_logs(events)
155
136
  json_body = JSON.generate({ 'events' => events, 'statsigMetadata' => Statsig.get_statsig_metadata })
156
137
  post('log_event', json_body, @post_logs_retry_limit)
data/lib/spec_store.rb CHANGED
@@ -13,7 +13,7 @@ module Statsig
13
13
  attr_accessor :initial_config_sync_time
14
14
  attr_accessor :init_reason
15
15
 
16
- def initialize(network, options, error_callback, diagnostics, error_boundary, logger)
16
+ def initialize(network, options, error_callback, diagnostics, error_boundary, logger, secret_key)
17
17
  @init_reason = EvaluationReason::UNINITIALIZED
18
18
  @network = network
19
19
  @options = options
@@ -35,6 +35,7 @@ module Statsig
35
35
  @diagnostics = diagnostics
36
36
  @error_boundary = error_boundary
37
37
  @logger = logger
38
+ @secret_key = secret_key
38
39
 
39
40
  @id_list_thread_pool = Concurrent::FixedThreadPool.new(
40
41
  options.idlist_threadpool_size,
@@ -47,7 +48,7 @@ module Statsig
47
48
  if !@options.data_store.nil?
48
49
  puts 'data_store gets priority over bootstrap_values. bootstrap_values will be ignored'
49
50
  else
50
- tracker = @diagnostics.track('bootstrap', 'process')
51
+ tracker = @diagnostics.track('initialize','bootstrap', 'process')
51
52
  begin
52
53
  if process_specs(options.bootstrap_values)
53
54
  @init_reason = EvaluationReason::BOOTSTRAP
@@ -62,18 +63,18 @@ module Statsig
62
63
 
63
64
  unless @options.data_store.nil?
64
65
  @options.data_store.init
65
- load_config_specs_from_storage_adapter
66
+ load_config_specs_from_storage_adapter('initialize')
66
67
  end
67
68
 
68
69
  if @init_reason == EvaluationReason::UNINITIALIZED
69
- download_config_specs
70
+ download_config_specs('initialize')
70
71
  end
71
72
 
72
73
  @initial_config_sync_time = @last_config_sync_time == 0 ? -1 : @last_config_sync_time
73
74
  if !@options.data_store.nil?
74
- get_id_lists_from_adapter
75
+ get_id_lists_from_adapter('initialize')
75
76
  else
76
- get_id_lists_from_network
77
+ get_id_lists_from_network('initialize')
77
78
  end
78
79
 
79
80
  @config_sync_thread = spawn_sync_config_specs_thread
@@ -121,6 +122,18 @@ module Statsig
121
122
  @specs[:layers][layer_name]
122
123
  end
123
124
 
125
+ def gates
126
+ @specs[:gates]
127
+ end
128
+
129
+ def configs
130
+ @specs[:configs]
131
+ end
132
+
133
+ def layers
134
+ @specs[:layers]
135
+ end
136
+
124
137
  def get_id_list(list_name)
125
138
  @specs[:id_lists][list_name]
126
139
  end
@@ -159,41 +172,39 @@ module Statsig
159
172
  end
160
173
 
161
174
  def sync_config_specs
162
- @diagnostics.context = 'config_sync'
163
175
  if @options.data_store&.should_be_used_for_querying_updates(Interfaces::IDataStore::CONFIG_SPECS_KEY)
164
- load_config_specs_from_storage_adapter
176
+ load_config_specs_from_storage_adapter('config_sync')
165
177
  else
166
- download_config_specs
178
+ download_config_specs('config_sync')
167
179
  end
168
- @logger.log_diagnostics_event(@diagnostics)
180
+ @logger.log_diagnostics_event(@diagnostics, 'config_sync')
169
181
  end
170
182
 
171
183
  def sync_id_lists
172
- @diagnostics.context = 'config_sync'
173
184
  if @options.data_store&.should_be_used_for_querying_updates(Interfaces::IDataStore::ID_LISTS_KEY)
174
- get_id_lists_from_adapter
185
+ get_id_lists_from_adapter('config_sync')
175
186
  else
176
- get_id_lists_from_network
187
+ get_id_lists_from_network('config_sync')
177
188
  end
178
- @logger.log_diagnostics_event(@diagnostics)
189
+ @logger.log_diagnostics_event(@diagnostics, 'config_sync')
179
190
  end
180
191
 
181
192
  private
182
193
 
183
- def load_config_specs_from_storage_adapter
184
- tracker = @diagnostics.track('data_store_config_specs', 'fetch')
194
+ def load_config_specs_from_storage_adapter(context)
195
+ tracker = @diagnostics.track(context, 'data_store_config_specs', 'fetch')
185
196
  cached_values = @options.data_store.get(Interfaces::IDataStore::CONFIG_SPECS_KEY)
186
197
  tracker.end(success: true)
187
198
  return if cached_values.nil?
188
199
 
189
- tracker = @diagnostics.track('data_store_config_specs', 'process')
200
+ tracker = @diagnostics.track(context, 'data_store_config_specs', 'process')
190
201
  process_specs(cached_values, from_adapter: true)
191
202
  @init_reason = EvaluationReason::DATA_ADAPTER
192
203
  tracker.end(success: true)
193
204
  rescue StandardError
194
205
  # Fallback to network
195
206
  tracker.end(success: false)
196
- download_config_specs
207
+ download_config_specs(context)
197
208
  end
198
209
 
199
210
  def save_config_specs_to_storage_adapter(specs_string)
@@ -233,8 +244,8 @@ module Statsig
233
244
  end
234
245
  end
235
246
 
236
- def download_config_specs
237
- tracker = @diagnostics.track('download_config_specs', 'network_request')
247
+ def download_config_specs(context)
248
+ tracker = @diagnostics.track(context, 'download_config_specs', 'network_request')
238
249
 
239
250
  error = nil
240
251
  begin
@@ -247,8 +258,7 @@ module Statsig
247
258
 
248
259
  if e.nil?
249
260
  unless response.nil?
250
- tracker = @diagnostics.track('download_config_specs', 'process')
251
-
261
+ tracker = @diagnostics.track(context, 'download_config_specs', 'process')
252
262
  if process_specs(response.body.to_s)
253
263
  @init_reason = EvaluationReason::NETWORK
254
264
  end
@@ -276,6 +286,12 @@ module Statsig
276
286
  specs_json = JSON.parse(specs_string)
277
287
  return false unless specs_json.is_a? Hash
278
288
 
289
+ hashed_sdk_key_used = specs_json['hashed_sdk_key_used']
290
+ unless hashed_sdk_key_used.nil? or hashed_sdk_key_used == Statsig::HashUtils.djb2(@secret_key)
291
+ err_boundary.log_exception(Statsig::InvalidSDKKeyResponse.new)
292
+ return false
293
+ end
294
+
279
295
  @last_config_sync_time = specs_json['time'] || @last_config_sync_time
280
296
  return false unless specs_json['has_updates'] == true &&
281
297
  !specs_json['feature_gates'].nil? &&
@@ -311,18 +327,18 @@ module Statsig
311
327
  true
312
328
  end
313
329
 
314
- def get_id_lists_from_adapter
315
- tracker = @diagnostics.track('data_store_id_lists', 'fetch')
330
+ def get_id_lists_from_adapter(context)
331
+ tracker = @diagnostics.track(context, 'data_store_id_lists', 'fetch')
316
332
  cached_values = @options.data_store.get(Interfaces::IDataStore::ID_LISTS_KEY)
317
333
  return if cached_values.nil?
318
334
 
319
335
  tracker.end(success: true)
320
336
  id_lists = JSON.parse(cached_values)
321
- process_id_lists(id_lists, from_adapter: true)
337
+ process_id_lists(id_lists, context, from_adapter: true)
322
338
  rescue StandardError
323
339
  # Fallback to network
324
340
  tracker.end(success: false)
325
- get_id_lists_from_network
341
+ get_id_lists_from_network(context)
326
342
  end
327
343
 
328
344
  def save_id_lists_to_adapter(id_lists_raw_json)
@@ -332,8 +348,8 @@ module Statsig
332
348
  @options.data_store.set(Interfaces::IDataStore::ID_LISTS_KEY, id_lists_raw_json)
333
349
  end
334
350
 
335
- def get_id_lists_from_network
336
- tracker = @diagnostics.track('get_id_list_sources', 'network_request')
351
+ def get_id_lists_from_network(context)
352
+ tracker = @diagnostics.track(context, 'get_id_list_sources', 'network_request')
337
353
  response, e = @network.post('get_id_lists', JSON.generate({ 'statsigMetadata' => Statsig.get_statsig_metadata }))
338
354
  code = response&.status.to_i
339
355
  if e.is_a? NetworkError
@@ -347,21 +363,21 @@ module Statsig
347
363
 
348
364
  begin
349
365
  server_id_lists = JSON.parse(response)
350
- process_id_lists(server_id_lists)
366
+ process_id_lists(server_id_lists, context)
351
367
  save_id_lists_to_adapter(response.body.to_s)
352
368
  rescue
353
369
  # Ignored, will try again
354
370
  end
355
371
  end
356
372
 
357
- def process_id_lists(new_id_lists, from_adapter: false)
373
+ def process_id_lists(new_id_lists, context, from_adapter: false)
358
374
  local_id_lists = @specs[:id_lists]
359
375
  if !new_id_lists.is_a?(Hash) || !local_id_lists.is_a?(Hash)
360
376
  return
361
377
  end
362
378
  tasks = []
363
379
 
364
- tracker = @diagnostics.track(
380
+ tracker = @diagnostics.track(context,
365
381
  from_adapter ? 'data_store_id_lists' : 'get_id_list_sources',
366
382
  'process',
367
383
  { idListCount: new_id_lists.length }
@@ -411,9 +427,9 @@ module Statsig
411
427
 
412
428
  tasks << Concurrent::Promise.execute(:executor => @id_list_thread_pool) do
413
429
  if from_adapter
414
- get_single_id_list_from_adapter(local_list)
430
+ get_single_id_list_from_adapter(local_list, context)
415
431
  else
416
- download_single_id_list(local_list)
432
+ download_single_id_list(local_list, context)
417
433
  end
418
434
  end
419
435
  end
@@ -422,12 +438,12 @@ module Statsig
422
438
  tracker.end(success: result.state == :fulfilled)
423
439
  end
424
440
 
425
- def get_single_id_list_from_adapter(list)
426
- tracker = @diagnostics.track('data_store_id_list', 'fetch', { url: list.url })
441
+ def get_single_id_list_from_adapter(list, context)
442
+ tracker = @diagnostics.track(context, 'data_store_id_list', 'fetch', { url: list.url })
427
443
  cached_values = @options.data_store.get("#{Interfaces::IDataStore::ID_LISTS_KEY}::#{list.name}")
428
444
  tracker.end(success: true)
429
445
  content = cached_values.to_s
430
- process_single_id_list(list, content, from_adapter: true)
446
+ process_single_id_list(list, context, content, from_adapter: true)
431
447
  rescue StandardError
432
448
  tracker.end(success: false)
433
449
  nil
@@ -439,10 +455,10 @@ module Statsig
439
455
  @options.data_store.set("#{Interfaces::IDataStore::ID_LISTS_KEY}::#{name}", content)
440
456
  end
441
457
 
442
- def download_single_id_list(list)
458
+ def download_single_id_list(list, context)
443
459
  nil unless list.is_a? IDList
444
460
  http = HTTP.headers({ 'Range' => "bytes=#{list&.size || 0}-" }).accept(:json)
445
- tracker = @diagnostics.track('get_id_list', 'network_request', { url: list.url })
461
+ tracker = @diagnostics.track(context, 'get_id_list', 'network_request', { url: list.url })
446
462
  begin
447
463
  res = http.get(list.url)
448
464
  tracker.end(statusCode: res.status.code, success: res.status.success?)
@@ -450,7 +466,7 @@ module Statsig
450
466
  content_length = Integer(res['content-length'])
451
467
  nil if content_length.nil? || content_length <= 0
452
468
  content = res.body.to_s
453
- success = process_single_id_list(list, content, content_length)
469
+ success = process_single_id_list(list, context, content, content_length)
454
470
  save_single_id_list_to_adapter(list.name, content) unless success.nil? || !success
455
471
  rescue
456
472
  tracker.end(success: false)
@@ -458,10 +474,10 @@ module Statsig
458
474
  end
459
475
  end
460
476
 
461
- def process_single_id_list(list, content, content_length = nil, from_adapter: false)
477
+ def process_single_id_list(list, context, content, content_length = nil, from_adapter: false)
462
478
  false unless list.is_a? IDList
463
479
  begin
464
- tracker = @diagnostics.track(from_adapter ? 'data_store_id_list' : 'get_id_list', 'process', { url: list.url })
480
+ tracker = @diagnostics.track(context, from_adapter ? 'data_store_id_list' : 'get_id_list', 'process', { url: list.url })
465
481
  unless content.is_a?(String) && (content[0] == '-' || content[0] == '+')
466
482
  @specs[:id_lists].delete(list.name)
467
483
  tracker.end(success: false)
@@ -495,4 +511,4 @@ module Statsig
495
511
  end
496
512
 
497
513
  end
498
- end
514
+ end
data/lib/statsig.rb CHANGED
@@ -26,6 +26,23 @@ module Statsig
26
26
  @shared_instance = StatsigDriver.new(secret_key, options, error_callback)
27
27
  end
28
28
 
29
+ class GetGateOptions < T::Struct
30
+ prop :disable_log_exposure, T::Boolean, default: false
31
+ prop :skip_evaluation, T::Boolean, default: false
32
+ end
33
+
34
+ sig { params(user: StatsigUser, gate_name: String, options: GetGateOptions).returns(FeatureGate) }
35
+ ##
36
+ # Gets the gate, evaluated against the given user. An exposure event will automatically be logged for the gate.
37
+ #
38
+ # @param user A StatsigUser object used for the evaluation
39
+ # @param gate_name The name of the gate being checked
40
+ # @param options Additional options for evaluating the gate
41
+ def self.get_gate(user, gate_name, options = GetGateOptions.new)
42
+ ensure_initialized
43
+ @shared_instance&.get_gate(user, gate_name, options)
44
+ end
45
+
29
46
  class CheckGateOptions < T::Struct
30
47
  prop :disable_log_exposure, T::Boolean, default: false
31
48
  end
@@ -212,6 +229,51 @@ module Statsig
212
229
  @shared_instance&.manually_sync_idlists
213
230
  end
214
231
 
232
+ sig { returns(T::Array[String]) }
233
+ ##
234
+ # Returns a list of all gate names
235
+ #
236
+ def self.list_gates
237
+ ensure_initialized
238
+ @shared_instance&.list_gates
239
+ end
240
+
241
+ sig { returns(T::Array[String]) }
242
+ ##
243
+ # Returns a list of all config names
244
+ #
245
+ def self.list_configs
246
+ ensure_initialized
247
+ @shared_instance&.list_configs
248
+ end
249
+
250
+ sig { returns(T::Array[String]) }
251
+ ##
252
+ # Returns a list of all experiment names
253
+ #
254
+ def self.list_experiments
255
+ ensure_initialized
256
+ @shared_instance&.list_experiments
257
+ end
258
+
259
+ sig { returns(T::Array[String]) }
260
+ ##
261
+ # Returns a list of all autotune names
262
+ #
263
+ def self.list_autotunes
264
+ ensure_initialized
265
+ @shared_instance&.list_autotunes
266
+ end
267
+
268
+ sig { returns(T::Array[String]) }
269
+ ##
270
+ # Returns a list of all layer names
271
+ #
272
+ def self.list_layers
273
+ ensure_initialized
274
+ @shared_instance&.list_layers
275
+ end
276
+
215
277
  sig { void }
216
278
  ##
217
279
  # Stops all Statsig activity and flushes any pending events.
@@ -265,7 +327,8 @@ module Statsig
265
327
  def self.get_statsig_metadata
266
328
  {
267
329
  'sdkType' => 'ruby-server',
268
- 'sdkVersion' => '1.29.0',
330
+ 'sdkVersion' => '1.31.0',
331
+ 'languageVersion' => RUBY_VERSION
269
332
  }
270
333
  end
271
334
 
@@ -10,6 +10,7 @@ require 'statsig_options'
10
10
  require 'statsig_user'
11
11
  require 'spec_store'
12
12
  require 'dynamic_config'
13
+ require 'feature_gate'
13
14
  require 'error_boundary'
14
15
  require 'layer'
15
16
  require 'sorbet-runtime'
@@ -30,42 +31,65 @@ class StatsigDriver
30
31
 
31
32
  @err_boundary = Statsig::ErrorBoundary.new(secret_key)
32
33
  @err_boundary.capture(task: lambda {
33
- @diagnostics = Statsig::Diagnostics.new('initialize')
34
- tracker = @diagnostics.track('overall')
34
+ @diagnostics = Statsig::Diagnostics.new()
35
+ tracker = @diagnostics.track('initialize', 'overall')
35
36
  @options = options || StatsigOptions.new
36
37
  @shutdown = false
37
38
  @secret_key = secret_key
38
39
  @net = Statsig::Network.new(secret_key, @options)
39
40
  @logger = Statsig::StatsigLogger.new(@net, @options, @err_boundary)
40
41
  @persistent_storage_utils = Statsig::UserPersistentStorageUtils.new(@options)
41
- @evaluator = Statsig::Evaluator.new(@net, @options, error_callback, @diagnostics, @err_boundary, @logger, @persistent_storage_utils)
42
+ @store = Statsig::SpecStore.new(@net, @options, error_callback, @diagnostics, @err_boundary, @logger, secret_key)
43
+ @evaluator = Statsig::Evaluator.new(@store, @options, @persistent_storage_utils)
42
44
  tracker.end(success: true)
43
45
 
44
- @logger.log_diagnostics_event(@diagnostics)
46
+ @logger.log_diagnostics_event(@diagnostics, 'initialize')
45
47
  }, caller: __method__.to_s)
46
48
  end
47
49
 
50
+ sig do
51
+ params(
52
+ user: StatsigUser,
53
+ gate_name: String,
54
+ disable_log_exposure: T::Boolean,
55
+ skip_evaluation: T::Boolean
56
+ ).returns(FeatureGate)
57
+ end
58
+ def get_gate_impl(user, gate_name, disable_log_exposure: false, skip_evaluation: false)
59
+ if skip_evaluation
60
+ gate = @store.get_gate(gate_name)
61
+ return FeatureGate.new(gate_name) if gate.nil?
62
+ return FeatureGate.new(gate['name'], target_app_ids: gate['targetAppIDs'])
63
+ end
64
+ user = verify_inputs(user, gate_name, 'gate_name')
65
+
66
+ res = @evaluator.check_gate(user, gate_name)
67
+ if res.nil?
68
+ res = Statsig::ConfigResult.new(gate_name)
69
+ end
70
+
71
+ unless disable_log_exposure
72
+ @logger.log_gate_exposure(
73
+ user, res.name, res.gate_value, res.rule_id, res.secondary_exposures, res.evaluation_details
74
+ )
75
+ end
76
+ FeatureGate.from_config_result(res)
77
+ end
78
+
79
+ sig { params(user: StatsigUser, gate_name: String, options: Statsig::GetGateOptions).returns(FeatureGate) }
80
+ def get_gate(user, gate_name, options = Statsig::GetGateOptions.new)
81
+ @err_boundary.capture(task: lambda {
82
+ run_with_diagnostics(task: lambda {
83
+ get_gate_impl(user, gate_name, disable_log_exposure: options.disable_log_exposure, skip_evaluation: options.skip_evaluation)
84
+ }, caller: __method__.to_s)
85
+ }, recover: -> { false }, caller: __method__.to_s)
86
+ end
87
+
48
88
  sig { params(user: StatsigUser, gate_name: String, options: Statsig::CheckGateOptions).returns(T::Boolean) }
49
89
  def check_gate(user, gate_name, options = Statsig::CheckGateOptions.new)
50
90
  @err_boundary.capture(task: lambda {
51
91
  run_with_diagnostics(task: lambda {
52
- user = verify_inputs(user, gate_name, "gate_name")
53
-
54
- res = @evaluator.check_gate(user, gate_name)
55
- if res.nil?
56
- res = Statsig::ConfigResult.new(gate_name)
57
- end
58
-
59
- if res == $fetch_from_server
60
- res = check_gate_fallback(user, gate_name)
61
- # exposure logged by the server
62
- else
63
- if !options.disable_log_exposure
64
- @logger.log_gate_exposure(user, res.name, res.gate_value, res.rule_id, res.secondary_exposures, res.evaluation_details)
65
- end
66
- end
67
-
68
- res.gate_value
92
+ get_gate_impl(user, gate_name, disable_log_exposure: options.disable_log_exposure).value
69
93
  }, caller: __method__.to_s)
70
94
  }, recover: -> { false }, caller: __method__.to_s)
71
95
  end
@@ -129,14 +153,6 @@ class StatsigDriver
129
153
  res = Statsig::ConfigResult.new(layer_name)
130
154
  end
131
155
 
132
- if res == $fetch_from_server
133
- if res.config_delegate.nil?
134
- return Layer.new(layer_name)
135
- end
136
- res = get_config_fallback(user, res.config_delegate)
137
- # exposure logged by the server
138
- end
139
-
140
156
  exposure_log_func = !options.disable_log_exposure ? lambda { |layer, parameter_name|
141
157
  @logger.log_layer_exposure(user, layer, parameter_name, res)
142
158
  } : nil
@@ -184,6 +200,41 @@ class StatsigDriver
184
200
  }, caller: __method__.to_s)
185
201
  end
186
202
 
203
+ sig { returns(T::Array[String]) }
204
+ def list_gates
205
+ @err_boundary.capture(task: lambda {
206
+ @evaluator.list_gates
207
+ }, caller: __method__.to_s)
208
+ end
209
+
210
+ sig { returns(T::Array[String]) }
211
+ def list_configs
212
+ @err_boundary.capture(task: lambda {
213
+ @evaluator.list_configs
214
+ }, caller: __method__.to_s)
215
+ end
216
+
217
+ sig { returns(T::Array[String]) }
218
+ def list_experiments
219
+ @err_boundary.capture(task: lambda {
220
+ @evaluator.list_experiments
221
+ }, caller: __method__.to_s)
222
+ end
223
+
224
+ sig { returns(T::Array[String]) }
225
+ def list_autotunes
226
+ @err_boundary.capture(task: lambda {
227
+ @evaluator.list_autotunes
228
+ }, caller: __method__.to_s)
229
+ end
230
+
231
+ sig { returns(T::Array[String]) }
232
+ def list_layers
233
+ @err_boundary.capture(task: lambda {
234
+ @evaluator.list_layers
235
+ }, caller: __method__.to_s)
236
+ end
237
+
187
238
  def shutdown
188
239
  @err_boundary.capture(task: lambda {
189
240
  @shutdown = true
@@ -231,8 +282,8 @@ class StatsigDriver
231
282
  def run_with_diagnostics(task:, caller:)
232
283
  diagnostics = nil
233
284
  if Statsig::Diagnostics::API_CALL_KEYS.include?(caller) && Statsig::Diagnostics.sample(1)
234
- diagnostics = Statsig::Diagnostics.new('api_call')
235
- tracker = diagnostics.track(caller)
285
+ diagnostics = Statsig::Diagnostics.new()
286
+ tracker = diagnostics.track('api_call', caller)
236
287
  end
237
288
  begin
238
289
  res = task.call
@@ -241,7 +292,7 @@ class StatsigDriver
241
292
  tracker&.end(success: false)
242
293
  raise e
243
294
  ensure
244
- @logger.log_diagnostics_event(diagnostics)
295
+ @logger.log_diagnostics_event(diagnostics, 'api_call')
245
296
  end
246
297
  return res
247
298
  end
@@ -272,13 +323,8 @@ class StatsigDriver
272
323
  res = Statsig::ConfigResult.new(config_name)
273
324
  end
274
325
 
275
- if res == $fetch_from_server
276
- res = get_config_fallback(user, config_name)
277
- # exposure logged by the server
278
- else
279
- if !disable_log_exposure
280
- @logger.log_config_exposure(user, res.name, res.rule_id, res.secondary_exposures, res.evaluation_details)
281
- end
326
+ unless disable_log_exposure
327
+ @logger.log_config_exposure(user, res.name, res.rule_id, res.secondary_exposures, res.evaluation_details)
282
328
  end
283
329
 
284
330
  DynamicConfig.new(res.name, res.json_value, res.rule_id, res.group_name, res.id_type, res.evaluation_details)
@@ -308,34 +354,4 @@ class StatsigDriver
308
354
  puts 'SDK has been shutdown. Updates in the Statsig Console will no longer reflect.'
309
355
  end
310
356
  end
311
-
312
- def check_gate_fallback(user, gate_name)
313
- network_result = @net.check_gate(user, gate_name)
314
- if network_result.nil?
315
- config_result = Statsig::ConfigResult.new(gate_name)
316
- return config_result
317
- end
318
-
319
- Statsig::ConfigResult.new(
320
- network_result['name'],
321
- network_result['value'],
322
- {},
323
- network_result['rule_id'],
324
- )
325
- end
326
-
327
- def get_config_fallback(user, dynamic_config_name)
328
- network_result = @net.get_config(user, dynamic_config_name)
329
- if network_result.nil?
330
- config_result = Statsig::ConfigResult.new(dynamic_config_name)
331
- return config_result
332
- end
333
-
334
- Statsig::ConfigResult.new(
335
- network_result['name'],
336
- false,
337
- network_result['value'],
338
- network_result['rule_id'],
339
- )
340
- end
341
357
  end
@@ -9,4 +9,10 @@ module Statsig
9
9
  class ValueError < StandardError
10
10
 
11
11
  end
12
+
13
+ class InvalidSDKKeyResponse < StandardError
14
+ def initialize(msg="Incorrect SDK Key used to generate response.")
15
+ super
16
+ end
17
+ end
12
18
  end
@@ -98,18 +98,21 @@ module Statsig
98
98
  log_event(event)
99
99
  end
100
100
 
101
- def log_diagnostics_event(diagnostics, user = nil)
102
- return if @options.disable_diagnostics_logging
101
+ def log_diagnostics_event(diagnostics, context, user = nil)
103
102
  return if diagnostics.nil?
103
+ if @options.disable_diagnostics_logging
104
+ diagnostics.clear_markers(context)
105
+ return
106
+ end
104
107
 
105
108
  event = StatsigEvent.new($diagnostics_event)
106
109
  event.user = user
107
- serialized = diagnostics.serialize_with_sampling
110
+ serialized = diagnostics.serialize_with_sampling(context)
111
+ diagnostics.clear_markers(context)
108
112
  return if serialized[:markers].empty?
109
113
 
110
114
  event.metadata = serialized
111
115
  log_event(event)
112
- diagnostics.clear_markers
113
116
  end
114
117
 
115
118
  def periodic_flush
@@ -204,4 +207,4 @@ module Statsig
204
207
  true
205
208
  end
206
209
  end
207
- end
210
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: statsig
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.29.0
4
+ version: 1.31.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-12-13 00:00:00.000000000 Z
11
+ date: 2024-01-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -330,6 +330,7 @@ files:
330
330
  - lib/evaluation_details.rb
331
331
  - lib/evaluation_helpers.rb
332
332
  - lib/evaluator.rb
333
+ - lib/feature_gate.rb
333
334
  - lib/hash_utils.rb
334
335
  - lib/id_list.rb
335
336
  - lib/interfaces/data_store.rb