statsig 1.16.2 → 1.17.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/client_initialize_helpers.rb +1 -0
- data/lib/config_result.rb +1 -0
- data/lib/dynamic_config.rb +31 -0
- data/lib/evaluation_details.rb +1 -0
- data/lib/evaluation_helpers.rb +1 -0
- data/lib/evaluator.rb +4 -3
- data/lib/id_list.rb +1 -0
- data/lib/interfaces/data_store.rb +1 -0
- data/lib/layer.rb +28 -0
- data/lib/network.rb +11 -0
- data/lib/spec_store.rb +18 -3
- data/lib/statsig.rb +93 -1
- data/lib/statsig_driver.rb +17 -2
- data/lib/statsig_event.rb +1 -0
- data/lib/statsig_logger.rb +7 -0
- data/lib/statsig_options.rb +75 -3
- data/lib/statsig_user.rb +79 -26
- metadata +17 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5b81f929cdedefaee6f57cb2b0f7aec05bbfd28cf6b4f273fc4fb3bbcf46858f
|
4
|
+
data.tar.gz: 5caa719d8a301cbcd95d68f9bc5802a89f0691600c7b5883bfdbfcc8fa21bb8f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9372cd1b25a0b91054d6315df027833cd2377e51e1f76385dd260fe9a17665da33330c357cf63756f6bc6701e51f76a7d5b81aa99d57a96bfeee807c575c3b19
|
7
|
+
data.tar.gz: 5dd1c6ad849e20c0ed15b451371a78e971f6da4ed83f7bf7f79eb5db65c88ba27f1b8e9da991587d8a46cbd2086d0b30692bbb81e48283e758b84d679a63eb3a
|
data/lib/config_result.rb
CHANGED
data/lib/dynamic_config.rb
CHANGED
@@ -1,19 +1,50 @@
|
|
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
|
16
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
|
17
48
|
def get_typed(index, default_value)
|
18
49
|
return default_value if @value.nil? || !@value.key?(index)
|
19
50
|
return default_value if @value[index].class != default_value.class and default_value.class != TrueClass and default_value.class != FalseClass
|
data/lib/evaluation_details.rb
CHANGED
data/lib/evaluation_helpers.rb
CHANGED
data/lib/evaluator.rb
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
# typed: false
|
1
2
|
require 'config_result'
|
2
3
|
require 'country_lookup'
|
3
4
|
require 'digest'
|
@@ -378,14 +379,14 @@ module Statsig
|
|
378
379
|
user_custom = user_lookup_table['custom']
|
379
380
|
if user_custom.is_a?(Hash)
|
380
381
|
user_custom.each do |key, value|
|
381
|
-
return value if key.downcase.casecmp?(field.downcase) && !value.nil?
|
382
|
+
return value if key.to_s.downcase.casecmp?(field.downcase) && !value.nil?
|
382
383
|
end
|
383
384
|
end
|
384
385
|
|
385
386
|
private_attributes = user_lookup_table['privateAttributes']
|
386
387
|
if private_attributes.is_a?(Hash)
|
387
388
|
private_attributes.each do |key, value|
|
388
|
-
return value if key.downcase.casecmp?(field.downcase) && !value.nil?
|
389
|
+
return value if key.to_s.downcase.casecmp?(field.downcase) && !value.nil?
|
389
390
|
end
|
390
391
|
end
|
391
392
|
|
@@ -397,7 +398,7 @@ module Statsig
|
|
397
398
|
field = field.downcase
|
398
399
|
return nil unless user.statsig_environment.is_a? Hash
|
399
400
|
user.statsig_environment.each do |key, value|
|
400
|
-
return value if key.downcase == (field)
|
401
|
+
return value if key.to_s.downcase == (field)
|
401
402
|
end
|
402
403
|
nil
|
403
404
|
end
|
data/lib/id_list.rb
CHANGED
data/lib/layer.rb
CHANGED
@@ -1,7 +1,22 @@
|
|
1
|
+
# typed: false
|
2
|
+
|
3
|
+
##
|
4
|
+
# Contains the current values from Statsig.
|
5
|
+
# Will contain layer default values for all shared parameters in that layer.
|
6
|
+
# If a parameter is in an active experiment, and the current user is allocated to that experiment,
|
7
|
+
# those parameters will be updated to reflect the experiment values not the layer defaults.
|
8
|
+
#
|
9
|
+
# Layers Documentation: https://docs.statsig.com/layers
|
1
10
|
class Layer
|
11
|
+
extend T::Sig
|
12
|
+
|
13
|
+
sig { returns(String) }
|
2
14
|
attr_accessor :name
|
15
|
+
|
16
|
+
sig { returns(String) }
|
3
17
|
attr_accessor :rule_id
|
4
18
|
|
19
|
+
sig { params(name: String, value: T::Hash[String, T.untyped], rule_id: String, exposure_log_func: T.any(Method, Proc, NilClass)).void }
|
5
20
|
def initialize(name, value = {}, rule_id = '', exposure_log_func = nil)
|
6
21
|
@name = name
|
7
22
|
@value = value
|
@@ -9,6 +24,12 @@ class Layer
|
|
9
24
|
@exposure_log_func = exposure_log_func
|
10
25
|
end
|
11
26
|
|
27
|
+
sig { params(index: String, default_value: T.untyped).returns(T.untyped) }
|
28
|
+
##
|
29
|
+
# Get the value for the given key (index), falling back to the default_value if it cannot be found.
|
30
|
+
#
|
31
|
+
# @param index The name of parameter being fetched
|
32
|
+
# @param default_value The fallback value if the name cannot be found
|
12
33
|
def get(index, default_value)
|
13
34
|
return default_value if @value.nil? || !@value.key?(index)
|
14
35
|
|
@@ -19,6 +40,13 @@ class Layer
|
|
19
40
|
@value[index]
|
20
41
|
end
|
21
42
|
|
43
|
+
sig { params(index: String, default_value: T.untyped).returns(T.untyped) }
|
44
|
+
##
|
45
|
+
# Get the value for the given key (index), falling back to the default_value if it cannot be found
|
46
|
+
# or is found to have a different type from the default_value.
|
47
|
+
#
|
48
|
+
# @param index The name of parameter being fetched
|
49
|
+
# @param default_value The fallback value if the name cannot be found
|
22
50
|
def get_typed(index, default_value)
|
23
51
|
return default_value if @value.nil? || !@value.key?(index)
|
24
52
|
return default_value if @value[index].class != default_value.class and default_value.class != TrueClass and default_value.class != FalseClass
|
data/lib/network.rb
CHANGED
@@ -1,11 +1,18 @@
|
|
1
|
+
# typed: true
|
2
|
+
|
1
3
|
require 'http'
|
2
4
|
require 'json'
|
3
5
|
require 'securerandom'
|
6
|
+
require 'sorbet-runtime'
|
4
7
|
|
5
8
|
$retry_codes = [408, 500, 502, 503, 504, 522, 524, 599]
|
6
9
|
|
7
10
|
module Statsig
|
8
11
|
class Network
|
12
|
+
extend T::Sig
|
13
|
+
|
14
|
+
sig { params(server_secret: String, api: String, local_mode: T::Boolean, backoff_mult: Integer).void }
|
15
|
+
|
9
16
|
def initialize(server_secret, api, local_mode, backoff_mult = 10)
|
10
17
|
super()
|
11
18
|
unless api.end_with?('/')
|
@@ -18,6 +25,10 @@ module Statsig
|
|
18
25
|
@session_id = SecureRandom.uuid
|
19
26
|
end
|
20
27
|
|
28
|
+
|
29
|
+
sig { params(endpoint: String, body: String, retries: Integer, backoff: Integer)
|
30
|
+
.returns([T.any(HTTP::Response, NilClass), T.any(StandardError, NilClass)]) }
|
31
|
+
|
21
32
|
def post_helper(endpoint, body, retries = 0, backoff = 1)
|
22
33
|
if @local_mode
|
23
34
|
return nil, nil
|
data/lib/spec_store.rb
CHANGED
@@ -1,7 +1,9 @@
|
|
1
|
+
# typed: false
|
1
2
|
require 'net/http'
|
2
3
|
require 'uri'
|
3
4
|
require 'evaluation_details'
|
4
5
|
require 'id_list'
|
6
|
+
require 'concurrent-ruby'
|
5
7
|
|
6
8
|
module Statsig
|
7
9
|
class SpecStore
|
@@ -30,6 +32,12 @@ module Statsig
|
|
30
32
|
:experiment_to_layer => {}
|
31
33
|
}
|
32
34
|
|
35
|
+
@id_list_thread_pool = Concurrent::FixedThreadPool.new(
|
36
|
+
options.idlist_threadpool_size,
|
37
|
+
max_queue: 100,
|
38
|
+
fallback_policy: :discard,
|
39
|
+
)
|
40
|
+
|
33
41
|
unless @options.bootstrap_values.nil?
|
34
42
|
begin
|
35
43
|
if !@options.data_store.nil?
|
@@ -62,6 +70,8 @@ module Statsig
|
|
62
70
|
def shutdown
|
63
71
|
@config_sync_thread&.exit
|
64
72
|
@id_lists_sync_thread&.exit
|
73
|
+
@id_list_thread_pool.shutdown
|
74
|
+
@id_list_thread_pool.wait_for_termination(timeout = 3)
|
65
75
|
unless @options.data_store.nil?
|
66
76
|
@options.data_store.shutdown
|
67
77
|
end
|
@@ -221,7 +231,7 @@ module Statsig
|
|
221
231
|
if !server_id_lists.is_a?(Hash) || !local_id_lists.is_a?(Hash)
|
222
232
|
return
|
223
233
|
end
|
224
|
-
|
234
|
+
tasks = []
|
225
235
|
|
226
236
|
server_id_lists.each do |list_name, list|
|
227
237
|
server_list = IDList.new(list)
|
@@ -250,11 +260,16 @@ module Statsig
|
|
250
260
|
next
|
251
261
|
end
|
252
262
|
|
253
|
-
|
263
|
+
tasks << Concurrent::Promise.execute(:executor => @id_list_thread_pool) do
|
254
264
|
download_single_id_list(local_list)
|
255
265
|
end
|
256
266
|
end
|
257
|
-
|
267
|
+
|
268
|
+
result = Concurrent::Promise.all?(*tasks).execute.wait(@id_lists_sync_interval)
|
269
|
+
if result.state != :fulfilled
|
270
|
+
return # timed out
|
271
|
+
end
|
272
|
+
|
258
273
|
delete_lists = []
|
259
274
|
local_id_lists.each do |list_name, list|
|
260
275
|
unless server_id_lists.key? list_name
|
data/lib/statsig.rb
CHANGED
@@ -1,6 +1,19 @@
|
|
1
|
+
# typed: true
|
2
|
+
|
1
3
|
require 'statsig_driver'
|
4
|
+
require 'sorbet-runtime'
|
2
5
|
|
3
6
|
module Statsig
|
7
|
+
extend T::Sig
|
8
|
+
|
9
|
+
|
10
|
+
sig { params(secret_key: String, options: T.any(StatsigOptions, NilClass), error_callback: T.any(Method, Proc, NilClass)).void }
|
11
|
+
##
|
12
|
+
# Initializes the Statsig SDK.
|
13
|
+
#
|
14
|
+
# @param secret_key The server SDK key copied from console.statsig.com
|
15
|
+
# @param options The StatsigOptions object used to configure the SDK
|
16
|
+
# @param error_callback A callback function, called if the initialize network call fails
|
4
17
|
def self.initialize(secret_key, options = nil, error_callback = nil)
|
5
18
|
unless @shared_instance.nil?
|
6
19
|
puts 'Statsig already initialized.'
|
@@ -11,31 +24,70 @@ module Statsig
|
|
11
24
|
@shared_instance = StatsigDriver.new(secret_key, options, error_callback)
|
12
25
|
end
|
13
26
|
|
27
|
+
sig { params(user: StatsigUser, gate_name: String).returns(T::Boolean) }
|
28
|
+
##
|
29
|
+
# Gets the boolean result of a gate, evaluated against the given user. An exposure event will automatically be logged for the gate.
|
30
|
+
#
|
31
|
+
# @param user A StatsigUser object used for the evaluation
|
32
|
+
# @param gate_name The name of the gate being checked
|
14
33
|
def self.check_gate(user, gate_name)
|
15
34
|
ensure_initialized
|
16
35
|
@shared_instance&.check_gate(user, gate_name)
|
17
36
|
end
|
18
37
|
|
38
|
+
sig { params(user: StatsigUser, dynamic_config_name: String).returns(DynamicConfig) }
|
39
|
+
##
|
40
|
+
# Get the values of a dynamic config, evaluated against the given user. An exposure event will automatically be logged for the dynamic config.
|
41
|
+
#
|
42
|
+
# @param user A StatsigUser object used for the evaluation
|
43
|
+
# @param dynamic_config_name The name of the dynamic config
|
19
44
|
def self.get_config(user, dynamic_config_name)
|
20
45
|
ensure_initialized
|
21
46
|
@shared_instance&.get_config(user, dynamic_config_name)
|
22
47
|
end
|
23
48
|
|
49
|
+
sig { params(user: StatsigUser, experiment_name: String).returns(DynamicConfig) }
|
50
|
+
##
|
51
|
+
# Get the values of an experiment, evaluated against the given user. An exposure event will automatically be logged for the experiment.
|
52
|
+
#
|
53
|
+
# @param user A StatsigUser object used for the evaluation
|
54
|
+
# @param experiment_name The name of the experiment
|
24
55
|
def self.get_experiment(user, experiment_name)
|
25
56
|
ensure_initialized
|
26
57
|
@shared_instance&.get_experiment(user, experiment_name)
|
27
58
|
end
|
28
59
|
|
60
|
+
sig { params(user: StatsigUser, layer_name: String).returns(Layer) }
|
61
|
+
##
|
62
|
+
# Get the values of a layer, evaluated against the given user.
|
63
|
+
# Exposure events will be fired when get or get_typed is called on the resulting Layer class.
|
64
|
+
#
|
65
|
+
# @param user A StatsigUser object used for the evaluation
|
66
|
+
# @param layer_name The name of the layer
|
29
67
|
def self.get_layer(user, layer_name)
|
30
68
|
ensure_initialized
|
31
69
|
@shared_instance&.get_layer(user, layer_name)
|
32
70
|
end
|
33
71
|
|
72
|
+
sig { params(user: StatsigUser,
|
73
|
+
event_name: String,
|
74
|
+
value: T.any(String, Integer, Float, NilClass),
|
75
|
+
metadata: T.any(T::Hash[String, T.untyped], NilClass)).void }
|
76
|
+
##
|
77
|
+
# Logs an event to Statsig with the provided values.
|
78
|
+
#
|
79
|
+
# @param user A StatsigUser object to be included in the log
|
80
|
+
# @param event_name The name given to the event
|
81
|
+
# @param value A top level value for the event
|
82
|
+
# @param metadata Any extra values to be logged
|
34
83
|
def self.log_event(user, event_name, value = nil, metadata = nil)
|
35
84
|
ensure_initialized
|
36
85
|
@shared_instance&.log_event(user, event_name, value, metadata)
|
37
86
|
end
|
38
87
|
|
88
|
+
sig { void }
|
89
|
+
##
|
90
|
+
# Stops all Statsig activity and flushes any pending events.
|
39
91
|
def self.shutdown
|
40
92
|
unless @shared_instance.nil?
|
41
93
|
@shared_instance.shutdown
|
@@ -43,25 +95,48 @@ module Statsig
|
|
43
95
|
@shared_instance = nil
|
44
96
|
end
|
45
97
|
|
98
|
+
sig { params(gate_name: String, gate_value: T::Boolean).void }
|
99
|
+
##
|
100
|
+
# Sets a value to be returned for the given gate instead of the actual evaluated value.
|
101
|
+
#
|
102
|
+
# @param gate_name The name of the gate to be overridden
|
103
|
+
# @param gate_value The value that will be returned
|
46
104
|
def self.override_gate(gate_name, gate_value)
|
47
105
|
ensure_initialized
|
48
106
|
@shared_instance&.override_gate(gate_name, gate_value)
|
49
107
|
end
|
50
108
|
|
109
|
+
sig { params(config_name: String, config_value: Hash).void }
|
110
|
+
##
|
111
|
+
# Sets a value to be returned for the given dynamic config/experiment instead of the actual evaluated value.
|
112
|
+
#
|
113
|
+
# @param config_name The name of the dynamic config or experiment to be overridden
|
114
|
+
# @param config_value The value that will be returned
|
51
115
|
def self.override_config(config_name, config_value)
|
52
116
|
ensure_initialized
|
53
117
|
@shared_instance&.override_config(config_name, config_value)
|
54
118
|
end
|
55
119
|
|
120
|
+
sig { params(user: StatsigUser).returns(T.any(T::Hash[String, T.untyped], NilClass)) }
|
121
|
+
##
|
122
|
+
# Gets all evaluated values for the given user.
|
123
|
+
# These values can then be given to a Statsig Client SDK via bootstrapping.
|
124
|
+
#
|
125
|
+
# @param user A StatsigUser object used for the evaluation
|
126
|
+
#
|
127
|
+
# @note See Ruby Documentation: https://docs.statsig.com/server/rubySDK)
|
56
128
|
def self.get_client_initialize_response(user)
|
57
129
|
ensure_initialized
|
58
130
|
@shared_instance&.get_client_initialize_response(user)
|
59
131
|
end
|
60
132
|
|
133
|
+
sig { returns(T::Hash[String, String]) }
|
134
|
+
##
|
135
|
+
# Internal Statsig metadata for this SDK
|
61
136
|
def self.get_statsig_metadata
|
62
137
|
{
|
63
138
|
'sdkType' => 'ruby-server',
|
64
|
-
'sdkVersion' => '1.
|
139
|
+
'sdkVersion' => '1.17.0',
|
65
140
|
}
|
66
141
|
end
|
67
142
|
|
@@ -72,4 +147,21 @@ module Statsig
|
|
72
147
|
raise 'Must call initialize first.'
|
73
148
|
end
|
74
149
|
end
|
150
|
+
|
151
|
+
T::Configuration.call_validation_error_handler = lambda do |signature, opts|
|
152
|
+
puts opts[:pretty_message]
|
153
|
+
end
|
154
|
+
|
155
|
+
T::Configuration.inline_type_error_handler = lambda do |error, opts|
|
156
|
+
puts error.message
|
157
|
+
end
|
158
|
+
|
159
|
+
T::Configuration.sig_builder_error_handler = lambda do |error, location|
|
160
|
+
puts error.message
|
161
|
+
end
|
162
|
+
|
163
|
+
T::Configuration.sig_validation_error_handler = lambda do |error, opts|
|
164
|
+
puts error.message
|
165
|
+
end
|
166
|
+
|
75
167
|
end
|
data/lib/statsig_driver.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# typed: true
|
2
|
+
|
1
3
|
require 'config_result'
|
2
4
|
require 'evaluator'
|
3
5
|
require 'network'
|
@@ -8,11 +10,15 @@ require 'statsig_user'
|
|
8
10
|
require 'spec_store'
|
9
11
|
require 'dynamic_config'
|
10
12
|
require 'layer'
|
13
|
+
require 'sorbet-runtime'
|
11
14
|
|
12
15
|
class StatsigDriver
|
16
|
+
extend T::Sig
|
17
|
+
|
18
|
+
sig { params(secret_key: String, options: T.any(StatsigOptions, NilClass), error_callback: T.any(Method, Proc, NilClass)).void }
|
19
|
+
|
13
20
|
def initialize(secret_key, options = nil, error_callback = nil)
|
14
|
-
|
15
|
-
if !secret_key.is_a?(String) || !secret_key.start_with?('secret-')
|
21
|
+
unless secret_key.start_with?('secret-')
|
16
22
|
raise 'Invalid secret key provided. Provide your project secret key from the Statsig console'
|
17
23
|
end
|
18
24
|
if !options.nil? && !options.instance_of?(StatsigOptions)
|
@@ -27,6 +33,8 @@ class StatsigDriver
|
|
27
33
|
@evaluator = Statsig::Evaluator.new(@net, @options, error_callback)
|
28
34
|
end
|
29
35
|
|
36
|
+
sig { params(user: StatsigUser, gate_name: String).returns(T::Boolean) }
|
37
|
+
|
30
38
|
def check_gate(user, gate_name)
|
31
39
|
user = verify_inputs(user, gate_name, "gate_name")
|
32
40
|
|
@@ -45,16 +53,22 @@ class StatsigDriver
|
|
45
53
|
res.gate_value
|
46
54
|
end
|
47
55
|
|
56
|
+
sig { params(user: StatsigUser, dynamic_config_name: String).returns(DynamicConfig) }
|
57
|
+
|
48
58
|
def get_config(user, dynamic_config_name)
|
49
59
|
user = verify_inputs(user, dynamic_config_name, "dynamic_config_name")
|
50
60
|
get_config_impl(user, dynamic_config_name)
|
51
61
|
end
|
52
62
|
|
63
|
+
sig { params(user: StatsigUser, experiment_name: String).returns(DynamicConfig) }
|
64
|
+
|
53
65
|
def get_experiment(user, experiment_name)
|
54
66
|
user = verify_inputs(user, experiment_name, "experiment_name")
|
55
67
|
get_config_impl(user, experiment_name)
|
56
68
|
end
|
57
69
|
|
70
|
+
sig { params(user: StatsigUser, layer_name: String).returns(Layer) }
|
71
|
+
|
58
72
|
def get_layer(user, layer_name)
|
59
73
|
user = verify_inputs(user, layer_name, "layer_name")
|
60
74
|
|
@@ -115,6 +129,7 @@ class StatsigDriver
|
|
115
129
|
|
116
130
|
def maybe_restart_background_threads
|
117
131
|
@evaluator.maybe_restart_background_threads
|
132
|
+
@logger.maybe_restart_background_threads
|
118
133
|
end
|
119
134
|
|
120
135
|
private
|
data/lib/statsig_event.rb
CHANGED
data/lib/statsig_logger.rb
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
# typed: true
|
1
2
|
require 'statsig_event'
|
2
3
|
require 'concurrent-ruby'
|
3
4
|
|
@@ -117,6 +118,12 @@ module Statsig
|
|
117
118
|
@network.post_logs(flush_events)
|
118
119
|
end
|
119
120
|
|
121
|
+
def maybe_restart_background_threads
|
122
|
+
if @background_flush.nil? or !@background_flush.alive?
|
123
|
+
@background_flush = periodic_flush
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
120
127
|
private
|
121
128
|
|
122
129
|
def safe_add_eval_details(eval_details, event)
|
data/lib/statsig_options.rb
CHANGED
@@ -1,18 +1,88 @@
|
|
1
|
+
# typed: true
|
2
|
+
|
3
|
+
require 'sorbet-runtime'
|
4
|
+
require_relative 'interfaces/data_store'
|
5
|
+
|
6
|
+
##
|
7
|
+
# Configuration options for the Statsig SDK.
|
1
8
|
class StatsigOptions
|
9
|
+
extend T::Sig
|
10
|
+
|
11
|
+
sig { returns(T.any(T::Hash[String, String], NilClass)) }
|
12
|
+
# Hash you can use to set environment variables that apply to all of your users in
|
13
|
+
# the same session and will be used for targeting purposes.
|
14
|
+
# eg. { "tier" => "development" }
|
2
15
|
attr_accessor :environment
|
16
|
+
|
17
|
+
sig { returns(String) }
|
18
|
+
# The base url used to make network calls to Statsig.
|
19
|
+
# default: https://statsigapi.net/v1
|
3
20
|
attr_accessor :api_url_base
|
21
|
+
|
22
|
+
sig { returns(T.any(Float, Integer)) }
|
23
|
+
# The interval (in seconds) to poll for changes to your Statsig configuration
|
24
|
+
# default: 10s
|
4
25
|
attr_accessor :rulesets_sync_interval
|
26
|
+
|
27
|
+
sig { returns(T.any(Float, Integer)) }
|
28
|
+
# The interval (in seconds) to poll for changes to your id lists
|
29
|
+
# default: 60s
|
5
30
|
attr_accessor :idlists_sync_interval
|
31
|
+
|
32
|
+
sig { returns(T.any(Float, Integer)) }
|
33
|
+
# How often to flush logs to Statsig
|
34
|
+
# default: 60s
|
6
35
|
attr_accessor :logging_interval_seconds
|
36
|
+
|
37
|
+
sig { returns(Integer) }
|
38
|
+
# The maximum number of events to batch before flushing logs to the server
|
39
|
+
# default: 1000
|
7
40
|
attr_accessor :logging_max_buffer_size
|
41
|
+
|
42
|
+
sig { returns(T::Boolean) }
|
43
|
+
# Restricts the SDK to not issue any network requests and only respond with default values (or local overrides)
|
44
|
+
# default: false
|
8
45
|
attr_accessor :local_mode
|
46
|
+
|
47
|
+
sig { returns(T.any(String, NilClass)) }
|
48
|
+
# A string that represents all rules for all feature gates, dynamic configs and experiments.
|
49
|
+
# It can be provided to bootstrap the Statsig server SDK at initialization in case your server runs
|
50
|
+
# into network issue or Statsig is down temporarily.
|
9
51
|
attr_accessor :bootstrap_values
|
52
|
+
|
53
|
+
sig { returns(T.any(Method, Proc, NilClass)) }
|
54
|
+
# A callback function that will be called anytime the rulesets are updated.
|
10
55
|
attr_accessor :rules_updated_callback
|
56
|
+
|
57
|
+
sig { returns(T.any(Statsig::Interfaces::IDataStore, NilClass)) }
|
58
|
+
# A class that extends IDataStore. Can be used to provide values from a
|
59
|
+
# common data store (like Redis) to initialize the Statsig SDK.
|
11
60
|
attr_accessor :data_store
|
12
61
|
|
62
|
+
sig { returns(Integer) }
|
63
|
+
# The number of threads allocated to syncing IDLists.
|
64
|
+
# default: 3
|
65
|
+
attr_accessor :idlist_threadpool_size
|
66
|
+
|
67
|
+
sig do
|
68
|
+
params(
|
69
|
+
environment: T.any(T::Hash[String, String], NilClass),
|
70
|
+
api_url_base: String,
|
71
|
+
rulesets_sync_interval: T.any(Float, Integer),
|
72
|
+
idlists_sync_interval: T.any(Float, Integer),
|
73
|
+
logging_interval_seconds: T.any(Float, Integer),
|
74
|
+
logging_max_buffer_size: Integer,
|
75
|
+
local_mode: T::Boolean,
|
76
|
+
bootstrap_values: T.any(String, NilClass),
|
77
|
+
rules_updated_callback: T.any(Method, Proc, NilClass),
|
78
|
+
data_store: T.any(Statsig::Interfaces::IDataStore, NilClass),
|
79
|
+
idlist_threadpool_size: Integer
|
80
|
+
).void
|
81
|
+
end
|
82
|
+
|
13
83
|
def initialize(
|
14
|
-
environment=nil,
|
15
|
-
api_url_base='https://statsigapi.net/v1',
|
84
|
+
environment = nil,
|
85
|
+
api_url_base = 'https://statsigapi.net/v1',
|
16
86
|
rulesets_sync_interval: 10,
|
17
87
|
idlists_sync_interval: 60,
|
18
88
|
logging_interval_seconds: 60,
|
@@ -20,7 +90,8 @@ class StatsigOptions
|
|
20
90
|
local_mode: false,
|
21
91
|
bootstrap_values: nil,
|
22
92
|
rules_updated_callback: nil,
|
23
|
-
data_store: nil
|
93
|
+
data_store: nil,
|
94
|
+
idlist_threadpool_size: 3)
|
24
95
|
@environment = environment.is_a?(Hash) ? environment : nil
|
25
96
|
@api_url_base = api_url_base
|
26
97
|
@rulesets_sync_interval = rulesets_sync_interval
|
@@ -31,5 +102,6 @@ class StatsigOptions
|
|
31
102
|
@bootstrap_values = bootstrap_values
|
32
103
|
@rules_updated_callback = rules_updated_callback
|
33
104
|
@data_store = data_store
|
105
|
+
@idlist_threadpool_size = idlist_threadpool_size
|
34
106
|
end
|
35
107
|
end
|
data/lib/statsig_user.rb
CHANGED
@@ -1,50 +1,78 @@
|
|
1
|
+
# typed: true
|
2
|
+
|
3
|
+
require 'sorbet-runtime'
|
4
|
+
|
5
|
+
##
|
6
|
+
# The user object to be evaluated against your Statsig configurations (gates/experiments/dynamic configs).
|
1
7
|
class StatsigUser
|
8
|
+
extend T::Sig
|
9
|
+
|
10
|
+
sig { returns(T.any(String, NilClass)) }
|
11
|
+
# An identifier for this user. Evaluated against the User ID criteria. (https://docs.statsig.com/feature-gates/conditions#userid)
|
2
12
|
attr_accessor :user_id
|
13
|
+
|
14
|
+
sig { returns(T.any(String, NilClass)) }
|
15
|
+
# An identifier for this user. Evaluated against the Email criteria. (https://docs.statsig.com/feature-gates/conditions#email)
|
3
16
|
attr_accessor :email
|
17
|
+
|
18
|
+
sig { returns(T.any(String, NilClass)) }
|
19
|
+
# An IP address associated with this user. Evaluated against the IP Address criteria. (https://docs.statsig.com/feature-gates/conditions#ip)
|
4
20
|
attr_accessor :ip
|
21
|
+
|
22
|
+
sig { returns(T.any(String, NilClass)) }
|
23
|
+
# A user agent string associated with this user. Evaluated against Browser Version and Name (https://docs.statsig.com/feature-gates/conditions#browser-version)
|
5
24
|
attr_accessor :user_agent
|
25
|
+
|
26
|
+
sig { returns(T.any(String, NilClass)) }
|
27
|
+
# 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)
|
6
28
|
attr_accessor :country
|
29
|
+
|
30
|
+
sig { returns(T.any(String, NilClass)) }
|
31
|
+
# An locale for this user.
|
7
32
|
attr_accessor :locale
|
33
|
+
|
34
|
+
sig { returns(T.any(String, NilClass)) }
|
35
|
+
# The current app version the user is interacting with. Evaluated against the App Version criteria. (https://docs.statsig.com/feature-gates/conditions#app-version)
|
8
36
|
attr_accessor :app_version
|
37
|
+
|
38
|
+
sig { returns(T.any(T::Hash[String, String], NilClass)) }
|
39
|
+
# A Hash you can use to set environment variables that apply to this user. e.g. { "tier" => "development" }
|
9
40
|
attr_accessor :statsig_environment
|
41
|
+
|
42
|
+
sig { returns(T.any(T::Hash[String, String], NilClass)) }
|
43
|
+
# Any Custom IDs to associated with the user. (See https://docs.statsig.com/guides/experiment-on-custom-id-types)
|
10
44
|
attr_accessor :custom_ids
|
45
|
+
|
46
|
+
sig { returns(T.any(T::Hash[String, String], NilClass)) }
|
47
|
+
# Any value you wish to use in evaluation, but do not want logged with events, can be stored in this field.
|
11
48
|
attr_accessor :private_attributes
|
12
49
|
|
50
|
+
sig { returns(T.any(T::Hash[String, T.untyped], NilClass)) }
|
51
|
+
|
13
52
|
def custom
|
14
53
|
@custom
|
15
54
|
end
|
16
55
|
|
56
|
+
sig { params(value: T.any(T::Hash[String, T.untyped], NilClass)).void }
|
57
|
+
# Any custom fields for this user. Evaluated against the Custom criteria. (https://docs.statsig.com/feature-gates/conditions#custom)
|
17
58
|
def custom=(value)
|
18
59
|
@custom = value.is_a?(Hash) ? value : Hash.new
|
19
60
|
end
|
20
61
|
|
62
|
+
sig { params(user_hash: T.any(T::Hash[T.any(String, Symbol), T.untyped], NilClass)).void }
|
63
|
+
|
21
64
|
def initialize(user_hash)
|
22
|
-
@user_id =
|
23
|
-
@email =
|
24
|
-
@ip =
|
25
|
-
@user_agent =
|
26
|
-
@country =
|
27
|
-
@locale =
|
28
|
-
@app_version =
|
29
|
-
@custom =
|
30
|
-
@private_attributes =
|
31
|
-
@custom_ids =
|
32
|
-
@statsig_environment = Hash
|
33
|
-
if user_hash.is_a?(Hash)
|
34
|
-
@user_id = user_hash['userID'] || user_hash['user_id']
|
35
|
-
@user_id = @user_id.to_s unless @user_id.nil?
|
36
|
-
@email = user_hash['email']
|
37
|
-
@ip = user_hash['ip']
|
38
|
-
@user_agent = user_hash['userAgent'] || user_hash['user_agent']
|
39
|
-
@country = user_hash['country']
|
40
|
-
@locale = user_hash['locale']
|
41
|
-
@app_version = user_hash['appVersion'] || user_hash['app_version']
|
42
|
-
@custom = user_hash['custom'] if user_hash['custom'].is_a? Hash
|
43
|
-
@statsig_environment = user_hash['statsigEnvironment']
|
44
|
-
@private_attributes = user_hash['privateAttributes'] if user_hash['privateAttributes'].is_a? Hash
|
45
|
-
custom_ids = user_hash['customIDs'] || user_hash['custom_ids']
|
46
|
-
@custom_ids = custom_ids if custom_ids.is_a? Hash
|
47
|
-
end
|
65
|
+
@user_id = from_hash(user_hash, [:user_id, :userID], String)
|
66
|
+
@email = from_hash(user_hash, [:email], String)
|
67
|
+
@ip = from_hash(user_hash, [:ip], String)
|
68
|
+
@user_agent = from_hash(user_hash, [:user_agent, :userAgent], String)
|
69
|
+
@country = from_hash(user_hash, [:country], String)
|
70
|
+
@locale = from_hash(user_hash, [:locale], String)
|
71
|
+
@app_version = from_hash(user_hash, [:app_version, :appVersion], String)
|
72
|
+
@custom = from_hash(user_hash, [:custom], Hash)
|
73
|
+
@private_attributes = from_hash(user_hash, [:private_attributes, :privateAttributes], Hash)
|
74
|
+
@custom_ids = from_hash(user_hash, [:custom_ids, :customIDs], Hash)
|
75
|
+
@statsig_environment = from_hash(user_hash, [:statsig_environment, :statsigEnvironment], Hash)
|
48
76
|
end
|
49
77
|
|
50
78
|
def serialize(for_logging)
|
@@ -86,4 +114,29 @@ class StatsigUser
|
|
86
114
|
'privateAttributes' => @private_attributes,
|
87
115
|
}
|
88
116
|
end
|
117
|
+
|
118
|
+
private
|
119
|
+
|
120
|
+
sig {
|
121
|
+
params(user_hash: T.any(T::Hash[T.any(String, Symbol), T.untyped], NilClass),
|
122
|
+
keys: T::Array[Symbol],
|
123
|
+
type: T.untyped)
|
124
|
+
.returns(T.untyped)
|
125
|
+
}
|
126
|
+
# Pulls fields from the user hash via Symbols and Strings
|
127
|
+
def from_hash(user_hash, keys, type)
|
128
|
+
if user_hash.nil?
|
129
|
+
return nil
|
130
|
+
end
|
131
|
+
|
132
|
+
keys.each do |key|
|
133
|
+
val = user_hash[key] || user_hash[key.to_s]
|
134
|
+
if not val.nil? and val.is_a? type
|
135
|
+
return val
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
nil
|
140
|
+
end
|
141
|
+
|
89
142
|
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.
|
4
|
+
version: 1.17.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: 2022-
|
11
|
+
date: 2022-11-01 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -114,6 +114,20 @@ dependencies:
|
|
114
114
|
- - '='
|
115
115
|
- !ruby/object:Gem::Version
|
116
116
|
version: 0.1.1
|
117
|
+
- !ruby/object:Gem::Dependency
|
118
|
+
name: sorbet-runtime
|
119
|
+
requirement: !ruby/object:Gem::Requirement
|
120
|
+
requirements:
|
121
|
+
- - ">="
|
122
|
+
- !ruby/object:Gem::Version
|
123
|
+
version: '0'
|
124
|
+
type: :runtime
|
125
|
+
prerelease: false
|
126
|
+
version_requirements: !ruby/object:Gem::Requirement
|
127
|
+
requirements:
|
128
|
+
- - ">="
|
129
|
+
- !ruby/object:Gem::Version
|
130
|
+
version: '0'
|
117
131
|
- !ruby/object:Gem::Dependency
|
118
132
|
name: concurrent-ruby
|
119
133
|
requirement: !ruby/object:Gem::Requirement
|
@@ -170,7 +184,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
170
184
|
- !ruby/object:Gem::Version
|
171
185
|
version: '0'
|
172
186
|
requirements: []
|
173
|
-
rubygems_version: 3.3.
|
187
|
+
rubygems_version: 3.3.22
|
174
188
|
signing_key:
|
175
189
|
specification_version: 4
|
176
190
|
summary: Statsig server SDK for Ruby
|