statsig 0.1.5 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/evaluation_helpers.rb +28 -5
- data/lib/evaluator.rb +69 -33
- data/lib/network.rb +14 -7
- data/lib/statsig.rb +10 -5
- data/lib/statsig_driver.rb +27 -8
- data/lib/statsig_event.rb +1 -1
- data/lib/statsig_options.rb +9 -0
- data/lib/statsig_user.rb +14 -9
- metadata +19 -16
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 90f13c55a79aa919b94e633f69d39b3cb997a92cceec3e27bdb3e00e00e8e3ef
|
4
|
+
data.tar.gz: cd40c863516da1b3ff0730959ccbc3fa664afe0e613775f0369a1377ccc06123
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a4c73b125853c8128d5aa57a60acfe1e80a0aa8ffc9f1fdcd89ea74e9a6ac033c93262103850d0c13ec84052ca9b9a464627836a1af945e6100c036c0fbc1359
|
7
|
+
data.tar.gz: 86bf2fe76cdc5cdc29126bd188a33e746f2a143e56fce1cd2e7d5a242622acb4b3783694f01e11a6e8ce10c707bc38a57a055694e15c965ee584bb951c6acb0d
|
data/lib/evaluation_helpers.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
require 'time'
|
2
|
+
|
1
3
|
module EvaluationHelpers
|
2
4
|
def self.compare_numbers(a, b, func)
|
3
5
|
return false unless self.is_numeric(a) && self.is_numeric(b)
|
@@ -5,16 +7,28 @@ module EvaluationHelpers
|
|
5
7
|
end
|
6
8
|
|
7
9
|
# returns true if array contains value, ignoring case when comparing strings
|
8
|
-
def self.array_contains(array, value)
|
10
|
+
def self.array_contains(array, value, ignore_case)
|
9
11
|
return false unless array.is_a?(Array) && !value.nil?
|
10
|
-
|
11
|
-
|
12
|
+
if value.is_a?(String) && match_string_in_array(array, value, ignore_case, ->(a, b) { a == b })
|
13
|
+
return true
|
14
|
+
end
|
15
|
+
return array.include?(value)
|
12
16
|
end
|
13
17
|
|
14
18
|
# returns true if array has any element that evaluates to true with value using func lambda, ignoring case
|
15
|
-
def self.match_string_in_array(array, value, func)
|
19
|
+
def self.match_string_in_array(array, value, ignore_case, func)
|
16
20
|
return false unless array.is_a?(Array) && value.is_a?(String)
|
17
|
-
array.any?{ |s| s.is_a?(String) && func.call(value.downcase, s.downcase) } rescue false
|
21
|
+
array.any?{ |s| s.is_a?(String) && ((ignore_case && func.call(value.downcase, s.downcase)) || func.call(value, s)) } rescue false
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.compare_times(a, b, func)
|
25
|
+
begin
|
26
|
+
time_1 = self.get_epoch_time(a)
|
27
|
+
time_2 = self.get_epoch_time(b)
|
28
|
+
func.call(time_1, time_2)
|
29
|
+
rescue
|
30
|
+
false
|
31
|
+
end
|
18
32
|
end
|
19
33
|
|
20
34
|
private
|
@@ -22,4 +36,13 @@ module EvaluationHelpers
|
|
22
36
|
def self.is_numeric(v)
|
23
37
|
!(v.to_s =~ /\A[-+]?\d*\.?\d+\z/).nil?
|
24
38
|
end
|
39
|
+
|
40
|
+
def self.get_epoch_time(v)
|
41
|
+
time = self.is_numeric(v) ? Time.at(v.to_f) : Time.parse(v)
|
42
|
+
if time.year > Time.now.year + 100
|
43
|
+
# divide by 1000 when the epoch time is in milliseconds instead of seconds
|
44
|
+
return time.to_i / 1000
|
45
|
+
end
|
46
|
+
return time.to_i
|
47
|
+
end
|
25
48
|
end
|
data/lib/evaluator.rb
CHANGED
@@ -1,8 +1,11 @@
|
|
1
|
-
require 'browser'
|
2
1
|
require 'config_result'
|
2
|
+
require 'country_lookup'
|
3
3
|
require 'digest'
|
4
4
|
require 'evaluation_helpers'
|
5
5
|
require 'spec_store'
|
6
|
+
require 'time'
|
7
|
+
require 'user_agent_parser'
|
8
|
+
require 'user_agent_parser/operating_system'
|
6
9
|
|
7
10
|
$fetch_from_server = :fetch_from_server
|
8
11
|
$type_dynamic_config = 'dynamic_config'
|
@@ -11,6 +14,8 @@ class Evaluator
|
|
11
14
|
def initialize(store)
|
12
15
|
@spec_store = store
|
13
16
|
@initialized = true
|
17
|
+
@ua_parser = UserAgentParser::Parser.new
|
18
|
+
CountryLookup.initialize
|
14
19
|
end
|
15
20
|
|
16
21
|
def check_gate(user, gate_name)
|
@@ -65,8 +70,10 @@ class Evaluator
|
|
65
70
|
target = condition['targetValue']
|
66
71
|
type = condition['type']
|
67
72
|
operator = condition['operator']
|
73
|
+
additional_values = condition['additionalValues']
|
74
|
+
additional_values = Hash.new unless additional_values.is_a? Hash
|
68
75
|
|
69
|
-
return $fetch_from_server unless type.is_a?
|
76
|
+
return $fetch_from_server unless type.is_a? String
|
70
77
|
type = type.downcase
|
71
78
|
|
72
79
|
case type
|
@@ -76,7 +83,7 @@ class Evaluator
|
|
76
83
|
when 'pass_gate'
|
77
84
|
other_gate_result = self.check_gate(user, target)
|
78
85
|
return $fetch_from_server if other_gate_result == $fetch_from_server
|
79
|
-
return type == 'pass_gate' ? other_gate_result
|
86
|
+
return type == 'pass_gate' ? other_gate_result.gate_value : !other_gate_result.gate_value
|
80
87
|
when 'ip_based'
|
81
88
|
value = get_value_from_user(user, field) || get_value_from_ip(user&.value_lookup['ip'], field)
|
82
89
|
return $fetch_from_server if value == $fetch_from_server
|
@@ -85,8 +92,19 @@ class Evaluator
|
|
85
92
|
return $fetch_from_server if value == $fetch_from_server
|
86
93
|
when 'user_field'
|
87
94
|
value = get_value_from_user(user, field)
|
95
|
+
when 'environment_field'
|
96
|
+
value = get_value_from_environment(user, field)
|
88
97
|
when 'current_time'
|
89
98
|
value = Time.now.to_f # epoch time in seconds
|
99
|
+
when 'user_bucket'
|
100
|
+
begin
|
101
|
+
salt = additional_values['salt']
|
102
|
+
user_id = user.user_id || ''
|
103
|
+
# there are only 1000 user buckets as opposed to 10k for gate pass %
|
104
|
+
value = compute_user_hash("#{salt}.#{user_id}") % 1000
|
105
|
+
rescue
|
106
|
+
return false
|
107
|
+
end
|
90
108
|
else
|
91
109
|
return $fetch_from_server
|
92
110
|
end
|
@@ -124,17 +142,21 @@ class Evaluator
|
|
124
142
|
|
125
143
|
# array operations
|
126
144
|
when 'any'
|
127
|
-
return EvaluationHelpers::array_contains(target, value)
|
145
|
+
return EvaluationHelpers::array_contains(target, value, true)
|
128
146
|
when 'none'
|
129
|
-
return !EvaluationHelpers::array_contains(target, value)
|
147
|
+
return !EvaluationHelpers::array_contains(target, value, true)
|
148
|
+
when 'any_case_sensitive'
|
149
|
+
return EvaluationHelpers::array_contains(target, value, false)
|
150
|
+
when 'none_case_sensitive'
|
151
|
+
return !EvaluationHelpers::array_contains(target, value, false)
|
130
152
|
|
131
153
|
#string
|
132
154
|
when 'str_starts_with_any'
|
133
|
-
return EvaluationHelpers::match_string_in_array(target, value, ->(a, b) { a.start_with?(b) })
|
155
|
+
return EvaluationHelpers::match_string_in_array(target, value, true, ->(a, b) { a.start_with?(b) })
|
134
156
|
when 'str_ends_with_any'
|
135
|
-
return EvaluationHelpers::match_string_in_array(target, value, ->(a, b) { a.end_with?(b) })
|
157
|
+
return EvaluationHelpers::match_string_in_array(target, value, true, ->(a, b) { a.end_with?(b) })
|
136
158
|
when 'str_contains_any'
|
137
|
-
return EvaluationHelpers::match_string_in_array(target, value, ->(a, b) { a.include?(b) })
|
159
|
+
return EvaluationHelpers::match_string_in_array(target, value, true, ->(a, b) { a.include?(b) })
|
138
160
|
when 'str_matches'
|
139
161
|
return (value.is_a?(String) && !(value =~ Regexp.new(target)).nil? rescue false)
|
140
162
|
when 'eq'
|
@@ -144,11 +166,11 @@ class Evaluator
|
|
144
166
|
|
145
167
|
# dates
|
146
168
|
when 'before'
|
147
|
-
|
169
|
+
return EvaluationHelpers::compare_times(value, target, ->(a, b) { a < b })
|
148
170
|
when 'after'
|
149
|
-
|
171
|
+
return EvaluationHelpers::compare_times(value, target, ->(a, b) { a > b })
|
150
172
|
when 'on'
|
151
|
-
|
173
|
+
return EvaluationHelpers::compare_times(value, target, ->(a, b) { a.year == b.year && a.month == b.month && a.day == b.day })
|
152
174
|
else
|
153
175
|
return $fetch_from_server
|
154
176
|
end
|
@@ -164,47 +186,61 @@ class Evaluator
|
|
164
186
|
user_custom = user_lookup_table['custom']
|
165
187
|
return nil unless user_custom.is_a?(Hash)
|
166
188
|
user_custom.each do |key, value|
|
167
|
-
return value if key.downcase.casecmp(field.downcase)
|
189
|
+
return value if key.downcase.casecmp?(field.downcase)
|
190
|
+
end
|
191
|
+
nil
|
192
|
+
end
|
193
|
+
|
194
|
+
def get_value_from_environment(user, field)
|
195
|
+
return nil unless user.instance_of?(StatsigUser) && field.is_a?(String)
|
196
|
+
field = field.downcase
|
197
|
+
return nil unless user.statsig_environment.is_a? Hash
|
198
|
+
user.statsig_environment.each do |key, value|
|
199
|
+
return value if key.downcase == (field)
|
168
200
|
end
|
201
|
+
nil
|
169
202
|
end
|
170
203
|
|
171
204
|
def get_value_from_ip(ip, field)
|
172
205
|
return nil unless ip.is_a?(String) && field.is_a?(String)
|
173
|
-
|
174
|
-
|
206
|
+
|
207
|
+
if field.downcase != 'country'
|
208
|
+
return $fetch_from_server
|
209
|
+
end
|
210
|
+
CountryLookup.lookup_ip_string(ip)
|
175
211
|
end
|
176
212
|
|
177
213
|
def get_value_from_ua(ua, field)
|
178
214
|
return nil unless ua.is_a?(String) && field.is_a?(String)
|
179
|
-
|
215
|
+
parsed = @ua_parser.parse ua
|
216
|
+
os = parsed.os
|
180
217
|
case field.downcase
|
181
|
-
when 'os_name'
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
when 'os_version'
|
190
|
-
return b.platform.version
|
191
|
-
when 'browser_name'
|
192
|
-
return b.name
|
193
|
-
when 'browser_version'
|
194
|
-
return b.full_version
|
218
|
+
when 'os_name', 'osname'
|
219
|
+
return os&.family
|
220
|
+
when 'os_version', 'osversion'
|
221
|
+
return os&.version unless os&.version.nil?
|
222
|
+
when 'browser_name', 'browsername'
|
223
|
+
return parsed.family
|
224
|
+
when 'browser_version', 'browserversion'
|
225
|
+
return parsed.version.to_s
|
195
226
|
else
|
196
227
|
nil
|
197
228
|
end
|
198
229
|
end
|
199
230
|
|
200
|
-
def eval_pass_percent(user, rule,
|
201
|
-
return false unless
|
231
|
+
def eval_pass_percent(user, rule, config_salt)
|
232
|
+
return false unless config_salt.is_a?(String) && !rule['passPercentage'].nil?
|
202
233
|
begin
|
203
234
|
user_id = user.user_id || ''
|
204
|
-
|
205
|
-
|
235
|
+
rule_salt = rule['salt'] || rule['id'] || ''
|
236
|
+
hash = compute_user_hash("#{config_salt}.#{rule_salt}.#{user_id}")
|
237
|
+
return (hash % 10000) < (rule['passPercentage'].to_f * 100)
|
206
238
|
rescue
|
207
239
|
return false
|
208
240
|
end
|
209
241
|
end
|
242
|
+
|
243
|
+
def compute_user_hash(user_hash)
|
244
|
+
Digest::SHA256.digest(user_hash).unpack('Q>')[0]
|
245
|
+
end
|
210
246
|
end
|
data/lib/network.rb
CHANGED
@@ -8,17 +8,24 @@ class Network
|
|
8
8
|
unless api.end_with?('/')
|
9
9
|
api += '/'
|
10
10
|
end
|
11
|
-
@
|
12
|
-
.headers({"STATSIG-API-KEY" => server_secret, "Content-Type" => "application/json; charset=UTF-8"})
|
13
|
-
.accept(:json)
|
11
|
+
@server_secret = server_secret
|
14
12
|
@api = api
|
15
13
|
@last_sync_time = 0
|
16
14
|
end
|
17
15
|
|
16
|
+
def post_helper(endpoint, body)
|
17
|
+
http = HTTP.headers(
|
18
|
+
{"STATSIG-API-KEY" => @server_secret,
|
19
|
+
"STATSIG-CLIENT-TIME" => (Time.now.to_f * 1000).to_s,
|
20
|
+
"Content-Type" => "application/json; charset=UTF-8"
|
21
|
+
}).accept(:json)
|
22
|
+
http.post(@api + endpoint, body: body)
|
23
|
+
end
|
24
|
+
|
18
25
|
def check_gate(user, gate_name)
|
19
26
|
begin
|
20
27
|
request_body = JSON.generate({'user' => user&.serialize, 'gateName' => gate_name})
|
21
|
-
response =
|
28
|
+
response = post_helper('check_gate', request_body)
|
22
29
|
return JSON.parse(response.body)
|
23
30
|
rescue
|
24
31
|
return false
|
@@ -28,7 +35,7 @@ class Network
|
|
28
35
|
def get_config(user, dynamic_config_name)
|
29
36
|
begin
|
30
37
|
request_body = JSON.generate({'user' => user&.serialize, 'configName' => dynamic_config_name})
|
31
|
-
response =
|
38
|
+
response = post_helper('get_config', request_body)
|
32
39
|
return JSON.parse(response.body)
|
33
40
|
rescue
|
34
41
|
return nil
|
@@ -37,7 +44,7 @@ class Network
|
|
37
44
|
|
38
45
|
def download_config_specs
|
39
46
|
begin
|
40
|
-
response =
|
47
|
+
response = post_helper('download_config_specs', JSON.generate({'sinceTime' => @last_sync_time}))
|
41
48
|
json_body = JSON.parse(response.body)
|
42
49
|
@last_sync_time = json_body['time']
|
43
50
|
return json_body
|
@@ -61,7 +68,7 @@ class Network
|
|
61
68
|
def post_logs(events, statsig_metadata)
|
62
69
|
begin
|
63
70
|
json_body = JSON.generate({'events' => events, 'statsigMetadata' => statsig_metadata})
|
64
|
-
|
71
|
+
post_helper('log_event', body: json_body)
|
65
72
|
rescue
|
66
73
|
# TODO: retries
|
67
74
|
end
|
data/lib/statsig.rb
CHANGED
@@ -1,28 +1,33 @@
|
|
1
1
|
require 'statsig_driver'
|
2
2
|
|
3
3
|
module Statsig
|
4
|
-
def self.initialize(secret_key)
|
4
|
+
def self.initialize(secret_key, options = nil)
|
5
5
|
unless @shared_instance.nil?
|
6
6
|
puts 'Statsig already initialized.'
|
7
7
|
return @shared_instance
|
8
8
|
end
|
9
9
|
|
10
|
-
@shared_instance = StatsigDriver.new(secret_key)
|
10
|
+
@shared_instance = StatsigDriver.new(secret_key, options)
|
11
11
|
end
|
12
12
|
|
13
13
|
def self.check_gate(user, gate_name)
|
14
14
|
self.ensure_initialized
|
15
|
-
@shared_instance
|
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
|
self.ensure_initialized
|
20
|
-
@shared_instance
|
20
|
+
@shared_instance&.get_config(user, dynamic_config_name)
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.get_experiment(user, experiment_name)
|
24
|
+
self.ensure_initialized
|
25
|
+
@shared_instance&.get_config(user, experiment_name)
|
21
26
|
end
|
22
27
|
|
23
28
|
def self.log_event(user, event_name, value, metadata)
|
24
29
|
self.ensure_initialized
|
25
|
-
@shared_instance
|
30
|
+
@shared_instance&.log_event(user, event_name, value, metadata)
|
26
31
|
end
|
27
32
|
|
28
33
|
def self.shutdown
|
data/lib/statsig_driver.rb
CHANGED
@@ -3,18 +3,24 @@ require 'evaluator'
|
|
3
3
|
require 'network'
|
4
4
|
require 'statsig_event'
|
5
5
|
require 'statsig_logger'
|
6
|
+
require 'statsig_options'
|
6
7
|
require 'statsig_user'
|
7
8
|
require 'spec_store'
|
8
9
|
|
9
10
|
class StatsigDriver
|
10
|
-
def initialize(secret_key)
|
11
|
+
def initialize(secret_key, options = nil)
|
11
12
|
super()
|
12
13
|
if !secret_key.is_a?(String) || !secret_key.start_with?('secret-')
|
13
14
|
raise 'Invalid secret key provided. Provide your project secret key from the Statsig console'
|
14
15
|
end
|
16
|
+
if !options.nil? && !options.instance_of?(StatsigOptions)
|
17
|
+
raise 'Invalid options provided. Either provide a valid StatsigOptions object or nil'
|
18
|
+
end
|
19
|
+
|
20
|
+
@options = options || StatsigOptions.new()
|
15
21
|
@shutdown = false
|
16
22
|
@secret_key = secret_key
|
17
|
-
@net = Network.new(secret_key,
|
23
|
+
@net = Network.new(secret_key, @options.api_url_base)
|
18
24
|
@statsig_metadata = {
|
19
25
|
'sdkType' => 'ruby-server',
|
20
26
|
'sdkVersion' => Gem::Specification::load('statsig.gemspec')&.version,
|
@@ -33,9 +39,8 @@ class StatsigDriver
|
|
33
39
|
end
|
34
40
|
|
35
41
|
def check_gate(user, gate_name)
|
36
|
-
|
37
|
-
|
38
|
-
end
|
42
|
+
validate_user(user)
|
43
|
+
user = normalize_user(user)
|
39
44
|
if !gate_name.is_a?(String) || gate_name.empty?
|
40
45
|
raise 'Invalid gate_name provided'
|
41
46
|
end
|
@@ -58,9 +63,8 @@ class StatsigDriver
|
|
58
63
|
end
|
59
64
|
|
60
65
|
def get_config(user, dynamic_config_name)
|
61
|
-
|
62
|
-
|
63
|
-
end
|
66
|
+
validate_user(user)
|
67
|
+
user = normalize_user(user)
|
64
68
|
if !dynamic_config_name.is_a?(String) || dynamic_config_name.empty?
|
65
69
|
raise "Invalid dynamic_config_name provided"
|
66
70
|
end
|
@@ -89,6 +93,8 @@ class StatsigDriver
|
|
89
93
|
end
|
90
94
|
check_shutdown
|
91
95
|
|
96
|
+
user = normalize_user(user)
|
97
|
+
|
92
98
|
event = StatsigEvent.new(event_name)
|
93
99
|
event.user = user&.serialize
|
94
100
|
event.value = value
|
@@ -105,6 +111,19 @@ class StatsigDriver
|
|
105
111
|
|
106
112
|
private
|
107
113
|
|
114
|
+
def validate_user(user)
|
115
|
+
if user.nil? || !user.instance_of?(StatsigUser) || !user.user_id.is_a?(String)
|
116
|
+
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.'
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def normalize_user(user)
|
121
|
+
if !@options&.environment.nil?
|
122
|
+
user.statsig_environment = @options.environment
|
123
|
+
end
|
124
|
+
user
|
125
|
+
end
|
126
|
+
|
108
127
|
def check_shutdown
|
109
128
|
if @shutdown
|
110
129
|
puts 'SDK has been shutdown. Updates in the Statsig Console will no longer reflect.'
|
data/lib/statsig_event.rb
CHANGED
data/lib/statsig_user.rb
CHANGED
@@ -5,7 +5,8 @@ class StatsigUser
|
|
5
5
|
attr_accessor :user_agent
|
6
6
|
attr_accessor :country
|
7
7
|
attr_accessor :locale
|
8
|
-
attr_accessor :
|
8
|
+
attr_accessor :app_version
|
9
|
+
attr_accessor :statsig_environment
|
9
10
|
|
10
11
|
def custom
|
11
12
|
@custom
|
@@ -15,16 +16,19 @@ class StatsigUser
|
|
15
16
|
@custom = value.is_a?(Hash) ? value : Hash.new
|
16
17
|
end
|
17
18
|
|
18
|
-
def initialize(user_hash
|
19
|
+
def initialize(user_hash)
|
20
|
+
@statsig_environment = Hash.new
|
19
21
|
if user_hash.is_a?(Hash)
|
20
|
-
@user_id = user_hash['userID']
|
22
|
+
@user_id = user_hash['userID'] || user_hash['user_id']
|
23
|
+
@user_id = @user_id.to_s unless @user_id.nil?
|
21
24
|
@email = user_hash['email']
|
22
25
|
@ip = user_hash['ip']
|
23
|
-
@user_agent = user_hash['userAgent']
|
26
|
+
@user_agent = user_hash['userAgent'] || user_hash['user_agent']
|
24
27
|
@country = user_hash['country']
|
25
28
|
@locale = user_hash['locale']
|
26
|
-
@
|
29
|
+
@app_version = user_hash['appVersion'] || user_hash['app_version']
|
27
30
|
@custom = user_hash['custom']
|
31
|
+
@statsig_environment = user_hash['statsigEnvironment']
|
28
32
|
end
|
29
33
|
end
|
30
34
|
|
@@ -36,8 +40,9 @@ class StatsigUser
|
|
36
40
|
'userAgent' => @user_agent,
|
37
41
|
'country' => @country,
|
38
42
|
'locale' => @locale,
|
39
|
-
'
|
43
|
+
'appVersion' => @app_version,
|
40
44
|
'custom' => @custom,
|
45
|
+
'statsigEnvironment' => @statsig_environment,
|
41
46
|
}
|
42
47
|
end
|
43
48
|
|
@@ -53,9 +58,9 @@ class StatsigUser
|
|
53
58
|
'user_agent' => @user_agent,
|
54
59
|
'country' => @country,
|
55
60
|
'locale' => @locale,
|
56
|
-
'
|
57
|
-
'
|
58
|
-
'
|
61
|
+
'appVersion' => @app_version,
|
62
|
+
'appversion' => @app_version,
|
63
|
+
'app_version' => @app_version,
|
59
64
|
'custom' => @custom,
|
60
65
|
}
|
61
66
|
end
|
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:
|
4
|
+
version: 1.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Statsig, Inc
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-
|
11
|
+
date: 2021-07-31 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -25,25 +25,19 @@ dependencies:
|
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '2.1'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
|
-
name:
|
28
|
+
name: user_agent_parser
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
31
|
- - "~>"
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: '
|
34
|
-
- - ">="
|
35
|
-
- !ruby/object:Gem::Version
|
36
|
-
version: 5.3.1
|
33
|
+
version: '2.7'
|
37
34
|
type: :runtime
|
38
35
|
prerelease: false
|
39
36
|
version_requirements: !ruby/object:Gem::Requirement
|
40
37
|
requirements:
|
41
38
|
- - "~>"
|
42
39
|
- !ruby/object:Gem::Version
|
43
|
-
version: '
|
44
|
-
- - ">="
|
45
|
-
- !ruby/object:Gem::Version
|
46
|
-
version: 5.3.1
|
40
|
+
version: '2.7'
|
47
41
|
- !ruby/object:Gem::Dependency
|
48
42
|
name: http
|
49
43
|
requirement: !ruby/object:Gem::Requirement
|
@@ -51,9 +45,6 @@ dependencies:
|
|
51
45
|
- - "~>"
|
52
46
|
- !ruby/object:Gem::Version
|
53
47
|
version: '4.4'
|
54
|
-
- - ">="
|
55
|
-
- !ruby/object:Gem::Version
|
56
|
-
version: 4.4.1
|
57
48
|
type: :runtime
|
58
49
|
prerelease: false
|
59
50
|
version_requirements: !ruby/object:Gem::Requirement
|
@@ -61,9 +52,20 @@ dependencies:
|
|
61
52
|
- - "~>"
|
62
53
|
- !ruby/object:Gem::Version
|
63
54
|
version: '4.4'
|
64
|
-
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: ip3country
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0.1'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
65
67
|
- !ruby/object:Gem::Version
|
66
|
-
version:
|
68
|
+
version: '0.1'
|
67
69
|
description: Statsig server SDK for feature gates and experimentation in Ruby
|
68
70
|
email: support@statsig.com
|
69
71
|
executables: []
|
@@ -80,6 +82,7 @@ files:
|
|
80
82
|
- lib/statsig_driver.rb
|
81
83
|
- lib/statsig_event.rb
|
82
84
|
- lib/statsig_logger.rb
|
85
|
+
- lib/statsig_options.rb
|
83
86
|
- lib/statsig_user.rb
|
84
87
|
homepage: https://rubygems.org/gems/statsig
|
85
88
|
licenses:
|