statsig 1.7.0.beta.2 → 1.8.2
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/config_result.rb +15 -12
- data/lib/evaluation_helpers.rb +6 -4
- data/lib/evaluator.rb +261 -230
- data/lib/network.rb +51 -74
- data/lib/spec_store.rb +138 -38
- data/lib/statsig.rb +13 -4
- data/lib/statsig_driver.rb +13 -32
- data/lib/statsig_logger.rb +48 -47
- data/lib/statsig_user.rb +6 -2
- metadata +4 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 572aeb29b8bbc76e436127efd30c2fc01d0faa66f95aca1c15fdc094e43000b4
|
4
|
+
data.tar.gz: c9cfa2cfb4ae0d974d261119ab056600793febe578f2d8bf0f34c828b3e93ec5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4e90695bad164518cb6f61a62b9b44c8ec2384a30d1e44d17bf4434230ddc85c56c8365b71e4d8768efda2026302e11118c60a69b0cdcbf6898d1bb6fe7d469b
|
7
|
+
data.tar.gz: 2a9f90c772b22f920d2f3821c872a5d28b87d2db9da80b0d2ce66cf7ea7d3003dea337c1f2cee44ce2c14c3fd7bc8428190708a7b4da1335d6fdf8075349e94c
|
data/lib/config_result.rb
CHANGED
@@ -1,15 +1,18 @@
|
|
1
|
-
class ConfigResult
|
2
|
-
attr_accessor :name
|
3
|
-
attr_accessor :gate_value
|
4
|
-
attr_accessor :json_value
|
5
|
-
attr_accessor :rule_id
|
6
|
-
attr_accessor :secondary_exposures
|
7
1
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
2
|
+
module Statsig
|
3
|
+
class ConfigResult
|
4
|
+
attr_accessor :name
|
5
|
+
attr_accessor :gate_value
|
6
|
+
attr_accessor :json_value
|
7
|
+
attr_accessor :rule_id
|
8
|
+
attr_accessor :secondary_exposures
|
9
|
+
|
10
|
+
def initialize(name, gate_value = false, json_value = {}, rule_id = '', secondary_exposures = [])
|
11
|
+
@name = name
|
12
|
+
@gate_value = gate_value
|
13
|
+
@json_value = json_value
|
14
|
+
@rule_id = rule_id
|
15
|
+
@secondary_exposures = secondary_exposures.is_a?(Array) ? secondary_exposures : []
|
16
|
+
end
|
14
17
|
end
|
15
18
|
end
|
data/lib/evaluation_helpers.rb
CHANGED
@@ -2,7 +2,7 @@ require 'time'
|
|
2
2
|
|
3
3
|
module EvaluationHelpers
|
4
4
|
def self.compare_numbers(a, b, func)
|
5
|
-
return false unless
|
5
|
+
return false unless is_numeric(a) && is_numeric(b)
|
6
6
|
func.call(a.to_f, b.to_f) rescue false
|
7
7
|
end
|
8
8
|
|
@@ -15,20 +15,22 @@ module EvaluationHelpers
|
|
15
15
|
|
16
16
|
def self.compare_times(a, b, func)
|
17
17
|
begin
|
18
|
-
time_1 =
|
19
|
-
time_2 =
|
18
|
+
time_1 = get_epoch_time(a)
|
19
|
+
time_2 = get_epoch_time(b)
|
20
20
|
func.call(time_1, time_2)
|
21
21
|
rescue
|
22
22
|
false
|
23
23
|
end
|
24
24
|
end
|
25
25
|
|
26
|
+
private
|
27
|
+
|
26
28
|
def self.is_numeric(v)
|
27
29
|
!(v.to_s =~ /\A[-+]?\d*\.?\d+\z/).nil?
|
28
30
|
end
|
29
31
|
|
30
32
|
def self.get_epoch_time(v)
|
31
|
-
time =
|
33
|
+
time = is_numeric(v) ? Time.at(v.to_f) : Time.parse(v)
|
32
34
|
if time.year > Time.now.year + 100
|
33
35
|
# divide by 1000 when the epoch time is in milliseconds instead of seconds
|
34
36
|
return time.to_i / 1000
|
data/lib/evaluator.rb
CHANGED
@@ -10,271 +10,302 @@ require 'user_agent_parser/operating_system'
|
|
10
10
|
$fetch_from_server = :fetch_from_server
|
11
11
|
$type_dynamic_config = 'dynamic_config'
|
12
12
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
13
|
+
module Statsig
|
14
|
+
class Evaluator
|
15
|
+
def initialize(network, error_callback)
|
16
|
+
@spec_store = Statsig::SpecStore.new(network, error_callback)
|
17
|
+
@ua_parser = UserAgentParser::Parser.new
|
18
|
+
CountryLookup.initialize
|
19
|
+
@initialized = true
|
20
|
+
end
|
20
21
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
22
|
+
def check_gate(user, gate_name)
|
23
|
+
return nil unless @initialized && @spec_store.has_gate?(gate_name)
|
24
|
+
eval_spec(user, @spec_store.get_gate(gate_name))
|
25
|
+
end
|
25
26
|
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
27
|
+
def get_config(user, config_name)
|
28
|
+
return nil unless @initialized && @spec_store.has_config?(config_name)
|
29
|
+
eval_spec(user, @spec_store.get_config(config_name))
|
30
|
+
end
|
31
|
+
|
32
|
+
def shutdown
|
33
|
+
@spec_store.shutdown
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
30
37
|
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
38
|
+
def eval_spec(user, config)
|
39
|
+
default_rule_id = 'default'
|
40
|
+
exposures = []
|
41
|
+
if config['enabled']
|
42
|
+
i = 0
|
43
|
+
until i >= config['rules'].length do
|
44
|
+
rule = config['rules'][i]
|
45
|
+
result = eval_rule(user, rule)
|
46
|
+
return $fetch_from_server if result == $fetch_from_server
|
47
|
+
exposures = exposures + result['exposures'] if result['exposures'].is_a? Array
|
48
|
+
if result['value']
|
49
|
+
pass = eval_pass_percent(user, rule, config['salt'])
|
50
|
+
return Statsig::ConfigResult.new(
|
51
|
+
config['name'],
|
52
|
+
pass,
|
53
|
+
pass ? rule['returnValue'] : config['defaultValue'],
|
54
|
+
rule['id'],
|
55
|
+
exposures
|
56
|
+
)
|
57
|
+
end
|
58
|
+
|
59
|
+
i += 1
|
60
|
+
end
|
61
|
+
else
|
62
|
+
default_rule_id = 'disabled'
|
63
|
+
end
|
64
|
+
|
65
|
+
Statsig::ConfigResult.new(config['name'], false, config['defaultValue'], default_rule_id, exposures)
|
66
|
+
end
|
67
|
+
|
68
|
+
def eval_rule(user, rule)
|
69
|
+
exposures = []
|
70
|
+
pass = true
|
35
71
|
i = 0
|
36
|
-
until i >=
|
37
|
-
|
38
|
-
result
|
39
|
-
|
40
|
-
exposures = exposures + result['exposures'] if result['exposures'].is_a? Array
|
41
|
-
if result['value']
|
42
|
-
pass = self.eval_pass_percent(user, rule, config['salt'])
|
43
|
-
return ConfigResult.new(
|
44
|
-
config['name'],
|
45
|
-
pass,
|
46
|
-
pass ? rule['returnValue'] : config['defaultValue'],
|
47
|
-
rule['id'],
|
48
|
-
exposures
|
49
|
-
)
|
72
|
+
until i >= rule['conditions'].length do
|
73
|
+
result = eval_condition(user, rule['conditions'][i])
|
74
|
+
if result == $fetch_from_server
|
75
|
+
return $fetch_from_server
|
50
76
|
end
|
51
77
|
|
78
|
+
if result.is_a?(Hash)
|
79
|
+
exposures = exposures + result['exposures'] if result['exposures'].is_a? Array
|
80
|
+
pass = false if result['value'] == false
|
81
|
+
elsif result == false
|
82
|
+
pass = false
|
83
|
+
end
|
52
84
|
i += 1
|
53
85
|
end
|
54
|
-
|
86
|
+
{ 'value' => pass, 'exposures' => exposures }
|
55
87
|
end
|
56
88
|
|
57
|
-
|
58
|
-
|
89
|
+
def eval_condition(user, condition)
|
90
|
+
value = nil
|
91
|
+
field = condition['field']
|
92
|
+
target = condition['targetValue']
|
93
|
+
type = condition['type']
|
94
|
+
operator = condition['operator']
|
95
|
+
additional_values = condition['additionalValues']
|
96
|
+
additional_values = Hash.new unless additional_values.is_a? Hash
|
97
|
+
idType = condition['idType']
|
98
|
+
|
99
|
+
return $fetch_from_server unless type.is_a? String
|
100
|
+
type = type.downcase
|
59
101
|
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
102
|
+
case type
|
103
|
+
when 'public'
|
104
|
+
return true
|
105
|
+
when 'fail_gate', 'pass_gate'
|
106
|
+
other_gate_result = check_gate(user, target)
|
107
|
+
return $fetch_from_server if other_gate_result == $fetch_from_server
|
108
|
+
|
109
|
+
gate_value = other_gate_result&.gate_value == true
|
110
|
+
new_exposure = {
|
111
|
+
'gate' => target,
|
112
|
+
'gateValue' => gate_value ? 'true' : 'false',
|
113
|
+
'ruleID' => other_gate_result&.rule_id
|
114
|
+
}
|
115
|
+
exposures = other_gate_result&.secondary_exposures&.append(new_exposure)
|
116
|
+
return {
|
117
|
+
'value' => type == 'pass_gate' ? gate_value : !gate_value,
|
118
|
+
'exposures' => exposures
|
119
|
+
}
|
120
|
+
when 'ip_based'
|
121
|
+
value = get_value_from_user(user, field) || get_value_from_ip(user, field)
|
122
|
+
return $fetch_from_server if value == $fetch_from_server
|
123
|
+
when 'ua_based'
|
124
|
+
value = get_value_from_user(user, field) || get_value_from_ua(user, field)
|
125
|
+
return $fetch_from_server if value == $fetch_from_server
|
126
|
+
when 'user_field'
|
127
|
+
value = get_value_from_user(user, field)
|
128
|
+
when 'environment_field'
|
129
|
+
value = get_value_from_environment(user, field)
|
130
|
+
when 'current_time'
|
131
|
+
value = Time.now.to_f # epoch time in seconds
|
132
|
+
when 'user_bucket'
|
133
|
+
begin
|
134
|
+
salt = additional_values['salt']
|
135
|
+
unit_id = get_unit_id(user, idType) || ''
|
136
|
+
# there are only 1000 user buckets as opposed to 10k for gate pass %
|
137
|
+
value = compute_user_hash("#{salt}.#{unit_id}") % 1000
|
138
|
+
rescue
|
139
|
+
return false
|
140
|
+
end
|
141
|
+
when 'unit_id'
|
142
|
+
value = get_unit_id(user, idType)
|
143
|
+
else
|
67
144
|
return $fetch_from_server
|
68
145
|
end
|
69
146
|
|
70
|
-
if
|
71
|
-
|
72
|
-
pass = false if result['value'] == false
|
73
|
-
elsif result == false
|
74
|
-
pass = false
|
75
|
-
end
|
76
|
-
i += 1
|
77
|
-
end
|
78
|
-
{ 'value' => pass, 'exposures' => exposures }
|
79
|
-
end
|
147
|
+
return $fetch_from_server if value == $fetch_from_server || !operator.is_a?(String)
|
148
|
+
operator = operator.downcase
|
80
149
|
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
value
|
129
|
-
|
130
|
-
return
|
150
|
+
case operator
|
151
|
+
# numerical comparison
|
152
|
+
when 'gt'
|
153
|
+
return EvaluationHelpers::compare_numbers(value, target, ->(a, b) { a > b })
|
154
|
+
when 'gte'
|
155
|
+
return EvaluationHelpers::compare_numbers(value, target, ->(a, b) { a >= b })
|
156
|
+
when 'lt'
|
157
|
+
return EvaluationHelpers::compare_numbers(value, target, ->(a, b) { a < b })
|
158
|
+
when 'lte'
|
159
|
+
return EvaluationHelpers::compare_numbers(value, target, ->(a, b) { a <= b })
|
160
|
+
|
161
|
+
# version comparison
|
162
|
+
when 'version_gt'
|
163
|
+
return (Gem::Version.new(value) > Gem::Version.new(target) rescue false)
|
164
|
+
when 'version_gte'
|
165
|
+
return (Gem::Version.new(value) >= Gem::Version.new(target) rescue false)
|
166
|
+
when 'version_lt'
|
167
|
+
return (Gem::Version.new(value) < Gem::Version.new(target) rescue false)
|
168
|
+
when 'version_lte'
|
169
|
+
return (Gem::Version.new(value) <= Gem::Version.new(target) rescue false)
|
170
|
+
when 'version_eq'
|
171
|
+
return (Gem::Version.new(value) == Gem::Version.new(target) rescue false)
|
172
|
+
when 'version_neq'
|
173
|
+
return (Gem::Version.new(value) != Gem::Version.new(target) rescue false)
|
174
|
+
|
175
|
+
# array operations
|
176
|
+
when 'any'
|
177
|
+
return EvaluationHelpers::match_string_in_array(target, value, true, ->(a, b) { a == b })
|
178
|
+
when 'none'
|
179
|
+
return !EvaluationHelpers::match_string_in_array(target, value, true, ->(a, b) { a == b })
|
180
|
+
when 'any_case_sensitive'
|
181
|
+
return EvaluationHelpers::match_string_in_array(target, value, false, ->(a, b) { a == b })
|
182
|
+
when 'none_case_sensitive'
|
183
|
+
return !EvaluationHelpers::match_string_in_array(target, value, false, ->(a, b) { a == b })
|
184
|
+
|
185
|
+
#string
|
186
|
+
when 'str_starts_with_any'
|
187
|
+
return EvaluationHelpers::match_string_in_array(target, value, true, ->(a, b) { a.start_with?(b) })
|
188
|
+
when 'str_ends_with_any'
|
189
|
+
return EvaluationHelpers::match_string_in_array(target, value, true, ->(a, b) { a.end_with?(b) })
|
190
|
+
when 'str_contains_any'
|
191
|
+
return EvaluationHelpers::match_string_in_array(target, value, true, ->(a, b) { a.include?(b) })
|
192
|
+
when 'str_contains_none'
|
193
|
+
return !EvaluationHelpers::match_string_in_array(target, value, true, ->(a, b) { a.include?(b) })
|
194
|
+
when 'str_matches'
|
195
|
+
return (value.is_a?(String) && !(value =~ Regexp.new(target)).nil? rescue false)
|
196
|
+
when 'eq'
|
197
|
+
return value == target
|
198
|
+
when 'neq'
|
199
|
+
return value != target
|
200
|
+
|
201
|
+
# dates
|
202
|
+
when 'before'
|
203
|
+
return EvaluationHelpers::compare_times(value, target, ->(a, b) { a < b })
|
204
|
+
when 'after'
|
205
|
+
return EvaluationHelpers::compare_times(value, target, ->(a, b) { a > b })
|
206
|
+
when 'on'
|
207
|
+
return EvaluationHelpers::compare_times(value, target, ->(a, b) { a.year == b.year && a.month == b.month && a.day == b.day })
|
208
|
+
when 'in_segment_list', 'not_in_segment_list'
|
209
|
+
begin
|
210
|
+
id_list = (@spec_store.get_id_list(target) || {:ids => {}})[:ids]
|
211
|
+
hashed_id = Digest::SHA256.base64digest(value.to_s)[0, 8]
|
212
|
+
is_in_list = id_list.is_a?(Hash) && id_list[hashed_id] == true
|
213
|
+
|
214
|
+
return is_in_list if operator == 'in_segment_list'
|
215
|
+
return !is_in_list
|
216
|
+
rescue StandardError => e
|
217
|
+
return false
|
218
|
+
end
|
219
|
+
else
|
220
|
+
return $fetch_from_server
|
131
221
|
end
|
132
|
-
else
|
133
|
-
return $fetch_from_server
|
134
222
|
end
|
135
223
|
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
case operator
|
140
|
-
# numerical comparison
|
141
|
-
when 'gt'
|
142
|
-
return EvaluationHelpers::compare_numbers(value, target, ->(a, b) { a > b })
|
143
|
-
when 'gte'
|
144
|
-
return EvaluationHelpers::compare_numbers(value, target, ->(a, b) { a >= b })
|
145
|
-
when 'lt'
|
146
|
-
return EvaluationHelpers::compare_numbers(value, target, ->(a, b) { a < b })
|
147
|
-
when 'lte'
|
148
|
-
return EvaluationHelpers::compare_numbers(value, target, ->(a, b) { a <= b })
|
149
|
-
|
150
|
-
# version comparison
|
151
|
-
when 'version_gt'
|
152
|
-
return (Gem::Version.new(value) > Gem::Version.new(target) rescue false)
|
153
|
-
when 'version_gte'
|
154
|
-
return (Gem::Version.new(value) >= Gem::Version.new(target) rescue false)
|
155
|
-
when 'version_lt'
|
156
|
-
return (Gem::Version.new(value) < Gem::Version.new(target) rescue false)
|
157
|
-
when 'version_lte'
|
158
|
-
return (Gem::Version.new(value) <= Gem::Version.new(target) rescue false)
|
159
|
-
when 'version_eq'
|
160
|
-
return (Gem::Version.new(value) == Gem::Version.new(target) rescue false)
|
161
|
-
when 'version_neq'
|
162
|
-
return (Gem::Version.new(value) != Gem::Version.new(target) rescue false)
|
163
|
-
|
164
|
-
# array operations
|
165
|
-
when 'any'
|
166
|
-
return EvaluationHelpers::match_string_in_array(target, value, true, ->(a, b) { a == b })
|
167
|
-
when 'none'
|
168
|
-
return !EvaluationHelpers::match_string_in_array(target, value, true, ->(a, b) { a == b })
|
169
|
-
when 'any_case_sensitive'
|
170
|
-
return EvaluationHelpers::match_string_in_array(target, value, false, ->(a, b) { a == b })
|
171
|
-
when 'none_case_sensitive'
|
172
|
-
return !EvaluationHelpers::match_string_in_array(target, value, false, ->(a, b) { a == b })
|
173
|
-
|
174
|
-
#string
|
175
|
-
when 'str_starts_with_any'
|
176
|
-
return EvaluationHelpers::match_string_in_array(target, value, true, ->(a, b) { a.start_with?(b) })
|
177
|
-
when 'str_ends_with_any'
|
178
|
-
return EvaluationHelpers::match_string_in_array(target, value, true, ->(a, b) { a.end_with?(b) })
|
179
|
-
when 'str_contains_any'
|
180
|
-
return EvaluationHelpers::match_string_in_array(target, value, true, ->(a, b) { a.include?(b) })
|
181
|
-
when 'str_contains_none'
|
182
|
-
return !EvaluationHelpers::match_string_in_array(target, value, true, ->(a, b) { a.include?(b) })
|
183
|
-
when 'str_matches'
|
184
|
-
return (value.is_a?(String) && !(value =~ Regexp.new(target)).nil? rescue false)
|
185
|
-
when 'eq'
|
186
|
-
return value == target
|
187
|
-
when 'neq'
|
188
|
-
return value != target
|
189
|
-
|
190
|
-
# dates
|
191
|
-
when 'before'
|
192
|
-
return EvaluationHelpers::compare_times(value, target, ->(a, b) { a < b })
|
193
|
-
when 'after'
|
194
|
-
return EvaluationHelpers::compare_times(value, target, ->(a, b) { a > b })
|
195
|
-
when 'on'
|
196
|
-
return EvaluationHelpers::compare_times(value, target, ->(a, b) { a.year == b.year && a.month == b.month && a.day == b.day })
|
197
|
-
else
|
198
|
-
return $fetch_from_server
|
199
|
-
end
|
200
|
-
end
|
224
|
+
def get_value_from_user(user, field)
|
225
|
+
return nil unless user.instance_of?(StatsigUser) && field.is_a?(String)
|
201
226
|
|
202
|
-
|
203
|
-
|
227
|
+
user_lookup_table = user&.value_lookup
|
228
|
+
return nil unless user_lookup_table.is_a?(Hash)
|
229
|
+
return user_lookup_table[field.downcase] if user_lookup_table.has_key?(field.downcase) && !user_lookup_table[field.downcase].nil?
|
204
230
|
|
205
|
-
|
206
|
-
|
207
|
-
|
231
|
+
user_custom = user_lookup_table['custom']
|
232
|
+
if user_custom.is_a?(Hash)
|
233
|
+
user_custom.each do |key, value|
|
234
|
+
return value if key.downcase.casecmp?(field.downcase) && !value.nil?
|
235
|
+
end
|
236
|
+
end
|
208
237
|
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
238
|
+
private_attributes = user_lookup_table['privateAttributes']
|
239
|
+
if private_attributes.is_a?(Hash)
|
240
|
+
private_attributes.each do |key, value|
|
241
|
+
return value if key.downcase.casecmp?(field.downcase) && !value.nil?
|
242
|
+
end
|
213
243
|
end
|
244
|
+
|
245
|
+
nil
|
214
246
|
end
|
215
247
|
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
248
|
+
def get_value_from_environment(user, field)
|
249
|
+
return nil unless user.instance_of?(StatsigUser) && field.is_a?(String)
|
250
|
+
field = field.downcase
|
251
|
+
return nil unless user.statsig_environment.is_a? Hash
|
252
|
+
user.statsig_environment.each do |key, value|
|
253
|
+
return value if key.downcase == (field)
|
220
254
|
end
|
255
|
+
nil
|
221
256
|
end
|
222
257
|
|
223
|
-
|
224
|
-
|
258
|
+
def get_value_from_ip(user, field)
|
259
|
+
return nil unless user.is_a?(StatsigUser) && field.is_a?(String) && field.downcase == 'country'
|
260
|
+
ip = get_value_from_user(user, 'ip')
|
261
|
+
return nil unless ip.is_a?(String)
|
225
262
|
|
226
|
-
|
227
|
-
return nil unless user.instance_of?(StatsigUser) && field.is_a?(String)
|
228
|
-
field = field.downcase
|
229
|
-
return nil unless user.statsig_environment.is_a? Hash
|
230
|
-
user.statsig_environment.each do |key, value|
|
231
|
-
return value if key.downcase == (field)
|
263
|
+
CountryLookup.lookup_ip_string(ip)
|
232
264
|
end
|
233
|
-
nil
|
234
|
-
end
|
235
265
|
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
266
|
+
def get_value_from_ua(user, field)
|
267
|
+
return nil unless user.is_a?(StatsigUser) && field.is_a?(String)
|
268
|
+
ua = get_value_from_user(user, 'userAgent')
|
269
|
+
return nil unless ua.is_a?(String)
|
240
270
|
|
241
|
-
|
242
|
-
|
271
|
+
parsed = @ua_parser.parse ua
|
272
|
+
os = parsed.os
|
273
|
+
case field.downcase
|
274
|
+
when 'os_name', 'osname'
|
275
|
+
return os&.family
|
276
|
+
when 'os_version', 'osversion'
|
277
|
+
return os&.version unless os&.version.nil?
|
278
|
+
when 'browser_name', 'browsername'
|
279
|
+
return parsed.family
|
280
|
+
when 'browser_version', 'browserversion'
|
281
|
+
return parsed.version.to_s
|
282
|
+
else
|
283
|
+
nil
|
284
|
+
end
|
285
|
+
end
|
243
286
|
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
when 'os_version', 'osversion'
|
255
|
-
return os&.version unless os&.version.nil?
|
256
|
-
when 'browser_name', 'browsername'
|
257
|
-
return parsed.family
|
258
|
-
when 'browser_version', 'browserversion'
|
259
|
-
return parsed.version.to_s
|
260
|
-
else
|
261
|
-
nil
|
287
|
+
def eval_pass_percent(user, rule, config_salt)
|
288
|
+
return false unless config_salt.is_a?(String) && !rule['passPercentage'].nil?
|
289
|
+
begin
|
290
|
+
unit_id = get_unit_id(user, rule['id_type']) || ''
|
291
|
+
rule_salt = rule['salt'] || rule['id'] || ''
|
292
|
+
hash = compute_user_hash("#{config_salt}.#{rule_salt}.#{unit_id}")
|
293
|
+
return (hash % 10000) < (rule['passPercentage'].to_f * 100)
|
294
|
+
rescue
|
295
|
+
return false
|
296
|
+
end
|
262
297
|
end
|
263
|
-
end
|
264
298
|
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
return (hash % 10000) < (rule['passPercentage'].to_f * 100)
|
272
|
-
rescue
|
273
|
-
return false
|
299
|
+
def get_unit_id(user, id_type)
|
300
|
+
if id_type.is_a?(String) && id_type.downcase != 'userid'
|
301
|
+
return nil unless user&.custom_ids.is_a? Hash
|
302
|
+
return user.custom_ids[id_type] || user.custom_ids[id_type.downcase]
|
303
|
+
end
|
304
|
+
user.user_id
|
274
305
|
end
|
275
|
-
end
|
276
306
|
|
277
|
-
|
278
|
-
|
307
|
+
def compute_user_hash(user_hash)
|
308
|
+
Digest::SHA256.digest(user_hash).unpack('Q>')[0]
|
309
|
+
end
|
279
310
|
end
|
280
311
|
end
|
data/lib/network.rb
CHANGED
@@ -4,90 +4,67 @@ require 'dynamic_config'
|
|
4
4
|
|
5
5
|
$retry_codes = [408, 500, 502, 503, 504, 522, 524, 599]
|
6
6
|
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
api
|
7
|
+
module Statsig
|
8
|
+
class Network
|
9
|
+
def initialize(server_secret, api, backoff_mult = 10)
|
10
|
+
super()
|
11
|
+
unless api.end_with?('/')
|
12
|
+
api += '/'
|
13
|
+
end
|
14
|
+
@server_secret = server_secret
|
15
|
+
@api = api
|
16
|
+
@backoff_multiplier = backoff_mult
|
12
17
|
end
|
13
|
-
@server_secret = server_secret
|
14
|
-
@api = api
|
15
|
-
@last_sync_time = 0
|
16
|
-
@backoff_multiplier = backoff_mult
|
17
|
-
end
|
18
18
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
19
|
+
def post_helper(endpoint, body, retries = 0, backoff = 1)
|
20
|
+
http = HTTP.headers(
|
21
|
+
{"STATSIG-API-KEY" => @server_secret,
|
22
|
+
"STATSIG-CLIENT-TIME" => (Time.now.to_f * 1000).to_s,
|
23
|
+
"Content-Type" => "application/json; charset=UTF-8"
|
24
|
+
}).accept(:json)
|
25
|
+
begin
|
26
|
+
res = http.post(@api + endpoint, body: body)
|
27
|
+
rescue StandardError => e
|
28
|
+
## network error retry
|
29
|
+
return nil, e unless retries > 0
|
30
|
+
sleep backoff
|
31
|
+
return post_helper(endpoint, body, retries - 1, backoff * @backoff_multiplier)
|
32
|
+
end
|
33
|
+
return res, nil unless !res.status.success?
|
34
|
+
return nil, StandardError.new("Got an exception when making request to #{@api + endpoint}: #{res.to_s}") unless retries > 0 && $retry_codes.include?(res.code)
|
35
|
+
## status code retry
|
30
36
|
sleep backoff
|
31
|
-
|
32
|
-
end
|
33
|
-
return res, nil unless !res.status.success?
|
34
|
-
return nil, StandardError.new("Got an exception when making request to #{@api + endpoint}: #{res.to_s}") unless retries > 0 && $retry_codes.include?(res.code)
|
35
|
-
## status code retry
|
36
|
-
sleep backoff
|
37
|
-
post_helper(endpoint, body, retries - 1, backoff * @backoff_multiplier)
|
38
|
-
end
|
39
|
-
|
40
|
-
def check_gate(user, gate_name)
|
41
|
-
begin
|
42
|
-
request_body = JSON.generate({'user' => user&.serialize(false), 'gateName' => gate_name})
|
43
|
-
response, _ = post_helper('check_gate', request_body)
|
44
|
-
return JSON.parse(response.body) unless response.nil?
|
45
|
-
false
|
46
|
-
rescue
|
47
|
-
return false
|
48
|
-
end
|
49
|
-
end
|
50
|
-
|
51
|
-
def get_config(user, dynamic_config_name)
|
52
|
-
begin
|
53
|
-
request_body = JSON.generate({'user' => user&.serialize(false), 'configName' => dynamic_config_name})
|
54
|
-
response, _ = post_helper('get_config', request_body)
|
55
|
-
return JSON.parse(response.body) unless response.nil?
|
56
|
-
nil
|
57
|
-
rescue
|
58
|
-
return nil
|
37
|
+
post_helper(endpoint, body, retries - 1, backoff * @backoff_multiplier)
|
59
38
|
end
|
60
|
-
end
|
61
39
|
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
40
|
+
def check_gate(user, gate_name)
|
41
|
+
begin
|
42
|
+
request_body = JSON.generate({'user' => user&.serialize(false), 'gateName' => gate_name})
|
43
|
+
response, _ = post_helper('check_gate', request_body)
|
44
|
+
return JSON.parse(response.body) unless response.nil?
|
45
|
+
false
|
46
|
+
rescue
|
47
|
+
return false
|
48
|
+
end
|
71
49
|
end
|
72
|
-
end
|
73
50
|
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
51
|
+
def get_config(user, dynamic_config_name)
|
52
|
+
begin
|
53
|
+
request_body = JSON.generate({'user' => user&.serialize(false), 'configName' => dynamic_config_name})
|
54
|
+
response, _ = post_helper('get_config', request_body)
|
55
|
+
return JSON.parse(response.body) unless response.nil?
|
56
|
+
nil
|
57
|
+
rescue
|
58
|
+
return nil
|
82
59
|
end
|
83
60
|
end
|
84
|
-
end
|
85
61
|
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
62
|
+
def post_logs(events)
|
63
|
+
begin
|
64
|
+
json_body = JSON.generate({'events' => events, 'statsigMetadata' => Statsig.get_statsig_metadata})
|
65
|
+
post_helper('log_event', json_body, retries: 5)
|
66
|
+
rescue
|
67
|
+
end
|
91
68
|
end
|
92
69
|
end
|
93
70
|
end
|
data/lib/spec_store.rb
CHANGED
@@ -1,51 +1,151 @@
|
|
1
1
|
require 'net/http'
|
2
2
|
require 'uri'
|
3
3
|
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
4
|
+
module Statsig
|
5
|
+
class SpecStore
|
6
|
+
def initialize(network, error_callback = nil, config_sync_interval = 10, id_lists_sync_interval = 60)
|
7
|
+
@network = network
|
8
|
+
@last_sync_time = 0
|
9
|
+
@config_sync_interval = config_sync_interval
|
10
|
+
@id_lists_sync_interval = id_lists_sync_interval
|
11
|
+
@store = {
|
12
|
+
:gates => {},
|
13
|
+
:configs => {},
|
14
|
+
:id_lists => {},
|
15
|
+
}
|
16
|
+
e = download_config_specs
|
17
|
+
error_callback.call(e) unless error_callback.nil?
|
18
|
+
download_id_lists
|
13
19
|
|
14
|
-
|
15
|
-
|
16
|
-
return
|
20
|
+
@config_sync_thread = sync_config_specs
|
21
|
+
@id_lists_sync_thread = sync_id_lists
|
17
22
|
end
|
18
23
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
24
|
+
def shutdown
|
25
|
+
@config_sync_thread&.exit
|
26
|
+
@id_lists_sync_thread&.exit
|
27
|
+
end
|
23
28
|
|
24
|
-
|
25
|
-
:gates
|
26
|
-
|
27
|
-
}
|
29
|
+
def has_gate?(gate_name)
|
30
|
+
@store[:gates].key?(gate_name)
|
31
|
+
end
|
28
32
|
|
29
|
-
|
30
|
-
|
31
|
-
|
33
|
+
def has_config?(config_name)
|
34
|
+
@store[:configs].key?(config_name)
|
35
|
+
end
|
32
36
|
|
33
|
-
|
34
|
-
|
35
|
-
|
37
|
+
def get_gate(gate_name)
|
38
|
+
return nil unless has_gate?(gate_name)
|
39
|
+
@store[:gates][gate_name]
|
40
|
+
end
|
36
41
|
|
37
|
-
|
38
|
-
|
39
|
-
|
42
|
+
def get_config(config_name)
|
43
|
+
return nil unless has_config?(config_name)
|
44
|
+
@store[:configs][config_name]
|
45
|
+
end
|
40
46
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
end
|
47
|
+
def get_id_list(list_name)
|
48
|
+
@store[:id_lists][list_name]
|
49
|
+
end
|
45
50
|
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
51
|
+
private
|
52
|
+
|
53
|
+
def sync_config_specs
|
54
|
+
Thread.new do
|
55
|
+
loop do
|
56
|
+
sleep @config_sync_interval
|
57
|
+
download_config_specs
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
50
61
|
|
51
|
-
|
62
|
+
def sync_id_lists
|
63
|
+
Thread.new do
|
64
|
+
loop do
|
65
|
+
sleep @id_lists_sync_interval
|
66
|
+
download_id_lists
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def download_config_specs
|
72
|
+
begin
|
73
|
+
response, e = @network.post_helper('download_config_specs', JSON.generate({'sinceTime' => @last_sync_time}))
|
74
|
+
if e.nil?
|
75
|
+
process(JSON.parse(response.body))
|
76
|
+
else
|
77
|
+
e
|
78
|
+
end
|
79
|
+
rescue StandardError => e
|
80
|
+
e
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def process(specs_json)
|
85
|
+
if specs_json.nil?
|
86
|
+
return
|
87
|
+
end
|
88
|
+
|
89
|
+
@last_sync_time = specs_json['time'] || @last_sync_time
|
90
|
+
return unless specs_json['has_updates'] == true &&
|
91
|
+
!specs_json['feature_gates'].nil? &&
|
92
|
+
!specs_json['dynamic_configs'].nil?
|
93
|
+
|
94
|
+
new_gates = {}
|
95
|
+
new_configs = {}
|
96
|
+
|
97
|
+
specs_json['feature_gates'].map{|gate| new_gates[gate['name']] = gate }
|
98
|
+
specs_json['dynamic_configs'].map{|config| new_configs[config['name']] = config }
|
99
|
+
@store[:gates] = new_gates
|
100
|
+
@store[:configs] = new_configs
|
101
|
+
|
102
|
+
new_id_lists = specs_json['id_lists']
|
103
|
+
if new_id_lists.is_a? Hash
|
104
|
+
new_id_lists.each do |list_name, _|
|
105
|
+
unless @store[:id_lists].key?(list_name)
|
106
|
+
@store[:id_lists][list_name] = { :ids => {}, :time => 0 }
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
@store[:id_lists].each do |list_name, _|
|
111
|
+
unless new_id_lists.key?(list_name)
|
112
|
+
@store[:id_lists].delete(list_name)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def download_id_lists
|
119
|
+
if @store[:id_lists].is_a? Hash
|
120
|
+
threads = []
|
121
|
+
id_lists = @store[:id_lists]
|
122
|
+
id_lists.each do |list_name, list|
|
123
|
+
threads << Thread.new do
|
124
|
+
response, e = @network.post_helper('download_id_list', JSON.generate({'listName' => list_name, 'statsigMetadata' => Statsig.get_statsig_metadata, 'sinceTime' => list['time'] || 0 }))
|
125
|
+
if e.nil? && !response.nil?
|
126
|
+
begin
|
127
|
+
data = JSON.parse(response)
|
128
|
+
if data['add_ids'].is_a? Array
|
129
|
+
data['add_ids'].each do |id|
|
130
|
+
list[:ids][id] = true
|
131
|
+
end
|
132
|
+
end
|
133
|
+
if data['remove_ids'].is_a? Array
|
134
|
+
data['remove_ids'].each do |id|
|
135
|
+
list[:ids]&.delete(id)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
if data['time'].is_a? Numeric
|
139
|
+
list[:time] = data['time']
|
140
|
+
end
|
141
|
+
rescue
|
142
|
+
# Ignored
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
threads.each(&:join)
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
data/lib/statsig.rb
CHANGED
@@ -11,22 +11,22 @@ module Statsig
|
|
11
11
|
end
|
12
12
|
|
13
13
|
def self.check_gate(user, gate_name)
|
14
|
-
|
14
|
+
ensure_initialized
|
15
15
|
@shared_instance&.check_gate(user, gate_name)
|
16
16
|
end
|
17
17
|
|
18
18
|
def self.get_config(user, dynamic_config_name)
|
19
|
-
|
19
|
+
ensure_initialized
|
20
20
|
@shared_instance&.get_config(user, dynamic_config_name)
|
21
21
|
end
|
22
22
|
|
23
23
|
def self.get_experiment(user, experiment_name)
|
24
|
-
|
24
|
+
ensure_initialized
|
25
25
|
@shared_instance&.get_config(user, experiment_name)
|
26
26
|
end
|
27
27
|
|
28
28
|
def self.log_event(user, event_name, value, metadata)
|
29
|
-
|
29
|
+
ensure_initialized
|
30
30
|
@shared_instance&.log_event(user, event_name, value, metadata)
|
31
31
|
end
|
32
32
|
|
@@ -37,6 +37,15 @@ module Statsig
|
|
37
37
|
@shared_instance = nil
|
38
38
|
end
|
39
39
|
|
40
|
+
def self.get_statsig_metadata
|
41
|
+
{
|
42
|
+
'sdkType' => 'ruby-server',
|
43
|
+
'sdkVersion' => '1.8.2',
|
44
|
+
}
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
40
49
|
def self.ensure_initialized
|
41
50
|
if @shared_instance.nil?
|
42
51
|
raise 'Must call initialize first.'
|
data/lib/statsig_driver.rb
CHANGED
@@ -20,24 +20,9 @@ class StatsigDriver
|
|
20
20
|
@options = options || StatsigOptions.new
|
21
21
|
@shutdown = false
|
22
22
|
@secret_key = secret_key
|
23
|
-
@net = Network.new(secret_key, @options.api_url_base)
|
24
|
-
@
|
25
|
-
|
26
|
-
'sdkVersion' => Gem::Specification::load('statsig.gemspec')&.version,
|
27
|
-
}
|
28
|
-
@logger = StatsigLogger.new(@net, @statsig_metadata)
|
29
|
-
|
30
|
-
downloaded_specs, e = @net.download_config_specs
|
31
|
-
unless downloaded_specs.nil?
|
32
|
-
@initialized = true
|
33
|
-
end
|
34
|
-
|
35
|
-
@store = SpecStore.new(downloaded_specs)
|
36
|
-
@evaluator = Evaluator.new(@store)
|
37
|
-
|
38
|
-
@polling_thread = @net.poll_for_changes(-> (config_specs) { @store.process(config_specs) })
|
39
|
-
|
40
|
-
error_callback.call(e) unless error_callback.nil?
|
23
|
+
@net = Statsig::Network.new(secret_key, @options.api_url_base)
|
24
|
+
@logger = Statsig::StatsigLogger.new(@net)
|
25
|
+
@evaluator = Statsig::Evaluator.new(@net, error_callback)
|
41
26
|
end
|
42
27
|
|
43
28
|
def check_gate(user, gate_name)
|
@@ -47,13 +32,10 @@ class StatsigDriver
|
|
47
32
|
raise 'Invalid gate_name provided'
|
48
33
|
end
|
49
34
|
check_shutdown
|
50
|
-
unless @initialized
|
51
|
-
return false
|
52
|
-
end
|
53
35
|
|
54
36
|
res = @evaluator.check_gate(user, gate_name)
|
55
37
|
if res.nil?
|
56
|
-
res = ConfigResult.new(gate_name)
|
38
|
+
res = Statsig::ConfigResult.new(gate_name)
|
57
39
|
end
|
58
40
|
|
59
41
|
if res == $fetch_from_server
|
@@ -73,13 +55,10 @@ class StatsigDriver
|
|
73
55
|
raise "Invalid dynamic_config_name provided"
|
74
56
|
end
|
75
57
|
check_shutdown
|
76
|
-
unless @initialized
|
77
|
-
return DynamicConfig.new(dynamic_config_name)
|
78
|
-
end
|
79
58
|
|
80
59
|
res = @evaluator.get_config(user, dynamic_config_name)
|
81
60
|
if res.nil?
|
82
|
-
res = ConfigResult.new(dynamic_config_name)
|
61
|
+
res = Statsig::ConfigResult.new(dynamic_config_name)
|
83
62
|
end
|
84
63
|
|
85
64
|
if res == $fetch_from_server
|
@@ -111,16 +90,18 @@ class StatsigDriver
|
|
111
90
|
event.user = user
|
112
91
|
event.value = value
|
113
92
|
event.metadata = metadata
|
114
|
-
event.statsig_metadata =
|
93
|
+
event.statsig_metadata = Statsig.get_statsig_metadata
|
115
94
|
@logger.log_event(event)
|
116
95
|
end
|
117
96
|
|
118
97
|
def shutdown
|
119
98
|
@shutdown = true
|
120
99
|
@logger.flush(true)
|
121
|
-
@
|
100
|
+
@evaluator.shutdown
|
122
101
|
end
|
123
102
|
|
103
|
+
private
|
104
|
+
|
124
105
|
def validate_user(user)
|
125
106
|
if user.nil? || !user.instance_of?(StatsigUser) || !user.user_id.is_a?(String)
|
126
107
|
raise 'Must provide a valid StatsigUser with a user_id to use the server SDK. See https://docs.statsig.com/messages/serverRequiredUserID/ for more details.'
|
@@ -143,11 +124,11 @@ class StatsigDriver
|
|
143
124
|
def check_gate_fallback(user, gate_name)
|
144
125
|
network_result = @net.check_gate(user, gate_name)
|
145
126
|
if network_result.nil?
|
146
|
-
config_result = ConfigResult.new(gate_name)
|
127
|
+
config_result = Statsig::ConfigResult.new(gate_name)
|
147
128
|
return config_result
|
148
129
|
end
|
149
130
|
|
150
|
-
ConfigResult.new(
|
131
|
+
Statsig::ConfigResult.new(
|
151
132
|
network_result['name'],
|
152
133
|
network_result['value'],
|
153
134
|
{},
|
@@ -158,11 +139,11 @@ class StatsigDriver
|
|
158
139
|
def get_config_fallback(user, dynamic_config_name)
|
159
140
|
network_result = @net.get_config(user, dynamic_config_name)
|
160
141
|
if network_result.nil?
|
161
|
-
config_result = ConfigResult.new(dynamic_config_name)
|
142
|
+
config_result = Statsig::ConfigResult.new(dynamic_config_name)
|
162
143
|
return config_result
|
163
144
|
end
|
164
145
|
|
165
|
-
ConfigResult.new(
|
146
|
+
Statsig::ConfigResult.new(
|
166
147
|
network_result['name'],
|
167
148
|
false,
|
168
149
|
network_result['value'],
|
data/lib/statsig_logger.rb
CHANGED
@@ -3,59 +3,60 @@ require 'statsig_event'
|
|
3
3
|
$gate_exposure_event = 'statsig::gate_exposure'
|
4
4
|
$config_exposure_event = 'statsig::config_exposure'
|
5
5
|
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
6
|
+
module Statsig
|
7
|
+
class StatsigLogger
|
8
|
+
def initialize(network)
|
9
|
+
@network = network
|
10
|
+
@events = []
|
11
|
+
@background_flush = Thread.new do
|
12
|
+
sleep 60
|
13
|
+
flush
|
14
|
+
end
|
14
15
|
end
|
15
|
-
end
|
16
16
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
17
|
+
def log_event(event)
|
18
|
+
@events.push(event)
|
19
|
+
if @events.length >= 500
|
20
|
+
flush
|
21
|
+
end
|
21
22
|
end
|
22
|
-
end
|
23
23
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
end
|
36
|
-
|
37
|
-
def log_config_exposure(user, config_name, rule_id, secondary_exposures)
|
38
|
-
event = StatsigEvent.new($config_exposure_event)
|
39
|
-
event.user = user
|
40
|
-
event.metadata = {
|
41
|
-
'config' => config_name,
|
42
|
-
'ruleID' => rule_id
|
43
|
-
}
|
44
|
-
event.statsig_metadata = @statsig_metadata
|
45
|
-
event.secondary_exposures = secondary_exposures.is_a?(Array) ? secondary_exposures : []
|
46
|
-
log_event(event)
|
47
|
-
end
|
48
|
-
|
49
|
-
def flush(closing = false)
|
50
|
-
if closing
|
51
|
-
@background_flush.exit
|
24
|
+
def log_gate_exposure(user, gate_name, value, rule_id, secondary_exposures)
|
25
|
+
event = StatsigEvent.new($gate_exposure_event)
|
26
|
+
event.user = user
|
27
|
+
event.metadata = {
|
28
|
+
'gate' => gate_name,
|
29
|
+
'gateValue' => value.to_s,
|
30
|
+
'ruleID' => rule_id
|
31
|
+
}
|
32
|
+
event.statsig_metadata = Statsig.get_statsig_metadata
|
33
|
+
event.secondary_exposures = secondary_exposures.is_a?(Array) ? secondary_exposures : []
|
34
|
+
log_event(event)
|
52
35
|
end
|
53
|
-
|
54
|
-
|
36
|
+
|
37
|
+
def log_config_exposure(user, config_name, rule_id, secondary_exposures)
|
38
|
+
event = StatsigEvent.new($config_exposure_event)
|
39
|
+
event.user = user
|
40
|
+
event.metadata = {
|
41
|
+
'config' => config_name,
|
42
|
+
'ruleID' => rule_id
|
43
|
+
}
|
44
|
+
event.statsig_metadata = Statsig.get_statsig_metadata
|
45
|
+
event.secondary_exposures = secondary_exposures.is_a?(Array) ? secondary_exposures : []
|
46
|
+
log_event(event)
|
55
47
|
end
|
56
|
-
flush_events = @events.map { |e| e.serialize }
|
57
|
-
@events = []
|
58
48
|
|
59
|
-
|
49
|
+
def flush(closing = false)
|
50
|
+
if closing
|
51
|
+
@background_flush.exit
|
52
|
+
end
|
53
|
+
if @events.length == 0
|
54
|
+
return
|
55
|
+
end
|
56
|
+
flush_events = @events.map { |e| e.serialize }
|
57
|
+
@events = []
|
58
|
+
|
59
|
+
@network.post_logs(flush_events)
|
60
|
+
end
|
60
61
|
end
|
61
62
|
end
|
data/lib/statsig_user.rb
CHANGED
@@ -7,6 +7,7 @@ class StatsigUser
|
|
7
7
|
attr_accessor :locale
|
8
8
|
attr_accessor :app_version
|
9
9
|
attr_accessor :statsig_environment
|
10
|
+
attr_accessor :custom_ids
|
10
11
|
attr_accessor :private_attributes
|
11
12
|
|
12
13
|
def custom
|
@@ -28,9 +29,11 @@ class StatsigUser
|
|
28
29
|
@country = user_hash['country']
|
29
30
|
@locale = user_hash['locale']
|
30
31
|
@app_version = user_hash['appVersion'] || user_hash['app_version']
|
31
|
-
@custom = user_hash['custom']
|
32
|
+
@custom = user_hash['custom'] if user_hash['custom'].is_a? Hash
|
32
33
|
@statsig_environment = user_hash['statsigEnvironment']
|
33
|
-
@private_attributes = user_hash['privateAttributes']
|
34
|
+
@private_attributes = user_hash['privateAttributes'] if user_hash['privateAttributes'].is_a? Hash
|
35
|
+
custom_ids = user_hash['customIDs'] || user_hash['custom_ids']
|
36
|
+
@custom_ids = custom_ids if custom_ids.is_a? Hash
|
34
37
|
end
|
35
38
|
end
|
36
39
|
|
@@ -46,6 +49,7 @@ class StatsigUser
|
|
46
49
|
'custom' => @custom,
|
47
50
|
'statsigEnvironment' => @statsig_environment,
|
48
51
|
'privateAttributes' => @private_attributes,
|
52
|
+
'customIDs' => @custom_ids,
|
49
53
|
}
|
50
54
|
if for_logging
|
51
55
|
hash.delete('privateAttributes')
|
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.8.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Statsig, Inc
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2022-01-12 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -141,9 +141,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
141
141
|
version: '0'
|
142
142
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
143
143
|
requirements:
|
144
|
-
- - "
|
144
|
+
- - ">="
|
145
145
|
- !ruby/object:Gem::Version
|
146
|
-
version:
|
146
|
+
version: '0'
|
147
147
|
requirements: []
|
148
148
|
rubygems_version: 3.2.3
|
149
149
|
signing_key:
|