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/dynamic_config.rb
CHANGED
@@ -1,7 +1,3 @@
|
|
1
|
-
# typed: false
|
2
|
-
|
3
|
-
require 'sorbet-runtime'
|
4
|
-
|
5
1
|
##
|
6
2
|
# Contains the current experiment/dynamic config values from Statsig
|
7
3
|
#
|
@@ -9,33 +5,28 @@ require 'sorbet-runtime'
|
|
9
5
|
#
|
10
6
|
# Experiments Documentation: https://docs.statsig.com/experiments-plus
|
11
7
|
class DynamicConfig
|
12
|
-
extend T::Sig
|
13
8
|
|
14
|
-
sig { returns(String) }
|
15
9
|
attr_accessor :name
|
16
10
|
|
17
|
-
sig { returns(T::Hash[String, T.untyped]) }
|
18
11
|
attr_accessor :value
|
19
12
|
|
20
|
-
sig { returns(String) }
|
21
13
|
attr_accessor :rule_id
|
22
14
|
|
23
|
-
sig { returns(T.nilable(String)) }
|
24
15
|
attr_accessor :group_name
|
25
16
|
|
26
|
-
sig { returns(String) }
|
27
17
|
attr_accessor :id_type
|
28
18
|
|
29
|
-
|
30
|
-
|
19
|
+
attr_accessor :evaluation_details
|
20
|
+
|
21
|
+
def initialize(name, value = {}, rule_id = '', group_name = nil, id_type = '', evaluation_details = nil)
|
31
22
|
@name = name
|
32
|
-
@value = value
|
23
|
+
@value = value || {}
|
33
24
|
@rule_id = rule_id
|
34
25
|
@group_name = group_name
|
35
26
|
@id_type = id_type
|
27
|
+
@evaluation_details = evaluation_details
|
36
28
|
end
|
37
29
|
|
38
|
-
sig { params(index: String, default_value: T.untyped).returns(T.untyped) }
|
39
30
|
##
|
40
31
|
# Get the value for the given key (index), falling back to the default_value if it cannot be found.
|
41
32
|
#
|
@@ -46,7 +37,6 @@ class DynamicConfig
|
|
46
37
|
@value[index]
|
47
38
|
end
|
48
39
|
|
49
|
-
sig { params(index: String, default_value: T.untyped).returns(T.untyped) }
|
50
40
|
##
|
51
41
|
# Get the value for the given key (index), falling back to the default_value if it cannot be found
|
52
42
|
# or is found to have a different type from the default_value.
|
data/lib/error_boundary.rb
CHANGED
@@ -1,78 +1,61 @@
|
|
1
|
-
# typed: true
|
2
|
-
|
3
1
|
require 'statsig_errors'
|
4
|
-
require 'sorbet-runtime'
|
5
2
|
|
6
3
|
$endpoint = 'https://statsigapi.net/v1/sdk_exception'
|
7
4
|
|
8
5
|
module Statsig
|
9
6
|
class ErrorBoundary
|
10
|
-
extend T::Sig
|
11
|
-
|
12
|
-
sig { returns(T.any(StatsigLogger, NilClass)) }
|
13
|
-
attr_accessor :logger
|
14
7
|
|
15
|
-
sig { params(sdk_key: String).void }
|
16
8
|
def initialize(sdk_key)
|
17
9
|
@sdk_key = sdk_key
|
18
10
|
@seen = Set.new
|
19
11
|
end
|
20
12
|
|
21
|
-
def sample_diagnostics
|
22
|
-
rand(10_000).zero?
|
23
|
-
end
|
24
|
-
|
25
13
|
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
14
|
begin
|
31
15
|
res = task.call
|
32
|
-
|
33
|
-
|
34
|
-
tracker&.end(false)
|
35
|
-
if e.is_a?(Statsig::UninitializedError) or e.is_a?(Statsig::ValueError)
|
16
|
+
rescue StandardError, SystemStackError => e
|
17
|
+
if e.is_a?(Statsig::UninitializedError) || e.is_a?(Statsig::ValueError)
|
36
18
|
raise e
|
37
19
|
end
|
20
|
+
|
38
21
|
puts '[Statsig]: An unexpected exception occurred.'
|
39
|
-
|
22
|
+
puts e.message
|
23
|
+
log_exception(e, tag: caller)
|
40
24
|
res = recover.call
|
41
25
|
end
|
42
|
-
@logger&.log_diagnostics_event(diagnostics)
|
43
26
|
return res
|
44
27
|
end
|
45
28
|
|
46
29
|
private
|
47
30
|
|
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
|
31
|
+
def log_exception(exception, tag: nil)
|
32
|
+
name = exception.class.name
|
33
|
+
if @seen.include?(name)
|
74
34
|
return
|
75
35
|
end
|
36
|
+
|
37
|
+
@seen << name
|
38
|
+
meta = Statsig.get_statsig_metadata
|
39
|
+
http = HTTP.headers(
|
40
|
+
{
|
41
|
+
'STATSIG-API-KEY' => @sdk_key,
|
42
|
+
'STATSIG-SDK-TYPE' => meta['sdkType'],
|
43
|
+
'STATSIG-SDK-VERSION' => meta['sdkVersion'],
|
44
|
+
'STATSIG-SDK-LANGUAGE-VERSION' => meta['languageVersion'],
|
45
|
+
'Content-Type' => 'application/json; charset=UTF-8'
|
46
|
+
}).accept(:json)
|
47
|
+
body = {
|
48
|
+
'exception' => name,
|
49
|
+
'info' => {
|
50
|
+
'trace' => exception.backtrace.to_s,
|
51
|
+
'message' => exception.message
|
52
|
+
}.to_s,
|
53
|
+
'statsigMetadata' => meta,
|
54
|
+
'tag' => tag
|
55
|
+
}
|
56
|
+
http.post($endpoint, body: JSON.generate(body))
|
57
|
+
rescue StandardError
|
58
|
+
return
|
76
59
|
end
|
77
60
|
end
|
78
|
-
end
|
61
|
+
end
|
data/lib/evaluation_details.rb
CHANGED
@@ -1,13 +1,14 @@
|
|
1
|
-
# typed: true
|
2
1
|
module Statsig
|
3
2
|
|
4
3
|
module EvaluationReason
|
5
|
-
NETWORK =
|
6
|
-
LOCAL_OVERRIDE =
|
7
|
-
UNRECOGNIZED =
|
8
|
-
UNINITIALIZED =
|
9
|
-
BOOTSTRAP =
|
10
|
-
DATA_ADAPTER =
|
4
|
+
NETWORK = 'Network'.freeze
|
5
|
+
LOCAL_OVERRIDE = 'LocalOverride'.freeze
|
6
|
+
UNRECOGNIZED = 'Unrecognized'.freeze
|
7
|
+
UNINITIALIZED = 'Uninitialized'.freeze
|
8
|
+
BOOTSTRAP = 'Bootstrap'.freeze
|
9
|
+
DATA_ADAPTER = 'DataAdapter'.freeze
|
10
|
+
PERSISTED = 'Persisted'.freeze
|
11
|
+
UNSUPPORTED = 'Unsupported'.freeze
|
11
12
|
end
|
12
13
|
|
13
14
|
class EvaluationDetails
|
@@ -23,6 +24,10 @@ module Statsig
|
|
23
24
|
@server_time = (Time.now.to_i * 1000).to_s
|
24
25
|
end
|
25
26
|
|
27
|
+
def self.unsupported(config_sync_time, init_time)
|
28
|
+
EvaluationDetails.new(config_sync_time, init_time, EvaluationReason::UNSUPPORTED)
|
29
|
+
end
|
30
|
+
|
26
31
|
def self.unrecognized(config_sync_time, init_time)
|
27
32
|
EvaluationDetails.new(config_sync_time, init_time, EvaluationReason::UNRECOGNIZED)
|
28
33
|
end
|
@@ -38,5 +43,9 @@ module Statsig
|
|
38
43
|
def self.local_override(config_sync_time, init_time)
|
39
44
|
EvaluationDetails.new(config_sync_time, init_time, EvaluationReason::LOCAL_OVERRIDE)
|
40
45
|
end
|
46
|
+
|
47
|
+
def self.persisted(config_sync_time, init_time)
|
48
|
+
EvaluationDetails.new(config_sync_time, init_time, EvaluationReason::PERSISTED)
|
49
|
+
end
|
41
50
|
end
|
42
|
-
end
|
51
|
+
end
|
data/lib/evaluation_helpers.rb
CHANGED
@@ -1,4 +1,3 @@
|
|
1
|
-
# typed: true
|
2
1
|
require 'time'
|
3
2
|
|
4
3
|
module EvaluationHelpers
|
@@ -9,9 +8,41 @@ module EvaluationHelpers
|
|
9
8
|
|
10
9
|
# returns true if array has any element that evaluates to true with value using func lambda, ignoring case
|
11
10
|
def self.match_string_in_array(array, value, ignore_case, func)
|
12
|
-
return false unless array.is_a?(Array) && !value.nil?
|
13
11
|
str_value = value.to_s
|
14
|
-
|
12
|
+
str_value_downcased = nil
|
13
|
+
|
14
|
+
return false if array.nil?
|
15
|
+
|
16
|
+
return array.any? do |item|
|
17
|
+
next false if item.nil?
|
18
|
+
item_str = item.to_s
|
19
|
+
|
20
|
+
return true if func.call(str_value, item_str)
|
21
|
+
next false unless ignore_case
|
22
|
+
|
23
|
+
str_value_downcased ||= str_value.downcase
|
24
|
+
func.call(str_value_downcased, item_str.downcase)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.equal_string_in_array(array, value, ignore_case)
|
29
|
+
str_value = value.to_s
|
30
|
+
str_value_downcased = nil
|
31
|
+
|
32
|
+
return false if array.nil?
|
33
|
+
|
34
|
+
return array.any? do |item|
|
35
|
+
next false if item.nil?
|
36
|
+
item_str = item.to_s
|
37
|
+
|
38
|
+
next false unless item_str.length == str_value.length
|
39
|
+
|
40
|
+
return true if item_str == str_value
|
41
|
+
next false unless ignore_case
|
42
|
+
|
43
|
+
str_value_downcased ||= str_value.downcase
|
44
|
+
item_str.downcase == str_value_downcased
|
45
|
+
end
|
15
46
|
end
|
16
47
|
|
17
48
|
def self.compare_times(a, b, func)
|
@@ -27,6 +58,7 @@ module EvaluationHelpers
|
|
27
58
|
private
|
28
59
|
|
29
60
|
def self.is_numeric(v)
|
61
|
+
return true if v.is_a?(Numeric)
|
30
62
|
!(v.to_s =~ /\A[-+]?\d*\.?\d+\z/).nil?
|
31
63
|
end
|
32
64
|
|