statsig 1.25.2 → 1.33.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/api_config.rb +128 -0
- data/lib/client_initialize_helpers.rb +110 -82
- data/lib/config_result.rb +42 -11
- data/lib/constants.rb +60 -0
- data/lib/diagnostics.rb +48 -70
- data/lib/dynamic_config.rb +5 -15
- data/lib/error_boundary.rb +32 -49
- data/lib/evaluation_details.rb +17 -8
- data/lib/evaluation_helpers.rb +35 -3
- data/lib/evaluator.rb +425 -300
- data/lib/feature_gate.rb +46 -0
- data/lib/hash_utils.rb +32 -0
- data/lib/id_list.rb +2 -2
- data/lib/interfaces/data_store.rb +1 -1
- data/lib/interfaces/user_persistent_storage.rb +12 -0
- data/lib/layer.rb +7 -12
- data/lib/network.rb +57 -55
- data/lib/spec_store.rb +213 -130
- data/lib/statsig.rb +186 -82
- data/lib/statsig_driver.rb +227 -147
- data/lib/statsig_errors.rb +7 -0
- data/lib/statsig_event.rb +8 -8
- data/lib/statsig_logger.rb +54 -42
- data/lib/statsig_options.rb +23 -49
- data/lib/statsig_user.rb +65 -57
- data/lib/ua_parser.rb +1 -0
- data/lib/uri_helper.rb +2 -10
- data/lib/user_persistent_storage_utils.rb +89 -0
- metadata +46 -20
data/lib/feature_gate.rb
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
class FeatureGate
|
2
|
+
|
3
|
+
attr_accessor :name
|
4
|
+
|
5
|
+
attr_accessor :value
|
6
|
+
|
7
|
+
attr_accessor :rule_id
|
8
|
+
|
9
|
+
attr_accessor :group_name
|
10
|
+
|
11
|
+
attr_accessor :id_type
|
12
|
+
|
13
|
+
attr_accessor :evaluation_details
|
14
|
+
|
15
|
+
attr_accessor :target_app_ids
|
16
|
+
|
17
|
+
def initialize(
|
18
|
+
name,
|
19
|
+
value: false,
|
20
|
+
rule_id: '',
|
21
|
+
group_name: nil,
|
22
|
+
id_type: '',
|
23
|
+
evaluation_details: nil,
|
24
|
+
target_app_ids: nil
|
25
|
+
)
|
26
|
+
@name = name
|
27
|
+
@value = value
|
28
|
+
@rule_id = rule_id
|
29
|
+
@group_name = group_name
|
30
|
+
@id_type = id_type
|
31
|
+
@evaluation_details = evaluation_details
|
32
|
+
@target_app_ids = target_app_ids
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.from_config_result(res)
|
36
|
+
new(
|
37
|
+
res.name,
|
38
|
+
value: res.gate_value,
|
39
|
+
rule_id: res.rule_id,
|
40
|
+
group_name: res.group_name,
|
41
|
+
id_type: res.id_type,
|
42
|
+
evaluation_details: res.evaluation_details,
|
43
|
+
target_app_ids: res.target_app_ids
|
44
|
+
)
|
45
|
+
end
|
46
|
+
end
|
data/lib/hash_utils.rb
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'json'
|
2
|
+
module Statsig
|
3
|
+
class HashUtils
|
4
|
+
def self.djb2(input_str)
|
5
|
+
hash = 0
|
6
|
+
input_str.each_char.each do |c|
|
7
|
+
hash = (hash << 5) - hash + c.ord
|
8
|
+
hash &= hash
|
9
|
+
end
|
10
|
+
hash &= 0xFFFFFFFF # Convert to unsigned 32-bit integer
|
11
|
+
return hash.to_s
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.djb2ForHash(input_hash)
|
15
|
+
return djb2(input_hash.to_json)
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.sha256(input_str)
|
19
|
+
return Digest::SHA256.base64digest(input_str)
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.sortHash(input_hash)
|
23
|
+
dictionary = input_hash.clone.sort_by { |key| key }.to_h;
|
24
|
+
input_hash.each do |key, value|
|
25
|
+
if value.is_a?(Hash)
|
26
|
+
dictionary[key] = self.sortHash(value)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
return dictionary
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
data/lib/id_list.rb
CHANGED
data/lib/layer.rb
CHANGED
@@ -1,6 +1,3 @@
|
|
1
|
-
# typed: false
|
2
|
-
|
3
|
-
require 'sorbet-runtime'
|
4
1
|
##
|
5
2
|
# Contains the current values from Statsig.
|
6
3
|
# Will contain layer default values for all shared parameters in that layer.
|
@@ -9,23 +6,22 @@ require 'sorbet-runtime'
|
|
9
6
|
#
|
10
7
|
# Layers Documentation: https://docs.statsig.com/layers
|
11
8
|
class Layer
|
12
|
-
extend T::Sig
|
13
9
|
|
14
|
-
sig { returns(String) }
|
15
10
|
attr_accessor :name
|
16
11
|
|
17
|
-
sig { returns(String) }
|
18
12
|
attr_accessor :rule_id
|
19
13
|
|
20
|
-
|
21
|
-
|
14
|
+
attr_accessor :group_name
|
15
|
+
|
16
|
+
def initialize(name, value = {}, rule_id = '', group_name = nil, allocated_experiment = nil, exposure_log_func = nil)
|
22
17
|
@name = name
|
23
|
-
@value = value
|
18
|
+
@value = value || {}
|
24
19
|
@rule_id = rule_id
|
20
|
+
@group_name = group_name
|
21
|
+
@allocated_experiment = allocated_experiment
|
25
22
|
@exposure_log_func = exposure_log_func
|
26
23
|
end
|
27
24
|
|
28
|
-
sig { params(index: String, default_value: T.untyped).returns(T.untyped) }
|
29
25
|
##
|
30
26
|
# Get the value for the given key (index), falling back to the default_value if it cannot be found.
|
31
27
|
#
|
@@ -41,7 +37,6 @@ class Layer
|
|
41
37
|
@value[index]
|
42
38
|
end
|
43
39
|
|
44
|
-
sig { params(index: String, default_value: T.untyped).returns(T.untyped) }
|
45
40
|
##
|
46
41
|
# Get the value for the given key (index), falling back to the default_value if it cannot be found
|
47
42
|
# or is found to have a different type from the default_value.
|
@@ -58,4 +53,4 @@ class Layer
|
|
58
53
|
|
59
54
|
@value[index]
|
60
55
|
end
|
61
|
-
end
|
56
|
+
end
|
data/lib/network.rb
CHANGED
@@ -1,12 +1,11 @@
|
|
1
|
-
# typed: true
|
2
|
-
|
3
1
|
require 'http'
|
4
2
|
require 'json'
|
5
3
|
require 'securerandom'
|
6
|
-
|
4
|
+
|
7
5
|
require 'uri_helper'
|
6
|
+
require 'connection_pool'
|
8
7
|
|
9
|
-
|
8
|
+
RETRY_CODES = [408, 500, 502, 503, 504, 522, 524, 599].freeze
|
10
9
|
|
11
10
|
module Statsig
|
12
11
|
class NetworkError < StandardError
|
@@ -19,9 +18,6 @@ module Statsig
|
|
19
18
|
end
|
20
19
|
|
21
20
|
class Network
|
22
|
-
extend T::Sig
|
23
|
-
|
24
|
-
sig { params(server_secret: String, options: StatsigOptions, backoff_mult: Integer).void }
|
25
21
|
|
26
22
|
def initialize(server_secret, options, backoff_mult = 10)
|
27
23
|
super()
|
@@ -33,29 +29,44 @@ module Statsig
|
|
33
29
|
@post_logs_retry_backoff = options.post_logs_retry_backoff
|
34
30
|
@post_logs_retry_limit = options.post_logs_retry_limit
|
35
31
|
@session_id = SecureRandom.uuid
|
32
|
+
@connection_pool = ConnectionPool.new(size: 3) do
|
33
|
+
meta = Statsig.get_statsig_metadata
|
34
|
+
client = HTTP.use(:auto_inflate).headers(
|
35
|
+
{
|
36
|
+
'STATSIG-API-KEY' => @server_secret,
|
37
|
+
'STATSIG-SERVER-SESSION-ID' => @session_id,
|
38
|
+
'Content-Type' => 'application/json; charset=UTF-8',
|
39
|
+
'STATSIG-SDK-TYPE' => meta['sdkType'],
|
40
|
+
'STATSIG-SDK-VERSION' => meta['sdkVersion'],
|
41
|
+
'STATSIG-SDK-LANGUAGE-VERSION' => meta['languageVersion'],
|
42
|
+
'Accept-Encoding' => 'gzip'
|
43
|
+
}
|
44
|
+
).accept(:json)
|
45
|
+
if @timeout
|
46
|
+
client = client.timeout(@timeout)
|
47
|
+
end
|
48
|
+
|
49
|
+
client
|
50
|
+
end
|
36
51
|
end
|
37
52
|
|
38
|
-
|
39
|
-
|
53
|
+
def download_config_specs(since_time)
|
54
|
+
get("download_config_specs/#{@server_secret}.json?sinceTime=#{since_time}")
|
55
|
+
end
|
40
56
|
|
41
|
-
def
|
57
|
+
def get(endpoint, retries = 0, backoff = 1)
|
58
|
+
request(:GET, endpoint, nil, retries, backoff)
|
59
|
+
end
|
60
|
+
|
61
|
+
def post(endpoint, body, retries = 0, backoff = 1)
|
62
|
+
request(:POST, endpoint, body, retries, backoff)
|
63
|
+
end
|
64
|
+
|
65
|
+
def request(method, endpoint, body, retries = 0, backoff = 1)
|
42
66
|
if @local_mode
|
43
67
|
return nil, nil
|
44
68
|
end
|
45
69
|
|
46
|
-
meta = Statsig.get_statsig_metadata
|
47
|
-
http = HTTP.headers(
|
48
|
-
{
|
49
|
-
"STATSIG-API-KEY" => @server_secret,
|
50
|
-
"STATSIG-CLIENT-TIME" => (Time.now.to_f * 1000).to_i.to_s,
|
51
|
-
"STATSIG-SERVER-SESSION-ID" => @session_id,
|
52
|
-
"Content-Type" => "application/json; charset=UTF-8",
|
53
|
-
"STATSIG-SDK-TYPE" => meta['sdkType'],
|
54
|
-
"STATSIG-SDK-VERSION" => meta['sdkVersion'],
|
55
|
-
}).accept(:json)
|
56
|
-
if @timeout
|
57
|
-
http = http.timeout(@timeout)
|
58
|
-
end
|
59
70
|
backoff_adjusted = backoff > 10 ? backoff += Random.rand(10) : backoff # to deter overlap
|
60
71
|
if @post_logs_retry_backoff
|
61
72
|
if @post_logs_retry_backoff.is_a? Integer
|
@@ -66,48 +77,39 @@ module Statsig
|
|
66
77
|
end
|
67
78
|
url = URIHelper.build_url(endpoint)
|
68
79
|
begin
|
69
|
-
res =
|
80
|
+
res = @connection_pool.with do |conn|
|
81
|
+
request = conn.headers('STATSIG-CLIENT-TIME' => (Time.now.to_f * 1000).to_i.to_s)
|
82
|
+
case method
|
83
|
+
when :GET
|
84
|
+
request.get(url)
|
85
|
+
when :POST
|
86
|
+
request.post(url, body: body)
|
87
|
+
end
|
88
|
+
end
|
70
89
|
rescue StandardError => e
|
71
90
|
## network error retry
|
72
|
-
return nil, e unless retries
|
91
|
+
return nil, e unless retries.positive?
|
92
|
+
|
73
93
|
sleep backoff_adjusted
|
74
|
-
return
|
94
|
+
return request(method, endpoint, body, retries - 1, backoff * @backoff_multiplier)
|
75
95
|
end
|
76
96
|
return res, nil if res.status.success?
|
77
|
-
return nil, NetworkError.new("Got an exception when making request to #{url}: #{res.to_s}", res.status.to_i) unless retries > 0 && $retry_codes.include?(res.code)
|
78
|
-
## status code retry
|
79
|
-
sleep backoff_adjusted
|
80
|
-
post_helper(endpoint, body, retries - 1, backoff * @backoff_multiplier)
|
81
|
-
end
|
82
97
|
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
response, _ = post_helper('check_gate', request_body)
|
87
|
-
return JSON.parse(response.body) unless response.nil?
|
88
|
-
false
|
89
|
-
rescue
|
90
|
-
return false
|
98
|
+
unless retries.positive? && RETRY_CODES.include?(res.code)
|
99
|
+
return res, NetworkError.new("Got an exception when making request to #{url}: #{res.to_s}",
|
100
|
+
res.status.to_i)
|
91
101
|
end
|
92
|
-
end
|
93
102
|
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
response, _ = post_helper('get_config', request_body)
|
98
|
-
return JSON.parse(response.body) unless response.nil?
|
99
|
-
nil
|
100
|
-
rescue
|
101
|
-
return nil
|
102
|
-
end
|
103
|
+
## status code retry
|
104
|
+
sleep backoff_adjusted
|
105
|
+
request(method, endpoint, body, retries - 1, backoff * @backoff_multiplier)
|
103
106
|
end
|
104
107
|
|
105
108
|
def post_logs(events)
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
end
|
109
|
+
json_body = JSON.generate({ events: events, statsigMetadata: Statsig.get_statsig_metadata })
|
110
|
+
post('log_event', json_body, @post_logs_retry_limit)
|
111
|
+
rescue StandardError
|
112
|
+
|
111
113
|
end
|
112
114
|
end
|
113
|
-
end
|
115
|
+
end
|