statsig 1.10.0 → 1.20.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: 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