statsig 0.1.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 +7 -0
- data/lib/config_result.rb +13 -0
- data/lib/dynamic_config.rb +16 -0
- data/lib/evaluation_helpers.rb +25 -0
- data/lib/evaluator.rb +212 -0
- data/lib/network.rb +72 -0
- data/lib/spec_store.rb +51 -0
- data/lib/statsig.rb +42 -0
- data/lib/statsig_driver.rb +146 -0
- data/lib/statsig_event.rb +21 -0
- data/lib/statsig_logger.rb +52 -0
- data/lib/statsig_user.rb +49 -0
- metadata +173 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 60035280630edb2172623545efa323cc6b3ec0374b8ebc99ba7f28312a227b21
|
4
|
+
data.tar.gz: af799a2649633d8cc364d6e69fed72f3a30ff96673212d348f20b5fa0ef83f30
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: f69cf4534af68927136ec9d9bf1900ea206e3c86c7bdcdd294e03c0109071449adc44db0d5026f98cae2ff281985022e19ddb6e9a8cc9a275c0f53513c292e0a
|
7
|
+
data.tar.gz: 39269a606a0291a83c5f486535b7a075ccde3a82a50436598aed93732cd95b27c765220c56de6b50e1e515f7346ec107081dc71ea23be2ee83e33a3e88b745eb
|
@@ -0,0 +1,13 @@
|
|
1
|
+
class ConfigResult
|
2
|
+
attr_accessor :name
|
3
|
+
attr_accessor :gate_value
|
4
|
+
attr_accessor :json_value
|
5
|
+
attr_accessor :rule_id
|
6
|
+
|
7
|
+
def initialize(name, gate_value = false, json_value = {}, rule_id = '')
|
8
|
+
@name = name
|
9
|
+
@gate_value = gate_value
|
10
|
+
@json_value = json_value
|
11
|
+
@rule_id = rule_id
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
class DynamicConfig
|
2
|
+
attr_accessor :name
|
3
|
+
attr_accessor :value
|
4
|
+
attr_accessor :rule_id
|
5
|
+
|
6
|
+
def initialize(name, value = {}, rule_id = '')
|
7
|
+
@name = name
|
8
|
+
@value = value
|
9
|
+
@rule_id = rule_id
|
10
|
+
end
|
11
|
+
|
12
|
+
def get(index)
|
13
|
+
return nil if @value.nil?
|
14
|
+
value[index]
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module EvaluationHelpers
|
2
|
+
def self.compare_numbers(a, b, func)
|
3
|
+
return false unless self.is_numeric(a) && self.is_numeric(b)
|
4
|
+
func.call(a.to_f, b.to_f) rescue false
|
5
|
+
end
|
6
|
+
|
7
|
+
# returns true if array contains value, ignoring case when comparing strings
|
8
|
+
def self.array_contains(array, value)
|
9
|
+
return false unless array.is_a?(Array) && !value.nil?
|
10
|
+
return array.include?(value) unless value.is_a?(String)
|
11
|
+
array.any?{ |s| s.is_a?(String) && s.casecmp(value) == 0 } rescue false
|
12
|
+
end
|
13
|
+
|
14
|
+
# 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)
|
16
|
+
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
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def self.is_numeric(v)
|
23
|
+
!(v.to_s =~ /\A[-+]?\d*\.?\d+\z/).nil?
|
24
|
+
end
|
25
|
+
end
|
data/lib/evaluator.rb
ADDED
@@ -0,0 +1,212 @@
|
|
1
|
+
require 'browser'
|
2
|
+
require 'config_result'
|
3
|
+
require 'digest'
|
4
|
+
require 'evaluation_helpers'
|
5
|
+
require 'spec_store'
|
6
|
+
|
7
|
+
$fetch_from_server = :fetch_from_server
|
8
|
+
$type_dynamic_config = 'dynamic_config'
|
9
|
+
|
10
|
+
class Evaluator
|
11
|
+
include EvaluationHelpers
|
12
|
+
|
13
|
+
def initialize(store)
|
14
|
+
@spec_store = store
|
15
|
+
@initialized = true
|
16
|
+
end
|
17
|
+
|
18
|
+
def check_gate(user, gate_name)
|
19
|
+
return nil unless @initialized && @spec_store.has_gate?(gate_name)
|
20
|
+
self.eval_spec(user, @spec_store.get_gate(gate_name))
|
21
|
+
end
|
22
|
+
|
23
|
+
def get_config(user, config_name)
|
24
|
+
return nil unless @initialized && @spec_store.has_config?(config_name)
|
25
|
+
self.eval_spec(user, @spec_store.get_config(config_name))
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def eval_spec(user, config)
|
31
|
+
if config['enabled']
|
32
|
+
i = 0
|
33
|
+
until i >= config['rules'].length do
|
34
|
+
rule = config['rules'][i]
|
35
|
+
result = self.eval_rule(user, rule)
|
36
|
+
return $fetch_from_server if result == $fetch_from_server
|
37
|
+
if result
|
38
|
+
pass = self.eval_pass_percent(user, rule, config['salt'])
|
39
|
+
return ConfigResult.new(
|
40
|
+
config['name'],
|
41
|
+
pass,
|
42
|
+
pass ? rule['returnValue'] : config['defaultValue'],
|
43
|
+
rule['id'],
|
44
|
+
)
|
45
|
+
end
|
46
|
+
|
47
|
+
i += 1
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
ConfigResult.new(config['name'], false, config['defaultValue'], 'default')
|
52
|
+
end
|
53
|
+
|
54
|
+
def eval_rule(user, rule)
|
55
|
+
i = 0
|
56
|
+
until i >= rule['conditions'].length do
|
57
|
+
result = self.eval_condition(user, rule['conditions'][i])
|
58
|
+
return result unless result == true
|
59
|
+
i += 1
|
60
|
+
end
|
61
|
+
true
|
62
|
+
end
|
63
|
+
|
64
|
+
def eval_condition(user, condition)
|
65
|
+
value = nil
|
66
|
+
field = condition['field']
|
67
|
+
target = condition['targetValue']
|
68
|
+
type = condition['type']
|
69
|
+
operator = condition['operator']
|
70
|
+
|
71
|
+
return $fetch_from_server unless type.is_a?(String)
|
72
|
+
type = type.downcase
|
73
|
+
|
74
|
+
case type
|
75
|
+
when 'public'
|
76
|
+
return true
|
77
|
+
when 'fail_gate'
|
78
|
+
when 'pass_gate'
|
79
|
+
other_gate_result = self.check_gate(user, target)
|
80
|
+
return $fetch_from_server if other_gate_result == $fetch_from_server
|
81
|
+
return type == 'pass_gate' ? other_gate_result[:gate_value] : !other_gate_result[:gate_value]
|
82
|
+
when 'ip_based'
|
83
|
+
value = get_value_from_user(user, field) || get_value_from_ip(user['ip'], field)
|
84
|
+
return $fetch_from_server if value == $fetch_from_server
|
85
|
+
when 'ua_based'
|
86
|
+
value = get_value_from_user(user, field) || get_value_from_ua(user['userAgent'], field)
|
87
|
+
return $fetch_from_server if value == $fetch_from_server
|
88
|
+
when 'user_field'
|
89
|
+
value = get_value_from_user(user, field)
|
90
|
+
when 'current_time'
|
91
|
+
value = Time.now.to_f # epoch time in seconds
|
92
|
+
else
|
93
|
+
return $fetch_from_server
|
94
|
+
end
|
95
|
+
|
96
|
+
return $fetch_from_server if value == $fetch_from_server
|
97
|
+
return false if value.nil?
|
98
|
+
|
99
|
+
return $fetch_from_server unless operator.is_a?(String)
|
100
|
+
operator = operator.downcase
|
101
|
+
|
102
|
+
case operator
|
103
|
+
# numerical comparison
|
104
|
+
when 'gt'
|
105
|
+
return compare_numbers(value, target, ->(a, b) { a > b })
|
106
|
+
when 'gte'
|
107
|
+
return compare_numbers(value, target, ->(a, b) { a >= b })
|
108
|
+
when 'lt'
|
109
|
+
return compare_numbers(value, target, ->(a, b) { a < b })
|
110
|
+
when 'lte'
|
111
|
+
return compare_numbers(value, target, ->(a, b) { a <= b })
|
112
|
+
|
113
|
+
# version comparison
|
114
|
+
when 'version_gt'
|
115
|
+
return (Gem::Version.new(value) > Gem::Version.new(target) rescue false)
|
116
|
+
when 'version_gte'
|
117
|
+
return (Gem::Version.new(value) >= Gem::Version.new(target) rescue false)
|
118
|
+
when 'version_lt'
|
119
|
+
return (Gem::Version.new(value) < Gem::Version.new(target) rescue false)
|
120
|
+
when 'version_lte'
|
121
|
+
return (Gem::Version.new(value) <= Gem::Version.new(target) rescue false)
|
122
|
+
when 'version_eq'
|
123
|
+
return (Gem::Version.new(value) == Gem::Version.new(target) rescue false)
|
124
|
+
when 'version_neq'
|
125
|
+
return (Gem::Version.new(value) != Gem::Version.new(target) rescue false)
|
126
|
+
|
127
|
+
# array operations
|
128
|
+
when 'any'
|
129
|
+
return array_contains(target, value)
|
130
|
+
when 'none'
|
131
|
+
return !array_contains(target, value)
|
132
|
+
|
133
|
+
#string
|
134
|
+
when 'str_starts_with_any'
|
135
|
+
return match_string_in_array(target, value, ->(a, b) { a.start_with?(b) })
|
136
|
+
when 'str_ends_with_any'
|
137
|
+
return match_string_in_array(target, value, ->(a, b) { a.end_with?(b) })
|
138
|
+
when 'str_contains_any'
|
139
|
+
return match_string_in_array(target, value, ->(a, b) { a.include?(b) })
|
140
|
+
when 'str_matches'
|
141
|
+
return (value.is_a?(String) && !(value =~ Regexp.new(target)).nil? rescue false)
|
142
|
+
when 'eq'
|
143
|
+
return value == target
|
144
|
+
when 'neq'
|
145
|
+
return value != target
|
146
|
+
|
147
|
+
# dates
|
148
|
+
when 'before'
|
149
|
+
# TODO - planned future conditions
|
150
|
+
when 'after'
|
151
|
+
# TODO - planned future conditions
|
152
|
+
when 'on'
|
153
|
+
# TODO - planned future conditions
|
154
|
+
else
|
155
|
+
return $fetch_from_server
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
def get_value_from_user(user, field)
|
160
|
+
return nil unless user.instance_of?(StatsigUser) && field.is_a?(String)
|
161
|
+
|
162
|
+
user_lookup_table = user&.value_lookup
|
163
|
+
return nil unless user_lookup_table.is_a?(Hash)
|
164
|
+
return user_lookup_table[field.downcase] if user_lookup_table.has_key?(field.downcase)
|
165
|
+
|
166
|
+
user_custom = user_lookup_table['custom']
|
167
|
+
return nil unless user_custom.is_a?(Hash)
|
168
|
+
user_custom.each do |key, value|
|
169
|
+
return value if key.downcase.casecmp(field.downcase)
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
def get_value_from_ip(ip, field)
|
174
|
+
return nil unless ip.is_a?(String) && field.is_a?(String)
|
175
|
+
# TODO
|
176
|
+
$fetch_from_server
|
177
|
+
end
|
178
|
+
|
179
|
+
def get_value_from_ua(ua, field)
|
180
|
+
return nil unless ua.is_a?(String) && field.is_a?(String)
|
181
|
+
b = Browser.new(ua)
|
182
|
+
case field.downcase
|
183
|
+
when 'os_name'
|
184
|
+
os_name = b.platform.name
|
185
|
+
# special case for iOS because value is 'iOS (iPhone)'
|
186
|
+
if os_name.include?('iOS') || os_name.include?('ios')
|
187
|
+
return 'iOS'
|
188
|
+
else
|
189
|
+
return os_name
|
190
|
+
end
|
191
|
+
when 'os_version'
|
192
|
+
return b.platform.version
|
193
|
+
when 'browser_name'
|
194
|
+
return b.name
|
195
|
+
when 'browser_version'
|
196
|
+
return b.full_version
|
197
|
+
else
|
198
|
+
nil
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
def eval_pass_percent(user, rule, salt)
|
203
|
+
return false unless salt.is_a?(String) && !rule['passPercentage'].nil?
|
204
|
+
begin
|
205
|
+
user_id = user.user_id || ''
|
206
|
+
hash = Digest::SHA256.digest("#{salt}.#{rule['name']}.#{user_id}").unpack('Q>')[0]
|
207
|
+
return hash % 10000 < rule['passPercentage'].to_f * 100
|
208
|
+
rescue
|
209
|
+
return false
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end
|
data/lib/network.rb
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
require 'concurrent'
|
2
|
+
require 'http'
|
3
|
+
require 'json'
|
4
|
+
require 'dynamic_config'
|
5
|
+
|
6
|
+
class Network
|
7
|
+
include Concurrent::Async
|
8
|
+
|
9
|
+
def initialize(server_secret, api)
|
10
|
+
super()
|
11
|
+
unless api.end_with?('/')
|
12
|
+
api += '/'
|
13
|
+
end
|
14
|
+
@http = HTTP
|
15
|
+
.headers({"STATSIG-API-KEY" => server_secret, "Content-Type" => "application/json; charset=UTF-8"})
|
16
|
+
.accept(:json)
|
17
|
+
@api = api
|
18
|
+
@last_sync_time = 0
|
19
|
+
end
|
20
|
+
|
21
|
+
def check_gate(user, gate_name)
|
22
|
+
begin
|
23
|
+
request_body = JSON.generate({'user' => user&.serialize(), 'gateName' => gate_name})
|
24
|
+
response = @http.post(@api + 'check_gate', body: request_body)
|
25
|
+
return JSON.parse(response.body)
|
26
|
+
rescue
|
27
|
+
return false
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def get_config(user, dynamic_config_name)
|
32
|
+
begin
|
33
|
+
request_body = JSON.generate({'user' => user&.serialize(), 'configName' => dynamic_config_name})
|
34
|
+
response = @http.post(@api + 'get_config', body: request_body)
|
35
|
+
return JSON.parse(response.body)
|
36
|
+
rescue
|
37
|
+
return nil
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def download_config_specs
|
42
|
+
begin
|
43
|
+
response = @http.post(@api + 'download_config_specs', body: JSON.generate({'sinceTime' => @last_sync_time}))
|
44
|
+
json_body = JSON.parse(response.body)
|
45
|
+
@last_sync_time = json_body['time']
|
46
|
+
return json_body
|
47
|
+
rescue
|
48
|
+
return nil
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def poll_for_changes(callback)
|
53
|
+
Thread.new do
|
54
|
+
loop do
|
55
|
+
sleep 10
|
56
|
+
specs = download_config_specs
|
57
|
+
unless specs.nil?
|
58
|
+
callback.call(specs)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def post_logs(events, statsig_metadata)
|
65
|
+
begin
|
66
|
+
json_body = JSON.generate({'events' => events, 'statsigMetadata' => statsig_metadata})
|
67
|
+
@http.post(@api + 'log_event', body: json_body)
|
68
|
+
rescue
|
69
|
+
# TODO: retries
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
data/lib/spec_store.rb
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
require 'uri'
|
3
|
+
|
4
|
+
class SpecStore
|
5
|
+
def initialize(specs_json)
|
6
|
+
@last_sync_time = 0
|
7
|
+
@store = {
|
8
|
+
:gates => {},
|
9
|
+
:configs => {},
|
10
|
+
}
|
11
|
+
process(specs_json)
|
12
|
+
end
|
13
|
+
|
14
|
+
def process(specs_json)
|
15
|
+
if specs_json.nil?
|
16
|
+
return
|
17
|
+
end
|
18
|
+
|
19
|
+
@last_sync_time = specs_json['time'] || @last_sync_time
|
20
|
+
return unless specs_json['has_updates'] == true &&
|
21
|
+
!specs_json['feature_gates'].nil? &&
|
22
|
+
!specs_json['dynamic_configs'].nil?
|
23
|
+
|
24
|
+
@store = {
|
25
|
+
:gates => {},
|
26
|
+
:configs => {},
|
27
|
+
}
|
28
|
+
|
29
|
+
specs_json['feature_gates'].map{|gate| @store[:gates][gate['name']] = gate }
|
30
|
+
specs_json['dynamic_configs'].map{|config| @store[:configs][config['name']] = config }
|
31
|
+
end
|
32
|
+
|
33
|
+
def has_gate?(gate_name)
|
34
|
+
return @store[:gates].key?(gate_name)
|
35
|
+
end
|
36
|
+
|
37
|
+
def has_config?(config_name)
|
38
|
+
return @store[:configs].key?(config_name)
|
39
|
+
end
|
40
|
+
|
41
|
+
def get_gate(gate_name)
|
42
|
+
return nil unless has_gate?(gate_name)
|
43
|
+
@store[:gates][gate_name]
|
44
|
+
end
|
45
|
+
|
46
|
+
def get_config(config_name)
|
47
|
+
return nil unless has_config?(config_name)
|
48
|
+
@store[:configs][config_name]
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
data/lib/statsig.rb
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'statsig_driver'
|
2
|
+
|
3
|
+
module Statsig
|
4
|
+
def self.initialize(secret_key)
|
5
|
+
unless @shared_instance.nil?
|
6
|
+
puts 'Statsig already initialized.'
|
7
|
+
return @shared_instance
|
8
|
+
end
|
9
|
+
|
10
|
+
@shared_instance = StatsigDriver.new(secret_key)
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.check_gate(user, gate_name)
|
14
|
+
self.ensure_initialized
|
15
|
+
@shared_instance.check_gate(user, gate_name)
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.get_config(user, dynamic_config_name)
|
19
|
+
self.ensure_initialized
|
20
|
+
@shared_instance.get_config(user, dynamic_config_name)
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.log_event(user, event_name, value, metadata)
|
24
|
+
self.ensure_initialized
|
25
|
+
@shared_instance.log_event(user, event_name, value, metadata)
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.shutdown
|
29
|
+
unless @shared_instance.nil?
|
30
|
+
@shared_instance.shutdown
|
31
|
+
end
|
32
|
+
@shared_instance = nil
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def self.ensure_initialized
|
38
|
+
if @shared_instance.nil?
|
39
|
+
raise 'Must call initialize first.'
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,146 @@
|
|
1
|
+
require 'concurrent'
|
2
|
+
require 'config_result'
|
3
|
+
require 'evaluator'
|
4
|
+
require 'network'
|
5
|
+
require 'statsig_event'
|
6
|
+
require 'statsig_logger'
|
7
|
+
require 'statsig_user'
|
8
|
+
require 'spec_store'
|
9
|
+
|
10
|
+
class StatsigDriver
|
11
|
+
include Concurrent::Async
|
12
|
+
|
13
|
+
def initialize(secret_key)
|
14
|
+
super()
|
15
|
+
if !secret_key.is_a?(String) || !secret_key.start_with?('secret-')
|
16
|
+
raise 'Invalid secret key provided. Provide your project secret key from the Statsig console'
|
17
|
+
end
|
18
|
+
@shutdown = false
|
19
|
+
@secret_key = secret_key
|
20
|
+
@net = Network.new(secret_key, 'https://api.statsig.com/v1/')
|
21
|
+
@statsig_metadata = {
|
22
|
+
'sdkType' => 'ruby-server',
|
23
|
+
'sdkVersion' => Gem::Specification::load('statsig.gemspec')&.version,
|
24
|
+
}
|
25
|
+
@logger = StatsigLogger.new(@net, @statsig_metadata)
|
26
|
+
|
27
|
+
downloaded_specs = @net.download_config_specs
|
28
|
+
unless downloaded_specs.nil?
|
29
|
+
@initialized = true
|
30
|
+
end
|
31
|
+
|
32
|
+
@store = SpecStore.new(downloaded_specs)
|
33
|
+
@evaluator = Evaluator.new(@store)
|
34
|
+
|
35
|
+
@polling_thread = @net.poll_for_changes(-> (config_specs) { @store.process(config_specs) })
|
36
|
+
end
|
37
|
+
|
38
|
+
def check_gate(user, gate_name)
|
39
|
+
if !user.nil? && !user.instance_of?(StatsigUser)
|
40
|
+
raise 'Must provide a valid StatsigUser'
|
41
|
+
end
|
42
|
+
if !gate_name.is_a?(String) || gate_name.empty?
|
43
|
+
raise 'Invalid gate_name provided'
|
44
|
+
end
|
45
|
+
check_shutdown
|
46
|
+
unless @initialized
|
47
|
+
return false
|
48
|
+
end
|
49
|
+
|
50
|
+
res = @evaluator.check_gate(user, gate_name)
|
51
|
+
if res.nil?
|
52
|
+
res = ConfigResult.new(gate_name)
|
53
|
+
end
|
54
|
+
|
55
|
+
if res == $fetch_from_server
|
56
|
+
res = check_gate_fallback(user, gate_name)
|
57
|
+
end
|
58
|
+
|
59
|
+
@logger.logGateExposure(user, res.name, res.gate_value, res.rule_id)
|
60
|
+
res.gate_value
|
61
|
+
end
|
62
|
+
|
63
|
+
def get_config(user, dynamic_config_name)
|
64
|
+
if !user.nil? && !user.instance_of?(StatsigUser)
|
65
|
+
raise 'Must provide a valid StatsigUser or nil'
|
66
|
+
end
|
67
|
+
if !dynamic_config_name.is_a?(String) || dynamic_config_name.empty?
|
68
|
+
raise "Invalid dynamic_config_name provided"
|
69
|
+
end
|
70
|
+
check_shutdown
|
71
|
+
unless @initialized
|
72
|
+
return DynamicConfig.new(dynamic_config_name)
|
73
|
+
end
|
74
|
+
|
75
|
+
res = @evaluator.get_config(user, dynamic_config_name)
|
76
|
+
if res.nil?
|
77
|
+
res = ConfigResult.new(dynamic_config_name)
|
78
|
+
end
|
79
|
+
|
80
|
+
if res == $fetch_from_server
|
81
|
+
res = get_config_fallback(user, dynamic_config_name)
|
82
|
+
end
|
83
|
+
|
84
|
+
result_config = DynamicConfig.new(res.name, res.json_value, res.rule_id)
|
85
|
+
@logger.logConfigExposure(user, result_config.name, result_config.rule_id)
|
86
|
+
result_config
|
87
|
+
end
|
88
|
+
|
89
|
+
def log_event(user, event_name, value = nil, metadata = nil)
|
90
|
+
if !user.nil? && !user.instance_of?(StatsigUser)
|
91
|
+
raise 'Must provide a valid StatsigUser or nil'
|
92
|
+
end
|
93
|
+
check_shutdown
|
94
|
+
|
95
|
+
event = StatsigEvent.new(event_name)
|
96
|
+
event.user = user&.serialize
|
97
|
+
event.value = value
|
98
|
+
event.metadata = metadata
|
99
|
+
event.statsig_metadata = @statsig_metadata
|
100
|
+
@logger.log_event(event)
|
101
|
+
end
|
102
|
+
|
103
|
+
def shutdown
|
104
|
+
@shutdown = true
|
105
|
+
@logger.flush
|
106
|
+
@polling_thread&.exit
|
107
|
+
end
|
108
|
+
|
109
|
+
private
|
110
|
+
|
111
|
+
def check_shutdown
|
112
|
+
if @shutdown
|
113
|
+
puts 'SDK has been shutdown. Updates in the Statsig Console will no longer reflect.'
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def check_gate_fallback(user, gate_name)
|
118
|
+
network_result = @net.check_gate(user, gate_name)
|
119
|
+
if network_result.nil?
|
120
|
+
config_result = ConfigResult.new(gate_name)
|
121
|
+
return config_result
|
122
|
+
end
|
123
|
+
|
124
|
+
ConfigResult.new(
|
125
|
+
network_result['name'],
|
126
|
+
network_result['value'],
|
127
|
+
{},
|
128
|
+
network_result['rule_id'],
|
129
|
+
)
|
130
|
+
end
|
131
|
+
|
132
|
+
def get_config_fallback(user, dynamic_config_name)
|
133
|
+
network_result = @net.get_config(user, dynamic_config_name)
|
134
|
+
if network_result.nil?
|
135
|
+
config_result = ConfigResult.new(dynamic_config_name)
|
136
|
+
return config_result
|
137
|
+
end
|
138
|
+
|
139
|
+
ConfigResult.new(
|
140
|
+
network_result['name'],
|
141
|
+
false,
|
142
|
+
network_result['value'],
|
143
|
+
network_result['rule_id'],
|
144
|
+
)
|
145
|
+
end
|
146
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
class StatsigEvent
|
2
|
+
attr_accessor :value
|
3
|
+
attr_accessor :user
|
4
|
+
attr_accessor :metadata
|
5
|
+
attr_accessor :statsig_metadata
|
6
|
+
def initialize(event_name)
|
7
|
+
@event_name = event_name
|
8
|
+
@time = Time.now.to_i * 1000
|
9
|
+
end
|
10
|
+
|
11
|
+
def serialize
|
12
|
+
return {
|
13
|
+
'eventName' => @event_name,
|
14
|
+
'metadata' => @metadata,
|
15
|
+
'value' => @value,
|
16
|
+
'user' => @user,
|
17
|
+
'time' => @time,
|
18
|
+
'statsigMetadata' => @statsig_metadata,
|
19
|
+
}
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'statsig_event'
|
2
|
+
|
3
|
+
$gate_exposure_event = 'statsig::gate_exposure'
|
4
|
+
$config_exposure_event = 'statsig::config_exposure'
|
5
|
+
|
6
|
+
class StatsigLogger
|
7
|
+
def initialize(network, statsig_metadata)
|
8
|
+
@network = network
|
9
|
+
@statsig_metadata = statsig_metadata
|
10
|
+
@events = []
|
11
|
+
end
|
12
|
+
|
13
|
+
def log_event(event)
|
14
|
+
@events.push(event)
|
15
|
+
if @events.length >= 500
|
16
|
+
flush()
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def logGateExposure(user, gate_name, value, rule_id)
|
21
|
+
event = StatsigEvent.new($gate_exposure_event)
|
22
|
+
event.user = user
|
23
|
+
event.metadata = {
|
24
|
+
'gate' => gate_name,
|
25
|
+
'gateValue' => value.to_s,
|
26
|
+
'ruleID' => rule_id
|
27
|
+
}
|
28
|
+
event.statsig_metadata = @statsig_metadata
|
29
|
+
log_event(event)
|
30
|
+
end
|
31
|
+
|
32
|
+
def logConfigExposure(user, config_name, rule_id)
|
33
|
+
event = StatsigEvent.new($config_exposure_event)
|
34
|
+
event.user = user
|
35
|
+
event.metadata = {
|
36
|
+
'config' => config_name,
|
37
|
+
'ruleID' => rule_id
|
38
|
+
}
|
39
|
+
event.statsig_metadata = @statsig_metadata
|
40
|
+
log_event(event)
|
41
|
+
end
|
42
|
+
|
43
|
+
def flush
|
44
|
+
if @events.length == 0
|
45
|
+
return
|
46
|
+
end
|
47
|
+
flush_events = @events.map { |e| e.serialize() }
|
48
|
+
@events = []
|
49
|
+
|
50
|
+
@network.post_logs(flush_events, @statsig_metadata)
|
51
|
+
end
|
52
|
+
end
|
data/lib/statsig_user.rb
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
class StatsigUser
|
2
|
+
attr_accessor :user_id
|
3
|
+
attr_accessor :email
|
4
|
+
attr_accessor :ip
|
5
|
+
attr_accessor :user_agent
|
6
|
+
attr_accessor :country
|
7
|
+
attr_accessor :locale
|
8
|
+
attr_accessor :client_version
|
9
|
+
|
10
|
+
def custom
|
11
|
+
@custom
|
12
|
+
end
|
13
|
+
|
14
|
+
def custom=(value)
|
15
|
+
@custom = value.is_a?(Hash) ? value : Hash.new
|
16
|
+
end
|
17
|
+
|
18
|
+
def serialize
|
19
|
+
{
|
20
|
+
'userID' => @user_id,
|
21
|
+
'email' => @email,
|
22
|
+
'ip' => @ip,
|
23
|
+
'userAgent' => @user_agent,
|
24
|
+
'country' => @country,
|
25
|
+
'locale' => @locale,
|
26
|
+
'clientVersion' => @client_version,
|
27
|
+
'custom' => @custom,
|
28
|
+
}
|
29
|
+
end
|
30
|
+
|
31
|
+
def value_lookup
|
32
|
+
{
|
33
|
+
'userID' => @user_id,
|
34
|
+
'userid' => @user_id,
|
35
|
+
'user_id' => @user_id,
|
36
|
+
'email' => @email,
|
37
|
+
'ip' => @ip,
|
38
|
+
'userAgent' => @user_agent,
|
39
|
+
'useragent' => @user_agent,
|
40
|
+
'user_agent' => @user_agent,
|
41
|
+
'country' => @country,
|
42
|
+
'locale' => @locale,
|
43
|
+
'clientVersion' => @client_version,
|
44
|
+
'clientversion' => @client_version,
|
45
|
+
'client_version' => @client_version,
|
46
|
+
'custom' => @custom,
|
47
|
+
}
|
48
|
+
end
|
49
|
+
end
|
metadata
ADDED
@@ -0,0 +1,173 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: statsig
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Statsig, Inc
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2021-06-01 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: concurrent-ruby
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.1'
|
20
|
+
- - ">="
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: 1.1.0
|
23
|
+
type: :development
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
requirements:
|
27
|
+
- - "~>"
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '1.1'
|
30
|
+
- - ">="
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: 1.1.0
|
33
|
+
- !ruby/object:Gem::Dependency
|
34
|
+
name: http
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - "~>"
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '4.4'
|
40
|
+
- - ">="
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
version: 4.4.1
|
43
|
+
type: :development
|
44
|
+
prerelease: false
|
45
|
+
version_requirements: !ruby/object:Gem::Requirement
|
46
|
+
requirements:
|
47
|
+
- - "~>"
|
48
|
+
- !ruby/object:Gem::Version
|
49
|
+
version: '4.4'
|
50
|
+
- - ">="
|
51
|
+
- !ruby/object:Gem::Version
|
52
|
+
version: 4.4.1
|
53
|
+
- !ruby/object:Gem::Dependency
|
54
|
+
name: browser
|
55
|
+
requirement: !ruby/object:Gem::Requirement
|
56
|
+
requirements:
|
57
|
+
- - "~>"
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
version: '5.3'
|
60
|
+
- - ">="
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: 5.3.1
|
63
|
+
type: :development
|
64
|
+
prerelease: false
|
65
|
+
version_requirements: !ruby/object:Gem::Requirement
|
66
|
+
requirements:
|
67
|
+
- - "~>"
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '5.3'
|
70
|
+
- - ">="
|
71
|
+
- !ruby/object:Gem::Version
|
72
|
+
version: 5.3.1
|
73
|
+
- !ruby/object:Gem::Dependency
|
74
|
+
name: browser
|
75
|
+
requirement: !ruby/object:Gem::Requirement
|
76
|
+
requirements:
|
77
|
+
- - "~>"
|
78
|
+
- !ruby/object:Gem::Version
|
79
|
+
version: '5.3'
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 5.3.1
|
83
|
+
type: :runtime
|
84
|
+
prerelease: false
|
85
|
+
version_requirements: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '5.3'
|
90
|
+
- - ">="
|
91
|
+
- !ruby/object:Gem::Version
|
92
|
+
version: 5.3.1
|
93
|
+
- !ruby/object:Gem::Dependency
|
94
|
+
name: concurrent-ruby
|
95
|
+
requirement: !ruby/object:Gem::Requirement
|
96
|
+
requirements:
|
97
|
+
- - "~>"
|
98
|
+
- !ruby/object:Gem::Version
|
99
|
+
version: '1.1'
|
100
|
+
- - ">="
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
version: 1.1.0
|
103
|
+
type: :runtime
|
104
|
+
prerelease: false
|
105
|
+
version_requirements: !ruby/object:Gem::Requirement
|
106
|
+
requirements:
|
107
|
+
- - "~>"
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '1.1'
|
110
|
+
- - ">="
|
111
|
+
- !ruby/object:Gem::Version
|
112
|
+
version: 1.1.0
|
113
|
+
- !ruby/object:Gem::Dependency
|
114
|
+
name: http
|
115
|
+
requirement: !ruby/object:Gem::Requirement
|
116
|
+
requirements:
|
117
|
+
- - "~>"
|
118
|
+
- !ruby/object:Gem::Version
|
119
|
+
version: '4.4'
|
120
|
+
- - ">="
|
121
|
+
- !ruby/object:Gem::Version
|
122
|
+
version: 4.4.1
|
123
|
+
type: :runtime
|
124
|
+
prerelease: false
|
125
|
+
version_requirements: !ruby/object:Gem::Requirement
|
126
|
+
requirements:
|
127
|
+
- - "~>"
|
128
|
+
- !ruby/object:Gem::Version
|
129
|
+
version: '4.4'
|
130
|
+
- - ">="
|
131
|
+
- !ruby/object:Gem::Version
|
132
|
+
version: 4.4.1
|
133
|
+
description: Statsig server SDK for feature gates and experimentation in Ruby
|
134
|
+
email: support@statsig.com
|
135
|
+
executables: []
|
136
|
+
extensions: []
|
137
|
+
extra_rdoc_files: []
|
138
|
+
files:
|
139
|
+
- lib/config_result.rb
|
140
|
+
- lib/dynamic_config.rb
|
141
|
+
- lib/evaluation_helpers.rb
|
142
|
+
- lib/evaluator.rb
|
143
|
+
- lib/network.rb
|
144
|
+
- lib/spec_store.rb
|
145
|
+
- lib/statsig.rb
|
146
|
+
- lib/statsig_driver.rb
|
147
|
+
- lib/statsig_event.rb
|
148
|
+
- lib/statsig_logger.rb
|
149
|
+
- lib/statsig_user.rb
|
150
|
+
homepage: https://rubygems.org/gems/statsig
|
151
|
+
licenses:
|
152
|
+
- ISC
|
153
|
+
metadata: {}
|
154
|
+
post_install_message:
|
155
|
+
rdoc_options: []
|
156
|
+
require_paths:
|
157
|
+
- lib
|
158
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
159
|
+
requirements:
|
160
|
+
- - ">="
|
161
|
+
- !ruby/object:Gem::Version
|
162
|
+
version: '0'
|
163
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
164
|
+
requirements:
|
165
|
+
- - ">="
|
166
|
+
- !ruby/object:Gem::Version
|
167
|
+
version: '0'
|
168
|
+
requirements: []
|
169
|
+
rubygems_version: 3.2.3
|
170
|
+
signing_key:
|
171
|
+
specification_version: 4
|
172
|
+
summary: Statsig server SDK for Ruby
|
173
|
+
test_files: []
|