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
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 56f4bfc568fa70750a5dc1376ffffacd1163344c63750492e0e0ff841fd73d18
|
4
|
+
data.tar.gz: 6e41d012096437a58a9ceea49c0aa5d71532d06d273b37593afcd7cb9bc07297
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7060859db57bed3e98e372c1b3808f53802bb1c7e4e76f70b366e138c022d1aef522605457d8e69f8eb029394c8bb8c32e1aa1eb0933d4fcd8746f60452afac3
|
7
|
+
data.tar.gz: a0857b56731a9fab6d53748700a932fc3d6a3c7b065dd8ce54d03bf89274002f215e6ae39fa0b72d49c60cb4c7cc11307f7e4d3b1f965f7f4f5a721545712978
|
data/lib/api_config.rb
ADDED
@@ -0,0 +1,128 @@
|
|
1
|
+
require 'constants'
|
2
|
+
|
3
|
+
class UnsupportedConfigException < StandardError
|
4
|
+
end
|
5
|
+
|
6
|
+
module Statsig
|
7
|
+
class APIConfig
|
8
|
+
attr_accessor :name, :type, :is_active, :salt, :default_value, :enabled,
|
9
|
+
:rules, :id_type, :entity, :explicit_parameters, :has_shared_params, :target_app_ids
|
10
|
+
|
11
|
+
def initialize(name:, type:, is_active:, salt:, default_value:, enabled:, rules:, id_type:, entity:,
|
12
|
+
explicit_parameters: nil, has_shared_params: nil, target_app_ids: nil)
|
13
|
+
@name = name
|
14
|
+
@type = type.to_sym unless entity.nil?
|
15
|
+
@is_active = is_active
|
16
|
+
@salt = salt
|
17
|
+
@default_value = JSON.parse(JSON.generate(default_value))
|
18
|
+
@enabled = enabled
|
19
|
+
@rules = rules
|
20
|
+
@id_type = id_type
|
21
|
+
@entity = entity.to_sym unless entity.nil?
|
22
|
+
@explicit_parameters = explicit_parameters
|
23
|
+
@has_shared_params = has_shared_params
|
24
|
+
@target_app_ids = target_app_ids
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.from_json(json)
|
28
|
+
new(
|
29
|
+
name: json[:name],
|
30
|
+
type: json[:type],
|
31
|
+
is_active: json[:isActive],
|
32
|
+
salt: json[:salt],
|
33
|
+
default_value: json[:defaultValue] || {},
|
34
|
+
enabled: json[:enabled],
|
35
|
+
rules: json[:rules]&.map do |rule|
|
36
|
+
APIRule.from_json(rule)
|
37
|
+
end,
|
38
|
+
id_type: json[:idType],
|
39
|
+
entity: json[:entity],
|
40
|
+
explicit_parameters: json[:explicitParameters],
|
41
|
+
has_shared_params: json[:hasSharedParams],
|
42
|
+
target_app_ids: json[:targetAppIDs]
|
43
|
+
)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
module Statsig
|
49
|
+
class APIRule
|
50
|
+
|
51
|
+
attr_accessor :name, :pass_percentage, :return_value, :id, :salt,
|
52
|
+
:conditions, :id_type, :group_name, :config_delegate, :is_experiment_group
|
53
|
+
|
54
|
+
def initialize(name:, pass_percentage:, return_value:, id:, salt:, conditions:, id_type:,
|
55
|
+
group_name: nil, config_delegate: nil, is_experiment_group: nil)
|
56
|
+
@name = name
|
57
|
+
@pass_percentage = pass_percentage.to_f
|
58
|
+
@return_value = JSON.parse(JSON.generate(return_value))
|
59
|
+
@id = id
|
60
|
+
@salt = salt
|
61
|
+
@conditions = conditions
|
62
|
+
@id_type = id_type
|
63
|
+
@group_name = group_name
|
64
|
+
@config_delegate = config_delegate
|
65
|
+
@is_experiment_group = is_experiment_group
|
66
|
+
end
|
67
|
+
|
68
|
+
def self.from_json(json)
|
69
|
+
new(
|
70
|
+
name: json[:name],
|
71
|
+
pass_percentage: json[:passPercentage],
|
72
|
+
return_value: json[:returnValue] || {},
|
73
|
+
id: json[:id],
|
74
|
+
salt: json[:salt],
|
75
|
+
conditions: json[:conditions]&.map do |condition|
|
76
|
+
APICondition.from_json(condition)
|
77
|
+
end,
|
78
|
+
id_type: json[:idType],
|
79
|
+
group_name: json[:groupName],
|
80
|
+
config_delegate: json[:configDelegate],
|
81
|
+
is_experiment_group: json[:isExperimentGroup]
|
82
|
+
)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
module Statsig
|
88
|
+
class APICondition
|
89
|
+
|
90
|
+
attr_accessor :type, :target_value, :operator, :field, :additional_values, :id_type
|
91
|
+
|
92
|
+
def initialize(type:, target_value:, operator:, field:, additional_values:, id_type:)
|
93
|
+
@type = type.to_sym unless type.nil?
|
94
|
+
@target_value = target_value
|
95
|
+
@operator = operator.to_sym unless operator.nil?
|
96
|
+
@field = field
|
97
|
+
@additional_values = additional_values || {}
|
98
|
+
@id_type = id_type
|
99
|
+
end
|
100
|
+
|
101
|
+
def self.from_json(json)
|
102
|
+
operator = json[:operator]
|
103
|
+
unless operator.nil?
|
104
|
+
operator = operator&.downcase&.to_sym
|
105
|
+
unless Const::SUPPORTED_OPERATORS.include?(operator)
|
106
|
+
raise UnsupportedConfigException
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
type = json[:type]
|
111
|
+
unless type.nil?
|
112
|
+
type = type&.downcase&.to_sym
|
113
|
+
unless Const::SUPPORTED_CONDITION_TYPES.include?(type)
|
114
|
+
raise UnsupportedConfigException
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
new(
|
119
|
+
type: json[:type],
|
120
|
+
target_value: json[:targetValue],
|
121
|
+
operator: json[:operator],
|
122
|
+
field: json[:field],
|
123
|
+
additional_values: json[:additionalValues],
|
124
|
+
id_type: json[:idType]
|
125
|
+
)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
@@ -1,132 +1,160 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
:is_experiment_group => false,
|
7
|
-
:secondary_exposures => []
|
8
|
-
}
|
9
|
-
|
10
|
-
module ClientInitializeHelpers
|
1
|
+
require_relative 'hash_utils'
|
2
|
+
|
3
|
+
require 'constants'
|
4
|
+
|
5
|
+
module Statsig
|
11
6
|
class ResponseFormatter
|
12
|
-
def
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
7
|
+
def self.get_responses(
|
8
|
+
entities,
|
9
|
+
evaluator,
|
10
|
+
user,
|
11
|
+
client_sdk_key,
|
12
|
+
hash_algo,
|
13
|
+
include_exposures: true,
|
14
|
+
include_local_overrides: false
|
15
|
+
)
|
16
|
+
result = {}
|
17
|
+
entities.each do |name, spec|
|
18
|
+
hashed_name, value = to_response(name, spec, evaluator, user, client_sdk_key, hash_algo, include_exposures, include_local_overrides)
|
19
|
+
if !hashed_name.nil? && !value.nil?
|
20
|
+
result[hashed_name] = value
|
21
|
+
end
|
22
|
+
end
|
17
23
|
|
18
|
-
|
19
|
-
@specs[key]
|
20
|
-
.map { |name, spec| to_response(name, spec) }
|
21
|
-
.delete_if { |v| v.nil? }.to_h
|
24
|
+
result
|
22
25
|
end
|
23
26
|
|
24
|
-
|
27
|
+
def self.to_response(config_name, config_spec, evaluator, user, client_sdk_key, hash_algo, include_exposures, include_local_overrides)
|
28
|
+
target_app_id = evaluator.spec_store.get_app_id_for_sdk_key(client_sdk_key)
|
29
|
+
config_target_apps = config_spec.target_app_ids
|
25
30
|
|
26
|
-
|
27
|
-
eval_result = @evaluator.eval_spec(@user, config_spec)
|
28
|
-
if eval_result.nil?
|
31
|
+
unless target_app_id.nil? || (!config_target_apps.nil? && config_target_apps.include?(target_app_id))
|
29
32
|
return nil
|
30
33
|
end
|
31
34
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
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
|
-
}
|
35
|
+
category = config_spec.type
|
36
|
+
entity_type = config_spec.entity
|
37
|
+
if entity_type == :segment || entity_type == :holdout
|
38
|
+
return nil
|
39
|
+
end
|
41
40
|
|
42
|
-
|
43
|
-
|
41
|
+
if include_local_overrides
|
42
|
+
case category
|
43
|
+
when :feature_gate
|
44
|
+
local_override = evaluator.lookup_gate_override(config_name)
|
45
|
+
when :dynamic_config
|
46
|
+
local_override = evaluator.lookup_config_override(config_name)
|
47
|
+
end
|
48
|
+
end
|
44
49
|
|
45
|
-
|
50
|
+
if local_override.nil?
|
51
|
+
eval_result = ConfigResult.new(
|
52
|
+
name: config_name,
|
53
|
+
disable_evaluation_details: true,
|
54
|
+
disable_exposures: !include_exposures
|
55
|
+
)
|
56
|
+
evaluator.eval_spec(user, config_spec, eval_result)
|
57
|
+
else
|
58
|
+
eval_result = local_override
|
59
|
+
end
|
46
60
|
|
47
|
-
|
61
|
+
result = {}
|
48
62
|
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
63
|
+
result[:id_type] = eval_result.id_type
|
64
|
+
unless eval_result.group_name.nil?
|
65
|
+
result[:group_name] = eval_result.group_name
|
66
|
+
end
|
53
67
|
|
54
|
-
|
55
|
-
when
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
result[
|
68
|
+
case category
|
69
|
+
when :feature_gate
|
70
|
+
result[:value] = eval_result.gate_value
|
71
|
+
when :dynamic_config
|
72
|
+
id_type = config_spec.id_type
|
73
|
+
result[:value] = eval_result.json_value
|
74
|
+
result[:group] = eval_result.rule_id
|
75
|
+
result[:is_device_based] = id_type.is_a?(String) && id_type.downcase == Statsig::Const::STABLEID
|
60
76
|
else
|
61
77
|
return nil
|
62
78
|
end
|
63
79
|
|
64
|
-
if entity_type ==
|
65
|
-
populate_experiment_fields(
|
80
|
+
if entity_type == :experiment
|
81
|
+
populate_experiment_fields(name, config_spec, eval_result, result, evaluator)
|
82
|
+
end
|
83
|
+
|
84
|
+
if entity_type == :layer
|
85
|
+
populate_layer_fields(config_spec, eval_result, result, evaluator, hash_algo, include_exposures)
|
86
|
+
result.delete(:id_type) # not exposed for layer configs in /initialize
|
66
87
|
end
|
67
88
|
|
68
|
-
|
69
|
-
|
89
|
+
hashed_name = hash_name(config_name, hash_algo)
|
90
|
+
|
91
|
+
result[:name] = hashed_name
|
92
|
+
result[:rule_id] = eval_result.rule_id
|
93
|
+
|
94
|
+
if include_exposures
|
95
|
+
result[:secondary_exposures] = clean_exposures(eval_result.secondary_exposures)
|
70
96
|
end
|
71
97
|
|
72
|
-
hashed_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
|
-
})]
|
98
|
+
[hashed_name, result]
|
79
99
|
end
|
80
100
|
|
81
|
-
def clean_exposures(exposures)
|
101
|
+
def self.clean_exposures(exposures)
|
82
102
|
seen = {}
|
83
103
|
exposures.reject do |exposure|
|
84
|
-
key = "#{exposure[
|
104
|
+
key = "#{exposure[:gate]}|#{exposure[:gateValue]}|#{exposure[:ruleID]}}"
|
85
105
|
should_reject = seen[key]
|
86
106
|
seen[key] = true
|
87
107
|
should_reject == true
|
88
108
|
end
|
89
109
|
end
|
90
110
|
|
91
|
-
def populate_experiment_fields(config_name, config_spec, eval_result, result)
|
92
|
-
result[
|
93
|
-
result[
|
111
|
+
def self.populate_experiment_fields(config_name, config_spec, eval_result, result, evaluator)
|
112
|
+
result[:is_user_in_experiment] = eval_result.is_experiment_group
|
113
|
+
result[:is_experiment_active] = config_spec.is_active == true
|
94
114
|
|
95
|
-
if config_spec
|
115
|
+
if config_spec.has_shared_params != true
|
96
116
|
return
|
97
117
|
end
|
98
118
|
|
99
|
-
result[
|
100
|
-
result[
|
119
|
+
result[:is_in_layer] = true
|
120
|
+
result[:explicit_parameters] = config_spec.explicit_parameters || []
|
101
121
|
|
102
|
-
layer_name =
|
103
|
-
if layer_name.nil? ||
|
122
|
+
layer_name = evaluator.spec_store.experiment_to_layer[config_name]
|
123
|
+
if layer_name.nil? || evaluator.spec_store.layers[layer_name].nil?
|
104
124
|
return
|
105
125
|
end
|
106
126
|
|
107
|
-
layer =
|
108
|
-
result[
|
127
|
+
layer = evaluator.spec_store.layers[layer_name]
|
128
|
+
result[:value] = layer[:defaultValue].merge(result[:value])
|
109
129
|
end
|
110
130
|
|
111
|
-
def populate_layer_fields(config_spec, eval_result, result)
|
112
|
-
delegate = eval_result
|
113
|
-
result[
|
131
|
+
def self.populate_layer_fields(config_spec, eval_result, result, evaluator, hash_algo, include_exposures)
|
132
|
+
delegate = eval_result.config_delegate
|
133
|
+
result[:explicit_parameters] = config_spec.explicit_parameters || []
|
114
134
|
|
115
135
|
if delegate.nil? == false && delegate.empty? == false
|
116
|
-
delegate_spec =
|
117
|
-
delegate_result = @evaluator.eval_spec(@user, delegate_spec)
|
136
|
+
delegate_spec = evaluator.spec_store.configs[delegate]
|
118
137
|
|
119
|
-
result[
|
120
|
-
result[
|
121
|
-
result[
|
122
|
-
result[
|
138
|
+
result[:allocated_experiment_name] = hash_name(delegate, hash_algo)
|
139
|
+
result[:is_user_in_experiment] = eval_result.is_experiment_group
|
140
|
+
result[:is_experiment_active] = delegate_spec.is_active == true
|
141
|
+
result[:explicit_parameters] = delegate_spec.explicit_parameters || []
|
123
142
|
end
|
124
143
|
|
125
|
-
|
144
|
+
if include_exposures
|
145
|
+
result[:undelegated_secondary_exposures] = clean_exposures(eval_result.undelegated_sec_exps || [])
|
146
|
+
end
|
126
147
|
end
|
127
148
|
|
128
|
-
def hash_name(name)
|
129
|
-
|
149
|
+
def self.hash_name(name, hash_algo)
|
150
|
+
case hash_algo
|
151
|
+
when Statsig::Const::NONE
|
152
|
+
return name
|
153
|
+
when Statsig::Const::DJB2
|
154
|
+
return Statsig::HashUtils.djb2(name)
|
155
|
+
else
|
156
|
+
return Statsig::HashUtils.sha256(name)
|
157
|
+
end
|
130
158
|
end
|
131
159
|
end
|
132
|
-
end
|
160
|
+
end
|
data/lib/config_result.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
|
-
# typed: true
|
2
1
|
module Statsig
|
3
2
|
class ConfigResult
|
3
|
+
|
4
4
|
attr_accessor :name
|
5
5
|
attr_accessor :gate_value
|
6
6
|
attr_accessor :json_value
|
@@ -13,24 +13,31 @@ module Statsig
|
|
13
13
|
attr_accessor :evaluation_details
|
14
14
|
attr_accessor :group_name
|
15
15
|
attr_accessor :id_type
|
16
|
+
attr_accessor :target_app_ids
|
17
|
+
attr_accessor :disable_evaluation_details
|
18
|
+
attr_accessor :disable_exposures
|
16
19
|
|
17
20
|
def initialize(
|
18
|
-
name
|
19
|
-
gate_value
|
20
|
-
json_value
|
21
|
-
rule_id
|
22
|
-
secondary_exposures
|
23
|
-
config_delegate
|
24
|
-
explicit_parameters
|
21
|
+
name:,
|
22
|
+
gate_value: false,
|
23
|
+
json_value: nil,
|
24
|
+
rule_id: nil,
|
25
|
+
secondary_exposures: [],
|
26
|
+
config_delegate: nil,
|
27
|
+
explicit_parameters: nil,
|
25
28
|
is_experiment_group: false,
|
26
29
|
evaluation_details: nil,
|
27
30
|
group_name: nil,
|
28
|
-
id_type:
|
31
|
+
id_type: nil,
|
32
|
+
target_app_ids: nil,
|
33
|
+
disable_evaluation_details: false,
|
34
|
+
disable_exposures: false
|
35
|
+
)
|
29
36
|
@name = name
|
30
37
|
@gate_value = gate_value
|
31
38
|
@json_value = json_value
|
32
39
|
@rule_id = rule_id
|
33
|
-
@secondary_exposures = secondary_exposures
|
40
|
+
@secondary_exposures = secondary_exposures
|
34
41
|
@undelegated_sec_exps = @secondary_exposures
|
35
42
|
@config_delegate = config_delegate
|
36
43
|
@explicit_parameters = explicit_parameters
|
@@ -38,6 +45,30 @@ module Statsig
|
|
38
45
|
@evaluation_details = evaluation_details
|
39
46
|
@group_name = group_name
|
40
47
|
@id_type = id_type
|
48
|
+
@target_app_ids = target_app_ids
|
49
|
+
@disable_evaluation_details = disable_evaluation_details
|
50
|
+
@disable_exposures = disable_exposures
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.from_user_persisted_values(config_name, user_persisted_values)
|
54
|
+
sticky_values = user_persisted_values[config_name]
|
55
|
+
return nil if sticky_values.nil?
|
56
|
+
|
57
|
+
from_hash(config_name, sticky_values)
|
58
|
+
end
|
59
|
+
|
60
|
+
def to_hash
|
61
|
+
{
|
62
|
+
json_value: @json_value,
|
63
|
+
gate_value: @gate_value,
|
64
|
+
rule_id: @rule_id,
|
65
|
+
secondary_exposures: @secondary_exposures,
|
66
|
+
config_sync_time: @evaluation_details.config_sync_time,
|
67
|
+
init_time: @init_time,
|
68
|
+
group_name: @group_name,
|
69
|
+
id_type: @id_type,
|
70
|
+
target_app_ids: @target_app_ids
|
71
|
+
}
|
41
72
|
end
|
42
73
|
end
|
43
|
-
end
|
74
|
+
end
|
data/lib/constants.rb
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
module Statsig
|
2
|
+
module Const
|
3
|
+
EMPTY_STR = ''.freeze
|
4
|
+
|
5
|
+
SUPPORTED_CONDITION_TYPES = Set.new(%i[
|
6
|
+
public fail_gate pass_gate ip_based ua_based user_field
|
7
|
+
environment_field current_time user_bucket unit_id
|
8
|
+
]).freeze
|
9
|
+
|
10
|
+
SUPPORTED_OPERATORS = Set.new(%i[
|
11
|
+
gt gte lt lte version_gt version_gte version_lt version_lte
|
12
|
+
version_eq version_neq any none any_case_sensitive none_case_sensitive
|
13
|
+
str_starts_with_any str_ends_with_any str_contains_any str_contains_none
|
14
|
+
str_matches eq neq before after on in_segment_list not_in_segment_list
|
15
|
+
]).freeze
|
16
|
+
|
17
|
+
APP_VERSION = 'app_version'.freeze
|
18
|
+
APPVERSION = 'appversion'.freeze
|
19
|
+
BROWSER_NAME = 'browser_name'.freeze
|
20
|
+
BROWSER_VERSION = 'browser_version'.freeze
|
21
|
+
BROWSERNAME = 'browsername'.freeze
|
22
|
+
BROWSERVERSION = 'browserversion'.freeze
|
23
|
+
CML_SHA_256 = 'sha256'.freeze
|
24
|
+
CML_USER_ID = 'userID'.freeze
|
25
|
+
COUNTRY = 'country'.freeze
|
26
|
+
DEFAULT = 'default'.freeze
|
27
|
+
DISABLED = 'disabled'.freeze
|
28
|
+
DJB2 = 'djb2'.freeze
|
29
|
+
EMAIL = 'email'.freeze
|
30
|
+
FALSE = 'false'.freeze
|
31
|
+
IP = 'ip'.freeze
|
32
|
+
LAYER = :layer
|
33
|
+
LOCALE = 'locale'.freeze
|
34
|
+
NONE = 'none'.freeze
|
35
|
+
OS_NAME = 'os_name'.freeze
|
36
|
+
OS_VERSION = 'os_version'.freeze
|
37
|
+
OSNAME = 'osname'.freeze
|
38
|
+
OSVERSION = 'osversion'.freeze
|
39
|
+
OVERRIDE = 'override'.freeze
|
40
|
+
Q_RIGHT_CHEVRON = 'Q>'.freeze
|
41
|
+
STABLEID = 'stableid'.freeze
|
42
|
+
STATSIG_RUBY_SDK = 'statsig-ruby-sdk'.freeze
|
43
|
+
TRUE = 'true'.freeze
|
44
|
+
USER_AGENT = 'user_agent'.freeze
|
45
|
+
USER_ID = 'user_id'.freeze
|
46
|
+
USERAGENT = 'useragent'.freeze
|
47
|
+
USERID = 'userid'.freeze
|
48
|
+
|
49
|
+
# Persisted Evaluations
|
50
|
+
GATE_VALUE = 'gate_value'.freeze
|
51
|
+
JSON_VALUE = 'json_value'.freeze
|
52
|
+
RULE_ID = 'rule_id'.freeze
|
53
|
+
SECONDARY_EXPOSURES = 'secondary_exposures'.freeze
|
54
|
+
GROUP_NAME = 'group_name'.freeze
|
55
|
+
ID_TYPE = 'id_type'.freeze
|
56
|
+
TARGET_APP_IDS = 'target_app_ids'.freeze
|
57
|
+
CONFIG_SYNC_TIME = 'config_sync_time'.freeze
|
58
|
+
INIT_TIME = 'init_time'.freeze
|
59
|
+
end
|
60
|
+
end
|
data/lib/diagnostics.rb
CHANGED
@@ -1,103 +1,81 @@
|
|
1
|
-
# typed: true
|
2
|
-
|
3
|
-
require 'sorbet-runtime'
|
4
|
-
|
5
1
|
module Statsig
|
6
2
|
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
3
|
attr_reader :markers
|
14
4
|
|
15
|
-
|
16
|
-
@context = context
|
17
|
-
@markers = []
|
18
|
-
end
|
5
|
+
attr_accessor :sample_rates
|
19
6
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
action: String,
|
24
|
-
step: T.any(String, NilClass),
|
25
|
-
value: T.any(String, Integer, T::Boolean, NilClass),
|
26
|
-
metadata: T.any(T::Hash[Symbol, T.untyped], NilClass)
|
27
|
-
).void
|
7
|
+
def initialize()
|
8
|
+
@markers = {:initialize => [], :api_call => [], :config_sync => []}
|
9
|
+
@sample_rates = {}
|
28
10
|
end
|
29
11
|
|
30
|
-
def mark(key, action, step
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
12
|
+
def mark(key, action, step, tags, context)
|
13
|
+
marker = {
|
14
|
+
key: key,
|
15
|
+
action: action,
|
16
|
+
timestamp: (Time.now.to_f * 1000).to_i
|
17
|
+
}
|
18
|
+
if !step.nil?
|
19
|
+
marker[:step] = step
|
20
|
+
end
|
21
|
+
tags.each do |key, val|
|
22
|
+
unless val.nil?
|
23
|
+
marker[key] = val
|
24
|
+
end
|
25
|
+
end
|
26
|
+
if @markers[context].nil?
|
27
|
+
@markers[context] = []
|
28
|
+
end
|
29
|
+
@markers[context].push(marker)
|
39
30
|
end
|
40
31
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
step: T.any(String, NilClass),
|
45
|
-
value: T.any(String, Integer, T::Boolean, NilClass),
|
46
|
-
metadata: T.any(T::Hash[Symbol, T.untyped], NilClass)
|
47
|
-
).returns(Tracker)
|
48
|
-
end
|
49
|
-
def track(key, step = nil, value = nil, metadata = nil)
|
50
|
-
tracker = Tracker.new(self, key, step, metadata)
|
51
|
-
tracker.start(value)
|
32
|
+
def track(context, key, step = nil, tags = {})
|
33
|
+
tracker = Tracker.new(self, context, key, step, tags)
|
34
|
+
tracker.start(**tags)
|
52
35
|
tracker
|
53
36
|
end
|
54
37
|
|
55
|
-
|
56
|
-
|
57
|
-
|
38
|
+
def serialize_with_sampling(context)
|
39
|
+
marker_keys = @markers[context].map { |e| e[:key] }
|
40
|
+
unique_marker_keys = marker_keys.uniq { |e| e }
|
41
|
+
sampled_marker_keys = unique_marker_keys.select do |key|
|
42
|
+
@sample_rates.key?(key) && !self.class.sample(@sample_rates[key])
|
43
|
+
end
|
44
|
+
final_markers = @markers[context].select do |marker|
|
45
|
+
!sampled_marker_keys.include?(marker[:key])
|
46
|
+
end
|
58
47
|
{
|
59
|
-
context:
|
60
|
-
markers:
|
48
|
+
context: context.clone,
|
49
|
+
markers: final_markers.clone
|
61
50
|
}
|
62
51
|
end
|
63
52
|
|
64
|
-
def clear_markers
|
65
|
-
@markers.clear
|
53
|
+
def clear_markers(context)
|
54
|
+
@markers[context].clear
|
66
55
|
end
|
67
56
|
|
68
|
-
|
69
|
-
|
70
|
-
CONFIG_SYNC = 'config_sync'.freeze
|
71
|
-
API_CALL = 'api_call'.freeze
|
57
|
+
def self.sample(rate_over_ten_thousand)
|
58
|
+
rand * 10_000 < rate_over_ten_thousand
|
72
59
|
end
|
73
60
|
|
74
61
|
API_CALL_KEYS = %w[check_gate get_config get_experiment get_layer].freeze
|
75
62
|
|
76
63
|
class Tracker
|
77
|
-
|
78
|
-
|
79
|
-
sig do
|
80
|
-
params(
|
81
|
-
diagnostics: Diagnostics,
|
82
|
-
key: String,
|
83
|
-
step: T.any(String, NilClass),
|
84
|
-
metadata: T.any(T::Hash[Symbol, T.untyped], NilClass)
|
85
|
-
).void
|
86
|
-
end
|
87
|
-
def initialize(diagnostics, key, step, metadata)
|
64
|
+
def initialize(diagnostics, context, key, step, tags = {})
|
88
65
|
@diagnostics = diagnostics
|
66
|
+
@context = context
|
89
67
|
@key = key
|
90
68
|
@step = step
|
91
|
-
@
|
69
|
+
@tags = tags
|
92
70
|
end
|
93
71
|
|
94
|
-
def start(
|
95
|
-
@diagnostics.mark(@key, 'start', @step,
|
72
|
+
def start(**tags)
|
73
|
+
@diagnostics.mark(@key, 'start', @step, tags.nil? ? {} : tags.merge(@tags), @context)
|
96
74
|
end
|
97
75
|
|
98
|
-
def end(
|
99
|
-
@diagnostics.mark(@key, 'end', @step,
|
76
|
+
def end(**tags)
|
77
|
+
@diagnostics.mark(@key, 'end', @step, tags.nil? ? {} : tags.merge(@tags), @context)
|
100
78
|
end
|
101
79
|
end
|
102
80
|
end
|
103
|
-
end
|
81
|
+
end
|