statsig 1.8.4 → 1.9.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e6b5d1c640eab6b3173784683e1e87c41c97410631aaea5e9e68306cd45df829
4
- data.tar.gz: c78cb2b73b63f496672a2486a8fb7fe212dc4192838b4cf72a5e191f9b4d065f
3
+ metadata.gz: afbf6d972c5aeead4eeaa8c352155c41b02a3710f55d80309939d5acc0fd1e80
4
+ data.tar.gz: c83f0399868af1d7978aeb2d2723f4bfbc860646de3ea0a7aaf840903489b92a
5
5
  SHA512:
6
- metadata.gz: 2b4601bb00d1287c24aceca4e85d9e4c786225d6c42a8abb8bd70ab687d6d0ce508e3e762b302306617d3f17e81d693293509aa1f1550ab3ad670c4bca464737
7
- data.tar.gz: d5518fc18cd937879281663b1592e286fbe45cd2a222e5ef6ab7659443fb5f5db4875ba7f29d7acbe28a5f81b8bec54195123617f58db09fa442b2ec224c1559
6
+ metadata.gz: babe4ac6dc398bd87df7537ea7c9391e72e4a32c4ec80944895adb507351a02042dd2bfc28deb2336e706fcf531ba4d30dfdeeb280404b108a25505fcd749ed8
7
+ data.tar.gz: c1bad8f27c2675b7dbc125090a56062e71c5fddb6f0cc8464c0e9ea03b910f9ca918a9ccf4e6c8db99815b7964aaf543d0cc69db5a873bcf58ddc806ef8932cc
data/lib/config_result.rb CHANGED
@@ -6,13 +6,19 @@ module Statsig
6
6
  attr_accessor :json_value
7
7
  attr_accessor :rule_id
8
8
  attr_accessor :secondary_exposures
9
+ attr_accessor :undelegated_sec_exps
10
+ attr_accessor :config_delegate
11
+ attr_accessor :explicit_parameters
9
12
 
10
- def initialize(name, gate_value = false, json_value = {}, rule_id = '', secondary_exposures = [])
13
+ def initialize(name, gate_value = false, json_value = {}, rule_id = '', secondary_exposures = [], undelegated_sec_exps = [], config_delegate = '', explicit_parameters = [])
11
14
  @name = name
12
15
  @gate_value = gate_value
13
16
  @json_value = json_value
14
17
  @rule_id = rule_id
15
18
  @secondary_exposures = secondary_exposures.is_a?(Array) ? secondary_exposures : []
19
+ @undelegated_sec_exps = undelegated_sec_exps.is_a?(Array) ? undelegated_sec_exps : []
20
+ @config_delegate = config_delegate
21
+ @explicit_parameters = explicit_parameters
16
22
  end
17
23
  end
18
24
  end
data/lib/evaluator.rb CHANGED
@@ -7,7 +7,7 @@ require 'time'
7
7
  require 'user_agent_parser'
8
8
  require 'user_agent_parser/operating_system'
9
9
 
10
- $fetch_from_server = :fetch_from_server
10
+ $fetch_from_server = 'fetch_from_server'
11
11
  $type_dynamic_config = 'dynamic_config'
12
12
 
13
13
  module Statsig
@@ -29,6 +29,11 @@ module Statsig
29
29
  eval_spec(user, @spec_store.get_config(config_name))
30
30
  end
31
31
 
32
+ def get_layer(user, layer_name)
33
+ return nil unless @initialized && @spec_store.has_layer?(layer_name)
34
+ eval_spec(user, @spec_store.get_layer(layer_name))
35
+ end
36
+
32
37
  def shutdown
33
38
  @spec_store.shutdown
34
39
  end
@@ -43,15 +48,20 @@ module Statsig
43
48
  until i >= config['rules'].length do
44
49
  rule = config['rules'][i]
45
50
  result = eval_rule(user, rule)
46
- return $fetch_from_server if result == $fetch_from_server
47
- exposures = exposures + result['exposures'] if result['exposures'].is_a? Array
48
- if result['value']
51
+ return $fetch_from_server if result.to_s == $fetch_from_server
52
+ exposures = exposures + result.secondary_exposures
53
+ if result.gate_value
54
+
55
+ if (delegated_result = eval_delegate(config['name'], user, rule, exposures))
56
+ return delegated_result
57
+ end
58
+
49
59
  pass = eval_pass_percent(user, rule, config['salt'])
50
60
  return Statsig::ConfigResult.new(
51
61
  config['name'],
52
62
  pass,
53
- pass ? rule['returnValue'] : config['defaultValue'],
54
- rule['id'],
63
+ pass ? result.json_value : config['defaultValue'],
64
+ result.rule_id,
55
65
  exposures
56
66
  )
57
67
  end
@@ -71,7 +81,7 @@ module Statsig
71
81
  i = 0
72
82
  until i >= rule['conditions'].length do
73
83
  result = eval_condition(user, rule['conditions'][i])
74
- if result == $fetch_from_server
84
+ if result.to_s == $fetch_from_server
75
85
  return $fetch_from_server
76
86
  end
77
87
 
@@ -83,7 +93,23 @@ module Statsig
83
93
  end
84
94
  i += 1
85
95
  end
86
- { 'value' => pass, 'exposures' => exposures }
96
+
97
+ Statsig::ConfigResult.new('', pass, rule['returnValue'], rule['id'], exposures)
98
+ end
99
+
100
+ def eval_delegate(name, user, rule, exposures)
101
+ return nil unless (delegate = rule['configDelegate'])
102
+ return nil unless (config = @spec_store.get_config(delegate))
103
+
104
+ delegated_result = self.eval_spec(user, config)
105
+ return $fetch_from_server if delegated_result.to_s == $fetch_from_server
106
+
107
+ delegated_result.name = name
108
+ delegated_result.config_delegate = delegate
109
+ delegated_result.secondary_exposures = exposures + delegated_result.secondary_exposures
110
+ delegated_result.undelegated_sec_exps = exposures
111
+ delegated_result.explicit_parameters = config['explicitParameters']
112
+ delegated_result
87
113
  end
88
114
 
89
115
  def eval_condition(user, condition)
@@ -104,7 +130,7 @@ module Statsig
104
130
  return true
105
131
  when 'fail_gate', 'pass_gate'
106
132
  other_gate_result = check_gate(user, target)
107
- return $fetch_from_server if other_gate_result == $fetch_from_server
133
+ return $fetch_from_server if other_gate_result.to_s == $fetch_from_server
108
134
 
109
135
  gate_value = other_gate_result&.gate_value == true
110
136
  new_exposure = {
@@ -119,10 +145,10 @@ module Statsig
119
145
  }
120
146
  when 'ip_based'
121
147
  value = get_value_from_user(user, field) || get_value_from_ip(user, field)
122
- return $fetch_from_server if value == $fetch_from_server
148
+ return $fetch_from_server if value.to_s == $fetch_from_server
123
149
  when 'ua_based'
124
150
  value = get_value_from_user(user, field) || get_value_from_ua(user, field)
125
- return $fetch_from_server if value == $fetch_from_server
151
+ return $fetch_from_server if value.to_s == $fetch_from_server
126
152
  when 'user_field'
127
153
  value = get_value_from_user(user, field)
128
154
  when 'environment_field'
@@ -144,7 +170,7 @@ module Statsig
144
170
  return $fetch_from_server
145
171
  end
146
172
 
147
- return $fetch_from_server if value == $fetch_from_server || !operator.is_a?(String)
173
+ return $fetch_from_server if value.to_s == $fetch_from_server || !operator.is_a?(String)
148
174
  operator = operator.downcase
149
175
 
150
176
  case operator
@@ -214,13 +240,15 @@ module Statsig
214
240
  return EvaluationHelpers::compare_times(value, target, ->(a, b) { a.year == b.year && a.month == b.month && a.day == b.day })
215
241
  when 'in_segment_list', 'not_in_segment_list'
216
242
  begin
217
- id_list = (@spec_store.get_id_list(target) || {:ids => {}})[:ids]
218
- hashed_id = Digest::SHA256.base64digest(value.to_s)[0, 8]
219
- is_in_list = id_list.is_a?(Hash) && id_list[hashed_id] == true
220
-
221
- return is_in_list if operator == 'in_segment_list'
222
- return !is_in_list
223
- rescue StandardError => e
243
+ is_in_list = false
244
+ id_list = @spec_store.get_id_list(target)
245
+ if id_list.is_a? IDList
246
+ hashed_id = Digest::SHA256.base64digest(value.to_s)[0, 8]
247
+ is_in_list = id_list.ids.include?(hashed_id)
248
+ end
249
+ return is_in_list if operator == 'in_segment_list'
250
+ return !is_in_list
251
+ rescue
224
252
  return false
225
253
  end
226
254
  else
@@ -294,7 +322,7 @@ module Statsig
294
322
  def eval_pass_percent(user, rule, config_salt)
295
323
  return false unless config_salt.is_a?(String) && !rule['passPercentage'].nil?
296
324
  begin
297
- unit_id = get_unit_id(user, rule['id_type']) || ''
325
+ unit_id = get_unit_id(user, rule['idType']) || ''
298
326
  rule_salt = rule['salt'] || rule['id'] || ''
299
327
  hash = compute_user_hash("#{config_salt}.#{rule_salt}.#{unit_id}")
300
328
  return (hash % 10000) < (rule['passPercentage'].to_f * 100)
data/lib/id_list.rb ADDED
@@ -0,0 +1,36 @@
1
+ module Statsig
2
+ class IDList
3
+ attr_accessor :name
4
+ attr_accessor :size
5
+ attr_accessor :creation_time
6
+ attr_accessor :url
7
+ attr_accessor :file_id
8
+ attr_accessor :ids
9
+
10
+ def initialize(json, ids = Set.new)
11
+ @name = json['name'] || ''
12
+ @size = json['size'] || 0
13
+ @creation_time = json['creationTime'] || 0
14
+ @url = json['url']
15
+ @file_id = json['fileID']
16
+
17
+ @ids = ids
18
+ end
19
+
20
+ def self.new_empty(json)
21
+ self.new(json)
22
+ @size = 0
23
+ end
24
+
25
+ def ==(other)
26
+ return false if other.nil?
27
+
28
+ self.name == other.name &&
29
+ self.size == other.size &&
30
+ self.creation_time == other.creation_time &&
31
+ self.url == other.url &&
32
+ self.file_id == other.file_id &&
33
+ self.ids == other.ids
34
+ end
35
+ end
36
+ end
data/lib/layer.rb ADDED
@@ -0,0 +1,21 @@
1
+ class Layer
2
+ attr_accessor :name
3
+ attr_accessor :rule_id
4
+
5
+ def initialize(name, value = {}, rule_id = '', exposure_log_func = nil)
6
+ @name = name
7
+ @value = value
8
+ @rule_id = rule_id
9
+ @exposure_log_func = exposure_log_func
10
+ end
11
+
12
+ def get(index, default_value)
13
+ return default_value if @value.nil? || !@value.key?(index)
14
+
15
+ if @exposure_log_func.is_a? Proc
16
+ @exposure_log_func.call(self, index)
17
+ end
18
+
19
+ @value[index]
20
+ end
21
+ end
data/lib/network.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  require 'http'
2
2
  require 'json'
3
- require 'dynamic_config'
3
+ require 'securerandom'
4
4
 
5
5
  $retry_codes = [408, 500, 502, 503, 504, 522, 524, 599]
6
6
 
@@ -14,12 +14,14 @@ module Statsig
14
14
  @server_secret = server_secret
15
15
  @api = api
16
16
  @backoff_multiplier = backoff_mult
17
+ @session_id = SecureRandom.uuid
17
18
  end
18
19
 
19
20
  def post_helper(endpoint, body, retries = 0, backoff = 1)
20
21
  http = HTTP.headers(
21
22
  {"STATSIG-API-KEY" => @server_secret,
22
23
  "STATSIG-CLIENT-TIME" => (Time.now.to_f * 1000).to_s,
24
+ "STATSIG-SERVER-SESSION-ID" => @session_id,
23
25
  "Content-Type" => "application/json; charset=UTF-8"
24
26
  }).accept(:json)
25
27
  begin
data/lib/spec_store.rb CHANGED
@@ -1,6 +1,8 @@
1
1
  require 'net/http'
2
2
  require 'uri'
3
3
 
4
+ require 'id_list'
5
+
4
6
  module Statsig
5
7
  class SpecStore
6
8
  def initialize(network, error_callback = nil, config_sync_interval = 10, id_lists_sync_interval = 60)
@@ -11,11 +13,12 @@ module Statsig
11
13
  @store = {
12
14
  :gates => {},
13
15
  :configs => {},
16
+ :layers => {},
14
17
  :id_lists => {},
15
18
  }
16
19
  e = download_config_specs
17
20
  error_callback.call(e) unless error_callback.nil?
18
- download_id_lists
21
+ get_id_lists
19
22
 
20
23
  @config_sync_thread = sync_config_specs
21
24
  @id_lists_sync_thread = sync_id_lists
@@ -34,6 +37,10 @@ module Statsig
34
37
  @store[:configs].key?(config_name)
35
38
  end
36
39
 
40
+ def has_layer?(layer_name)
41
+ @store[:layers].key?(layer_name)
42
+ end
43
+
37
44
  def get_gate(gate_name)
38
45
  return nil unless has_gate?(gate_name)
39
46
  @store[:gates][gate_name]
@@ -44,6 +51,11 @@ module Statsig
44
51
  @store[:configs][config_name]
45
52
  end
46
53
 
54
+ def get_layer(layer_name)
55
+ return nil unless has_layer?(layer_name)
56
+ @store[:layers][layer_name]
57
+ end
58
+
47
59
  def get_id_list(list_name)
48
60
  @store[:id_lists][list_name]
49
61
  end
@@ -63,7 +75,7 @@ module Statsig
63
75
  Thread.new do
64
76
  loop do
65
77
  sleep @id_lists_sync_interval
66
- download_id_lists
78
+ get_id_lists
67
79
  end
68
80
  end
69
81
  end
@@ -89,62 +101,111 @@ module Statsig
89
101
  @last_sync_time = specs_json['time'] || @last_sync_time
90
102
  return unless specs_json['has_updates'] == true &&
91
103
  !specs_json['feature_gates'].nil? &&
92
- !specs_json['dynamic_configs'].nil?
104
+ !specs_json['dynamic_configs'].nil? &&
105
+ !specs_json['layer_configs'].nil?
93
106
 
94
107
  new_gates = {}
95
108
  new_configs = {}
109
+ new_layers = {}
96
110
 
97
111
  specs_json['feature_gates'].map{|gate| new_gates[gate['name']] = gate }
98
112
  specs_json['dynamic_configs'].map{|config| new_configs[config['name']] = config }
113
+ specs_json['layer_configs'].map{|layer| new_layers[layer['name']] = layer }
99
114
  @store[:gates] = new_gates
100
115
  @store[:configs] = new_configs
116
+ @store[:layers] = new_layers
117
+ end
101
118
 
102
- new_id_lists = specs_json['id_lists']
103
- if new_id_lists.is_a? Hash
104
- new_id_lists.each do |list_name, _|
105
- unless @store[:id_lists].key?(list_name)
106
- @store[:id_lists][list_name] = { :ids => {}, :time => 0 }
107
- end
119
+ def get_id_lists
120
+ response, e = @network.post_helper('get_id_lists', JSON.generate({'statsigMetadata' => Statsig.get_statsig_metadata}))
121
+ if !e.nil? || response.nil?
122
+ return
123
+ end
124
+
125
+ begin
126
+ server_id_lists = JSON.parse(response)
127
+ local_id_lists = @store[:id_lists]
128
+ if !server_id_lists.is_a?(Hash) || !local_id_lists.is_a?(Hash)
129
+ return
108
130
  end
131
+ threads = []
109
132
 
110
- @store[:id_lists].each do |list_name, _|
111
- unless new_id_lists.key?(list_name)
112
- @store[:id_lists].delete(list_name)
133
+ server_id_lists.each do |list_name, list|
134
+ server_list = IDList.new(list)
135
+ local_list = get_id_list(list_name)
136
+
137
+ unless local_list.is_a? IDList
138
+ local_list = IDList.new(list)
139
+ local_list.size = 0
140
+ local_id_lists[list_name] = local_list
141
+ end
142
+
143
+ # skip if server list is invalid
144
+ if server_list.url.nil? || server_list.creation_time < local_list.creation_time || server_list.file_id.nil?
145
+ next
146
+ end
147
+
148
+ # skip if server list returns a newer file
149
+ if server_list.file_id != local_list.file_id && server_list.creation_time >= local_list.creation_time
150
+ local_list = IDList.new(list)
151
+ local_list.size = 0
152
+ local_id_lists[list_name] = local_list
153
+ end
154
+
155
+ # skip if server list is no bigger than local list, which means nothing new to read
156
+ if server_list.size <= local_list.size
157
+ next
158
+ end
159
+
160
+ threads << Thread.new do
161
+ download_single_id_list(local_list)
113
162
  end
114
163
  end
164
+ threads.each(&:join)
165
+ delete_lists = []
166
+ local_id_lists.each do |list_name, list|
167
+ unless server_id_lists.key? list_name
168
+ delete_lists.push list_name
169
+ end
170
+ end
171
+ delete_lists.each do |list_name|
172
+ local_id_lists.delete list_name
173
+ end
174
+ rescue
175
+ # Ignored, will try again
115
176
  end
116
177
  end
117
178
 
118
- def download_id_lists
119
- if @store[:id_lists].is_a? Hash
120
- threads = []
121
- id_lists = @store[:id_lists]
122
- id_lists.each do |list_name, list|
123
- threads << Thread.new do
124
- response, e = @network.post_helper('download_id_list', JSON.generate({'listName' => list_name, 'statsigMetadata' => Statsig.get_statsig_metadata, 'sinceTime' => list['time'] || 0 }))
125
- if e.nil? && !response.nil?
126
- begin
127
- data = JSON.parse(response)
128
- if data['add_ids'].is_a? Array
129
- data['add_ids'].each do |id|
130
- list[:ids][id] = true
131
- end
132
- end
133
- if data['remove_ids'].is_a? Array
134
- data['remove_ids'].each do |id|
135
- list[:ids]&.delete(id)
136
- end
137
- end
138
- if data['time'].is_a? Numeric
139
- list[:time] = data['time']
140
- end
141
- rescue
142
- # Ignored
143
- end
144
- end
179
+ def download_single_id_list(list)
180
+ nil unless list.is_a? IDList
181
+ http = HTTP.headers({'Range' => "bytes=#{list&.size || 0}-"}).accept(:json)
182
+ begin
183
+ res = http.get(list.url)
184
+ nil unless res.status.success?
185
+ content_length = Integer(res['content-length'])
186
+ nil if content_length.nil? || content_length <= 0
187
+ content = res.body.to_s
188
+ unless content.is_a?(String) && (content[0] == '-' || content[0] == '+')
189
+ @store[:id_lists].delete(list.name)
190
+ return
191
+ end
192
+ ids_clone = list.ids # clone the list, operate on the new list, and swap out the old list, so the operation is thread-safe
193
+ lines = content.split(/\r?\n/)
194
+ lines.each do |li|
195
+ line = li.strip
196
+ next if line.length <= 1
197
+ op = line[0]
198
+ id = line[1..]
199
+ if op == '+'
200
+ ids_clone.add(id)
201
+ elsif op == '-'
202
+ ids_clone.delete(id)
145
203
  end
146
204
  end
147
- threads.each(&:join)
205
+ list.ids = ids_clone
206
+ list.size = list.size + content_length
207
+ rescue
208
+ nil
148
209
  end
149
210
  end
150
211
  end
data/lib/statsig.rb CHANGED
@@ -22,7 +22,12 @@ module Statsig
22
22
 
23
23
  def self.get_experiment(user, experiment_name)
24
24
  ensure_initialized
25
- @shared_instance&.get_config(user, experiment_name)
25
+ @shared_instance&.get_experiment(user, experiment_name)
26
+ end
27
+
28
+ def self.get_layer(user, layer_name)
29
+ ensure_initialized
30
+ @shared_instance&.get_layer(user, layer_name)
26
31
  end
27
32
 
28
33
  def self.log_event(user, event_name, value, metadata)
@@ -40,7 +45,7 @@ module Statsig
40
45
  def self.get_statsig_metadata
41
46
  {
42
47
  'sdkType' => 'ruby-server',
43
- 'sdkVersion' => '1.8.4',
48
+ 'sdkVersion' => '1.9.2',
44
49
  }
45
50
  end
46
51
 
@@ -6,6 +6,8 @@ require 'statsig_logger'
6
6
  require 'statsig_options'
7
7
  require 'statsig_user'
8
8
  require 'spec_store'
9
+ require 'dynamic_config'
10
+ require 'layer'
9
11
 
10
12
  class StatsigDriver
11
13
  def initialize(secret_key, options = nil, error_callback = nil)
@@ -26,12 +28,7 @@ class StatsigDriver
26
28
  end
27
29
 
28
30
  def check_gate(user, gate_name)
29
- validate_user(user)
30
- user = normalize_user(user)
31
- if !gate_name.is_a?(String) || gate_name.empty?
32
- raise 'Invalid gate_name provided'
33
- end
34
- check_shutdown
31
+ user = verify_inputs(user, gate_name, "gate_name")
35
32
 
36
33
  res = @evaluator.check_gate(user, gate_name)
37
34
  if res.nil?
@@ -49,33 +46,34 @@ class StatsigDriver
49
46
  end
50
47
 
51
48
  def get_config(user, dynamic_config_name)
52
- validate_user(user)
53
- user = normalize_user(user)
54
- if !dynamic_config_name.is_a?(String) || dynamic_config_name.empty?
55
- raise "Invalid dynamic_config_name provided"
56
- end
57
- check_shutdown
49
+ user = verify_inputs(user, dynamic_config_name, "dynamic_config_name")
50
+ get_config_impl(user, dynamic_config_name)
51
+ end
58
52
 
59
- res = @evaluator.get_config(user, dynamic_config_name)
53
+ def get_experiment(user, experiment_name)
54
+ user = verify_inputs(user, experiment_name, "experiment_name")
55
+ get_config_impl(user, experiment_name)
56
+ end
57
+
58
+ def get_layer(user, layer_name)
59
+ user = verify_inputs(user, layer_name, "layer_name")
60
+
61
+ res = @evaluator.get_layer(user, layer_name)
60
62
  if res.nil?
61
- res = Statsig::ConfigResult.new(dynamic_config_name)
63
+ res = Statsig::ConfigResult.new(layer_name)
62
64
  end
63
65
 
64
66
  if res == $fetch_from_server
65
- res = get_config_fallback(user, dynamic_config_name)
67
+ if res.config_delegate.empty?
68
+ return Layer.new(layer_name)
69
+ end
70
+ res = get_config_fallback(user, res.config_delegate)
66
71
  # exposure logged by the server
67
- else
68
- @logger.log_config_exposure(user, res.name, res.rule_id, res.secondary_exposures)
69
72
  end
70
73
 
71
- DynamicConfig.new(res.name, res.json_value, res.rule_id)
72
- end
73
-
74
- def get_experiment(user, experiment_name)
75
- if !experiment_name.is_a?(String) || experiment_name.empty?
76
- raise "Invalid experiment_name provided"
77
- end
78
- get_config(user, experiment_name)
74
+ Layer.new(res.name, res.json_value, res.rule_id, lambda { |layer, parameter_name|
75
+ @logger.log_layer_exposure(user, layer, parameter_name, res)
76
+ })
79
77
  end
80
78
 
81
79
  def log_event(user, event_name, value = nil, metadata = nil)
@@ -102,6 +100,32 @@ class StatsigDriver
102
100
 
103
101
  private
104
102
 
103
+ def verify_inputs(user, config_name, variable_name)
104
+ validate_user(user)
105
+ if !config_name.is_a?(String) || config_name.empty?
106
+ raise "Invalid #{variable_name} provided"
107
+ end
108
+
109
+ check_shutdown
110
+ normalize_user(user)
111
+ end
112
+
113
+ def get_config_impl(user, config_name)
114
+ res = @evaluator.get_config(user, config_name)
115
+ if res.nil?
116
+ res = Statsig::ConfigResult.new(config_name)
117
+ end
118
+
119
+ if res == $fetch_from_server
120
+ res = get_config_fallback(user, config_name)
121
+ # exposure logged by the server
122
+ else
123
+ @logger.log_config_exposure(user, res.name, res.rule_id, res.secondary_exposures)
124
+ end
125
+
126
+ DynamicConfig.new(res.name, res.json_value, res.rule_id)
127
+ end
128
+
105
129
  def validate_user(user)
106
130
  if user.nil? || !user.instance_of?(StatsigUser) || !user.user_id.is_a?(String)
107
131
  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.'
@@ -2,6 +2,7 @@ require 'statsig_event'
2
2
 
3
3
  $gate_exposure_event = 'statsig::gate_exposure'
4
4
  $config_exposure_event = 'statsig::config_exposure'
5
+ $layer_exposure_event = 'statsig::layer_exposure'
5
6
 
6
7
  module Statsig
7
8
  class StatsigLogger
@@ -43,6 +44,29 @@ module Statsig
43
44
  log_event(event)
44
45
  end
45
46
 
47
+ def log_layer_exposure(user, layer, parameter_name, config_evaluation)
48
+ exposures = config_evaluation.undelegated_sec_exps
49
+ allocated_experiment = ''
50
+ is_explicit = (config_evaluation.explicit_parameters&.include? parameter_name) || false
51
+ if is_explicit
52
+ allocated_experiment = config_evaluation.config_delegate
53
+ exposures = config_evaluation.secondary_exposures
54
+ end
55
+
56
+ event = StatsigEvent.new($layer_exposure_event)
57
+ event.user = user
58
+ event.metadata = {
59
+ 'config' => layer.name,
60
+ 'ruleID' => layer.rule_id,
61
+ 'allocatedExperiment' => allocated_experiment,
62
+ 'parameterName' => parameter_name,
63
+ 'isExplicitParameter' => String(is_explicit)
64
+ }
65
+ event.statsig_metadata = Statsig.get_statsig_metadata
66
+ event.secondary_exposures = exposures.is_a?(Array) ? exposures : []
67
+ log_event(event)
68
+ end
69
+
46
70
  def periodic_flush
47
71
  Thread.new do
48
72
  loop do
@@ -2,7 +2,7 @@ class StatsigOptions
2
2
  attr_reader :environment
3
3
  attr_reader :api_url_base
4
4
 
5
- def initialize(environment = nil, api_url_base = 'https://api.statsig.com/v1')
5
+ def initialize(environment = nil, api_url_base = 'https://statsigapi.net/v1')
6
6
  @environment = environment.is_a?(Hash) ? environment : nil
7
7
  @api_url_base = api_url_base
8
8
  end
data/lib/statsig_user.rb CHANGED
@@ -54,7 +54,7 @@ class StatsigUser
54
54
  if for_logging
55
55
  hash.delete('privateAttributes')
56
56
  end
57
- hash
57
+ hash.compact
58
58
  end
59
59
 
60
60
  def value_lookup
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: statsig
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.8.4
4
+ version: 1.9.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Statsig, Inc
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-02-11 00:00:00.000000000 Z
11
+ date: 2022-05-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -84,16 +84,22 @@ dependencies:
84
84
  name: http
85
85
  requirement: !ruby/object:Gem::Requirement
86
86
  requirements:
87
- - - "~>"
87
+ - - ">="
88
88
  - !ruby/object:Gem::Version
89
89
  version: '4.4'
90
+ - - "<"
91
+ - !ruby/object:Gem::Version
92
+ version: '6.0'
90
93
  type: :runtime
91
94
  prerelease: false
92
95
  version_requirements: !ruby/object:Gem::Requirement
93
96
  requirements:
94
- - - "~>"
97
+ - - ">="
95
98
  - !ruby/object:Gem::Version
96
99
  version: '4.4'
100
+ - - "<"
101
+ - !ruby/object:Gem::Version
102
+ version: '6.0'
97
103
  - !ruby/object:Gem::Dependency
98
104
  name: ip3country
99
105
  requirement: !ruby/object:Gem::Requirement
@@ -118,6 +124,8 @@ files:
118
124
  - lib/dynamic_config.rb
119
125
  - lib/evaluation_helpers.rb
120
126
  - lib/evaluator.rb
127
+ - lib/id_list.rb
128
+ - lib/layer.rb
121
129
  - lib/network.rb
122
130
  - lib/spec_store.rb
123
131
  - lib/statsig.rb