statsig 1.6.2 → 1.7.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/config_result.rb +3 -1
- data/lib/evaluation_helpers.rb +4 -4
- data/lib/evaluator.rb +38 -11
- data/lib/network.rb +12 -12
- data/lib/statsig.rb +6 -6
- data/lib/statsig_driver.rb +14 -5
- data/lib/statsig_event.rb +2 -0
- data/lib/statsig_logger.rb +5 -3
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d24b945d9500781cda02ab594f003360d81c5a1bca99f366a2ceb95013b53dad
|
4
|
+
data.tar.gz: 63c9ea1a9d6347ecec9bff74949665b2af20bfe774e21a6851e96ba9b7eea397
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e47350b9ba8e20f659b65a909955d40e66bf2397306aa798aaab60e73fb3b58fd1d3cade5ea0077fd37f847c5f6e976f7ecbfc978db4830884e463bfa6656574
|
7
|
+
data.tar.gz: 8e8b11dd34de6f35585c916d3125be3a36f6f897b69facb1788364702bcd15d1729109d8ad8aadfda98f9909a7f053077433ba6221f3ca7c27d2b5686214fda6
|
data/lib/config_result.rb
CHANGED
@@ -3,11 +3,13 @@ class ConfigResult
|
|
3
3
|
attr_accessor :gate_value
|
4
4
|
attr_accessor :json_value
|
5
5
|
attr_accessor :rule_id
|
6
|
+
attr_accessor :secondary_exposures
|
6
7
|
|
7
|
-
def initialize(name, gate_value = false, json_value = {}, rule_id = '')
|
8
|
+
def initialize(name, gate_value = false, json_value = {}, rule_id = '', secondary_exposures = [])
|
8
9
|
@name = name
|
9
10
|
@gate_value = gate_value
|
10
11
|
@json_value = json_value
|
11
12
|
@rule_id = rule_id
|
13
|
+
@secondary_exposures = secondary_exposures.is_a?(Array) ? secondary_exposures : []
|
12
14
|
end
|
13
15
|
end
|
data/lib/evaluation_helpers.rb
CHANGED
@@ -2,7 +2,7 @@ require 'time'
|
|
2
2
|
|
3
3
|
module EvaluationHelpers
|
4
4
|
def self.compare_numbers(a, b, func)
|
5
|
-
return false unless
|
5
|
+
return false unless is_numeric(a) && is_numeric(b)
|
6
6
|
func.call(a.to_f, b.to_f) rescue false
|
7
7
|
end
|
8
8
|
|
@@ -15,8 +15,8 @@ module EvaluationHelpers
|
|
15
15
|
|
16
16
|
def self.compare_times(a, b, func)
|
17
17
|
begin
|
18
|
-
time_1 =
|
19
|
-
time_2 =
|
18
|
+
time_1 = get_epoch_time(a)
|
19
|
+
time_2 = get_epoch_time(b)
|
20
20
|
func.call(time_1, time_2)
|
21
21
|
rescue
|
22
22
|
false
|
@@ -30,7 +30,7 @@ module EvaluationHelpers
|
|
30
30
|
end
|
31
31
|
|
32
32
|
def self.get_epoch_time(v)
|
33
|
-
time =
|
33
|
+
time = is_numeric(v) ? Time.at(v.to_f) : Time.parse(v)
|
34
34
|
if time.year > Time.now.year + 100
|
35
35
|
# divide by 1000 when the epoch time is in milliseconds instead of seconds
|
36
36
|
return time.to_i / 1000
|
data/lib/evaluator.rb
CHANGED
@@ -20,48 +20,64 @@ class Evaluator
|
|
20
20
|
|
21
21
|
def check_gate(user, gate_name)
|
22
22
|
return nil unless @initialized && @spec_store.has_gate?(gate_name)
|
23
|
-
|
23
|
+
eval_spec(user, @spec_store.get_gate(gate_name))
|
24
24
|
end
|
25
25
|
|
26
26
|
def get_config(user, config_name)
|
27
27
|
return nil unless @initialized && @spec_store.has_config?(config_name)
|
28
|
-
|
28
|
+
eval_spec(user, @spec_store.get_config(config_name))
|
29
29
|
end
|
30
30
|
|
31
31
|
private
|
32
32
|
|
33
33
|
def eval_spec(user, config)
|
34
|
+
default_rule_id = 'default'
|
35
|
+
exposures = []
|
34
36
|
if config['enabled']
|
35
37
|
i = 0
|
36
38
|
until i >= config['rules'].length do
|
37
39
|
rule = config['rules'][i]
|
38
|
-
result =
|
40
|
+
result = eval_rule(user, rule)
|
39
41
|
return $fetch_from_server if result == $fetch_from_server
|
40
|
-
if result
|
41
|
-
|
42
|
+
exposures = exposures + result['exposures'] if result['exposures'].is_a? Array
|
43
|
+
if result['value']
|
44
|
+
pass = eval_pass_percent(user, rule, config['salt'])
|
42
45
|
return ConfigResult.new(
|
43
46
|
config['name'],
|
44
47
|
pass,
|
45
48
|
pass ? rule['returnValue'] : config['defaultValue'],
|
46
49
|
rule['id'],
|
50
|
+
exposures
|
47
51
|
)
|
48
52
|
end
|
49
53
|
|
50
54
|
i += 1
|
51
55
|
end
|
56
|
+
elsif (default_rule_id = 'disabled')
|
52
57
|
end
|
53
58
|
|
54
|
-
ConfigResult.new(config['name'], false, config['defaultValue'],
|
59
|
+
ConfigResult.new(config['name'], false, config['defaultValue'], default_rule_id, exposures)
|
55
60
|
end
|
56
61
|
|
57
62
|
def eval_rule(user, rule)
|
63
|
+
exposures = []
|
64
|
+
pass = true
|
58
65
|
i = 0
|
59
66
|
until i >= rule['conditions'].length do
|
60
|
-
result =
|
61
|
-
|
67
|
+
result = eval_condition(user, rule['conditions'][i])
|
68
|
+
if result == $fetch_from_server
|
69
|
+
return $fetch_from_server
|
70
|
+
end
|
71
|
+
|
72
|
+
if result.is_a?(Hash)
|
73
|
+
exposures = exposures + result['exposures'] if result['exposures'].is_a? Array
|
74
|
+
pass = false if result['value'] == false
|
75
|
+
elsif result == false
|
76
|
+
pass = false
|
77
|
+
end
|
62
78
|
i += 1
|
63
79
|
end
|
64
|
-
|
80
|
+
{ 'value' => pass, 'exposures' => exposures }
|
65
81
|
end
|
66
82
|
|
67
83
|
def eval_condition(user, condition)
|
@@ -80,9 +96,20 @@ class Evaluator
|
|
80
96
|
when 'public'
|
81
97
|
return true
|
82
98
|
when 'fail_gate', 'pass_gate'
|
83
|
-
other_gate_result =
|
99
|
+
other_gate_result = check_gate(user, target)
|
84
100
|
return $fetch_from_server if other_gate_result == $fetch_from_server
|
85
|
-
|
101
|
+
|
102
|
+
gate_value = other_gate_result&.gate_value == true
|
103
|
+
new_exposure = {
|
104
|
+
'gate' => target,
|
105
|
+
'gateValue' => gate_value ? 'true' : 'false',
|
106
|
+
'ruleID' => other_gate_result&.rule_id
|
107
|
+
}
|
108
|
+
exposures = other_gate_result&.secondary_exposures&.append(new_exposure)
|
109
|
+
return {
|
110
|
+
'value' => type == 'pass_gate' ? gate_value : !gate_value,
|
111
|
+
'exposures' => exposures
|
112
|
+
}
|
86
113
|
when 'ip_based'
|
87
114
|
value = get_value_from_user(user, field) || get_value_from_ip(user, field)
|
88
115
|
return $fetch_from_server if value == $fetch_from_server
|
data/lib/network.rb
CHANGED
@@ -24,14 +24,14 @@ class Network
|
|
24
24
|
}).accept(:json)
|
25
25
|
begin
|
26
26
|
res = http.post(@api + endpoint, body: body)
|
27
|
-
rescue
|
27
|
+
rescue StandardError => e
|
28
28
|
## network error retry
|
29
|
-
return nil unless retries > 0
|
29
|
+
return nil, e unless retries > 0
|
30
30
|
sleep backoff
|
31
31
|
return post_helper(endpoint, body, retries - 1, backoff * @backoff_multiplier)
|
32
32
|
end
|
33
|
-
return res unless !res.status.success?
|
34
|
-
return nil unless retries > 0 && $retry_codes.include?(res.code)
|
33
|
+
return res, nil unless !res.status.success?
|
34
|
+
return nil, StandardError.new("Got an exception when making request to #{@api + endpoint}: #{res.to_s}") unless retries > 0 && $retry_codes.include?(res.code)
|
35
35
|
## status code retry
|
36
36
|
sleep backoff
|
37
37
|
post_helper(endpoint, body, retries - 1, backoff * @backoff_multiplier)
|
@@ -40,7 +40,7 @@ class Network
|
|
40
40
|
def check_gate(user, gate_name)
|
41
41
|
begin
|
42
42
|
request_body = JSON.generate({'user' => user&.serialize(false), 'gateName' => gate_name})
|
43
|
-
response = post_helper('check_gate', request_body)
|
43
|
+
response, _ = post_helper('check_gate', request_body)
|
44
44
|
return JSON.parse(response.body) unless response.nil?
|
45
45
|
false
|
46
46
|
rescue
|
@@ -51,7 +51,7 @@ class Network
|
|
51
51
|
def get_config(user, dynamic_config_name)
|
52
52
|
begin
|
53
53
|
request_body = JSON.generate({'user' => user&.serialize(false), 'configName' => dynamic_config_name})
|
54
|
-
response = post_helper('get_config', request_body)
|
54
|
+
response, _ = post_helper('get_config', request_body)
|
55
55
|
return JSON.parse(response.body) unless response.nil?
|
56
56
|
nil
|
57
57
|
rescue
|
@@ -61,13 +61,13 @@ class Network
|
|
61
61
|
|
62
62
|
def download_config_specs
|
63
63
|
begin
|
64
|
-
response = post_helper('download_config_specs', JSON.generate({'sinceTime' => @last_sync_time}))
|
65
|
-
return nil
|
64
|
+
response, e = post_helper('download_config_specs', JSON.generate({'sinceTime' => @last_sync_time}))
|
65
|
+
return nil, e if response.nil?
|
66
66
|
json_body = JSON.parse(response.body)
|
67
67
|
@last_sync_time = json_body['time']
|
68
|
-
return json_body
|
69
|
-
rescue
|
70
|
-
return nil
|
68
|
+
return json_body, nil
|
69
|
+
rescue StandardError => e
|
70
|
+
return nil, e
|
71
71
|
end
|
72
72
|
end
|
73
73
|
|
@@ -75,7 +75,7 @@ class Network
|
|
75
75
|
Thread.new do
|
76
76
|
loop do
|
77
77
|
sleep 10
|
78
|
-
specs = download_config_specs
|
78
|
+
specs, _ = download_config_specs
|
79
79
|
unless specs.nil?
|
80
80
|
callback.call(specs)
|
81
81
|
end
|
data/lib/statsig.rb
CHANGED
@@ -1,32 +1,32 @@
|
|
1
1
|
require 'statsig_driver'
|
2
2
|
|
3
3
|
module Statsig
|
4
|
-
def self.initialize(secret_key, options = nil)
|
4
|
+
def self.initialize(secret_key, options = nil, error_callback = nil)
|
5
5
|
unless @shared_instance.nil?
|
6
6
|
puts 'Statsig already initialized.'
|
7
7
|
return @shared_instance
|
8
8
|
end
|
9
9
|
|
10
|
-
@shared_instance = StatsigDriver.new(secret_key, options)
|
10
|
+
@shared_instance = StatsigDriver.new(secret_key, options, error_callback)
|
11
11
|
end
|
12
12
|
|
13
13
|
def self.check_gate(user, gate_name)
|
14
|
-
|
14
|
+
ensure_initialized
|
15
15
|
@shared_instance&.check_gate(user, gate_name)
|
16
16
|
end
|
17
17
|
|
18
18
|
def self.get_config(user, dynamic_config_name)
|
19
|
-
|
19
|
+
ensure_initialized
|
20
20
|
@shared_instance&.get_config(user, dynamic_config_name)
|
21
21
|
end
|
22
22
|
|
23
23
|
def self.get_experiment(user, experiment_name)
|
24
|
-
|
24
|
+
ensure_initialized
|
25
25
|
@shared_instance&.get_config(user, experiment_name)
|
26
26
|
end
|
27
27
|
|
28
28
|
def self.log_event(user, event_name, value, metadata)
|
29
|
-
|
29
|
+
ensure_initialized
|
30
30
|
@shared_instance&.log_event(user, event_name, value, metadata)
|
31
31
|
end
|
32
32
|
|
data/lib/statsig_driver.rb
CHANGED
@@ -8,7 +8,7 @@ require 'statsig_user'
|
|
8
8
|
require 'spec_store'
|
9
9
|
|
10
10
|
class StatsigDriver
|
11
|
-
def initialize(secret_key, options = nil)
|
11
|
+
def initialize(secret_key, options = nil, error_callback = nil)
|
12
12
|
super()
|
13
13
|
if !secret_key.is_a?(String) || !secret_key.start_with?('secret-')
|
14
14
|
raise 'Invalid secret key provided. Provide your project secret key from the Statsig console'
|
@@ -17,7 +17,7 @@ class StatsigDriver
|
|
17
17
|
raise 'Invalid options provided. Either provide a valid StatsigOptions object or nil'
|
18
18
|
end
|
19
19
|
|
20
|
-
@options = options || StatsigOptions.new
|
20
|
+
@options = options || StatsigOptions.new
|
21
21
|
@shutdown = false
|
22
22
|
@secret_key = secret_key
|
23
23
|
@net = Network.new(secret_key, @options.api_url_base)
|
@@ -27,7 +27,7 @@ class StatsigDriver
|
|
27
27
|
}
|
28
28
|
@logger = StatsigLogger.new(@net, @statsig_metadata)
|
29
29
|
|
30
|
-
downloaded_specs = @net.download_config_specs
|
30
|
+
downloaded_specs, e = @net.download_config_specs
|
31
31
|
unless downloaded_specs.nil?
|
32
32
|
@initialized = true
|
33
33
|
end
|
@@ -36,6 +36,8 @@ class StatsigDriver
|
|
36
36
|
@evaluator = Evaluator.new(@store)
|
37
37
|
|
38
38
|
@polling_thread = @net.poll_for_changes(-> (config_specs) { @store.process(config_specs) })
|
39
|
+
|
40
|
+
error_callback.call(e) unless error_callback.nil?
|
39
41
|
end
|
40
42
|
|
41
43
|
def check_gate(user, gate_name)
|
@@ -58,7 +60,7 @@ class StatsigDriver
|
|
58
60
|
res = check_gate_fallback(user, gate_name)
|
59
61
|
# exposure logged by the server
|
60
62
|
else
|
61
|
-
@logger.log_gate_exposure(user, res.name, res.gate_value, res.rule_id)
|
63
|
+
@logger.log_gate_exposure(user, res.name, res.gate_value, res.rule_id, res.secondary_exposures)
|
62
64
|
end
|
63
65
|
|
64
66
|
res.gate_value
|
@@ -84,12 +86,19 @@ class StatsigDriver
|
|
84
86
|
res = get_config_fallback(user, dynamic_config_name)
|
85
87
|
# exposure logged by the server
|
86
88
|
else
|
87
|
-
@logger.log_config_exposure(user, res.name, res.rule_id)
|
89
|
+
@logger.log_config_exposure(user, res.name, res.rule_id, res.secondary_exposures)
|
88
90
|
end
|
89
91
|
|
90
92
|
DynamicConfig.new(res.name, res.json_value, res.rule_id)
|
91
93
|
end
|
92
94
|
|
95
|
+
def get_experiment(user, experiment_name)
|
96
|
+
if !experiment_name.is_a?(String) || experiment_name.empty?
|
97
|
+
raise "Invalid experiment_name provided"
|
98
|
+
end
|
99
|
+
get_config(user, experiment_name)
|
100
|
+
end
|
101
|
+
|
93
102
|
def log_event(user, event_name, value = nil, metadata = nil)
|
94
103
|
if !user.nil? && !user.instance_of?(StatsigUser)
|
95
104
|
raise 'Must provide a valid StatsigUser or nil'
|
data/lib/statsig_event.rb
CHANGED
@@ -2,6 +2,7 @@ class StatsigEvent
|
|
2
2
|
attr_accessor :value
|
3
3
|
attr_accessor :metadata
|
4
4
|
attr_accessor :statsig_metadata
|
5
|
+
attr_accessor :secondary_exposures
|
5
6
|
attr_reader :user
|
6
7
|
|
7
8
|
def initialize(event_name)
|
@@ -23,6 +24,7 @@ class StatsigEvent
|
|
23
24
|
'user' => @user,
|
24
25
|
'time' => @time,
|
25
26
|
'statsigMetadata' => @statsig_metadata,
|
27
|
+
'secondaryExposures' => @secondary_exposures
|
26
28
|
}
|
27
29
|
end
|
28
30
|
end
|
data/lib/statsig_logger.rb
CHANGED
@@ -21,7 +21,7 @@ class StatsigLogger
|
|
21
21
|
end
|
22
22
|
end
|
23
23
|
|
24
|
-
def log_gate_exposure(user, gate_name, value, rule_id)
|
24
|
+
def log_gate_exposure(user, gate_name, value, rule_id, secondary_exposures)
|
25
25
|
event = StatsigEvent.new($gate_exposure_event)
|
26
26
|
event.user = user
|
27
27
|
event.metadata = {
|
@@ -30,10 +30,11 @@ class StatsigLogger
|
|
30
30
|
'ruleID' => rule_id
|
31
31
|
}
|
32
32
|
event.statsig_metadata = @statsig_metadata
|
33
|
+
event.secondary_exposures = secondary_exposures.is_a?(Array) ? secondary_exposures : []
|
33
34
|
log_event(event)
|
34
35
|
end
|
35
36
|
|
36
|
-
def log_config_exposure(user, config_name, rule_id)
|
37
|
+
def log_config_exposure(user, config_name, rule_id, secondary_exposures)
|
37
38
|
event = StatsigEvent.new($config_exposure_event)
|
38
39
|
event.user = user
|
39
40
|
event.metadata = {
|
@@ -41,6 +42,7 @@ class StatsigLogger
|
|
41
42
|
'ruleID' => rule_id
|
42
43
|
}
|
43
44
|
event.statsig_metadata = @statsig_metadata
|
45
|
+
event.secondary_exposures = secondary_exposures.is_a?(Array) ? secondary_exposures : []
|
44
46
|
log_event(event)
|
45
47
|
end
|
46
48
|
|
@@ -51,7 +53,7 @@ class StatsigLogger
|
|
51
53
|
if @events.length == 0
|
52
54
|
return
|
53
55
|
end
|
54
|
-
flush_events = @events.map { |e| e.serialize
|
56
|
+
flush_events = @events.map { |e| e.serialize }
|
55
57
|
@events = []
|
56
58
|
|
57
59
|
@network.post_logs(flush_events, @statsig_metadata)
|
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.7.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: 2021-
|
11
|
+
date: 2021-10-28 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|