statsig 1.25.2 → 1.26.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/client_initialize_helpers.rb +29 -3
- data/lib/diagnostics.rb +31 -23
- data/lib/error_boundary.rb +29 -43
- data/lib/evaluator.rb +6 -5
- data/lib/hash_utils.rb +17 -0
- data/lib/network.rb +54 -41
- data/lib/spec_store.rb +76 -49
- data/lib/statsig.rb +6 -4
- data/lib/statsig_driver.rb +89 -59
- data/lib/statsig_errors.rb +1 -0
- data/lib/statsig_logger.rb +21 -15
- data/lib/ua_parser.rb +1 -0
- metadata +51 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 42f5328314de8727a2baaf89da54999627a5505ad95cd5ab816877741e02dd55
|
4
|
+
data.tar.gz: f6af1d20f540ceae4e24c70bdc702fbd9db3acc38776b71ecc50ded6ef40f67d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8c4375a325bf7dd1afaa584951b31eca2fd19422301b72397729fdca89e1ef8ae9b7a979aff562889cf49fc98fcd7db3edfc022ca66eb063b2680645c90df2db
|
7
|
+
data.tar.gz: ab23702707825b1ed5ada0df1982d765b97b75e8d813aeeaa9f24ae31974bb0cafa58249e82cc041eaa845bea9ee7639189c8778f8e21e0908b7ec3174d41661
|
@@ -1,4 +1,7 @@
|
|
1
1
|
# typed: true
|
2
|
+
|
3
|
+
require_relative 'hash_utils'
|
4
|
+
|
2
5
|
$empty_eval_result = {
|
3
6
|
:gate_value => false,
|
4
7
|
:json_value => {},
|
@@ -9,10 +12,12 @@ $empty_eval_result = {
|
|
9
12
|
|
10
13
|
module ClientInitializeHelpers
|
11
14
|
class ResponseFormatter
|
12
|
-
def initialize(evaluator, user)
|
15
|
+
def initialize(evaluator, user, hash, client_sdk_key)
|
13
16
|
@evaluator = evaluator
|
14
17
|
@user = user
|
15
18
|
@specs = evaluator.spec_store.get_raw_specs
|
19
|
+
@hash = hash
|
20
|
+
@client_sdk_key = client_sdk_key
|
16
21
|
end
|
17
22
|
|
18
23
|
def get_responses(key)
|
@@ -24,6 +29,13 @@ module ClientInitializeHelpers
|
|
24
29
|
private
|
25
30
|
|
26
31
|
def to_response(config_name, config_spec)
|
32
|
+
target_app_id = @evaluator.spec_store.get_app_id_for_sdk_key(@client_sdk_key)
|
33
|
+
config_target_apps = config_spec['targetAppIDs']
|
34
|
+
|
35
|
+
unless target_app_id.nil? || config_target_apps.nil? || config_target_apps.include?(target_app_id)
|
36
|
+
return nil
|
37
|
+
end
|
38
|
+
|
27
39
|
eval_result = @evaluator.eval_spec(@user, config_spec)
|
28
40
|
if eval_result.nil?
|
29
41
|
return nil
|
@@ -33,6 +45,8 @@ module ClientInitializeHelpers
|
|
33
45
|
:gate_value => eval_result.gate_value,
|
34
46
|
:json_value => eval_result.json_value,
|
35
47
|
:rule_id => eval_result.rule_id,
|
48
|
+
:group_name => eval_result.group_name,
|
49
|
+
:id_type => eval_result.id_type,
|
36
50
|
:config_delegate => eval_result.config_delegate,
|
37
51
|
:is_experiment_group => eval_result.is_experiment_group,
|
38
52
|
:secondary_exposures => eval_result.secondary_exposures,
|
@@ -52,10 +66,14 @@ module ClientInitializeHelpers
|
|
52
66
|
end
|
53
67
|
|
54
68
|
result['value'] = safe_eval_result[:gate_value]
|
69
|
+
result["group_name"] = safe_eval_result[:group_name]
|
70
|
+
result["id_type"] = safe_eval_result[:id_type]
|
55
71
|
when 'dynamic_config'
|
56
72
|
id_type = config_spec['idType']
|
57
73
|
result['value'] = safe_eval_result[:json_value]
|
58
74
|
result["group"] = safe_eval_result[:rule_id]
|
75
|
+
result["group_name"] = safe_eval_result[:group_name]
|
76
|
+
result["id_type"] = safe_eval_result[:id_type]
|
59
77
|
result["is_device_based"] = id_type.is_a?(String) && id_type.downcase == 'stableid'
|
60
78
|
else
|
61
79
|
return nil
|
@@ -67,6 +85,7 @@ module ClientInitializeHelpers
|
|
67
85
|
|
68
86
|
if entity_type == 'layer'
|
69
87
|
populate_layer_fields(config_spec, safe_eval_result, result)
|
88
|
+
result.delete('id_type') # not exposed for layer configs in /initialize
|
70
89
|
end
|
71
90
|
|
72
91
|
hashed_name = hash_name(config_name)
|
@@ -126,7 +145,14 @@ module ClientInitializeHelpers
|
|
126
145
|
end
|
127
146
|
|
128
147
|
def hash_name(name)
|
129
|
-
|
148
|
+
case @hash
|
149
|
+
when 'none'
|
150
|
+
return name
|
151
|
+
when 'sha256'
|
152
|
+
return Statsig::HashUtils.sha256(name)
|
153
|
+
when 'djb2'
|
154
|
+
return Statsig::HashUtils.djb2(name)
|
155
|
+
end
|
130
156
|
end
|
131
157
|
end
|
132
|
-
end
|
158
|
+
end
|
data/lib/diagnostics.rb
CHANGED
@@ -22,33 +22,37 @@ module Statsig
|
|
22
22
|
key: String,
|
23
23
|
action: String,
|
24
24
|
step: T.any(String, NilClass),
|
25
|
-
|
26
|
-
metadata: T.any(T::Hash[Symbol, T.untyped], NilClass)
|
25
|
+
tags: T::Hash[Symbol, T.untyped]
|
27
26
|
).void
|
28
27
|
end
|
29
28
|
|
30
|
-
def mark(key, action, step
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
29
|
+
def mark(key, action, step, tags)
|
30
|
+
marker = {
|
31
|
+
key: key,
|
32
|
+
action: action,
|
33
|
+
timestamp: (Time.now.to_f * 1000).to_i
|
34
|
+
}
|
35
|
+
if !step.nil?
|
36
|
+
marker[:step] = step
|
37
|
+
end
|
38
|
+
tags.each do |key, val|
|
39
|
+
unless val.nil?
|
40
|
+
marker[key] = val
|
41
|
+
end
|
42
|
+
end
|
43
|
+
@markers.push(marker)
|
39
44
|
end
|
40
45
|
|
41
46
|
sig do
|
42
47
|
params(
|
43
48
|
key: String,
|
44
49
|
step: T.any(String, NilClass),
|
45
|
-
|
46
|
-
metadata: T.any(T::Hash[Symbol, T.untyped], NilClass)
|
50
|
+
tags: T::Hash[Symbol, T.untyped]
|
47
51
|
).returns(Tracker)
|
48
52
|
end
|
49
|
-
def track(key, step = nil,
|
50
|
-
tracker = Tracker.new(self, key, step,
|
51
|
-
tracker.start(
|
53
|
+
def track(key, step = nil, tags = {})
|
54
|
+
tracker = Tracker.new(self, key, step, tags)
|
55
|
+
tracker.start(**tags)
|
52
56
|
tracker
|
53
57
|
end
|
54
58
|
|
@@ -65,6 +69,10 @@ module Statsig
|
|
65
69
|
@markers.clear
|
66
70
|
end
|
67
71
|
|
72
|
+
def self.sample(rate)
|
73
|
+
rand(rate).zero?
|
74
|
+
end
|
75
|
+
|
68
76
|
class Context
|
69
77
|
INITIALIZE = 'initialize'.freeze
|
70
78
|
CONFIG_SYNC = 'config_sync'.freeze
|
@@ -81,22 +89,22 @@ module Statsig
|
|
81
89
|
diagnostics: Diagnostics,
|
82
90
|
key: String,
|
83
91
|
step: T.any(String, NilClass),
|
84
|
-
|
92
|
+
tags: T::Hash[Symbol, T.untyped]
|
85
93
|
).void
|
86
94
|
end
|
87
|
-
def initialize(diagnostics, key, step,
|
95
|
+
def initialize(diagnostics, key, step, tags = {})
|
88
96
|
@diagnostics = diagnostics
|
89
97
|
@key = key
|
90
98
|
@step = step
|
91
|
-
@
|
99
|
+
@tags = tags
|
92
100
|
end
|
93
101
|
|
94
|
-
def start(
|
95
|
-
@diagnostics.mark(@key, 'start', @step,
|
102
|
+
def start(**tags)
|
103
|
+
@diagnostics.mark(@key, 'start', @step, tags.nil? ? {} : tags.merge(@tags))
|
96
104
|
end
|
97
105
|
|
98
|
-
def end(
|
99
|
-
@diagnostics.mark(@key, 'end', @step,
|
106
|
+
def end(**tags)
|
107
|
+
@diagnostics.mark(@key, 'end', @step, tags.nil? ? {} : tags.merge(@tags))
|
100
108
|
end
|
101
109
|
end
|
102
110
|
end
|
data/lib/error_boundary.rb
CHANGED
@@ -9,70 +9,56 @@ module Statsig
|
|
9
9
|
class ErrorBoundary
|
10
10
|
extend T::Sig
|
11
11
|
|
12
|
-
sig { returns(T.any(StatsigLogger, NilClass)) }
|
13
|
-
attr_accessor :logger
|
14
|
-
|
15
12
|
sig { params(sdk_key: String).void }
|
16
13
|
def initialize(sdk_key)
|
17
14
|
@sdk_key = sdk_key
|
18
15
|
@seen = Set.new
|
19
16
|
end
|
20
17
|
|
21
|
-
def sample_diagnostics
|
22
|
-
rand(10_000).zero?
|
23
|
-
end
|
24
|
-
|
25
18
|
def capture(task:, recover: -> {}, caller: nil)
|
26
|
-
if !caller.nil? && Diagnostics::API_CALL_KEYS.include?(caller) && sample_diagnostics
|
27
|
-
diagnostics = Diagnostics.new('api_call')
|
28
|
-
tracker = diagnostics.track(caller)
|
29
|
-
end
|
30
19
|
begin
|
31
20
|
res = task.call
|
32
|
-
tracker&.end(true)
|
33
21
|
rescue StandardError => e
|
34
|
-
|
35
|
-
if e.is_a?(Statsig::UninitializedError) or e.is_a?(Statsig::ValueError)
|
22
|
+
if e.is_a?(Statsig::UninitializedError) || e.is_a?(Statsig::ValueError)
|
36
23
|
raise e
|
37
24
|
end
|
25
|
+
|
38
26
|
puts '[Statsig]: An unexpected exception occurred.'
|
39
|
-
log_exception(e)
|
27
|
+
log_exception(e, tag: caller)
|
40
28
|
res = recover.call
|
41
29
|
end
|
42
|
-
@logger&.log_diagnostics_event(diagnostics)
|
43
30
|
return res
|
44
31
|
end
|
45
32
|
|
46
33
|
private
|
47
34
|
|
48
|
-
def log_exception(exception)
|
49
|
-
|
50
|
-
|
51
|
-
if @seen.include?(name)
|
52
|
-
return
|
53
|
-
end
|
54
|
-
|
55
|
-
@seen << name
|
56
|
-
meta = Statsig.get_statsig_metadata
|
57
|
-
http = HTTP.headers(
|
58
|
-
{
|
59
|
-
'STATSIG-API-KEY' => @sdk_key,
|
60
|
-
'STATSIG-SDK-TYPE' => meta['sdkType'],
|
61
|
-
'STATSIG-SDK-VERSION' => meta['sdkVersion'],
|
62
|
-
'Content-Type' => 'application/json; charset=UTF-8'
|
63
|
-
}).accept(:json)
|
64
|
-
body = {
|
65
|
-
'exception' => name,
|
66
|
-
'info' => {
|
67
|
-
'trace' => exception.backtrace.to_s,
|
68
|
-
'message' => exception.message
|
69
|
-
}.to_s,
|
70
|
-
'statsigMetadata' => meta
|
71
|
-
}
|
72
|
-
http.post($endpoint, body: JSON.generate(body))
|
73
|
-
rescue
|
35
|
+
def log_exception(exception, tag: nil)
|
36
|
+
name = exception.class.name
|
37
|
+
if @seen.include?(name)
|
74
38
|
return
|
75
39
|
end
|
40
|
+
|
41
|
+
@seen << name
|
42
|
+
meta = Statsig.get_statsig_metadata
|
43
|
+
http = HTTP.headers(
|
44
|
+
{
|
45
|
+
'STATSIG-API-KEY' => @sdk_key,
|
46
|
+
'STATSIG-SDK-TYPE' => meta['sdkType'],
|
47
|
+
'STATSIG-SDK-VERSION' => meta['sdkVersion'],
|
48
|
+
'Content-Type' => 'application/json; charset=UTF-8'
|
49
|
+
}).accept(:json)
|
50
|
+
body = {
|
51
|
+
'exception' => name,
|
52
|
+
'info' => {
|
53
|
+
'trace' => exception.backtrace.to_s,
|
54
|
+
'message' => exception.message
|
55
|
+
}.to_s,
|
56
|
+
'statsigMetadata' => meta,
|
57
|
+
'tag' => tag
|
58
|
+
}
|
59
|
+
http.post($endpoint, body: JSON.generate(body))
|
60
|
+
rescue StandardError
|
61
|
+
return
|
76
62
|
end
|
77
63
|
end
|
78
|
-
end
|
64
|
+
end
|
data/lib/evaluator.rb
CHANGED
@@ -17,8 +17,8 @@ module Statsig
|
|
17
17
|
class Evaluator
|
18
18
|
attr_accessor :spec_store
|
19
19
|
|
20
|
-
def initialize(network, options, error_callback, diagnostics)
|
21
|
-
@spec_store = Statsig::SpecStore.new(network, options, error_callback, diagnostics)
|
20
|
+
def initialize(network, options, error_callback, diagnostics, error_boundary, logger)
|
21
|
+
@spec_store = Statsig::SpecStore.new(network, options, error_callback, diagnostics, error_boundary, logger)
|
22
22
|
UAParser.initialize_async
|
23
23
|
CountryLookup.initialize_async
|
24
24
|
|
@@ -98,12 +98,12 @@ module Statsig
|
|
98
98
|
eval_spec(user, @spec_store.get_layer(layer_name))
|
99
99
|
end
|
100
100
|
|
101
|
-
def get_client_initialize_response(user)
|
101
|
+
def get_client_initialize_response(user, hash, client_sdk_key)
|
102
102
|
if @spec_store.is_ready_for_checks == false
|
103
103
|
return nil
|
104
104
|
end
|
105
105
|
|
106
|
-
formatter = ClientInitializeHelpers::ResponseFormatter.new(self, user)
|
106
|
+
formatter = ClientInitializeHelpers::ResponseFormatter.new(self, user, hash, client_sdk_key)
|
107
107
|
|
108
108
|
evaluated_keys = {}
|
109
109
|
if user.user_id.nil? == false
|
@@ -123,6 +123,7 @@ module Statsig
|
|
123
123
|
"generator" => "statsig-ruby-sdk",
|
124
124
|
"evaluated_keys" => evaluated_keys,
|
125
125
|
"time" => 0,
|
126
|
+
"hash_used" => hash
|
126
127
|
}
|
127
128
|
end
|
128
129
|
|
@@ -199,7 +200,7 @@ module Statsig
|
|
199
200
|
@spec_store.initial_config_sync_time,
|
200
201
|
@spec_store.init_reason
|
201
202
|
),
|
202
|
-
group_name:
|
203
|
+
group_name: 'default',
|
203
204
|
id_type: config['idType']
|
204
205
|
)
|
205
206
|
end
|
data/lib/hash_utils.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
module Statsig
|
2
|
+
class HashUtils
|
3
|
+
def self.djb2(input_str)
|
4
|
+
hash = 0
|
5
|
+
input_str.each_char.each do |c|
|
6
|
+
hash = (hash << 5) - hash + c.ord
|
7
|
+
hash &= hash
|
8
|
+
end
|
9
|
+
hash &= 0xFFFFFFFF # Convert to unsigned 32-bit integer
|
10
|
+
return hash.to_s
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.sha256(input_str)
|
14
|
+
return Digest::SHA256.base64digest(input_str)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
data/lib/network.rb
CHANGED
@@ -5,8 +5,9 @@ require 'json'
|
|
5
5
|
require 'securerandom'
|
6
6
|
require 'sorbet-runtime'
|
7
7
|
require 'uri_helper'
|
8
|
+
require 'connection_pool'
|
8
9
|
|
9
|
-
|
10
|
+
RETRY_CODES = [408, 500, 502, 503, 504, 522, 524, 599].freeze
|
10
11
|
|
11
12
|
module Statsig
|
12
13
|
class NetworkError < StandardError
|
@@ -33,29 +34,36 @@ module Statsig
|
|
33
34
|
@post_logs_retry_backoff = options.post_logs_retry_backoff
|
34
35
|
@post_logs_retry_limit = options.post_logs_retry_limit
|
35
36
|
@session_id = SecureRandom.uuid
|
37
|
+
@connection_pool = ConnectionPool.new(size: 3) do
|
38
|
+
meta = Statsig.get_statsig_metadata
|
39
|
+
client = HTTP.headers(
|
40
|
+
{
|
41
|
+
'STATSIG-API-KEY' => @server_secret,
|
42
|
+
'STATSIG-CLIENT-TIME' => (Time.now.to_f * 1000).to_i.to_s,
|
43
|
+
'STATSIG-SERVER-SESSION-ID' => @session_id,
|
44
|
+
'Content-Type' => 'application/json; charset=UTF-8',
|
45
|
+
'STATSIG-SDK-TYPE' => meta['sdkType'],
|
46
|
+
'STATSIG-SDK-VERSION' => meta['sdkVersion']
|
47
|
+
}
|
48
|
+
).accept(:json)
|
49
|
+
if @timeout
|
50
|
+
client = client.timeout(@timeout)
|
51
|
+
end
|
52
|
+
|
53
|
+
client
|
54
|
+
end
|
36
55
|
end
|
37
56
|
|
38
|
-
sig
|
39
|
-
|
57
|
+
sig do
|
58
|
+
params(endpoint: String, body: String, retries: Integer, backoff: Integer)
|
59
|
+
.returns([T.any(HTTP::Response, NilClass), T.any(StandardError, NilClass)])
|
60
|
+
end
|
40
61
|
|
41
62
|
def post_helper(endpoint, body, retries = 0, backoff = 1)
|
42
63
|
if @local_mode
|
43
64
|
return nil, nil
|
44
65
|
end
|
45
66
|
|
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
67
|
backoff_adjusted = backoff > 10 ? backoff += Random.rand(10) : backoff # to deter overlap
|
60
68
|
if @post_logs_retry_backoff
|
61
69
|
if @post_logs_retry_backoff.is_a? Integer
|
@@ -66,48 +74,53 @@ module Statsig
|
|
66
74
|
end
|
67
75
|
url = URIHelper.build_url(endpoint)
|
68
76
|
begin
|
69
|
-
res =
|
77
|
+
res = @connection_pool.with do |conn|
|
78
|
+
conn.headers('STATSIG-CLIENT-TIME' => (Time.now.to_f * 1000).to_i.to_s).post(url, body: body)
|
79
|
+
end
|
70
80
|
rescue StandardError => e
|
71
81
|
## network error retry
|
72
|
-
return nil, e unless retries
|
82
|
+
return nil, e unless retries.positive?
|
83
|
+
|
73
84
|
sleep backoff_adjusted
|
74
85
|
return post_helper(endpoint, body, retries - 1, backoff * @backoff_multiplier)
|
75
86
|
end
|
76
87
|
return res, nil if res.status.success?
|
77
|
-
|
88
|
+
|
89
|
+
unless retries.positive? && RETRY_CODES.include?(res.code)
|
90
|
+
return res, NetworkError.new("Got an exception when making request to #{url}: #{res.to_s}",
|
91
|
+
res.status.to_i)
|
92
|
+
end
|
93
|
+
|
78
94
|
## status code retry
|
79
95
|
sleep backoff_adjusted
|
80
96
|
post_helper(endpoint, body, retries - 1, backoff * @backoff_multiplier)
|
81
97
|
end
|
82
98
|
|
83
99
|
def check_gate(user, gate_name)
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
end
|
100
|
+
request_body = JSON.generate({ 'user' => user&.serialize(false), 'gateName' => gate_name })
|
101
|
+
response, = post_helper('check_gate', request_body)
|
102
|
+
return JSON.parse(response.body) unless response.nil?
|
103
|
+
|
104
|
+
false
|
105
|
+
rescue StandardError
|
106
|
+
false
|
92
107
|
end
|
93
108
|
|
94
109
|
def get_config(user, dynamic_config_name)
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
end
|
110
|
+
request_body = JSON.generate({ 'user' => user&.serialize(false), 'configName' => dynamic_config_name })
|
111
|
+
response, = post_helper('get_config', request_body)
|
112
|
+
return JSON.parse(response.body) unless response.nil?
|
113
|
+
|
114
|
+
nil
|
115
|
+
rescue StandardError
|
116
|
+
nil
|
103
117
|
end
|
104
118
|
|
105
119
|
def post_logs(events)
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
end
|
120
|
+
json_body = JSON.generate({ 'events' => events, 'statsigMetadata' => Statsig.get_statsig_metadata })
|
121
|
+
post_helper('log_event', json_body, @post_logs_retry_limit)
|
122
|
+
rescue StandardError
|
123
|
+
|
111
124
|
end
|
112
125
|
end
|
113
|
-
end
|
126
|
+
end
|
data/lib/spec_store.rb
CHANGED
@@ -12,7 +12,7 @@ module Statsig
|
|
12
12
|
attr_accessor :initial_config_sync_time
|
13
13
|
attr_accessor :init_reason
|
14
14
|
|
15
|
-
def initialize(network, options, error_callback, diagnostics)
|
15
|
+
def initialize(network, options, error_callback, diagnostics, error_boundary, logger)
|
16
16
|
@init_reason = EvaluationReason::UNINITIALIZED
|
17
17
|
@network = network
|
18
18
|
@options = options
|
@@ -27,9 +27,12 @@ module Statsig
|
|
27
27
|
:configs => {},
|
28
28
|
:layers => {},
|
29
29
|
:id_lists => {},
|
30
|
-
:experiment_to_layer => {}
|
30
|
+
:experiment_to_layer => {},
|
31
|
+
:sdk_keys_to_app_ids => {}
|
31
32
|
}
|
32
33
|
@diagnostics = diagnostics
|
34
|
+
@error_boundary = error_boundary
|
35
|
+
@logger = logger
|
33
36
|
|
34
37
|
@id_list_thread_pool = Concurrent::FixedThreadPool.new(
|
35
38
|
options.idlist_threadpool_size,
|
@@ -39,18 +42,19 @@ module Statsig
|
|
39
42
|
)
|
40
43
|
|
41
44
|
unless @options.bootstrap_values.nil?
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
45
|
+
if !@options.data_store.nil?
|
46
|
+
puts 'data_store gets priority over bootstrap_values. bootstrap_values will be ignored'
|
47
|
+
else
|
48
|
+
tracker = @diagnostics.track('bootstrap', 'process')
|
49
|
+
begin
|
47
50
|
if process_specs(options.bootstrap_values)
|
48
51
|
@init_reason = EvaluationReason::BOOTSTRAP
|
49
52
|
end
|
50
|
-
|
53
|
+
rescue
|
54
|
+
puts 'the provided bootstrapValues is not a valid JSON string'
|
55
|
+
ensure
|
56
|
+
tracker.end(success: @init_reason == EvaluationReason::BOOTSTRAP)
|
51
57
|
end
|
52
|
-
rescue
|
53
|
-
puts 'the provided bootstrapValues is not a valid JSON string'
|
54
58
|
end
|
55
59
|
end
|
56
60
|
|
@@ -119,15 +123,27 @@ module Statsig
|
|
119
123
|
@specs[:id_lists][list_name]
|
120
124
|
end
|
121
125
|
|
126
|
+
def has_sdk_key?(sdk_key)
|
127
|
+
@specs[:sdk_keys_to_app_ids].key?(sdk_key)
|
128
|
+
end
|
129
|
+
|
130
|
+
def get_app_id_for_sdk_key(sdk_key)
|
131
|
+
if sdk_key.nil?
|
132
|
+
return nil
|
133
|
+
end
|
134
|
+
return nil unless has_sdk_key?(sdk_key)
|
135
|
+
@specs[:sdk_keys_to_app_ids][sdk_key]
|
136
|
+
end
|
137
|
+
|
122
138
|
def get_raw_specs
|
123
139
|
@specs
|
124
140
|
end
|
125
141
|
|
126
142
|
def maybe_restart_background_threads
|
127
|
-
if @config_sync_thread.nil?
|
143
|
+
if @config_sync_thread.nil? || !@config_sync_thread.alive?
|
128
144
|
@config_sync_thread = sync_config_specs
|
129
145
|
end
|
130
|
-
if @id_lists_sync_thread.nil?
|
146
|
+
if @id_lists_sync_thread.nil? || !@id_lists_sync_thread.alive?
|
131
147
|
@id_lists_sync_thread = sync_id_lists
|
132
148
|
end
|
133
149
|
end
|
@@ -137,16 +153,16 @@ module Statsig
|
|
137
153
|
def load_config_specs_from_storage_adapter
|
138
154
|
tracker = @diagnostics.track('data_store_config_specs', 'fetch')
|
139
155
|
cached_values = @options.data_store.get(Interfaces::IDataStore::CONFIG_SPECS_KEY)
|
140
|
-
tracker.end(true)
|
156
|
+
tracker.end(success: true)
|
141
157
|
return if cached_values.nil?
|
142
158
|
|
143
159
|
tracker = @diagnostics.track('data_store_config_specs', 'process')
|
144
160
|
process_specs(cached_values, from_adapter: true)
|
145
161
|
@init_reason = EvaluationReason::DATA_ADAPTER
|
146
|
-
tracker.end(true)
|
162
|
+
tracker.end(success: true)
|
147
163
|
rescue StandardError
|
148
164
|
# Fallback to network
|
149
|
-
tracker.end(false)
|
165
|
+
tracker.end(success: false)
|
150
166
|
download_config_specs
|
151
167
|
end
|
152
168
|
|
@@ -159,29 +175,35 @@ module Statsig
|
|
159
175
|
|
160
176
|
def sync_config_specs
|
161
177
|
Thread.new do
|
162
|
-
@
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
178
|
+
@error_boundary.capture(task: lambda {
|
179
|
+
loop do
|
180
|
+
@diagnostics = Diagnostics.new('config_sync')
|
181
|
+
sleep @options.rulesets_sync_interval
|
182
|
+
if @options.data_store&.should_be_used_for_querying_updates(Interfaces::IDataStore::CONFIG_SPECS_KEY)
|
183
|
+
load_config_specs_from_storage_adapter
|
184
|
+
else
|
185
|
+
download_config_specs
|
186
|
+
end
|
187
|
+
@logger.log_diagnostics_event(@diagnostics)
|
169
188
|
end
|
170
|
-
|
189
|
+
})
|
171
190
|
end
|
172
191
|
end
|
173
192
|
|
174
193
|
def sync_id_lists
|
175
194
|
Thread.new do
|
176
|
-
@
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
195
|
+
@error_boundary.capture(task: lambda {
|
196
|
+
loop do
|
197
|
+
@diagnostics = Diagnostics.new('config_sync')
|
198
|
+
sleep @id_lists_sync_interval
|
199
|
+
if @options.data_store&.should_be_used_for_querying_updates(Interfaces::IDataStore::ID_LISTS_KEY)
|
200
|
+
get_id_lists_from_adapter
|
201
|
+
else
|
202
|
+
get_id_lists_from_network
|
203
|
+
end
|
204
|
+
@logger.log_diagnostics_event(@diagnostics)
|
183
205
|
end
|
184
|
-
|
206
|
+
})
|
185
207
|
end
|
186
208
|
end
|
187
209
|
|
@@ -195,7 +217,7 @@ module Statsig
|
|
195
217
|
if e.is_a? NetworkError
|
196
218
|
code = e.http_code
|
197
219
|
end
|
198
|
-
tracker.end(code)
|
220
|
+
tracker.end(statusCode: code, success: e.nil?, sdkRegion: response&.headers&.[]('X-Statsig-Region'))
|
199
221
|
|
200
222
|
if e.nil?
|
201
223
|
unless response.nil?
|
@@ -203,7 +225,7 @@ module Statsig
|
|
203
225
|
if process_specs(response.body.to_s)
|
204
226
|
@init_reason = EvaluationReason::NETWORK
|
205
227
|
end
|
206
|
-
tracker.end(@init_reason == EvaluationReason::NETWORK)
|
228
|
+
tracker.end(success: @init_reason == EvaluationReason::NETWORK)
|
207
229
|
|
208
230
|
@rules_updated_callback.call(response.body.to_s, @last_config_sync_time) unless response.body.nil? or @rules_updated_callback.nil?
|
209
231
|
end
|
@@ -252,6 +274,9 @@ module Statsig
|
|
252
274
|
@specs[:configs] = new_configs
|
253
275
|
@specs[:layers] = new_layers
|
254
276
|
@specs[:experiment_to_layer] = new_exp_to_layer
|
277
|
+
@specs[:sdk_keys_to_app_ids] = specs_json['sdk_keys_to_app_ids'] || {}
|
278
|
+
|
279
|
+
specs_json['diagnostics']
|
255
280
|
|
256
281
|
unless from_adapter
|
257
282
|
save_config_specs_to_storage_adapter(specs_string)
|
@@ -264,12 +289,12 @@ module Statsig
|
|
264
289
|
cached_values = @options.data_store.get(Interfaces::IDataStore::ID_LISTS_KEY)
|
265
290
|
return if cached_values.nil?
|
266
291
|
|
267
|
-
tracker.end(true)
|
292
|
+
tracker.end(success: true)
|
268
293
|
id_lists = JSON.parse(cached_values)
|
269
294
|
process_id_lists(id_lists, from_adapter: true)
|
270
295
|
rescue StandardError
|
271
296
|
# Fallback to network
|
272
|
-
tracker.end(false)
|
297
|
+
tracker.end(success: false)
|
273
298
|
get_id_lists_from_network
|
274
299
|
end
|
275
300
|
|
@@ -287,8 +312,9 @@ module Statsig
|
|
287
312
|
if e.is_a? NetworkError
|
288
313
|
code = e.http_code
|
289
314
|
end
|
290
|
-
|
291
|
-
|
315
|
+
success = e.nil? && !response.nil?
|
316
|
+
tracker.end(statusCode: code, success: success, sdkRegion: response&.headers&.[]('X-Statsig-Region'))
|
317
|
+
if !success
|
292
318
|
return
|
293
319
|
end
|
294
320
|
|
@@ -311,11 +337,11 @@ module Statsig
|
|
311
337
|
tracker = @diagnostics.track(
|
312
338
|
from_adapter ? 'data_store_id_lists' : 'get_id_list_sources',
|
313
339
|
'process',
|
314
|
-
new_id_lists.length
|
340
|
+
{ idListCount: new_id_lists.length }
|
315
341
|
)
|
316
342
|
|
317
343
|
if new_id_lists.empty?
|
318
|
-
tracker.end
|
344
|
+
tracker.end(success: true)
|
319
345
|
return
|
320
346
|
end
|
321
347
|
|
@@ -366,17 +392,17 @@ module Statsig
|
|
366
392
|
end
|
367
393
|
|
368
394
|
result = Concurrent::Promise.all?(*tasks).execute.wait(@id_lists_sync_interval)
|
369
|
-
tracker.end(result.state == :fulfilled)
|
395
|
+
tracker.end(success: result.state == :fulfilled)
|
370
396
|
end
|
371
397
|
|
372
398
|
def get_single_id_list_from_adapter(list)
|
373
|
-
tracker = @diagnostics.track('data_store_id_list', 'fetch',
|
399
|
+
tracker = @diagnostics.track('data_store_id_list', 'fetch', { url: list.url })
|
374
400
|
cached_values = @options.data_store.get("#{Interfaces::IDataStore::ID_LISTS_KEY}::#{list.name}")
|
375
|
-
tracker.end(true)
|
401
|
+
tracker.end(success: true)
|
376
402
|
content = cached_values.to_s
|
377
403
|
process_single_id_list(list, content, from_adapter: true)
|
378
404
|
rescue StandardError
|
379
|
-
tracker.end(false)
|
405
|
+
tracker.end(success: false)
|
380
406
|
nil
|
381
407
|
end
|
382
408
|
|
@@ -389,10 +415,10 @@ module Statsig
|
|
389
415
|
def download_single_id_list(list)
|
390
416
|
nil unless list.is_a? IDList
|
391
417
|
http = HTTP.headers({ 'Range' => "bytes=#{list&.size || 0}-" }).accept(:json)
|
418
|
+
tracker = @diagnostics.track('get_id_list', 'network_request', { url: list.url })
|
392
419
|
begin
|
393
|
-
tracker = @diagnostics.track('get_id_list', 'network_request', nil, { url: list.url })
|
394
420
|
res = http.get(list.url)
|
395
|
-
tracker.end(res.status.code)
|
421
|
+
tracker.end(statusCode: res.status.code, success: res.status.success?)
|
396
422
|
nil unless res.status.success?
|
397
423
|
content_length = Integer(res['content-length'])
|
398
424
|
nil if content_length.nil? || content_length <= 0
|
@@ -400,6 +426,7 @@ module Statsig
|
|
400
426
|
success = process_single_id_list(list, content, content_length)
|
401
427
|
save_single_id_list_to_adapter(list.name, content) unless success.nil? || !success
|
402
428
|
rescue
|
429
|
+
tracker.end(success: false)
|
403
430
|
nil
|
404
431
|
end
|
405
432
|
end
|
@@ -407,10 +434,10 @@ module Statsig
|
|
407
434
|
def process_single_id_list(list, content, content_length = nil, from_adapter: false)
|
408
435
|
false unless list.is_a? IDList
|
409
436
|
begin
|
410
|
-
tracker = @diagnostics.track(from_adapter ? 'data_store_id_list' : 'get_id_list', 'process',
|
437
|
+
tracker = @diagnostics.track(from_adapter ? 'data_store_id_list' : 'get_id_list', 'process', { url: list.url })
|
411
438
|
unless content.is_a?(String) && (content[0] == '-' || content[0] == '+')
|
412
439
|
@specs[:id_lists].delete(list.name)
|
413
|
-
tracker.end(false)
|
440
|
+
tracker.end(success: false)
|
414
441
|
return false
|
415
442
|
end
|
416
443
|
ids_clone = list.ids # clone the list, operate on the new list, and swap out the old list, so the operation is thread-safe
|
@@ -432,10 +459,10 @@ module Statsig
|
|
432
459
|
else
|
433
460
|
list.size + content_length
|
434
461
|
end
|
435
|
-
tracker.end(true)
|
462
|
+
tracker.end(success: true)
|
436
463
|
return true
|
437
464
|
rescue
|
438
|
-
tracker.end(false)
|
465
|
+
tracker.end(success: false)
|
439
466
|
return false
|
440
467
|
end
|
441
468
|
end
|
data/lib/statsig.rb
CHANGED
@@ -208,17 +208,19 @@ module Statsig
|
|
208
208
|
@shared_instance&.override_config(config_name, config_value)
|
209
209
|
end
|
210
210
|
|
211
|
-
sig { params(user: StatsigUser).returns(T.any(T::Hash[String, T.untyped], NilClass)) }
|
211
|
+
sig { params(user: StatsigUser, hash: String, client_sdk_key: T.any(String, NilClass)).returns(T.any(T::Hash[String, T.untyped], NilClass)) }
|
212
212
|
##
|
213
213
|
# Gets all evaluated values for the given user.
|
214
214
|
# These values can then be given to a Statsig Client SDK via bootstrapping.
|
215
215
|
#
|
216
216
|
# @param user A StatsigUser object used for the evaluation
|
217
|
+
# @param hash The type of hashing algorithm to use ('sha256', 'djb2', 'none')
|
218
|
+
# @param client_sdk_key A optional client sdk key to be used for the evaluation
|
217
219
|
#
|
218
220
|
# @note See Ruby Documentation: https://docs.statsig.com/server/rubySDK)
|
219
|
-
def self.get_client_initialize_response(user)
|
221
|
+
def self.get_client_initialize_response(user, hash: 'sha256', client_sdk_key: nil)
|
220
222
|
ensure_initialized
|
221
|
-
@shared_instance&.get_client_initialize_response(user)
|
223
|
+
@shared_instance&.get_client_initialize_response(user, hash, client_sdk_key)
|
222
224
|
end
|
223
225
|
|
224
226
|
sig { returns(T::Hash[String, String]) }
|
@@ -227,7 +229,7 @@ module Statsig
|
|
227
229
|
def self.get_statsig_metadata
|
228
230
|
{
|
229
231
|
'sdkType' => 'ruby-server',
|
230
|
-
'sdkVersion' => '1.
|
232
|
+
'sdkVersion' => '1.26.0',
|
231
233
|
}
|
232
234
|
end
|
233
235
|
|
data/lib/statsig_driver.rb
CHANGED
@@ -37,13 +37,12 @@ class StatsigDriver
|
|
37
37
|
@shutdown = false
|
38
38
|
@secret_key = secret_key
|
39
39
|
@net = Statsig::Network.new(secret_key, @options)
|
40
|
-
@logger = Statsig::StatsigLogger.new(@net, @options)
|
41
|
-
@evaluator = Statsig::Evaluator.new(@net, @options, error_callback, @diagnostics)
|
42
|
-
tracker.end(
|
40
|
+
@logger = Statsig::StatsigLogger.new(@net, @options, @err_boundary)
|
41
|
+
@evaluator = Statsig::Evaluator.new(@net, @options, error_callback, @diagnostics, @err_boundary, @logger)
|
42
|
+
tracker.end(success: true)
|
43
43
|
|
44
44
|
@logger.log_diagnostics_event(@diagnostics)
|
45
|
-
})
|
46
|
-
@err_boundary.logger = @logger
|
45
|
+
}, caller: __method__.to_s)
|
47
46
|
end
|
48
47
|
|
49
48
|
class CheckGateOptions < T::Struct
|
@@ -54,32 +53,36 @@ class StatsigDriver
|
|
54
53
|
|
55
54
|
def check_gate(user, gate_name, options = CheckGateOptions.new)
|
56
55
|
@err_boundary.capture(task: lambda {
|
57
|
-
|
56
|
+
run_with_diagnostics(task: lambda {
|
57
|
+
user = verify_inputs(user, gate_name, "gate_name")
|
58
58
|
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
59
|
+
res = @evaluator.check_gate(user, gate_name)
|
60
|
+
if res.nil?
|
61
|
+
res = Statsig::ConfigResult.new(gate_name)
|
62
|
+
end
|
63
63
|
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
64
|
+
if res == $fetch_from_server
|
65
|
+
res = check_gate_fallback(user, gate_name)
|
66
|
+
# exposure logged by the server
|
67
|
+
else
|
68
|
+
if options.log_exposure
|
69
|
+
@logger.log_gate_exposure(user, res.name, res.gate_value, res.rule_id, res.secondary_exposures, res.evaluation_details)
|
70
|
+
end
|
70
71
|
end
|
71
|
-
end
|
72
72
|
|
73
|
-
|
73
|
+
res.gate_value
|
74
|
+
}, caller: __method__.to_s)
|
74
75
|
}, recover: -> { false }, caller: __method__.to_s)
|
75
76
|
end
|
76
77
|
|
77
78
|
sig { params(user: StatsigUser, gate_name: String).void }
|
78
79
|
|
79
80
|
def manually_log_gate_exposure(user, gate_name)
|
80
|
-
|
81
|
-
|
82
|
-
|
81
|
+
@err_boundary.capture(task: lambda {
|
82
|
+
res = @evaluator.check_gate(user, gate_name)
|
83
|
+
context = { 'is_manual_exposure' => true }
|
84
|
+
@logger.log_gate_exposure(user, gate_name, res.gate_value, res.rule_id, res.secondary_exposures, res.evaluation_details, context)
|
85
|
+
})
|
83
86
|
end
|
84
87
|
|
85
88
|
class GetConfigOptions < T::Struct
|
@@ -90,8 +93,10 @@ class StatsigDriver
|
|
90
93
|
|
91
94
|
def get_config(user, dynamic_config_name, options = GetConfigOptions.new)
|
92
95
|
@err_boundary.capture(task: lambda {
|
93
|
-
|
94
|
-
|
96
|
+
run_with_diagnostics(task: lambda {
|
97
|
+
user = verify_inputs(user, dynamic_config_name, "dynamic_config_name")
|
98
|
+
get_config_impl(user, dynamic_config_name, options)
|
99
|
+
}, caller: __method__.to_s)
|
95
100
|
}, recover: -> { DynamicConfig.new(dynamic_config_name) }, caller: __method__.to_s)
|
96
101
|
end
|
97
102
|
|
@@ -103,17 +108,21 @@ class StatsigDriver
|
|
103
108
|
|
104
109
|
def get_experiment(user, experiment_name, options = GetExperimentOptions.new)
|
105
110
|
@err_boundary.capture(task: lambda {
|
106
|
-
|
107
|
-
|
111
|
+
run_with_diagnostics(task: lambda {
|
112
|
+
user = verify_inputs(user, experiment_name, "experiment_name")
|
113
|
+
get_config_impl(user, experiment_name, options)
|
114
|
+
}, caller: __method__.to_s)
|
108
115
|
}, recover: -> { DynamicConfig.new(experiment_name) }, caller: __method__.to_s)
|
109
116
|
end
|
110
117
|
|
111
118
|
sig { params(user: StatsigUser, config_name: String).void }
|
112
119
|
|
113
120
|
def manually_log_config_exposure(user, config_name)
|
114
|
-
|
115
|
-
|
116
|
-
|
121
|
+
@err_boundary.capture(task: lambda {
|
122
|
+
res = @evaluator.get_config(user, config_name)
|
123
|
+
context = { 'is_manual_exposure' => true }
|
124
|
+
@logger.log_config_exposure(user, res.name, res.rule_id, res.secondary_exposures, res.evaluation_details, context)
|
125
|
+
}, caller: __method__.to_s)
|
117
126
|
end
|
118
127
|
|
119
128
|
class GetLayerOptions < T::Struct
|
@@ -124,37 +133,39 @@ class StatsigDriver
|
|
124
133
|
|
125
134
|
def get_layer(user, layer_name, options = GetLayerOptions.new)
|
126
135
|
@err_boundary.capture(task: lambda {
|
127
|
-
|
136
|
+
run_with_diagnostics(task: lambda {
|
137
|
+
user = verify_inputs(user, layer_name, "layer_name")
|
128
138
|
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
139
|
+
res = @evaluator.get_layer(user, layer_name)
|
140
|
+
if res.nil?
|
141
|
+
res = Statsig::ConfigResult.new(layer_name)
|
142
|
+
end
|
133
143
|
|
134
|
-
|
135
|
-
|
136
|
-
|
144
|
+
if res == $fetch_from_server
|
145
|
+
if res.config_delegate.empty?
|
146
|
+
return Layer.new(layer_name)
|
147
|
+
end
|
148
|
+
res = get_config_fallback(user, res.config_delegate)
|
149
|
+
# exposure logged by the server
|
137
150
|
end
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
Layer.new(res.name, res.json_value, res.rule_id, exposure_log_func)
|
146
|
-
}, recover: lambda {
|
147
|
-
Layer.new(layer_name)
|
148
|
-
}, caller: __method__.to_s)
|
151
|
+
|
152
|
+
exposure_log_func = options.log_exposure ? lambda { |layer, parameter_name|
|
153
|
+
@logger.log_layer_exposure(user, layer, parameter_name, res)
|
154
|
+
} : nil
|
155
|
+
Layer.new(res.name, res.json_value, res.rule_id, exposure_log_func)
|
156
|
+
}, caller: __method__.to_s)
|
157
|
+
}, recover: lambda { Layer.new(layer_name) }, caller: __method__.to_s)
|
149
158
|
end
|
150
159
|
|
151
160
|
sig { params(user: StatsigUser, layer_name: String, parameter_name: String).void }
|
152
161
|
|
153
162
|
def manually_log_layer_parameter_exposure(user, layer_name, parameter_name)
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
163
|
+
@err_boundary.capture(task: lambda {
|
164
|
+
res = @evaluator.get_layer(user, layer_name)
|
165
|
+
layer = Layer.new(layer_name, res.json_value, res.rule_id)
|
166
|
+
context = { 'is_manual_exposure' => true }
|
167
|
+
@logger.log_layer_exposure(user, layer, parameter_name, res, context)
|
168
|
+
}, caller: __method__.to_s)
|
158
169
|
end
|
159
170
|
|
160
171
|
def log_event(user, event_name, value = nil, metadata = nil)
|
@@ -171,7 +182,7 @@ class StatsigDriver
|
|
171
182
|
event.value = value
|
172
183
|
event.metadata = metadata
|
173
184
|
@logger.log_event(event)
|
174
|
-
})
|
185
|
+
}, caller: __method__.to_s)
|
175
186
|
end
|
176
187
|
|
177
188
|
def shutdown
|
@@ -179,29 +190,30 @@ class StatsigDriver
|
|
179
190
|
@shutdown = true
|
180
191
|
@logger.shutdown
|
181
192
|
@evaluator.shutdown
|
182
|
-
})
|
193
|
+
}, caller: __method__.to_s)
|
183
194
|
end
|
184
195
|
|
185
196
|
def override_gate(gate_name, gate_value)
|
186
197
|
@err_boundary.capture(task: lambda {
|
187
198
|
@evaluator.override_gate(gate_name, gate_value)
|
188
|
-
})
|
199
|
+
}, caller: __method__.to_s)
|
189
200
|
end
|
190
201
|
|
191
202
|
def override_config(config_name, config_value)
|
192
203
|
@err_boundary.capture(task: lambda {
|
193
204
|
@evaluator.override_config(config_name, config_value)
|
194
|
-
})
|
205
|
+
}, caller: __method__.to_s)
|
195
206
|
end
|
196
207
|
|
197
208
|
# @param [StatsigUser] user
|
209
|
+
# @param [String | nil] client_sdk_key
|
198
210
|
# @return [Hash]
|
199
|
-
def get_client_initialize_response(user)
|
211
|
+
def get_client_initialize_response(user, hash, client_sdk_key)
|
200
212
|
@err_boundary.capture(task: lambda {
|
201
213
|
validate_user(user)
|
202
214
|
normalize_user(user)
|
203
|
-
@evaluator.get_client_initialize_response(user)
|
204
|
-
}, recover: -> { nil })
|
215
|
+
@evaluator.get_client_initialize_response(user, hash, client_sdk_key)
|
216
|
+
}, recover: -> { nil }, caller: __method__.to_s)
|
205
217
|
end
|
206
218
|
|
207
219
|
def maybe_restart_background_threads
|
@@ -212,11 +224,29 @@ class StatsigDriver
|
|
212
224
|
@err_boundary.capture(task: lambda {
|
213
225
|
@evaluator.maybe_restart_background_threads
|
214
226
|
@logger.maybe_restart_background_threads
|
215
|
-
})
|
227
|
+
}, caller: __method__.to_s)
|
216
228
|
end
|
217
229
|
|
218
230
|
private
|
219
231
|
|
232
|
+
def run_with_diagnostics(task:, caller:)
|
233
|
+
diagnostics = nil
|
234
|
+
if Statsig::Diagnostics::API_CALL_KEYS.include?(caller) && Statsig::Diagnostics.sample(10_000)
|
235
|
+
diagnostics = Statsig::Diagnostics.new('api_call')
|
236
|
+
tracker = diagnostics.track(caller)
|
237
|
+
end
|
238
|
+
begin
|
239
|
+
res = task.call
|
240
|
+
tracker&.end(success: true)
|
241
|
+
rescue StandardError => e
|
242
|
+
tracker&.end(success: false)
|
243
|
+
raise e
|
244
|
+
ensure
|
245
|
+
@logger.log_diagnostics_event(diagnostics)
|
246
|
+
end
|
247
|
+
return res
|
248
|
+
end
|
249
|
+
|
220
250
|
sig { params(user: StatsigUser, config_name: String, variable_name: String).returns(StatsigUser) }
|
221
251
|
|
222
252
|
def verify_inputs(user, config_name, variable_name)
|
data/lib/statsig_errors.rb
CHANGED
data/lib/statsig_logger.rb
CHANGED
@@ -9,7 +9,7 @@ $diagnostics_event = 'statsig::diagnostics'
|
|
9
9
|
$ignored_metadata_keys = ['serverTime', 'configSyncTime', 'initTime', 'reason']
|
10
10
|
module Statsig
|
11
11
|
class StatsigLogger
|
12
|
-
def initialize(network, options)
|
12
|
+
def initialize(network, options, error_boundary)
|
13
13
|
@network = network
|
14
14
|
@events = []
|
15
15
|
@options = options
|
@@ -23,9 +23,11 @@ module Statsig
|
|
23
23
|
fallback_policy: :discard
|
24
24
|
)
|
25
25
|
|
26
|
+
@error_boundary = error_boundary
|
26
27
|
@background_flush = periodic_flush
|
27
28
|
@deduper = Concurrent::Set.new()
|
28
29
|
@interval = 0
|
30
|
+
@flush_mutex = Mutex.new
|
29
31
|
end
|
30
32
|
|
31
33
|
def log_event(event)
|
@@ -109,12 +111,14 @@ module Statsig
|
|
109
111
|
|
110
112
|
def periodic_flush
|
111
113
|
Thread.new do
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
114
|
+
@error_boundary.capture(task: lambda {
|
115
|
+
loop do
|
116
|
+
sleep @options.logging_interval_seconds
|
117
|
+
flush_async
|
118
|
+
@interval += 1
|
119
|
+
@deduper.clear if @interval % 2 == 0
|
120
|
+
end
|
121
|
+
})
|
118
122
|
end
|
119
123
|
end
|
120
124
|
|
@@ -132,18 +136,20 @@ module Statsig
|
|
132
136
|
end
|
133
137
|
|
134
138
|
def flush
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
@events = []
|
140
|
-
flush_events = events_clone.map { |e| e.serialize }
|
139
|
+
@flush_mutex.synchronize do
|
140
|
+
if @events.length.zero?
|
141
|
+
return
|
142
|
+
end
|
141
143
|
|
142
|
-
|
144
|
+
events_clone = @events
|
145
|
+
@events = []
|
146
|
+
flush_events = events_clone.map { |e| e.serialize }
|
147
|
+
@network.post_logs(flush_events)
|
148
|
+
end
|
143
149
|
end
|
144
150
|
|
145
151
|
def maybe_restart_background_threads
|
146
|
-
if @background_flush.nil?
|
152
|
+
if @background_flush.nil? || !@background_flush.alive?
|
147
153
|
@background_flush = periodic_flush
|
148
154
|
end
|
149
155
|
end
|
data/lib/ua_parser.rb
CHANGED
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.26.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: 2023-
|
11
|
+
date: 2023-07-27 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -52,6 +52,34 @@ dependencies:
|
|
52
52
|
- - "~>"
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: 5.14.0
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: minitest-reporters
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '1.6'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '1.6'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: minitest-suite
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: 0.0.3
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 0.0.3
|
55
83
|
- !ruby/object:Gem::Dependency
|
56
84
|
name: spy
|
57
85
|
requirement: !ruby/object:Gem::Requirement
|
@@ -226,6 +254,26 @@ dependencies:
|
|
226
254
|
- - "<"
|
227
255
|
- !ruby/object:Gem::Version
|
228
256
|
version: '6.0'
|
257
|
+
- !ruby/object:Gem::Dependency
|
258
|
+
name: connection_pool
|
259
|
+
requirement: !ruby/object:Gem::Requirement
|
260
|
+
requirements:
|
261
|
+
- - "~>"
|
262
|
+
- !ruby/object:Gem::Version
|
263
|
+
version: '2.4'
|
264
|
+
- - ">="
|
265
|
+
- !ruby/object:Gem::Version
|
266
|
+
version: 2.4.1
|
267
|
+
type: :runtime
|
268
|
+
prerelease: false
|
269
|
+
version_requirements: !ruby/object:Gem::Requirement
|
270
|
+
requirements:
|
271
|
+
- - "~>"
|
272
|
+
- !ruby/object:Gem::Version
|
273
|
+
version: '2.4'
|
274
|
+
- - ">="
|
275
|
+
- !ruby/object:Gem::Version
|
276
|
+
version: 2.4.1
|
229
277
|
- !ruby/object:Gem::Dependency
|
230
278
|
name: ip3country
|
231
279
|
requirement: !ruby/object:Gem::Requirement
|
@@ -282,6 +330,7 @@ files:
|
|
282
330
|
- lib/evaluation_details.rb
|
283
331
|
- lib/evaluation_helpers.rb
|
284
332
|
- lib/evaluator.rb
|
333
|
+
- lib/hash_utils.rb
|
285
334
|
- lib/id_list.rb
|
286
335
|
- lib/interfaces/data_store.rb
|
287
336
|
- lib/layer.rb
|