statsig 1.10.0 → 1.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dd12cded3010fd966063f91670f1a8f0531196afb96d81f4470311a8d978606d
4
- data.tar.gz: f5e3f6715c245263cc1702172da20ad164d20e76dc69a285bcfafe72f7e809ef
3
+ metadata.gz: 31a3f19ad19ba6b7ce4ebd18e2632ee04a85d3a2176b2c914e75fcd8f326f578
4
+ data.tar.gz: b3d6905b985889b60a0e713231d7bb5576cb9638972d331243bf93c881557130
5
5
  SHA512:
6
- metadata.gz: 1dde66bd943e314af8359a243eab9251c2b6bff7a11cb7d988f9ce02d5608b740f0aeb82030e751b553fe355b2a65a41ecc6ac7a23f84dc64c382926a73c72da
7
- data.tar.gz: b3d22016c1753ba81daab0039b67a51109fcd240d38514458701c73fb1af0c2cd6a7ac219cd9ef5a34ff2bbac760fe595d17cc4410297799fecfff8a0211bb4f
6
+ metadata.gz: 4a15075766b794eeefdc187cd225edac10a35810a8066ffab521db2228466a5687007a58077a9c788b668b5ed25cddc0c125f5f74fbf4bdea0488318159e9c00
7
+ data.tar.gz: 74e4ded54a6dc0198ca99cca62d12517109957316a45b562c26df5eca6f154db9a2f1cb59f4a1c6575035a6863f5a7055ee7d4d7e38c6f8eeaa7930eb487cebd
@@ -0,0 +1,132 @@
1
+ # typed: true
2
+ $empty_eval_result = {
3
+ :gate_value => false,
4
+ :json_value => {},
5
+ :rule_id => "",
6
+ :is_experiment_group => false,
7
+ :secondary_exposures => []
8
+ }
9
+
10
+ module ClientInitializeHelpers
11
+ class ResponseFormatter
12
+ def initialize(evaluator, user)
13
+ @evaluator = evaluator
14
+ @user = user
15
+ @specs = evaluator.spec_store.get_raw_specs
16
+ end
17
+
18
+ def get_responses(key)
19
+ @specs[key]
20
+ .map { |name, spec| to_response(name, spec) }
21
+ .delete_if { |v| v.nil? }.to_h
22
+ end
23
+
24
+ private
25
+
26
+ def to_response(config_name, config_spec)
27
+ eval_result = @evaluator.eval_spec(@user, config_spec)
28
+ if eval_result.nil?
29
+ return nil
30
+ end
31
+
32
+ safe_eval_result = eval_result == $fetch_from_server ? $empty_eval_result : {
33
+ :gate_value => eval_result.gate_value,
34
+ :json_value => eval_result.json_value,
35
+ :rule_id => eval_result.rule_id,
36
+ :config_delegate => eval_result.config_delegate,
37
+ :is_experiment_group => eval_result.is_experiment_group,
38
+ :secondary_exposures => eval_result.secondary_exposures,
39
+ :undelegated_sec_exps => eval_result.undelegated_sec_exps
40
+ }
41
+
42
+ category = config_spec['type']
43
+ entity_type = config_spec['entity']
44
+
45
+ result = {}
46
+
47
+ case category
48
+
49
+ when 'feature_gate'
50
+ if entity_type == 'segment' || entity_type == 'holdout'
51
+ return nil
52
+ end
53
+
54
+ result['value'] = safe_eval_result[:gate_value]
55
+ when 'dynamic_config'
56
+ id_type = config_spec['idType']
57
+ result['value'] = safe_eval_result[:json_value]
58
+ result["group"] = safe_eval_result[:rule_id]
59
+ result["is_device_based"] = id_type.is_a?(String) && id_type.downcase == 'stableid'
60
+ else
61
+ return nil
62
+ end
63
+
64
+ if entity_type == 'experiment'
65
+ populate_experiment_fields(config_name, config_spec, safe_eval_result, result)
66
+ end
67
+
68
+ if entity_type == 'layer'
69
+ populate_layer_fields(config_spec, safe_eval_result, result)
70
+ end
71
+
72
+ hashed_name = hash_name(config_name)
73
+ [hashed_name, result.merge(
74
+ {
75
+ "name" => hashed_name,
76
+ "rule_id" => safe_eval_result[:rule_id],
77
+ "secondary_exposures" => clean_exposures(safe_eval_result[:secondary_exposures])
78
+ })]
79
+ end
80
+
81
+ def clean_exposures(exposures)
82
+ seen = {}
83
+ exposures.reject do |exposure|
84
+ key = "#{exposure["gate"]}|#{exposure["gateValue"]}|#{exposure["ruleID"]}}"
85
+ should_reject = seen[key]
86
+ seen[key] = true
87
+ should_reject == true
88
+ end
89
+ end
90
+
91
+ def populate_experiment_fields(config_name, config_spec, eval_result, result)
92
+ result["is_user_in_experiment"] = eval_result[:is_experiment_group]
93
+ result["is_experiment_active"] = config_spec['isActive'] == true
94
+
95
+ if config_spec['hasSharedParams'] != true
96
+ return
97
+ end
98
+
99
+ result["is_in_layer"] = true
100
+ result["explicit_parameters"] = config_spec["explicitParameters"] || []
101
+
102
+ layer_name = @specs[:experiment_to_layer][config_name]
103
+ if layer_name.nil? || @specs[:layers][layer_name].nil?
104
+ return
105
+ end
106
+
107
+ layer = @specs[:layers][layer_name]
108
+ result["value"] = layer["defaultValue"].merge(result["value"])
109
+ end
110
+
111
+ def populate_layer_fields(config_spec, eval_result, result)
112
+ delegate = eval_result[:config_delegate]
113
+ result["explicit_parameters"] = config_spec["explicitParameters"] || []
114
+
115
+ if delegate.nil? == false && delegate.empty? == false
116
+ delegate_spec = @specs[:configs][delegate]
117
+ delegate_result = @evaluator.eval_spec(@user, delegate_spec)
118
+
119
+ result["allocated_experiment_name"] = hash_name(delegate)
120
+ result["is_user_in_experiment"] = delegate_result.is_experiment_group
121
+ result["is_experiment_active"] = delegate_spec['isActive'] == true
122
+ result["explicit_parameters"] = delegate_spec["explicitParameters"] || []
123
+ end
124
+
125
+ result["undelegated_secondary_exposures"] = clean_exposures(eval_result[:undelegated_sec_exps] || [])
126
+ end
127
+
128
+ def hash_name(name)
129
+ Digest::SHA256.base64digest(name)
130
+ end
131
+ end
132
+ end
data/lib/config_result.rb CHANGED
@@ -1,4 +1,4 @@
1
-
1
+ # typed: true
2
2
  module Statsig
3
3
  class ConfigResult
4
4
  attr_accessor :name
@@ -9,16 +9,29 @@ module Statsig
9
9
  attr_accessor :undelegated_sec_exps
10
10
  attr_accessor :config_delegate
11
11
  attr_accessor :explicit_parameters
12
+ attr_accessor :is_experiment_group
13
+ attr_accessor :evaluation_details
12
14
 
13
- def initialize(name, gate_value = false, json_value = {}, rule_id = '', secondary_exposures = [], undelegated_sec_exps = [], config_delegate = '', explicit_parameters = [])
15
+ def initialize(
16
+ name,
17
+ gate_value = false,
18
+ json_value = {},
19
+ rule_id = '',
20
+ secondary_exposures = [],
21
+ config_delegate = '',
22
+ explicit_parameters = [],
23
+ is_experiment_group: false,
24
+ evaluation_details: nil)
14
25
  @name = name
15
26
  @gate_value = gate_value
16
27
  @json_value = json_value
17
28
  @rule_id = rule_id
18
29
  @secondary_exposures = secondary_exposures.is_a?(Array) ? secondary_exposures : []
19
- @undelegated_sec_exps = undelegated_sec_exps.is_a?(Array) ? undelegated_sec_exps : []
30
+ @undelegated_sec_exps = @secondary_exposures
20
31
  @config_delegate = config_delegate
21
32
  @explicit_parameters = explicit_parameters
33
+ @is_experiment_group = is_experiment_group
34
+ @evaluation_details = evaluation_details
22
35
  end
23
36
  end
24
37
  end
@@ -0,0 +1,44 @@
1
+ # typed: true
2
+
3
+ require 'sorbet-runtime'
4
+
5
+ module Statsig
6
+ class Diagnostics
7
+ extend T::Sig
8
+
9
+ sig { returns(String) }
10
+ attr_reader :context
11
+
12
+ sig { returns(T::Array[T::Hash[Symbol, T.untyped]]) }
13
+ attr_reader :markers
14
+
15
+ sig { params(context: String).void }
16
+
17
+ def initialize(context)
18
+ @context = context
19
+ @markers = []
20
+ end
21
+
22
+ sig { params(key: String, action: String, step: T.any(String, NilClass), value: T.any(String, Integer, T::Boolean, NilClass)).void }
23
+
24
+ def mark(key, action, step = nil, value = nil)
25
+ @markers.push({
26
+ key: key,
27
+ step: step,
28
+ action: action,
29
+ value: value,
30
+ timestamp: (Time.now.to_f * 1000).to_i
31
+ })
32
+ end
33
+
34
+ sig { returns(T::Hash[Symbol, T.untyped]) }
35
+
36
+ def serialize
37
+ {
38
+ context: @context,
39
+ markers: @markers
40
+ }
41
+ end
42
+ end
43
+
44
+ end
@@ -1,16 +1,53 @@
1
+ # typed: false
2
+
3
+ require 'sorbet-runtime'
4
+
5
+ ##
6
+ # Contains the current experiment/dynamic config values from Statsig
7
+ #
8
+ # Dynamic Config Documentation: https://docs.statsig.com/dynamic-config
9
+ #
10
+ # Experiments Documentation: https://docs.statsig.com/experiments-plus
1
11
  class DynamicConfig
12
+ extend T::Sig
13
+
14
+ sig { returns(String) }
2
15
  attr_accessor :name
16
+
17
+ sig { returns(T::Hash[String, T.untyped]) }
3
18
  attr_accessor :value
19
+
20
+ sig { returns(String) }
4
21
  attr_accessor :rule_id
5
22
 
23
+ sig { params(name: String, value: T::Hash[String, T.untyped], rule_id: String).void }
6
24
  def initialize(name, value = {}, rule_id = '')
7
25
  @name = name
8
26
  @value = value
9
27
  @rule_id = rule_id
10
28
  end
11
29
 
30
+ sig { params(index: String, default_value: T.untyped).returns(T.untyped) }
31
+ ##
32
+ # Get the value for the given key (index), falling back to the default_value if it cannot be found.
33
+ #
34
+ # @param index The name of parameter being fetched
35
+ # @param default_value The fallback value if the name cannot be found
12
36
  def get(index, default_value)
13
37
  return default_value if @value.nil? || !@value.key?(index)
14
38
  @value[index]
15
39
  end
40
+
41
+ sig { params(index: String, default_value: T.untyped).returns(T.untyped) }
42
+ ##
43
+ # Get the value for the given key (index), falling back to the default_value if it cannot be found
44
+ # or is found to have a different type from the default_value.
45
+ #
46
+ # @param index The name of parameter being fetched
47
+ # @param default_value The fallback value if the name cannot be found
48
+ def get_typed(index, default_value)
49
+ return default_value if @value.nil? || !@value.key?(index)
50
+ return default_value if @value[index].class != default_value.class and default_value.class != TrueClass and default_value.class != FalseClass
51
+ @value[index]
52
+ end
16
53
  end
@@ -0,0 +1,57 @@
1
+ require "statsig_errors"
2
+
3
+ $endpoint = 'https://statsigapi.net/v1/sdk_exception'
4
+
5
+ module Statsig
6
+ class ErrorBoundary
7
+ def initialize(sdk_key)
8
+ @sdk_key = sdk_key
9
+ @seen = Set.new
10
+ end
11
+
12
+ def capture(task, recover = -> {})
13
+ begin
14
+ return task.call
15
+ rescue StandardError => e
16
+ if e.is_a?(Statsig::UninitializedError) or e.is_a?(Statsig::ValueError)
17
+ raise e
18
+ end
19
+ puts "[Statsig]: An unexpected exception occurred."
20
+ log_exception(e)
21
+ return recover.call
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def log_exception(exception)
28
+ begin
29
+ name = exception.class.name
30
+ if @seen.include?(name)
31
+ return
32
+ end
33
+
34
+ @seen << name
35
+ meta = Statsig.get_statsig_metadata
36
+ http = HTTP.headers(
37
+ {
38
+ "STATSIG-API-KEY" => @sdk_key,
39
+ "STATSIG-SDK-TYPE" => meta['sdkType'],
40
+ "STATSIG-SDK-VERSION" => meta['sdkVersion'],
41
+ "Content-Type" => "application/json; charset=UTF-8"
42
+ }).accept(:json)
43
+ body = {
44
+ "exception" => name,
45
+ "info" => {
46
+ "trace" => exception.backtrace.to_s,
47
+ "message" => exception.message
48
+ }.to_s,
49
+ "statsigMetadata" => meta
50
+ }
51
+ http.post($endpoint, body: JSON.generate(body))
52
+ rescue
53
+ return
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,42 @@
1
+ # typed: true
2
+ module Statsig
3
+
4
+ module EvaluationReason
5
+ NETWORK = "Network"
6
+ LOCAL_OVERRIDE = "LocalOverride"
7
+ UNRECOGNIZED = "Unrecognized"
8
+ UNINITIALIZED = "Uninitialized"
9
+ BOOTSTRAP = "Bootstrap"
10
+ DATA_ADAPTER = "DataAdapter"
11
+ end
12
+
13
+ class EvaluationDetails
14
+ attr_accessor :config_sync_time
15
+ attr_accessor :init_time
16
+ attr_accessor :reason
17
+ attr_accessor :server_time
18
+
19
+ def initialize(config_sync_time, init_time, reason)
20
+ @config_sync_time = config_sync_time
21
+ @init_time = init_time
22
+ @reason = reason
23
+ @server_time = (Time.now.to_i * 1000).to_s
24
+ end
25
+
26
+ def self.unrecognized(config_sync_time, init_time)
27
+ EvaluationDetails.new(config_sync_time, init_time, EvaluationReason::UNRECOGNIZED)
28
+ end
29
+
30
+ def self.uninitialized
31
+ EvaluationDetails.new(0, 0, EvaluationReason::UNINITIALIZED)
32
+ end
33
+
34
+ def self.network(config_sync_time, init_time)
35
+ EvaluationDetails.new(config_sync_time, init_time, EvaluationReason::NETWORK)
36
+ end
37
+
38
+ def self.local_override(config_sync_time, init_time)
39
+ EvaluationDetails.new(config_sync_time, init_time, EvaluationReason::LOCAL_OVERRIDE)
40
+ end
41
+ end
42
+ end
@@ -1,3 +1,4 @@
1
+ # typed: true
1
2
  require 'time'
2
3
 
3
4
  module EvaluationHelpers
data/lib/evaluator.rb CHANGED
@@ -1,10 +1,13 @@
1
+ # typed: false
1
2
  require 'config_result'
2
3
  require 'country_lookup'
3
4
  require 'digest'
4
5
  require 'evaluation_helpers'
6
+ require 'client_initialize_helpers'
5
7
  require 'spec_store'
6
8
  require 'time'
7
9
  require 'user_agent_parser'
10
+ require 'evaluation_details'
8
11
  require 'user_agent_parser/operating_system'
9
12
 
10
13
  $fetch_from_server = 'fetch_from_server'
@@ -12,33 +15,126 @@ $type_dynamic_config = 'dynamic_config'
12
15
 
13
16
  module Statsig
14
17
  class Evaluator
15
- def initialize(network, options, error_callback)
16
- @spec_store = Statsig::SpecStore.new(network, error_callback, options.rulesets_sync_interval, options.idlists_sync_interval)
18
+ attr_accessor :spec_store
19
+
20
+ def initialize(network, options, error_callback, init_diagnostics = nil)
21
+ @spec_store = Statsig::SpecStore.new(network, options, error_callback, init_diagnostics)
17
22
  @ua_parser = UserAgentParser::Parser.new
18
23
  CountryLookup.initialize
19
- @initialized = true
24
+
25
+ @gate_overrides = {}
26
+ @config_overrides = {}
27
+ end
28
+
29
+ def maybe_restart_background_threads
30
+ @spec_store.maybe_restart_background_threads
20
31
  end
21
32
 
22
33
  def check_gate(user, gate_name)
23
- return nil unless @initialized && @spec_store.has_gate?(gate_name)
34
+ if @gate_overrides.has_key?(gate_name)
35
+ return Statsig::ConfigResult.new(
36
+ gate_name,
37
+ @gate_overrides[gate_name],
38
+ @gate_overrides[gate_name],
39
+ 'override',
40
+ [],
41
+ evaluation_details: EvaluationDetails.local_override(@spec_store.last_config_sync_time, @spec_store.initial_config_sync_time))
42
+ end
43
+
44
+ if @spec_store.init_reason == EvaluationReason::UNINITIALIZED
45
+ return Statsig::ConfigResult.new(gate_name, evaluation_details: EvaluationDetails.uninitialized)
46
+ end
47
+
48
+ unless @spec_store.has_gate?(gate_name)
49
+ return Statsig::ConfigResult.new(gate_name, evaluation_details: EvaluationDetails.unrecognized(@spec_store.last_config_sync_time, @spec_store.initial_config_sync_time))
50
+ end
51
+
24
52
  eval_spec(user, @spec_store.get_gate(gate_name))
25
53
  end
26
54
 
27
55
  def get_config(user, config_name)
28
- return nil unless @initialized && @spec_store.has_config?(config_name)
56
+ if @config_overrides.has_key?(config_name)
57
+ return Statsig::ConfigResult.new(
58
+ config_name,
59
+ false,
60
+ @config_overrides[config_name],
61
+ 'override',
62
+ [],
63
+ evaluation_details: EvaluationDetails.local_override(@spec_store.last_config_sync_time, @spec_store.initial_config_sync_time))
64
+ end
65
+
66
+ if @spec_store.init_reason == EvaluationReason::UNINITIALIZED
67
+ return Statsig::ConfigResult.new(config_name, evaluation_details: EvaluationDetails.uninitialized)
68
+ end
69
+
70
+ unless @spec_store.has_config?(config_name)
71
+ return Statsig::ConfigResult.new(config_name, evaluation_details: EvaluationDetails.unrecognized(@spec_store.last_config_sync_time, @spec_store.initial_config_sync_time))
72
+ end
73
+
29
74
  eval_spec(user, @spec_store.get_config(config_name))
30
75
  end
31
76
 
32
77
  def get_layer(user, layer_name)
33
- return nil unless @initialized && @spec_store.has_layer?(layer_name)
78
+ if @spec_store.init_reason == EvaluationReason::UNINITIALIZED
79
+ return Statsig::ConfigResult.new(layer_name, evaluation_details: EvaluationDetails.uninitialized)
80
+ end
81
+
82
+ unless @spec_store.has_layer?(layer_name)
83
+ return Statsig::ConfigResult.new(layer_name, evaluation_details: EvaluationDetails.unrecognized(@spec_store.last_config_sync_time, @spec_store.initial_config_sync_time))
84
+ end
85
+
34
86
  eval_spec(user, @spec_store.get_layer(layer_name))
35
87
  end
36
88
 
89
+ def get_client_initialize_response(user)
90
+ if @spec_store.is_ready_for_checks == false
91
+ return nil
92
+ end
93
+
94
+ formatter = ClientInitializeHelpers::ResponseFormatter.new(self, user)
95
+
96
+ evaluated_keys = {}
97
+ if user.user_id.nil? == false
98
+ evaluated_keys['userID'] = user.user_id
99
+ end
100
+
101
+ if user.custom_ids.nil? == false
102
+ evaluated_keys['customIDs'] = user.custom_ids
103
+ end
104
+
105
+ {
106
+ "feature_gates" => formatter.get_responses(:gates),
107
+ "dynamic_configs" => formatter.get_responses(:configs),
108
+ "layer_configs" => formatter.get_responses(:layers),
109
+ "sdkParams" => {},
110
+ "has_updates" => true,
111
+ "generator" => "statsig-ruby-sdk",
112
+ "evaluated_keys" => evaluated_keys,
113
+ "time" => 0,
114
+ }
115
+ end
116
+
117
+ def clean_exposures(exposures)
118
+ seen = {}
119
+ exposures.reject do |exposure|
120
+ key = "#{exposure["gate"]}|#{exposure["gateValue"]}|#{exposure["ruleID"]}}"
121
+ should_reject = seen[key]
122
+ seen[key] = true
123
+ should_reject == true
124
+ end
125
+ end
126
+
37
127
  def shutdown
38
128
  @spec_store.shutdown
39
129
  end
40
130
 
41
- private
131
+ def override_gate(gate, value)
132
+ @gate_overrides[gate] = value
133
+ end
134
+
135
+ def override_config(config, value)
136
+ @config_overrides[config] = value
137
+ end
42
138
 
43
139
  def eval_spec(user, config)
44
140
  default_rule_id = 'default'
@@ -62,7 +158,9 @@ module Statsig
62
158
  pass,
63
159
  pass ? result.json_value : config['defaultValue'],
64
160
  result.rule_id,
65
- exposures
161
+ exposures,
162
+ evaluation_details: EvaluationDetails.new(@spec_store.last_config_sync_time, @spec_store.initial_config_sync_time, @spec_store.init_reason),
163
+ is_experiment_group: result.is_experiment_group,
66
164
  )
67
165
  end
68
166
 
@@ -72,9 +170,17 @@ module Statsig
72
170
  default_rule_id = 'disabled'
73
171
  end
74
172
 
75
- Statsig::ConfigResult.new(config['name'], false, config['defaultValue'], default_rule_id, exposures)
173
+ Statsig::ConfigResult.new(
174
+ config['name'],
175
+ false,
176
+ config['defaultValue'],
177
+ default_rule_id,
178
+ exposures,
179
+ evaluation_details: EvaluationDetails.new(@spec_store.last_config_sync_time, @spec_store.initial_config_sync_time, @spec_store.init_reason))
76
180
  end
77
181
 
182
+ private
183
+
78
184
  def eval_rule(user, rule)
79
185
  exposures = []
80
186
  pass = true
@@ -94,7 +200,14 @@ module Statsig
94
200
  i += 1
95
201
  end
96
202
 
97
- Statsig::ConfigResult.new('', pass, rule['returnValue'], rule['id'], exposures)
203
+ Statsig::ConfigResult.new(
204
+ '',
205
+ pass,
206
+ rule['returnValue'],
207
+ rule['id'],
208
+ exposures,
209
+ evaluation_details: EvaluationDetails.new(@spec_store.last_config_sync_time, @spec_store.initial_config_sync_time, @spec_store.init_reason),
210
+ is_experiment_group: rule["isExperimentGroup"] == true)
98
211
  end
99
212
 
100
213
  def eval_delegate(name, user, rule, exposures)
@@ -154,7 +267,7 @@ module Statsig
154
267
  when 'environment_field'
155
268
  value = get_value_from_environment(user, field)
156
269
  when 'current_time'
157
- value = Time.now.to_f # epoch time in seconds
270
+ value = Time.now.to_i # epoch time in seconds
158
271
  when 'user_bucket'
159
272
  begin
160
273
  salt = additional_values['salt']
@@ -266,14 +379,14 @@ module Statsig
266
379
  user_custom = user_lookup_table['custom']
267
380
  if user_custom.is_a?(Hash)
268
381
  user_custom.each do |key, value|
269
- return value if key.downcase.casecmp?(field.downcase) && !value.nil?
382
+ return value if key.to_s.downcase.casecmp?(field.downcase) && !value.nil?
270
383
  end
271
384
  end
272
385
 
273
386
  private_attributes = user_lookup_table['privateAttributes']
274
387
  if private_attributes.is_a?(Hash)
275
388
  private_attributes.each do |key, value|
276
- return value if key.downcase.casecmp?(field.downcase) && !value.nil?
389
+ return value if key.to_s.downcase.casecmp?(field.downcase) && !value.nil?
277
390
  end
278
391
  end
279
392
 
@@ -285,7 +398,7 @@ module Statsig
285
398
  field = field.downcase
286
399
  return nil unless user.statsig_environment.is_a? Hash
287
400
  user.statsig_environment.each do |key, value|
288
- return value if key.downcase == (field)
401
+ return value if key.to_s.downcase == (field)
289
402
  end
290
403
  nil
291
404
  end
data/lib/id_list.rb CHANGED
@@ -1,3 +1,4 @@
1
+ # typed: true
1
2
  module Statsig
2
3
  class IDList
3
4
  attr_accessor :name
@@ -0,0 +1,19 @@
1
+ # typed: true
2
+ module Statsig
3
+ module Interfaces
4
+ class IDataStore
5
+ def init
6
+ end
7
+
8
+ def get(key)
9
+ nil
10
+ end
11
+
12
+ def set(key, value)
13
+ end
14
+
15
+ def shutdown
16
+ end
17
+ end
18
+ end
19
+ end