statsig 1.6.1 → 1.7.0.beta.2
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 +0 -2
- data/lib/evaluator.rb +32 -7
- data/lib/network.rb +13 -13
- data/lib/statsig.rb +2 -4
- data/lib/statsig_driver.rb +14 -7
- data/lib/statsig_event.rb +2 -0
- data/lib/statsig_logger.rb +6 -6
- metadata +4 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 889dec377c0c563738b53a97153d54cde36d34518bfa67d1e56a1f2ac6b161b3
|
|
4
|
+
data.tar.gz: 9da0ba2bf4b241042e5e2a2ae0822700f2906f0454d5ddfda625b99b69c1fd2f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fd0c026a5d93e99c8862fa36e215c185aef5704173c1f56fde1ff01765d276633880233e0d60b65f079b00f63d6618c447dfd10f1f037311b30804b5362db916
|
|
7
|
+
data.tar.gz: 4cf98a1164ce38977d05d682d94b761eda719e7cb1188a363c0b565e1ea9ad402f8719454363edf83dfefba4603666e2e1bd64573b04cbb899d008815e20b665
|
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
data/lib/evaluator.rb
CHANGED
|
@@ -28,40 +28,54 @@ class Evaluator
|
|
|
28
28
|
self.eval_spec(user, @spec_store.get_config(config_name))
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
-
private
|
|
32
|
-
|
|
33
31
|
def eval_spec(user, config)
|
|
32
|
+
default_rule_id = 'default'
|
|
33
|
+
exposures = []
|
|
34
34
|
if config['enabled']
|
|
35
35
|
i = 0
|
|
36
36
|
until i >= config['rules'].length do
|
|
37
37
|
rule = config['rules'][i]
|
|
38
38
|
result = self.eval_rule(user, rule)
|
|
39
39
|
return $fetch_from_server if result == $fetch_from_server
|
|
40
|
-
if result
|
|
40
|
+
exposures = exposures + result['exposures'] if result['exposures'].is_a? Array
|
|
41
|
+
if result['value']
|
|
41
42
|
pass = self.eval_pass_percent(user, rule, config['salt'])
|
|
42
43
|
return ConfigResult.new(
|
|
43
44
|
config['name'],
|
|
44
45
|
pass,
|
|
45
46
|
pass ? rule['returnValue'] : config['defaultValue'],
|
|
46
47
|
rule['id'],
|
|
48
|
+
exposures
|
|
47
49
|
)
|
|
48
50
|
end
|
|
49
51
|
|
|
50
52
|
i += 1
|
|
51
53
|
end
|
|
54
|
+
elsif (default_rule_id = 'disabled')
|
|
52
55
|
end
|
|
53
56
|
|
|
54
|
-
ConfigResult.new(config['name'], false, config['defaultValue'],
|
|
57
|
+
ConfigResult.new(config['name'], false, config['defaultValue'], default_rule_id, exposures)
|
|
55
58
|
end
|
|
56
59
|
|
|
57
60
|
def eval_rule(user, rule)
|
|
61
|
+
exposures = []
|
|
62
|
+
pass = true
|
|
58
63
|
i = 0
|
|
59
64
|
until i >= rule['conditions'].length do
|
|
60
65
|
result = self.eval_condition(user, rule['conditions'][i])
|
|
61
|
-
|
|
66
|
+
if result == $fetch_from_server
|
|
67
|
+
return $fetch_from_server
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
if result.is_a?(Hash)
|
|
71
|
+
exposures = exposures + result['exposures'] if result['exposures'].is_a? Array
|
|
72
|
+
pass = false if result['value'] == false
|
|
73
|
+
elsif result == false
|
|
74
|
+
pass = false
|
|
75
|
+
end
|
|
62
76
|
i += 1
|
|
63
77
|
end
|
|
64
|
-
|
|
78
|
+
{ 'value' => pass, 'exposures' => exposures }
|
|
65
79
|
end
|
|
66
80
|
|
|
67
81
|
def eval_condition(user, condition)
|
|
@@ -82,7 +96,18 @@ class Evaluator
|
|
|
82
96
|
when 'fail_gate', 'pass_gate'
|
|
83
97
|
other_gate_result = self.check_gate(user, target)
|
|
84
98
|
return $fetch_from_server if other_gate_result == $fetch_from_server
|
|
85
|
-
|
|
99
|
+
|
|
100
|
+
gate_value = other_gate_result&.gate_value == true
|
|
101
|
+
new_exposure = {
|
|
102
|
+
'gate' => target,
|
|
103
|
+
'gateValue' => gate_value ? 'true' : 'false',
|
|
104
|
+
'ruleID' => other_gate_result&.rule_id
|
|
105
|
+
}
|
|
106
|
+
exposures = other_gate_result&.secondary_exposures&.append(new_exposure)
|
|
107
|
+
return {
|
|
108
|
+
'value' => type == 'pass_gate' ? gate_value : !gate_value,
|
|
109
|
+
'exposures' => exposures
|
|
110
|
+
}
|
|
86
111
|
when 'ip_based'
|
|
87
112
|
value = get_value_from_user(user, field) || get_value_from_ip(user, field)
|
|
88
113
|
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
|
|
@@ -86,7 +86,7 @@ class Network
|
|
|
86
86
|
def post_logs(events, statsig_metadata)
|
|
87
87
|
begin
|
|
88
88
|
json_body = JSON.generate({'events' => events, 'statsigMetadata' => statsig_metadata})
|
|
89
|
-
post_helper('log_event',
|
|
89
|
+
post_helper('log_event', json_body, retries: 5)
|
|
90
90
|
rescue
|
|
91
91
|
end
|
|
92
92
|
end
|
data/lib/statsig.rb
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
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)
|
|
@@ -37,8 +37,6 @@ module Statsig
|
|
|
37
37
|
@shared_instance = nil
|
|
38
38
|
end
|
|
39
39
|
|
|
40
|
-
private
|
|
41
|
-
|
|
42
40
|
def self.ensure_initialized
|
|
43
41
|
if @shared_instance.nil?
|
|
44
42
|
raise 'Must call initialize first.'
|
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'
|
|
@@ -112,8 +121,6 @@ class StatsigDriver
|
|
|
112
121
|
@polling_thread&.exit
|
|
113
122
|
end
|
|
114
123
|
|
|
115
|
-
private
|
|
116
|
-
|
|
117
124
|
def validate_user(user)
|
|
118
125
|
if user.nil? || !user.instance_of?(StatsigUser) || !user.user_id.is_a?(String)
|
|
119
126
|
raise 'Must provide a valid StatsigUser with a user_id to use the server SDK. See https://docs.statsig.com/messages/serverRequiredUserID/ for more details.'
|
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,11 +53,9 @@ 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
|
-
|
|
58
|
-
@network.post_logs(flush_events, @statsig_metadata)
|
|
59
|
-
end
|
|
59
|
+
@network.post_logs(flush_events, @statsig_metadata)
|
|
60
60
|
end
|
|
61
61
|
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.7.0.beta.2
|
|
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
|
|
@@ -141,9 +141,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
141
141
|
version: '0'
|
|
142
142
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
143
143
|
requirements:
|
|
144
|
-
- - "
|
|
144
|
+
- - ">"
|
|
145
145
|
- !ruby/object:Gem::Version
|
|
146
|
-
version:
|
|
146
|
+
version: 1.3.1
|
|
147
147
|
requirements: []
|
|
148
148
|
rubygems_version: 3.2.3
|
|
149
149
|
signing_key:
|