statsig 1.10.0 → 1.20.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 +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