statsig 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -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: []