statsig 1.16.2 → 1.17.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 +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
|