statsig 1.7.0.beta.1 → 1.8.1.beta.1
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 +4 -4
- data/lib/evaluator.rb +263 -234
- data/lib/network.rb +51 -74
- data/lib/spec_store.rb +138 -38
- data/lib/statsig.rb +11 -4
- data/lib/statsig_driver.rb +11 -32
- data/lib/statsig_logger.rb +48 -47
- data/lib/statsig_user.rb +6 -2
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 85cc5f1ff296df6832034a396c9e575cd21e54502cff95467c691289143d0067
|
4
|
+
data.tar.gz: a3ca082effe4a16dde1e73b700c30322db55a00e3e8f384015ae8d22640e41f8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 182507d8e780f6d37f0f94fe5e2183ad0c1fb8f6500ea8271203df45debcfa00a9630e03670a2e82e010aa77f41abb1d3d38ca5067c7e5df88dbf697c7aa7716
|
7
|
+
data.tar.gz: de3029066c82f7886a55a0c0beecddd65751ebc337d3a6138ccb437c5ba506a4e238f55cca1e301530a4168dd126ea2fba833dfe8af70cc2936bbe7c72f68f3f
|
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,8 +15,8 @@ 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
|
@@ -30,7 +30,7 @@ module EvaluationHelpers
|
|
30
30
|
end
|
31
31
|
|
32
32
|
def self.get_epoch_time(v)
|
33
|
-
time =
|
33
|
+
time = is_numeric(v) ? Time.at(v.to_f) : Time.parse(v)
|
34
34
|
if time.year > Time.now.year + 100
|
35
35
|
# divide by 1000 when the epoch time is in milliseconds instead of seconds
|
36
36
|
return time.to_i / 1000
|
data/lib/evaluator.rb
CHANGED
@@ -10,273 +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
|
30
31
|
|
31
|
-
|
32
|
+
def shutdown
|
33
|
+
@spec_store.shutdown
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
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
|
32
67
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
if config['enabled']
|
68
|
+
def eval_rule(user, rule)
|
69
|
+
exposures = []
|
70
|
+
pass = true
|
37
71
|
i = 0
|
38
|
-
until i >=
|
39
|
-
|
40
|
-
result
|
41
|
-
|
42
|
-
exposures = exposures + result['exposures'] if result['exposures'].is_a? Array
|
43
|
-
if result['value']
|
44
|
-
pass = self.eval_pass_percent(user, rule, config['salt'])
|
45
|
-
return ConfigResult.new(
|
46
|
-
config['name'],
|
47
|
-
pass,
|
48
|
-
pass ? rule['returnValue'] : config['defaultValue'],
|
49
|
-
rule['id'],
|
50
|
-
exposures
|
51
|
-
)
|
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
|
52
76
|
end
|
53
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
|
54
84
|
i += 1
|
55
85
|
end
|
56
|
-
|
86
|
+
{ 'value' => pass, 'exposures' => exposures }
|
57
87
|
end
|
58
88
|
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
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
|
101
|
+
|
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
|
69
144
|
return $fetch_from_server
|
70
145
|
end
|
71
146
|
|
72
|
-
if
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
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
|
-
|
129
|
-
|
130
|
-
value
|
131
|
-
|
132
|
-
return
|
147
|
+
return $fetch_from_server if value == $fetch_from_server || !operator.is_a?(String)
|
148
|
+
operator = operator.downcase
|
149
|
+
|
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
|
133
221
|
end
|
134
|
-
else
|
135
|
-
return $fetch_from_server
|
136
222
|
end
|
137
223
|
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
case operator
|
142
|
-
# numerical comparison
|
143
|
-
when 'gt'
|
144
|
-
return EvaluationHelpers::compare_numbers(value, target, ->(a, b) { a > b })
|
145
|
-
when 'gte'
|
146
|
-
return EvaluationHelpers::compare_numbers(value, target, ->(a, b) { a >= b })
|
147
|
-
when 'lt'
|
148
|
-
return EvaluationHelpers::compare_numbers(value, target, ->(a, b) { a < b })
|
149
|
-
when 'lte'
|
150
|
-
return EvaluationHelpers::compare_numbers(value, target, ->(a, b) { a <= b })
|
151
|
-
|
152
|
-
# version comparison
|
153
|
-
when 'version_gt'
|
154
|
-
return (Gem::Version.new(value) > Gem::Version.new(target) rescue false)
|
155
|
-
when 'version_gte'
|
156
|
-
return (Gem::Version.new(value) >= Gem::Version.new(target) rescue false)
|
157
|
-
when 'version_lt'
|
158
|
-
return (Gem::Version.new(value) < Gem::Version.new(target) rescue false)
|
159
|
-
when 'version_lte'
|
160
|
-
return (Gem::Version.new(value) <= Gem::Version.new(target) rescue false)
|
161
|
-
when 'version_eq'
|
162
|
-
return (Gem::Version.new(value) == Gem::Version.new(target) rescue false)
|
163
|
-
when 'version_neq'
|
164
|
-
return (Gem::Version.new(value) != Gem::Version.new(target) rescue false)
|
165
|
-
|
166
|
-
# array operations
|
167
|
-
when 'any'
|
168
|
-
return EvaluationHelpers::match_string_in_array(target, value, true, ->(a, b) { a == b })
|
169
|
-
when 'none'
|
170
|
-
return !EvaluationHelpers::match_string_in_array(target, value, true, ->(a, b) { a == b })
|
171
|
-
when 'any_case_sensitive'
|
172
|
-
return EvaluationHelpers::match_string_in_array(target, value, false, ->(a, b) { a == b })
|
173
|
-
when 'none_case_sensitive'
|
174
|
-
return !EvaluationHelpers::match_string_in_array(target, value, false, ->(a, b) { a == b })
|
175
|
-
|
176
|
-
#string
|
177
|
-
when 'str_starts_with_any'
|
178
|
-
return EvaluationHelpers::match_string_in_array(target, value, true, ->(a, b) { a.start_with?(b) })
|
179
|
-
when 'str_ends_with_any'
|
180
|
-
return EvaluationHelpers::match_string_in_array(target, value, true, ->(a, b) { a.end_with?(b) })
|
181
|
-
when 'str_contains_any'
|
182
|
-
return EvaluationHelpers::match_string_in_array(target, value, true, ->(a, b) { a.include?(b) })
|
183
|
-
when 'str_contains_none'
|
184
|
-
return !EvaluationHelpers::match_string_in_array(target, value, true, ->(a, b) { a.include?(b) })
|
185
|
-
when 'str_matches'
|
186
|
-
return (value.is_a?(String) && !(value =~ Regexp.new(target)).nil? rescue false)
|
187
|
-
when 'eq'
|
188
|
-
return value == target
|
189
|
-
when 'neq'
|
190
|
-
return value != target
|
191
|
-
|
192
|
-
# dates
|
193
|
-
when 'before'
|
194
|
-
return EvaluationHelpers::compare_times(value, target, ->(a, b) { a < b })
|
195
|
-
when 'after'
|
196
|
-
return EvaluationHelpers::compare_times(value, target, ->(a, b) { a > b })
|
197
|
-
when 'on'
|
198
|
-
return EvaluationHelpers::compare_times(value, target, ->(a, b) { a.year == b.year && a.month == b.month && a.day == b.day })
|
199
|
-
else
|
200
|
-
return $fetch_from_server
|
201
|
-
end
|
202
|
-
end
|
224
|
+
def get_value_from_user(user, field)
|
225
|
+
return nil unless user.instance_of?(StatsigUser) && field.is_a?(String)
|
203
226
|
|
204
|
-
|
205
|
-
|
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?
|
206
230
|
|
207
|
-
|
208
|
-
|
209
|
-
|
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
|
210
237
|
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
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
|
215
243
|
end
|
244
|
+
|
245
|
+
nil
|
216
246
|
end
|
217
247
|
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
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)
|
222
254
|
end
|
255
|
+
nil
|
223
256
|
end
|
224
257
|
|
225
|
-
|
226
|
-
|
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)
|
227
262
|
|
228
|
-
|
229
|
-
return nil unless user.instance_of?(StatsigUser) && field.is_a?(String)
|
230
|
-
field = field.downcase
|
231
|
-
return nil unless user.statsig_environment.is_a? Hash
|
232
|
-
user.statsig_environment.each do |key, value|
|
233
|
-
return value if key.downcase == (field)
|
263
|
+
CountryLookup.lookup_ip_string(ip)
|
234
264
|
end
|
235
|
-
nil
|
236
|
-
end
|
237
|
-
|
238
|
-
def get_value_from_ip(user, field)
|
239
|
-
return nil unless user.is_a?(StatsigUser) && field.is_a?(String) && field.downcase == 'country'
|
240
|
-
ip = get_value_from_user(user, 'ip')
|
241
|
-
return nil unless ip.is_a?(String)
|
242
265
|
|
243
|
-
|
244
|
-
|
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)
|
270
|
+
|
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
|
245
286
|
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
when 'os_version', 'osversion'
|
257
|
-
return os&.version unless os&.version.nil?
|
258
|
-
when 'browser_name', 'browsername'
|
259
|
-
return parsed.family
|
260
|
-
when 'browser_version', 'browserversion'
|
261
|
-
return parsed.version.to_s
|
262
|
-
else
|
263
|
-
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
|
264
297
|
end
|
265
|
-
end
|
266
298
|
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
return (hash % 10000) < (rule['passPercentage'].to_f * 100)
|
274
|
-
rescue
|
275
|
-
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
|
276
305
|
end
|
277
|
-
end
|
278
306
|
|
279
|
-
|
280
|
-
|
307
|
+
def compute_user_hash(user_hash)
|
308
|
+
Digest::SHA256.digest(user_hash).unpack('Q>')[0]
|
309
|
+
end
|
281
310
|
end
|
282
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,13 @@ 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.1.beta.1',
|
44
|
+
}
|
45
|
+
end
|
46
|
+
|
40
47
|
private
|
41
48
|
|
42
49
|
def self.ensure_initialized
|
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,14 +90,14 @@ 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
|
|
124
103
|
private
|
@@ -145,11 +124,11 @@ class StatsigDriver
|
|
145
124
|
def check_gate_fallback(user, gate_name)
|
146
125
|
network_result = @net.check_gate(user, gate_name)
|
147
126
|
if network_result.nil?
|
148
|
-
config_result = ConfigResult.new(gate_name)
|
127
|
+
config_result = Statsig::ConfigResult.new(gate_name)
|
149
128
|
return config_result
|
150
129
|
end
|
151
130
|
|
152
|
-
ConfigResult.new(
|
131
|
+
Statsig::ConfigResult.new(
|
153
132
|
network_result['name'],
|
154
133
|
network_result['value'],
|
155
134
|
{},
|
@@ -160,11 +139,11 @@ class StatsigDriver
|
|
160
139
|
def get_config_fallback(user, dynamic_config_name)
|
161
140
|
network_result = @net.get_config(user, dynamic_config_name)
|
162
141
|
if network_result.nil?
|
163
|
-
config_result = ConfigResult.new(dynamic_config_name)
|
142
|
+
config_result = Statsig::ConfigResult.new(dynamic_config_name)
|
164
143
|
return config_result
|
165
144
|
end
|
166
145
|
|
167
|
-
ConfigResult.new(
|
146
|
+
Statsig::ConfigResult.new(
|
168
147
|
network_result['name'],
|
169
148
|
false,
|
170
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.1.beta.1
|
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
|