statsig 1.10.0 → 1.20.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/client_initialize_helpers.rb +132 -0
- data/lib/config_result.rb +16 -3
- data/lib/diagnostics.rb +44 -0
- data/lib/dynamic_config.rb +37 -0
- data/lib/error_boundary.rb +57 -0
- data/lib/evaluation_details.rb +42 -0
- data/lib/evaluation_helpers.rb +1 -0
- data/lib/evaluator.rb +127 -14
- data/lib/id_list.rb +1 -0
- data/lib/interfaces/data_store.rb +19 -0
- data/lib/layer.rb +39 -0
- data/lib/network.rb +39 -11
- data/lib/spec_store.rb +183 -43
- data/lib/statsig.rb +213 -4
- data/lib/statsig_driver.rb +192 -62
- data/lib/statsig_errors.rb +11 -0
- data/lib/statsig_event.rb +6 -1
- data/lib/statsig_logger.rb +81 -13
- data/lib/statsig_options.rb +114 -7
- data/lib/statsig_user.rb +79 -16
- metadata +70 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 31a3f19ad19ba6b7ce4ebd18e2632ee04a85d3a2176b2c914e75fcd8f326f578
|
4
|
+
data.tar.gz: b3d6905b985889b60a0e713231d7bb5576cb9638972d331243bf93c881557130
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4a15075766b794eeefdc187cd225edac10a35810a8066ffab521db2228466a5687007a58077a9c788b668b5ed25cddc0c125f5f74fbf4bdea0488318159e9c00
|
7
|
+
data.tar.gz: 74e4ded54a6dc0198ca99cca62d12517109957316a45b562c26df5eca6f154db9a2f1cb59f4a1c6575035a6863f5a7055ee7d4d7e38c6f8eeaa7930eb487cebd
|
@@ -0,0 +1,132 @@
|
|
1
|
+
# typed: true
|
2
|
+
$empty_eval_result = {
|
3
|
+
:gate_value => false,
|
4
|
+
:json_value => {},
|
5
|
+
:rule_id => "",
|
6
|
+
:is_experiment_group => false,
|
7
|
+
:secondary_exposures => []
|
8
|
+
}
|
9
|
+
|
10
|
+
module ClientInitializeHelpers
|
11
|
+
class ResponseFormatter
|
12
|
+
def initialize(evaluator, user)
|
13
|
+
@evaluator = evaluator
|
14
|
+
@user = user
|
15
|
+
@specs = evaluator.spec_store.get_raw_specs
|
16
|
+
end
|
17
|
+
|
18
|
+
def get_responses(key)
|
19
|
+
@specs[key]
|
20
|
+
.map { |name, spec| to_response(name, spec) }
|
21
|
+
.delete_if { |v| v.nil? }.to_h
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def to_response(config_name, config_spec)
|
27
|
+
eval_result = @evaluator.eval_spec(@user, config_spec)
|
28
|
+
if eval_result.nil?
|
29
|
+
return nil
|
30
|
+
end
|
31
|
+
|
32
|
+
safe_eval_result = eval_result == $fetch_from_server ? $empty_eval_result : {
|
33
|
+
:gate_value => eval_result.gate_value,
|
34
|
+
:json_value => eval_result.json_value,
|
35
|
+
:rule_id => eval_result.rule_id,
|
36
|
+
:config_delegate => eval_result.config_delegate,
|
37
|
+
:is_experiment_group => eval_result.is_experiment_group,
|
38
|
+
:secondary_exposures => eval_result.secondary_exposures,
|
39
|
+
:undelegated_sec_exps => eval_result.undelegated_sec_exps
|
40
|
+
}
|
41
|
+
|
42
|
+
category = config_spec['type']
|
43
|
+
entity_type = config_spec['entity']
|
44
|
+
|
45
|
+
result = {}
|
46
|
+
|
47
|
+
case category
|
48
|
+
|
49
|
+
when 'feature_gate'
|
50
|
+
if entity_type == 'segment' || entity_type == 'holdout'
|
51
|
+
return nil
|
52
|
+
end
|
53
|
+
|
54
|
+
result['value'] = safe_eval_result[:gate_value]
|
55
|
+
when 'dynamic_config'
|
56
|
+
id_type = config_spec['idType']
|
57
|
+
result['value'] = safe_eval_result[:json_value]
|
58
|
+
result["group"] = safe_eval_result[:rule_id]
|
59
|
+
result["is_device_based"] = id_type.is_a?(String) && id_type.downcase == 'stableid'
|
60
|
+
else
|
61
|
+
return nil
|
62
|
+
end
|
63
|
+
|
64
|
+
if entity_type == 'experiment'
|
65
|
+
populate_experiment_fields(config_name, config_spec, safe_eval_result, result)
|
66
|
+
end
|
67
|
+
|
68
|
+
if entity_type == 'layer'
|
69
|
+
populate_layer_fields(config_spec, safe_eval_result, result)
|
70
|
+
end
|
71
|
+
|
72
|
+
hashed_name = hash_name(config_name)
|
73
|
+
[hashed_name, result.merge(
|
74
|
+
{
|
75
|
+
"name" => hashed_name,
|
76
|
+
"rule_id" => safe_eval_result[:rule_id],
|
77
|
+
"secondary_exposures" => clean_exposures(safe_eval_result[:secondary_exposures])
|
78
|
+
})]
|
79
|
+
end
|
80
|
+
|
81
|
+
def clean_exposures(exposures)
|
82
|
+
seen = {}
|
83
|
+
exposures.reject do |exposure|
|
84
|
+
key = "#{exposure["gate"]}|#{exposure["gateValue"]}|#{exposure["ruleID"]}}"
|
85
|
+
should_reject = seen[key]
|
86
|
+
seen[key] = true
|
87
|
+
should_reject == true
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def populate_experiment_fields(config_name, config_spec, eval_result, result)
|
92
|
+
result["is_user_in_experiment"] = eval_result[:is_experiment_group]
|
93
|
+
result["is_experiment_active"] = config_spec['isActive'] == true
|
94
|
+
|
95
|
+
if config_spec['hasSharedParams'] != true
|
96
|
+
return
|
97
|
+
end
|
98
|
+
|
99
|
+
result["is_in_layer"] = true
|
100
|
+
result["explicit_parameters"] = config_spec["explicitParameters"] || []
|
101
|
+
|
102
|
+
layer_name = @specs[:experiment_to_layer][config_name]
|
103
|
+
if layer_name.nil? || @specs[:layers][layer_name].nil?
|
104
|
+
return
|
105
|
+
end
|
106
|
+
|
107
|
+
layer = @specs[:layers][layer_name]
|
108
|
+
result["value"] = layer["defaultValue"].merge(result["value"])
|
109
|
+
end
|
110
|
+
|
111
|
+
def populate_layer_fields(config_spec, eval_result, result)
|
112
|
+
delegate = eval_result[:config_delegate]
|
113
|
+
result["explicit_parameters"] = config_spec["explicitParameters"] || []
|
114
|
+
|
115
|
+
if delegate.nil? == false && delegate.empty? == false
|
116
|
+
delegate_spec = @specs[:configs][delegate]
|
117
|
+
delegate_result = @evaluator.eval_spec(@user, delegate_spec)
|
118
|
+
|
119
|
+
result["allocated_experiment_name"] = hash_name(delegate)
|
120
|
+
result["is_user_in_experiment"] = delegate_result.is_experiment_group
|
121
|
+
result["is_experiment_active"] = delegate_spec['isActive'] == true
|
122
|
+
result["explicit_parameters"] = delegate_spec["explicitParameters"] || []
|
123
|
+
end
|
124
|
+
|
125
|
+
result["undelegated_secondary_exposures"] = clean_exposures(eval_result[:undelegated_sec_exps] || [])
|
126
|
+
end
|
127
|
+
|
128
|
+
def hash_name(name)
|
129
|
+
Digest::SHA256.base64digest(name)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
data/lib/config_result.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
|
1
|
+
# typed: true
|
2
2
|
module Statsig
|
3
3
|
class ConfigResult
|
4
4
|
attr_accessor :name
|
@@ -9,16 +9,29 @@ module Statsig
|
|
9
9
|
attr_accessor :undelegated_sec_exps
|
10
10
|
attr_accessor :config_delegate
|
11
11
|
attr_accessor :explicit_parameters
|
12
|
+
attr_accessor :is_experiment_group
|
13
|
+
attr_accessor :evaluation_details
|
12
14
|
|
13
|
-
def initialize(
|
15
|
+
def initialize(
|
16
|
+
name,
|
17
|
+
gate_value = false,
|
18
|
+
json_value = {},
|
19
|
+
rule_id = '',
|
20
|
+
secondary_exposures = [],
|
21
|
+
config_delegate = '',
|
22
|
+
explicit_parameters = [],
|
23
|
+
is_experiment_group: false,
|
24
|
+
evaluation_details: nil)
|
14
25
|
@name = name
|
15
26
|
@gate_value = gate_value
|
16
27
|
@json_value = json_value
|
17
28
|
@rule_id = rule_id
|
18
29
|
@secondary_exposures = secondary_exposures.is_a?(Array) ? secondary_exposures : []
|
19
|
-
@undelegated_sec_exps =
|
30
|
+
@undelegated_sec_exps = @secondary_exposures
|
20
31
|
@config_delegate = config_delegate
|
21
32
|
@explicit_parameters = explicit_parameters
|
33
|
+
@is_experiment_group = is_experiment_group
|
34
|
+
@evaluation_details = evaluation_details
|
22
35
|
end
|
23
36
|
end
|
24
37
|
end
|
data/lib/diagnostics.rb
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
# typed: true
|
2
|
+
|
3
|
+
require 'sorbet-runtime'
|
4
|
+
|
5
|
+
module Statsig
|
6
|
+
class Diagnostics
|
7
|
+
extend T::Sig
|
8
|
+
|
9
|
+
sig { returns(String) }
|
10
|
+
attr_reader :context
|
11
|
+
|
12
|
+
sig { returns(T::Array[T::Hash[Symbol, T.untyped]]) }
|
13
|
+
attr_reader :markers
|
14
|
+
|
15
|
+
sig { params(context: String).void }
|
16
|
+
|
17
|
+
def initialize(context)
|
18
|
+
@context = context
|
19
|
+
@markers = []
|
20
|
+
end
|
21
|
+
|
22
|
+
sig { params(key: String, action: String, step: T.any(String, NilClass), value: T.any(String, Integer, T::Boolean, NilClass)).void }
|
23
|
+
|
24
|
+
def mark(key, action, step = nil, value = nil)
|
25
|
+
@markers.push({
|
26
|
+
key: key,
|
27
|
+
step: step,
|
28
|
+
action: action,
|
29
|
+
value: value,
|
30
|
+
timestamp: (Time.now.to_f * 1000).to_i
|
31
|
+
})
|
32
|
+
end
|
33
|
+
|
34
|
+
sig { returns(T::Hash[Symbol, T.untyped]) }
|
35
|
+
|
36
|
+
def serialize
|
37
|
+
{
|
38
|
+
context: @context,
|
39
|
+
markers: @markers
|
40
|
+
}
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
data/lib/dynamic_config.rb
CHANGED
@@ -1,16 +1,53 @@
|
|
1
|
+
# typed: false
|
2
|
+
|
3
|
+
require 'sorbet-runtime'
|
4
|
+
|
5
|
+
##
|
6
|
+
# Contains the current experiment/dynamic config values from Statsig
|
7
|
+
#
|
8
|
+
# Dynamic Config Documentation: https://docs.statsig.com/dynamic-config
|
9
|
+
#
|
10
|
+
# Experiments Documentation: https://docs.statsig.com/experiments-plus
|
1
11
|
class DynamicConfig
|
12
|
+
extend T::Sig
|
13
|
+
|
14
|
+
sig { returns(String) }
|
2
15
|
attr_accessor :name
|
16
|
+
|
17
|
+
sig { returns(T::Hash[String, T.untyped]) }
|
3
18
|
attr_accessor :value
|
19
|
+
|
20
|
+
sig { returns(String) }
|
4
21
|
attr_accessor :rule_id
|
5
22
|
|
23
|
+
sig { params(name: String, value: T::Hash[String, T.untyped], rule_id: String).void }
|
6
24
|
def initialize(name, value = {}, rule_id = '')
|
7
25
|
@name = name
|
8
26
|
@value = value
|
9
27
|
@rule_id = rule_id
|
10
28
|
end
|
11
29
|
|
30
|
+
sig { params(index: String, default_value: T.untyped).returns(T.untyped) }
|
31
|
+
##
|
32
|
+
# Get the value for the given key (index), falling back to the default_value if it cannot be found.
|
33
|
+
#
|
34
|
+
# @param index The name of parameter being fetched
|
35
|
+
# @param default_value The fallback value if the name cannot be found
|
12
36
|
def get(index, default_value)
|
13
37
|
return default_value if @value.nil? || !@value.key?(index)
|
14
38
|
@value[index]
|
15
39
|
end
|
40
|
+
|
41
|
+
sig { params(index: String, default_value: T.untyped).returns(T.untyped) }
|
42
|
+
##
|
43
|
+
# Get the value for the given key (index), falling back to the default_value if it cannot be found
|
44
|
+
# or is found to have a different type from the default_value.
|
45
|
+
#
|
46
|
+
# @param index The name of parameter being fetched
|
47
|
+
# @param default_value The fallback value if the name cannot be found
|
48
|
+
def get_typed(index, default_value)
|
49
|
+
return default_value if @value.nil? || !@value.key?(index)
|
50
|
+
return default_value if @value[index].class != default_value.class and default_value.class != TrueClass and default_value.class != FalseClass
|
51
|
+
@value[index]
|
52
|
+
end
|
16
53
|
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require "statsig_errors"
|
2
|
+
|
3
|
+
$endpoint = 'https://statsigapi.net/v1/sdk_exception'
|
4
|
+
|
5
|
+
module Statsig
|
6
|
+
class ErrorBoundary
|
7
|
+
def initialize(sdk_key)
|
8
|
+
@sdk_key = sdk_key
|
9
|
+
@seen = Set.new
|
10
|
+
end
|
11
|
+
|
12
|
+
def capture(task, recover = -> {})
|
13
|
+
begin
|
14
|
+
return task.call
|
15
|
+
rescue StandardError => e
|
16
|
+
if e.is_a?(Statsig::UninitializedError) or e.is_a?(Statsig::ValueError)
|
17
|
+
raise e
|
18
|
+
end
|
19
|
+
puts "[Statsig]: An unexpected exception occurred."
|
20
|
+
log_exception(e)
|
21
|
+
return recover.call
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def log_exception(exception)
|
28
|
+
begin
|
29
|
+
name = exception.class.name
|
30
|
+
if @seen.include?(name)
|
31
|
+
return
|
32
|
+
end
|
33
|
+
|
34
|
+
@seen << name
|
35
|
+
meta = Statsig.get_statsig_metadata
|
36
|
+
http = HTTP.headers(
|
37
|
+
{
|
38
|
+
"STATSIG-API-KEY" => @sdk_key,
|
39
|
+
"STATSIG-SDK-TYPE" => meta['sdkType'],
|
40
|
+
"STATSIG-SDK-VERSION" => meta['sdkVersion'],
|
41
|
+
"Content-Type" => "application/json; charset=UTF-8"
|
42
|
+
}).accept(:json)
|
43
|
+
body = {
|
44
|
+
"exception" => name,
|
45
|
+
"info" => {
|
46
|
+
"trace" => exception.backtrace.to_s,
|
47
|
+
"message" => exception.message
|
48
|
+
}.to_s,
|
49
|
+
"statsigMetadata" => meta
|
50
|
+
}
|
51
|
+
http.post($endpoint, body: JSON.generate(body))
|
52
|
+
rescue
|
53
|
+
return
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# typed: true
|
2
|
+
module Statsig
|
3
|
+
|
4
|
+
module EvaluationReason
|
5
|
+
NETWORK = "Network"
|
6
|
+
LOCAL_OVERRIDE = "LocalOverride"
|
7
|
+
UNRECOGNIZED = "Unrecognized"
|
8
|
+
UNINITIALIZED = "Uninitialized"
|
9
|
+
BOOTSTRAP = "Bootstrap"
|
10
|
+
DATA_ADAPTER = "DataAdapter"
|
11
|
+
end
|
12
|
+
|
13
|
+
class EvaluationDetails
|
14
|
+
attr_accessor :config_sync_time
|
15
|
+
attr_accessor :init_time
|
16
|
+
attr_accessor :reason
|
17
|
+
attr_accessor :server_time
|
18
|
+
|
19
|
+
def initialize(config_sync_time, init_time, reason)
|
20
|
+
@config_sync_time = config_sync_time
|
21
|
+
@init_time = init_time
|
22
|
+
@reason = reason
|
23
|
+
@server_time = (Time.now.to_i * 1000).to_s
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.unrecognized(config_sync_time, init_time)
|
27
|
+
EvaluationDetails.new(config_sync_time, init_time, EvaluationReason::UNRECOGNIZED)
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.uninitialized
|
31
|
+
EvaluationDetails.new(0, 0, EvaluationReason::UNINITIALIZED)
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.network(config_sync_time, init_time)
|
35
|
+
EvaluationDetails.new(config_sync_time, init_time, EvaluationReason::NETWORK)
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.local_override(config_sync_time, init_time)
|
39
|
+
EvaluationDetails.new(config_sync_time, init_time, EvaluationReason::LOCAL_OVERRIDE)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
data/lib/evaluation_helpers.rb
CHANGED
data/lib/evaluator.rb
CHANGED
@@ -1,10 +1,13 @@
|
|
1
|
+
# typed: false
|
1
2
|
require 'config_result'
|
2
3
|
require 'country_lookup'
|
3
4
|
require 'digest'
|
4
5
|
require 'evaluation_helpers'
|
6
|
+
require 'client_initialize_helpers'
|
5
7
|
require 'spec_store'
|
6
8
|
require 'time'
|
7
9
|
require 'user_agent_parser'
|
10
|
+
require 'evaluation_details'
|
8
11
|
require 'user_agent_parser/operating_system'
|
9
12
|
|
10
13
|
$fetch_from_server = 'fetch_from_server'
|
@@ -12,33 +15,126 @@ $type_dynamic_config = 'dynamic_config'
|
|
12
15
|
|
13
16
|
module Statsig
|
14
17
|
class Evaluator
|
15
|
-
|
16
|
-
|
18
|
+
attr_accessor :spec_store
|
19
|
+
|
20
|
+
def initialize(network, options, error_callback, init_diagnostics = nil)
|
21
|
+
@spec_store = Statsig::SpecStore.new(network, options, error_callback, init_diagnostics)
|
17
22
|
@ua_parser = UserAgentParser::Parser.new
|
18
23
|
CountryLookup.initialize
|
19
|
-
|
24
|
+
|
25
|
+
@gate_overrides = {}
|
26
|
+
@config_overrides = {}
|
27
|
+
end
|
28
|
+
|
29
|
+
def maybe_restart_background_threads
|
30
|
+
@spec_store.maybe_restart_background_threads
|
20
31
|
end
|
21
32
|
|
22
33
|
def check_gate(user, gate_name)
|
23
|
-
|
34
|
+
if @gate_overrides.has_key?(gate_name)
|
35
|
+
return Statsig::ConfigResult.new(
|
36
|
+
gate_name,
|
37
|
+
@gate_overrides[gate_name],
|
38
|
+
@gate_overrides[gate_name],
|
39
|
+
'override',
|
40
|
+
[],
|
41
|
+
evaluation_details: EvaluationDetails.local_override(@spec_store.last_config_sync_time, @spec_store.initial_config_sync_time))
|
42
|
+
end
|
43
|
+
|
44
|
+
if @spec_store.init_reason == EvaluationReason::UNINITIALIZED
|
45
|
+
return Statsig::ConfigResult.new(gate_name, evaluation_details: EvaluationDetails.uninitialized)
|
46
|
+
end
|
47
|
+
|
48
|
+
unless @spec_store.has_gate?(gate_name)
|
49
|
+
return Statsig::ConfigResult.new(gate_name, evaluation_details: EvaluationDetails.unrecognized(@spec_store.last_config_sync_time, @spec_store.initial_config_sync_time))
|
50
|
+
end
|
51
|
+
|
24
52
|
eval_spec(user, @spec_store.get_gate(gate_name))
|
25
53
|
end
|
26
54
|
|
27
55
|
def get_config(user, config_name)
|
28
|
-
|
56
|
+
if @config_overrides.has_key?(config_name)
|
57
|
+
return Statsig::ConfigResult.new(
|
58
|
+
config_name,
|
59
|
+
false,
|
60
|
+
@config_overrides[config_name],
|
61
|
+
'override',
|
62
|
+
[],
|
63
|
+
evaluation_details: EvaluationDetails.local_override(@spec_store.last_config_sync_time, @spec_store.initial_config_sync_time))
|
64
|
+
end
|
65
|
+
|
66
|
+
if @spec_store.init_reason == EvaluationReason::UNINITIALIZED
|
67
|
+
return Statsig::ConfigResult.new(config_name, evaluation_details: EvaluationDetails.uninitialized)
|
68
|
+
end
|
69
|
+
|
70
|
+
unless @spec_store.has_config?(config_name)
|
71
|
+
return Statsig::ConfigResult.new(config_name, evaluation_details: EvaluationDetails.unrecognized(@spec_store.last_config_sync_time, @spec_store.initial_config_sync_time))
|
72
|
+
end
|
73
|
+
|
29
74
|
eval_spec(user, @spec_store.get_config(config_name))
|
30
75
|
end
|
31
76
|
|
32
77
|
def get_layer(user, layer_name)
|
33
|
-
|
78
|
+
if @spec_store.init_reason == EvaluationReason::UNINITIALIZED
|
79
|
+
return Statsig::ConfigResult.new(layer_name, evaluation_details: EvaluationDetails.uninitialized)
|
80
|
+
end
|
81
|
+
|
82
|
+
unless @spec_store.has_layer?(layer_name)
|
83
|
+
return Statsig::ConfigResult.new(layer_name, evaluation_details: EvaluationDetails.unrecognized(@spec_store.last_config_sync_time, @spec_store.initial_config_sync_time))
|
84
|
+
end
|
85
|
+
|
34
86
|
eval_spec(user, @spec_store.get_layer(layer_name))
|
35
87
|
end
|
36
88
|
|
89
|
+
def get_client_initialize_response(user)
|
90
|
+
if @spec_store.is_ready_for_checks == false
|
91
|
+
return nil
|
92
|
+
end
|
93
|
+
|
94
|
+
formatter = ClientInitializeHelpers::ResponseFormatter.new(self, user)
|
95
|
+
|
96
|
+
evaluated_keys = {}
|
97
|
+
if user.user_id.nil? == false
|
98
|
+
evaluated_keys['userID'] = user.user_id
|
99
|
+
end
|
100
|
+
|
101
|
+
if user.custom_ids.nil? == false
|
102
|
+
evaluated_keys['customIDs'] = user.custom_ids
|
103
|
+
end
|
104
|
+
|
105
|
+
{
|
106
|
+
"feature_gates" => formatter.get_responses(:gates),
|
107
|
+
"dynamic_configs" => formatter.get_responses(:configs),
|
108
|
+
"layer_configs" => formatter.get_responses(:layers),
|
109
|
+
"sdkParams" => {},
|
110
|
+
"has_updates" => true,
|
111
|
+
"generator" => "statsig-ruby-sdk",
|
112
|
+
"evaluated_keys" => evaluated_keys,
|
113
|
+
"time" => 0,
|
114
|
+
}
|
115
|
+
end
|
116
|
+
|
117
|
+
def clean_exposures(exposures)
|
118
|
+
seen = {}
|
119
|
+
exposures.reject do |exposure|
|
120
|
+
key = "#{exposure["gate"]}|#{exposure["gateValue"]}|#{exposure["ruleID"]}}"
|
121
|
+
should_reject = seen[key]
|
122
|
+
seen[key] = true
|
123
|
+
should_reject == true
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
37
127
|
def shutdown
|
38
128
|
@spec_store.shutdown
|
39
129
|
end
|
40
130
|
|
41
|
-
|
131
|
+
def override_gate(gate, value)
|
132
|
+
@gate_overrides[gate] = value
|
133
|
+
end
|
134
|
+
|
135
|
+
def override_config(config, value)
|
136
|
+
@config_overrides[config] = value
|
137
|
+
end
|
42
138
|
|
43
139
|
def eval_spec(user, config)
|
44
140
|
default_rule_id = 'default'
|
@@ -62,7 +158,9 @@ module Statsig
|
|
62
158
|
pass,
|
63
159
|
pass ? result.json_value : config['defaultValue'],
|
64
160
|
result.rule_id,
|
65
|
-
exposures
|
161
|
+
exposures,
|
162
|
+
evaluation_details: EvaluationDetails.new(@spec_store.last_config_sync_time, @spec_store.initial_config_sync_time, @spec_store.init_reason),
|
163
|
+
is_experiment_group: result.is_experiment_group,
|
66
164
|
)
|
67
165
|
end
|
68
166
|
|
@@ -72,9 +170,17 @@ module Statsig
|
|
72
170
|
default_rule_id = 'disabled'
|
73
171
|
end
|
74
172
|
|
75
|
-
Statsig::ConfigResult.new(
|
173
|
+
Statsig::ConfigResult.new(
|
174
|
+
config['name'],
|
175
|
+
false,
|
176
|
+
config['defaultValue'],
|
177
|
+
default_rule_id,
|
178
|
+
exposures,
|
179
|
+
evaluation_details: EvaluationDetails.new(@spec_store.last_config_sync_time, @spec_store.initial_config_sync_time, @spec_store.init_reason))
|
76
180
|
end
|
77
181
|
|
182
|
+
private
|
183
|
+
|
78
184
|
def eval_rule(user, rule)
|
79
185
|
exposures = []
|
80
186
|
pass = true
|
@@ -94,7 +200,14 @@ module Statsig
|
|
94
200
|
i += 1
|
95
201
|
end
|
96
202
|
|
97
|
-
Statsig::ConfigResult.new(
|
203
|
+
Statsig::ConfigResult.new(
|
204
|
+
'',
|
205
|
+
pass,
|
206
|
+
rule['returnValue'],
|
207
|
+
rule['id'],
|
208
|
+
exposures,
|
209
|
+
evaluation_details: EvaluationDetails.new(@spec_store.last_config_sync_time, @spec_store.initial_config_sync_time, @spec_store.init_reason),
|
210
|
+
is_experiment_group: rule["isExperimentGroup"] == true)
|
98
211
|
end
|
99
212
|
|
100
213
|
def eval_delegate(name, user, rule, exposures)
|
@@ -154,7 +267,7 @@ module Statsig
|
|
154
267
|
when 'environment_field'
|
155
268
|
value = get_value_from_environment(user, field)
|
156
269
|
when 'current_time'
|
157
|
-
value = Time.now.
|
270
|
+
value = Time.now.to_i # epoch time in seconds
|
158
271
|
when 'user_bucket'
|
159
272
|
begin
|
160
273
|
salt = additional_values['salt']
|
@@ -266,14 +379,14 @@ module Statsig
|
|
266
379
|
user_custom = user_lookup_table['custom']
|
267
380
|
if user_custom.is_a?(Hash)
|
268
381
|
user_custom.each do |key, value|
|
269
|
-
return value if key.downcase.casecmp?(field.downcase) && !value.nil?
|
382
|
+
return value if key.to_s.downcase.casecmp?(field.downcase) && !value.nil?
|
270
383
|
end
|
271
384
|
end
|
272
385
|
|
273
386
|
private_attributes = user_lookup_table['privateAttributes']
|
274
387
|
if private_attributes.is_a?(Hash)
|
275
388
|
private_attributes.each do |key, value|
|
276
|
-
return value if key.downcase.casecmp?(field.downcase) && !value.nil?
|
389
|
+
return value if key.to_s.downcase.casecmp?(field.downcase) && !value.nil?
|
277
390
|
end
|
278
391
|
end
|
279
392
|
|
@@ -285,7 +398,7 @@ module Statsig
|
|
285
398
|
field = field.downcase
|
286
399
|
return nil unless user.statsig_environment.is_a? Hash
|
287
400
|
user.statsig_environment.each do |key, value|
|
288
|
-
return value if key.downcase == (field)
|
401
|
+
return value if key.to_s.downcase == (field)
|
289
402
|
end
|
290
403
|
nil
|
291
404
|
end
|
data/lib/id_list.rb
CHANGED