statsig 1.8.4 → 1.9.2

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