statsig 1.2.0 → 1.6.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/dynamic_config.rb +3 -3
- data/lib/evaluation_helpers.rb +3 -11
- data/lib/evaluator.rb +32 -23
- data/lib/network.rb +26 -9
- data/lib/statsig_driver.rb +9 -6
- data/lib/statsig_event.rb +9 -2
- data/lib/statsig_logger.rb +11 -2
- data/lib/statsig_user.rb +10 -2
- metadata +44 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8dfdb320cf540cd81046ba96aae19d9d3f947ec557ad7fb7b405a35065eb67fa
|
4
|
+
data.tar.gz: 0e8d15d9c3f9aa0245c2e834e55394479775d2a9de23208e54dfb41be89e0326
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e7a479f8ed9b2cbf49f4b567bdf16000b5ba23a88015593cec6e9806477419011a13b5ed64fd578208cec5f6633101c881e3fa0b98b0474a2faebc91aaf8be1e
|
7
|
+
data.tar.gz: d731197be16b7ef43d6c9060f00254b3db3a205bef736243a326a382e9fca1aa72b1335102612ab13c044a03f4dc247e0b3443badfad147dffdd8be8391a038d
|
data/lib/dynamic_config.rb
CHANGED
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
@@ -79,16 +79,15 @@ class Evaluator
|
|
79
79
|
case type
|
80
80
|
when 'public'
|
81
81
|
return true
|
82
|
-
when 'fail_gate'
|
83
|
-
when 'pass_gate'
|
82
|
+
when 'fail_gate', 'pass_gate'
|
84
83
|
other_gate_result = self.check_gate(user, target)
|
85
84
|
return $fetch_from_server if other_gate_result == $fetch_from_server
|
86
85
|
return type == 'pass_gate' ? other_gate_result.gate_value : !other_gate_result.gate_value
|
87
86
|
when 'ip_based'
|
88
|
-
value = get_value_from_user(user, field) || get_value_from_ip(user
|
87
|
+
value = get_value_from_user(user, field) || get_value_from_ip(user, field)
|
89
88
|
return $fetch_from_server if value == $fetch_from_server
|
90
89
|
when 'ua_based'
|
91
|
-
value = get_value_from_user(user, field) || get_value_from_ua(user
|
90
|
+
value = get_value_from_user(user, field) || get_value_from_ua(user, field)
|
92
91
|
return $fetch_from_server if value == $fetch_from_server
|
93
92
|
when 'user_field'
|
94
93
|
value = get_value_from_user(user, field)
|
@@ -109,10 +108,7 @@ class Evaluator
|
|
109
108
|
return $fetch_from_server
|
110
109
|
end
|
111
110
|
|
112
|
-
return $fetch_from_server if value == $fetch_from_server
|
113
|
-
return false if value.nil?
|
114
|
-
|
115
|
-
return $fetch_from_server unless operator.is_a?(String)
|
111
|
+
return $fetch_from_server if value == $fetch_from_server || !operator.is_a?(String)
|
116
112
|
operator = operator.downcase
|
117
113
|
|
118
114
|
case operator
|
@@ -142,13 +138,13 @@ class Evaluator
|
|
142
138
|
|
143
139
|
# array operations
|
144
140
|
when 'any'
|
145
|
-
return EvaluationHelpers::
|
141
|
+
return EvaluationHelpers::match_string_in_array(target, value, true, ->(a, b) { a == b })
|
146
142
|
when 'none'
|
147
|
-
return !EvaluationHelpers::
|
143
|
+
return !EvaluationHelpers::match_string_in_array(target, value, true, ->(a, b) { a == b })
|
148
144
|
when 'any_case_sensitive'
|
149
|
-
return EvaluationHelpers::
|
145
|
+
return EvaluationHelpers::match_string_in_array(target, value, false, ->(a, b) { a == b })
|
150
146
|
when 'none_case_sensitive'
|
151
|
-
return !EvaluationHelpers::
|
147
|
+
return !EvaluationHelpers::match_string_in_array(target, value, false, ->(a, b) { a == b })
|
152
148
|
|
153
149
|
#string
|
154
150
|
when 'str_starts_with_any'
|
@@ -157,6 +153,8 @@ class Evaluator
|
|
157
153
|
return EvaluationHelpers::match_string_in_array(target, value, true, ->(a, b) { a.end_with?(b) })
|
158
154
|
when 'str_contains_any'
|
159
155
|
return EvaluationHelpers::match_string_in_array(target, value, true, ->(a, b) { a.include?(b) })
|
156
|
+
when 'str_contains_none'
|
157
|
+
return !EvaluationHelpers::match_string_in_array(target, value, true, ->(a, b) { a.include?(b) })
|
160
158
|
when 'str_matches'
|
161
159
|
return (value.is_a?(String) && !(value =~ Regexp.new(target)).nil? rescue false)
|
162
160
|
when 'eq'
|
@@ -181,13 +179,22 @@ class Evaluator
|
|
181
179
|
|
182
180
|
user_lookup_table = user&.value_lookup
|
183
181
|
return nil unless user_lookup_table.is_a?(Hash)
|
184
|
-
return user_lookup_table[field.downcase] if user_lookup_table.has_key?(field.downcase)
|
182
|
+
return user_lookup_table[field.downcase] if user_lookup_table.has_key?(field.downcase) && !user_lookup_table[field.downcase].nil?
|
185
183
|
|
186
184
|
user_custom = user_lookup_table['custom']
|
187
|
-
|
188
|
-
|
189
|
-
|
185
|
+
if user_custom.is_a?(Hash)
|
186
|
+
user_custom.each do |key, value|
|
187
|
+
return value if key.downcase.casecmp?(field.downcase) && !value.nil?
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
private_attributes = user_lookup_table['privateAttributes']
|
192
|
+
if private_attributes.is_a?(Hash)
|
193
|
+
private_attributes.each do |key, value|
|
194
|
+
return value if key.downcase.casecmp?(field.downcase) && !value.nil?
|
195
|
+
end
|
190
196
|
end
|
197
|
+
|
191
198
|
nil
|
192
199
|
end
|
193
200
|
|
@@ -201,17 +208,19 @@ class Evaluator
|
|
201
208
|
nil
|
202
209
|
end
|
203
210
|
|
204
|
-
def get_value_from_ip(
|
205
|
-
return nil unless
|
211
|
+
def get_value_from_ip(user, field)
|
212
|
+
return nil unless user.is_a?(StatsigUser) && field.is_a?(String) && field.downcase == 'country'
|
213
|
+
ip = get_value_from_user(user, 'ip')
|
214
|
+
return nil unless ip.is_a?(String)
|
206
215
|
|
207
|
-
if field.downcase != 'country'
|
208
|
-
return $fetch_from_server
|
209
|
-
end
|
210
216
|
CountryLookup.lookup_ip_string(ip)
|
211
217
|
end
|
212
218
|
|
213
|
-
def get_value_from_ua(
|
214
|
-
return nil unless
|
219
|
+
def get_value_from_ua(user, field)
|
220
|
+
return nil unless user.is_a?(StatsigUser) && field.is_a?(String)
|
221
|
+
ua = get_value_from_user(user, 'userAgent')
|
222
|
+
return nil unless ua.is_a?(String)
|
223
|
+
|
215
224
|
parsed = @ua_parser.parse ua
|
216
225
|
os = parsed.os
|
217
226
|
case field.downcase
|
data/lib/network.rb
CHANGED
@@ -2,8 +2,10 @@ require 'http'
|
|
2
2
|
require 'json'
|
3
3
|
require 'dynamic_config'
|
4
4
|
|
5
|
+
$retry_codes = [408, 500, 502, 503, 504, 522, 524, 599]
|
6
|
+
|
5
7
|
class Network
|
6
|
-
def initialize(server_secret, api)
|
8
|
+
def initialize(server_secret, api, backoff_mult = 10)
|
7
9
|
super()
|
8
10
|
unless api.end_with?('/')
|
9
11
|
api += '/'
|
@@ -11,22 +13,36 @@ class Network
|
|
11
13
|
@server_secret = server_secret
|
12
14
|
@api = api
|
13
15
|
@last_sync_time = 0
|
16
|
+
@backoff_multiplier = backoff_mult
|
14
17
|
end
|
15
18
|
|
16
|
-
def post_helper(endpoint, body)
|
19
|
+
def post_helper(endpoint, body, retries = 0, backoff = 1)
|
17
20
|
http = HTTP.headers(
|
18
21
|
{"STATSIG-API-KEY" => @server_secret,
|
19
22
|
"STATSIG-CLIENT-TIME" => (Time.now.to_f * 1000).to_s,
|
20
23
|
"Content-Type" => "application/json; charset=UTF-8"
|
21
24
|
}).accept(:json)
|
22
|
-
|
25
|
+
begin
|
26
|
+
res = http.post(@api + endpoint, body: body)
|
27
|
+
rescue
|
28
|
+
## network error retry
|
29
|
+
return nil unless retries > 0
|
30
|
+
sleep backoff
|
31
|
+
return post_helper(endpoint, body, retries - 1, backoff * @backoff_multiplier)
|
32
|
+
end
|
33
|
+
return res unless !res.status.success?
|
34
|
+
return nil unless retries > 0 && $retry_codes.include?(res.code)
|
35
|
+
## status code retry
|
36
|
+
sleep backoff
|
37
|
+
post_helper(endpoint, body, retries - 1, backoff * @backoff_multiplier)
|
23
38
|
end
|
24
39
|
|
25
40
|
def check_gate(user, gate_name)
|
26
41
|
begin
|
27
|
-
request_body = JSON.generate({'user' => user&.serialize, 'gateName' => gate_name})
|
42
|
+
request_body = JSON.generate({'user' => user&.serialize(false), 'gateName' => gate_name})
|
28
43
|
response = post_helper('check_gate', request_body)
|
29
|
-
return JSON.parse(response.body)
|
44
|
+
return JSON.parse(response.body) unless response.nil?
|
45
|
+
false
|
30
46
|
rescue
|
31
47
|
return false
|
32
48
|
end
|
@@ -34,9 +50,10 @@ class Network
|
|
34
50
|
|
35
51
|
def get_config(user, dynamic_config_name)
|
36
52
|
begin
|
37
|
-
request_body = JSON.generate({'user' => user&.serialize, 'configName' => dynamic_config_name})
|
53
|
+
request_body = JSON.generate({'user' => user&.serialize(false), 'configName' => dynamic_config_name})
|
38
54
|
response = post_helper('get_config', request_body)
|
39
|
-
return JSON.parse(response.body)
|
55
|
+
return JSON.parse(response.body) unless response.nil?
|
56
|
+
nil
|
40
57
|
rescue
|
41
58
|
return nil
|
42
59
|
end
|
@@ -45,6 +62,7 @@ class Network
|
|
45
62
|
def download_config_specs
|
46
63
|
begin
|
47
64
|
response = post_helper('download_config_specs', JSON.generate({'sinceTime' => @last_sync_time}))
|
65
|
+
return nil unless !response.nil?
|
48
66
|
json_body = JSON.parse(response.body)
|
49
67
|
@last_sync_time = json_body['time']
|
50
68
|
return json_body
|
@@ -68,9 +86,8 @@ class Network
|
|
68
86
|
def post_logs(events, statsig_metadata)
|
69
87
|
begin
|
70
88
|
json_body = JSON.generate({'events' => events, 'statsigMetadata' => statsig_metadata})
|
71
|
-
post_helper('log_event', body: json_body)
|
89
|
+
post_helper('log_event', body: json_body, retries: 5)
|
72
90
|
rescue
|
73
|
-
# TODO: retries
|
74
91
|
end
|
75
92
|
end
|
76
93
|
end
|
data/lib/statsig_driver.rb
CHANGED
@@ -56,9 +56,11 @@ class StatsigDriver
|
|
56
56
|
|
57
57
|
if res == $fetch_from_server
|
58
58
|
res = check_gate_fallback(user, gate_name)
|
59
|
+
# exposure logged by the server
|
60
|
+
else
|
61
|
+
@logger.log_gate_exposure(user, res.name, res.gate_value, res.rule_id)
|
59
62
|
end
|
60
63
|
|
61
|
-
@logger.log_gate_exposure(user, res.name, res.gate_value, res.rule_id)
|
62
64
|
res.gate_value
|
63
65
|
end
|
64
66
|
|
@@ -80,11 +82,12 @@ class StatsigDriver
|
|
80
82
|
|
81
83
|
if res == $fetch_from_server
|
82
84
|
res = get_config_fallback(user, dynamic_config_name)
|
85
|
+
# exposure logged by the server
|
86
|
+
else
|
87
|
+
@logger.log_config_exposure(user, res.name, res.rule_id)
|
83
88
|
end
|
84
89
|
|
85
|
-
|
86
|
-
@logger.log_config_exposure(user, result_config.name, result_config.rule_id)
|
87
|
-
result_config
|
90
|
+
DynamicConfig.new(res.name, res.json_value, res.rule_id)
|
88
91
|
end
|
89
92
|
|
90
93
|
def log_event(user, event_name, value = nil, metadata = nil)
|
@@ -96,7 +99,7 @@ class StatsigDriver
|
|
96
99
|
user = normalize_user(user)
|
97
100
|
|
98
101
|
event = StatsigEvent.new(event_name)
|
99
|
-
event.user = user
|
102
|
+
event.user = user
|
100
103
|
event.value = value
|
101
104
|
event.metadata = metadata
|
102
105
|
event.statsig_metadata = @statsig_metadata
|
@@ -105,7 +108,7 @@ class StatsigDriver
|
|
105
108
|
|
106
109
|
def shutdown
|
107
110
|
@shutdown = true
|
108
|
-
@logger.flush
|
111
|
+
@logger.flush(true)
|
109
112
|
@polling_thread&.exit
|
110
113
|
end
|
111
114
|
|
data/lib/statsig_event.rb
CHANGED
@@ -1,15 +1,22 @@
|
|
1
1
|
class StatsigEvent
|
2
2
|
attr_accessor :value
|
3
|
-
attr_accessor :user
|
4
3
|
attr_accessor :metadata
|
5
4
|
attr_accessor :statsig_metadata
|
5
|
+
attr_reader :user
|
6
|
+
|
6
7
|
def initialize(event_name)
|
7
8
|
@event_name = event_name
|
8
9
|
@time = Time.now.to_f * 1000
|
9
10
|
end
|
10
11
|
|
12
|
+
def user=(value)
|
13
|
+
if value.is_a?(StatsigUser)
|
14
|
+
@user = value.serialize(true)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
11
18
|
def serialize
|
12
|
-
|
19
|
+
{
|
13
20
|
'eventName' => @event_name,
|
14
21
|
'metadata' => @metadata,
|
15
22
|
'value' => @value,
|
data/lib/statsig_logger.rb
CHANGED
@@ -8,6 +8,10 @@ class StatsigLogger
|
|
8
8
|
@network = network
|
9
9
|
@statsig_metadata = statsig_metadata
|
10
10
|
@events = []
|
11
|
+
@background_flush = Thread.new do
|
12
|
+
sleep 60
|
13
|
+
flush
|
14
|
+
end
|
11
15
|
end
|
12
16
|
|
13
17
|
def log_event(event)
|
@@ -40,13 +44,18 @@ class StatsigLogger
|
|
40
44
|
log_event(event)
|
41
45
|
end
|
42
46
|
|
43
|
-
def flush
|
47
|
+
def flush(closing = false)
|
48
|
+
if closing
|
49
|
+
@background_flush.exit
|
50
|
+
end
|
44
51
|
if @events.length == 0
|
45
52
|
return
|
46
53
|
end
|
47
54
|
flush_events = @events.map { |e| e.serialize() }
|
48
55
|
@events = []
|
49
56
|
|
50
|
-
|
57
|
+
Thread.new do
|
58
|
+
@network.post_logs(flush_events, @statsig_metadata)
|
59
|
+
end
|
51
60
|
end
|
52
61
|
end
|
data/lib/statsig_user.rb
CHANGED
@@ -7,6 +7,7 @@ class StatsigUser
|
|
7
7
|
attr_accessor :locale
|
8
8
|
attr_accessor :app_version
|
9
9
|
attr_accessor :statsig_environment
|
10
|
+
attr_accessor :private_attributes
|
10
11
|
|
11
12
|
def custom
|
12
13
|
@custom
|
@@ -29,11 +30,12 @@ class StatsigUser
|
|
29
30
|
@app_version = user_hash['appVersion'] || user_hash['app_version']
|
30
31
|
@custom = user_hash['custom']
|
31
32
|
@statsig_environment = user_hash['statsigEnvironment']
|
33
|
+
@private_attributes = user_hash['privateAttributes']
|
32
34
|
end
|
33
35
|
end
|
34
36
|
|
35
|
-
def serialize
|
36
|
-
{
|
37
|
+
def serialize(for_logging)
|
38
|
+
hash = {
|
37
39
|
'userID' => @user_id,
|
38
40
|
'email' => @email,
|
39
41
|
'ip' => @ip,
|
@@ -43,7 +45,12 @@ class StatsigUser
|
|
43
45
|
'appVersion' => @app_version,
|
44
46
|
'custom' => @custom,
|
45
47
|
'statsigEnvironment' => @statsig_environment,
|
48
|
+
'privateAttributes' => @private_attributes,
|
46
49
|
}
|
50
|
+
if for_logging
|
51
|
+
hash.delete('privateAttributes')
|
52
|
+
end
|
53
|
+
hash
|
47
54
|
end
|
48
55
|
|
49
56
|
def value_lookup
|
@@ -62,6 +69,7 @@ class StatsigUser
|
|
62
69
|
'appversion' => @app_version,
|
63
70
|
'app_version' => @app_version,
|
64
71
|
'custom' => @custom,
|
72
|
+
'privateAttributes' => @private_attributes,
|
65
73
|
}
|
66
74
|
end
|
67
75
|
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.6.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-09-11 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -24,6 +24,48 @@ dependencies:
|
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '2.1'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: webmock
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '3.13'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '3.13'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: minitest
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '5.14'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '5.14'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: spy
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '1.0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '1.0'
|
27
69
|
- !ruby/object:Gem::Dependency
|
28
70
|
name: user_agent_parser
|
29
71
|
requirement: !ruby/object:Gem::Requirement
|