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 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: []