statsig 1.6.0 → 1.7.0.beta.1
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 +3 -11
- data/lib/evaluator.rb +36 -9
- data/lib/network.rb +13 -13
- data/lib/statsig.rb +2 -2
- data/lib/statsig_driver.rb +14 -5
- 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: cf5fda9f980ecae86129f6ea0bd4052b6af45cba14c83e38062b495cd06cfeb1
|
4
|
+
data.tar.gz: 5b1a231a8af145fec2303fc89998f3e9acf05323cbc55d9fea3850ba841ecd1e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7f1c7cf9a5a549021f02c16bac3d1095143ee9231a3f4165db9040efbf9b16cef083b6955dc5f748dd5fcfe12953064e8c48874a1eb8f477c24f61d061a57890
|
7
|
+
data.tar.gz: a5f291bd14edb61286416c293eae1c14f3c519dc1c6548356fe7147667cc33ed6b327488412e2f0b5516139d71d06bacf0db3f6ee907685be9c7e8ca2cc6b119
|
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
@@ -6,19 +6,11 @@ module EvaluationHelpers
|
|
6
6
|
func.call(a.to_f, b.to_f) rescue false
|
7
7
|
end
|
8
8
|
|
9
|
-
# returns true if array contains value, ignoring case when comparing strings
|
10
|
-
def self.array_contains(array, value, ignore_case)
|
11
|
-
return false unless array.is_a?(Array) && !value.nil?
|
12
|
-
if value.is_a?(String) && match_string_in_array(array, value, ignore_case, ->(a, b) { a == b })
|
13
|
-
return true
|
14
|
-
end
|
15
|
-
return array.include?(value)
|
16
|
-
end
|
17
|
-
|
18
9
|
# returns true if array has any element that evaluates to true with value using func lambda, ignoring case
|
19
10
|
def self.match_string_in_array(array, value, ignore_case, func)
|
20
|
-
return false unless array.is_a?(Array) && value.
|
21
|
-
|
11
|
+
return false unless array.is_a?(Array) && !value.nil?
|
12
|
+
str_value = value.to_s
|
13
|
+
array.any?{ |s| !s.nil? && ((ignore_case && func.call(str_value.downcase, s.to_s.downcase)) || func.call(str_value, s.to_s)) } rescue false
|
22
14
|
end
|
23
15
|
|
24
16
|
def self.compare_times(a, b, func)
|
data/lib/evaluator.rb
CHANGED
@@ -31,37 +31,53 @@ class Evaluator
|
|
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
40
|
result = self.eval_rule(user, rule)
|
39
41
|
return $fetch_from_server if result == $fetch_from_server
|
40
|
-
if result
|
42
|
+
exposures = exposures + result['exposures'] if result['exposures'].is_a? Array
|
43
|
+
if result['value']
|
41
44
|
pass = self.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
67
|
result = self.eval_condition(user, rule['conditions'][i])
|
61
|
-
|
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)
|
@@ -82,7 +98,18 @@ class Evaluator
|
|
82
98
|
when 'fail_gate', 'pass_gate'
|
83
99
|
other_gate_result = self.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
|
@@ -138,13 +165,13 @@ class Evaluator
|
|
138
165
|
|
139
166
|
# array operations
|
140
167
|
when 'any'
|
141
|
-
return EvaluationHelpers::
|
168
|
+
return EvaluationHelpers::match_string_in_array(target, value, true, ->(a, b) { a == b })
|
142
169
|
when 'none'
|
143
|
-
return !EvaluationHelpers::
|
170
|
+
return !EvaluationHelpers::match_string_in_array(target, value, true, ->(a, b) { a == b })
|
144
171
|
when 'any_case_sensitive'
|
145
|
-
return EvaluationHelpers::
|
172
|
+
return EvaluationHelpers::match_string_in_array(target, value, false, ->(a, b) { a == b })
|
146
173
|
when 'none_case_sensitive'
|
147
|
-
return !EvaluationHelpers::
|
174
|
+
return !EvaluationHelpers::match_string_in_array(target, value, false, ->(a, b) { a == b })
|
148
175
|
|
149
176
|
#string
|
150
177
|
when 'str_starts_with_any'
|
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)
|
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,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.1
|
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-12 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:
|