statsig 1.6.3 → 1.8.0

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: 9192f1c9886d3a3b18c0a2e70599e998b4784b23245e9064018ec31c98881b25
4
- data.tar.gz: eb329d5f742ce2f77391f3f33ba4229852c2c68827643159b51fd870c1fb21d9
3
+ metadata.gz: 4268658d92961175796c877e357cc3cf18f40656cf8aa272034fd4953bdb641e
4
+ data.tar.gz: e0a62e60cdca3b8b7ef8fe0302032badb88f8b9b5d86635bf20f79dc7fc011da
5
5
  SHA512:
6
- metadata.gz: e8a090dc289fe6994b08578b2d2884f9196f37f4104120c546dd11b3036bc939b84a343a643f15f8e12ca01a494f920206ee01a2302de5bcb71a94efb3048b76
7
- data.tar.gz: 995f91cb599608d0691e8f598d7bc2aa50f0f1b652ad1fac83f3afdb9e950132e21a34aaee44c57797849fb99005cc9dea9d4664f3f4f0ba13afb75dd120c251
6
+ metadata.gz: e40a7d816b2c36ec211272bfa21e1bacb035ae7a6a9a1366eb4bae9b0af917f1694f66a1d35f146018b49be6c0df1ec07fe549612d3a49e54c69ffeeb7e3e733
7
+ data.tar.gz: a53f398b7574bdaa47f26b5dcda0d2e11f83fbc85401b071592198ae24b59dbaec21cc74ec04f102ad909a06aeb0f552be1a49838241b035a2153b81c3250c04
@@ -2,7 +2,7 @@ require 'time'
2
2
 
3
3
  module EvaluationHelpers
4
4
  def self.compare_numbers(a, b, func)
5
- return false unless self.is_numeric(a) && self.is_numeric(b)
5
+ return false unless is_numeric(a) && is_numeric(b)
6
6
  func.call(a.to_f, b.to_f) rescue false
7
7
  end
8
8
 
@@ -15,8 +15,8 @@ module EvaluationHelpers
15
15
 
16
16
  def self.compare_times(a, b, func)
17
17
  begin
18
- time_1 = self.get_epoch_time(a)
19
- time_2 = self.get_epoch_time(b)
18
+ time_1 = get_epoch_time(a)
19
+ time_2 = get_epoch_time(b)
20
20
  func.call(time_1, time_2)
21
21
  rescue
22
22
  false
@@ -30,7 +30,7 @@ module EvaluationHelpers
30
30
  end
31
31
 
32
32
  def self.get_epoch_time(v)
33
- time = self.is_numeric(v) ? Time.at(v.to_f) : Time.parse(v)
33
+ time = is_numeric(v) ? Time.at(v.to_f) : Time.parse(v)
34
34
  if time.year > Time.now.year + 100
35
35
  # divide by 1000 when the epoch time is in milliseconds instead of seconds
36
36
  return time.to_i / 1000
data/lib/evaluator.rb CHANGED
@@ -11,36 +11,41 @@ $fetch_from_server = :fetch_from_server
11
11
  $type_dynamic_config = 'dynamic_config'
12
12
 
13
13
  class Evaluator
14
- def initialize(store)
15
- @spec_store = store
16
- @initialized = true
14
+ def initialize(network, error_callback)
15
+ @spec_store = SpecStore.new(network, error_callback)
17
16
  @ua_parser = UserAgentParser::Parser.new
18
17
  CountryLookup.initialize
18
+ @initialized = true
19
19
  end
20
20
 
21
21
  def check_gate(user, gate_name)
22
22
  return nil unless @initialized && @spec_store.has_gate?(gate_name)
23
- self.eval_spec(user, @spec_store.get_gate(gate_name))
23
+ eval_spec(user, @spec_store.get_gate(gate_name))
24
24
  end
25
25
 
26
26
  def get_config(user, config_name)
27
27
  return nil unless @initialized && @spec_store.has_config?(config_name)
28
- self.eval_spec(user, @spec_store.get_config(config_name))
28
+ eval_spec(user, @spec_store.get_config(config_name))
29
+ end
30
+
31
+ def shutdown
32
+ @spec_store.shutdown
29
33
  end
30
34
 
31
35
  private
32
36
 
33
37
  def eval_spec(user, config)
38
+ default_rule_id = 'default'
39
+ exposures = []
34
40
  if config['enabled']
35
- exposures = []
36
41
  i = 0
37
42
  until i >= config['rules'].length do
38
43
  rule = config['rules'][i]
39
- result = self.eval_rule(user, rule)
44
+ result = eval_rule(user, rule)
40
45
  return $fetch_from_server if result == $fetch_from_server
41
- exposures = exposures + result["exposures"] if result["exposures"].is_a? Array
46
+ exposures = exposures + result['exposures'] if result['exposures'].is_a? Array
42
47
  if result['value']
43
- pass = self.eval_pass_percent(user, rule, config['salt'])
48
+ pass = eval_pass_percent(user, rule, config['salt'])
44
49
  return ConfigResult.new(
45
50
  config['name'],
46
51
  pass,
@@ -52,9 +57,11 @@ class Evaluator
52
57
 
53
58
  i += 1
54
59
  end
60
+ else
61
+ default_rule_id = 'disabled'
55
62
  end
56
63
 
57
- ConfigResult.new(config['name'], false, config['defaultValue'], 'default', [])
64
+ ConfigResult.new(config['name'], false, config['defaultValue'], default_rule_id, exposures)
58
65
  end
59
66
 
60
67
  def eval_rule(user, rule)
@@ -62,20 +69,20 @@ class Evaluator
62
69
  pass = true
63
70
  i = 0
64
71
  until i >= rule['conditions'].length do
65
- result = self.eval_condition(user, rule['conditions'][i])
72
+ result = eval_condition(user, rule['conditions'][i])
66
73
  if result == $fetch_from_server
67
74
  return $fetch_from_server
68
75
  end
69
76
 
70
77
  if result.is_a?(Hash)
71
- exposures = exposures + result["exposures"] if result["exposures"].is_a? Array
72
- pass = false if result["value"] == false
78
+ exposures = exposures + result['exposures'] if result['exposures'].is_a? Array
79
+ pass = false if result['value'] == false
73
80
  elsif result == false
74
81
  pass = false
75
82
  end
76
83
  i += 1
77
84
  end
78
- { "value" => pass, "exposures" => exposures }
85
+ { 'value' => pass, 'exposures' => exposures }
79
86
  end
80
87
 
81
88
  def eval_condition(user, condition)
@@ -86,6 +93,7 @@ class Evaluator
86
93
  operator = condition['operator']
87
94
  additional_values = condition['additionalValues']
88
95
  additional_values = Hash.new unless additional_values.is_a? Hash
96
+ idType = condition['idType']
89
97
 
90
98
  return $fetch_from_server unless type.is_a? String
91
99
  type = type.downcase
@@ -94,19 +102,19 @@ class Evaluator
94
102
  when 'public'
95
103
  return true
96
104
  when 'fail_gate', 'pass_gate'
97
- other_gate_result = self.check_gate(user, target)
105
+ other_gate_result = check_gate(user, target)
98
106
  return $fetch_from_server if other_gate_result == $fetch_from_server
99
107
 
100
108
  gate_value = other_gate_result&.gate_value == true
101
109
  new_exposure = {
102
- "gate" => target,
103
- "gateValue" => gate_value ? "true" : "false",
104
- "ruleID" => other_gate_result&.rule_id
110
+ 'gate' => target,
111
+ 'gateValue' => gate_value ? 'true' : 'false',
112
+ 'ruleID' => other_gate_result&.rule_id
105
113
  }
106
114
  exposures = other_gate_result&.secondary_exposures&.append(new_exposure)
107
115
  return {
108
- "value" => type == 'pass_gate' ? gate_value : !gate_value,
109
- "exposures" => exposures
116
+ 'value' => type == 'pass_gate' ? gate_value : !gate_value,
117
+ 'exposures' => exposures
110
118
  }
111
119
  when 'ip_based'
112
120
  value = get_value_from_user(user, field) || get_value_from_ip(user, field)
@@ -123,12 +131,14 @@ class Evaluator
123
131
  when 'user_bucket'
124
132
  begin
125
133
  salt = additional_values['salt']
126
- user_id = user.user_id || ''
134
+ unit_id = get_unit_id(user, idType) || ''
127
135
  # there are only 1000 user buckets as opposed to 10k for gate pass %
128
- value = compute_user_hash("#{salt}.#{user_id}") % 1000
136
+ value = compute_user_hash("#{salt}.#{unit_id}") % 1000
129
137
  rescue
130
138
  return false
131
139
  end
140
+ when 'unit_id'
141
+ value = get_unit_id(user, idType)
132
142
  else
133
143
  return $fetch_from_server
134
144
  end
@@ -194,6 +204,17 @@ class Evaluator
194
204
  return EvaluationHelpers::compare_times(value, target, ->(a, b) { a > b })
195
205
  when 'on'
196
206
  return EvaluationHelpers::compare_times(value, target, ->(a, b) { a.year == b.year && a.month == b.month && a.day == b.day })
207
+ when 'in_segment_list', 'not_in_segment_list'
208
+ begin
209
+ id_list = (@spec_store.get_id_list(target) || {:ids => {}})[:ids]
210
+ hashed_id = Digest::SHA256.base64digest(value.to_s)[0, 8]
211
+ is_in_list = id_list.is_a?(Hash) && id_list[hashed_id] == true
212
+
213
+ return is_in_list if operator == 'in_segment_list'
214
+ return !is_in_list
215
+ rescue StandardError => e
216
+ return false
217
+ end
197
218
  else
198
219
  return $fetch_from_server
199
220
  end
@@ -265,15 +286,23 @@ class Evaluator
265
286
  def eval_pass_percent(user, rule, config_salt)
266
287
  return false unless config_salt.is_a?(String) && !rule['passPercentage'].nil?
267
288
  begin
268
- user_id = user.user_id || ''
289
+ unit_id = get_unit_id(user, rule['id_type']) || ''
269
290
  rule_salt = rule['salt'] || rule['id'] || ''
270
- hash = compute_user_hash("#{config_salt}.#{rule_salt}.#{user_id}")
291
+ hash = compute_user_hash("#{config_salt}.#{rule_salt}.#{unit_id}")
271
292
  return (hash % 10000) < (rule['passPercentage'].to_f * 100)
272
293
  rescue
273
294
  return false
274
295
  end
275
296
  end
276
297
 
298
+ def get_unit_id(user, id_type)
299
+ if id_type.is_a?(String) && id_type.downcase != 'userid'
300
+ return nil unless user&.custom_ids.is_a? Hash
301
+ return user.custom_ids[id_type] || user.custom_ids[id_type.downcase]
302
+ end
303
+ user.user_id
304
+ end
305
+
277
306
  def compute_user_hash(user_hash)
278
307
  Digest::SHA256.digest(user_hash).unpack('Q>')[0]
279
308
  end
data/lib/network.rb CHANGED
@@ -12,7 +12,6 @@ class Network
12
12
  end
13
13
  @server_secret = server_secret
14
14
  @api = api
15
- @last_sync_time = 0
16
15
  @backoff_multiplier = backoff_mult
17
16
  end
18
17
 
@@ -24,14 +23,14 @@ class Network
24
23
  }).accept(:json)
25
24
  begin
26
25
  res = http.post(@api + endpoint, body: body)
27
- rescue
26
+ rescue StandardError => e
28
27
  ## network error retry
29
- return nil unless retries > 0
28
+ return nil, e unless retries > 0
30
29
  sleep backoff
31
30
  return post_helper(endpoint, body, retries - 1, backoff * @backoff_multiplier)
32
31
  end
33
- return res unless !res.status.success?
34
- return nil unless retries > 0 && $retry_codes.include?(res.code)
32
+ return res, nil unless !res.status.success?
33
+ return nil, StandardError.new("Got an exception when making request to #{@api + endpoint}: #{res.to_s}") unless retries > 0 && $retry_codes.include?(res.code)
35
34
  ## status code retry
36
35
  sleep backoff
37
36
  post_helper(endpoint, body, retries - 1, backoff * @backoff_multiplier)
@@ -40,7 +39,7 @@ class Network
40
39
  def check_gate(user, gate_name)
41
40
  begin
42
41
  request_body = JSON.generate({'user' => user&.serialize(false), 'gateName' => gate_name})
43
- response = post_helper('check_gate', request_body)
42
+ response, _ = post_helper('check_gate', request_body)
44
43
  return JSON.parse(response.body) unless response.nil?
45
44
  false
46
45
  rescue
@@ -51,7 +50,7 @@ class Network
51
50
  def get_config(user, dynamic_config_name)
52
51
  begin
53
52
  request_body = JSON.generate({'user' => user&.serialize(false), 'configName' => dynamic_config_name})
54
- response = post_helper('get_config', request_body)
53
+ response, _ = post_helper('get_config', request_body)
55
54
  return JSON.parse(response.body) unless response.nil?
56
55
  nil
57
56
  rescue
@@ -59,33 +58,9 @@ class Network
59
58
  end
60
59
  end
61
60
 
62
- def download_config_specs
63
- begin
64
- response = post_helper('download_config_specs', JSON.generate({'sinceTime' => @last_sync_time}))
65
- return nil unless !response.nil?
66
- json_body = JSON.parse(response.body)
67
- @last_sync_time = json_body['time']
68
- return json_body
69
- rescue
70
- return nil
71
- end
72
- end
73
-
74
- def poll_for_changes(callback)
75
- Thread.new do
76
- loop do
77
- sleep 10
78
- specs = download_config_specs
79
- unless specs.nil?
80
- callback.call(specs)
81
- end
82
- end
83
- end
84
- end
85
-
86
- def post_logs(events, statsig_metadata)
61
+ def post_logs(events)
87
62
  begin
88
- json_body = JSON.generate({'events' => events, 'statsigMetadata' => statsig_metadata})
63
+ json_body = JSON.generate({'events' => events, 'statsigMetadata' => Statsig.get_statsig_metadata})
89
64
  post_helper('log_event', json_body, retries: 5)
90
65
  rescue
91
66
  end
data/lib/spec_store.rb CHANGED
@@ -2,40 +2,35 @@ require 'net/http'
2
2
  require 'uri'
3
3
 
4
4
  class SpecStore
5
- def initialize(specs_json)
5
+ def initialize(network, error_callback = nil, config_sync_interval = 10, id_lists_sync_interval = 60)
6
+ @network = network
6
7
  @last_sync_time = 0
8
+ @config_sync_interval = config_sync_interval
9
+ @id_lists_sync_interval = id_lists_sync_interval
7
10
  @store = {
8
11
  :gates => {},
9
12
  :configs => {},
13
+ :id_lists => {},
10
14
  }
11
- process(specs_json)
12
- end
13
-
14
- def process(specs_json)
15
- if specs_json.nil?
16
- return
17
- end
15
+ e = download_config_specs
16
+ error_callback.call(e) unless error_callback.nil?
17
+ download_id_lists
18
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
- }
19
+ @config_sync_thread = sync_config_specs
20
+ @id_lists_sync_thread = sync_id_lists
21
+ end
28
22
 
29
- specs_json['feature_gates'].map{|gate| @store[:gates][gate['name']] = gate }
30
- specs_json['dynamic_configs'].map{|config| @store[:configs][config['name']] = config }
23
+ def shutdown
24
+ @config_sync_thread&.exit
25
+ @id_lists_sync_thread&.exit
31
26
  end
32
27
 
33
28
  def has_gate?(gate_name)
34
- return @store[:gates].key?(gate_name)
29
+ @store[:gates].key?(gate_name)
35
30
  end
36
31
 
37
32
  def has_config?(config_name)
38
- return @store[:configs].key?(config_name)
33
+ @store[:configs].key?(config_name)
39
34
  end
40
35
 
41
36
  def get_gate(gate_name)
@@ -48,4 +43,108 @@ class SpecStore
48
43
  @store[:configs][config_name]
49
44
  end
50
45
 
46
+ def get_id_list(list_name)
47
+ @store[:id_lists][list_name]
48
+ end
49
+
50
+ private
51
+
52
+ def sync_config_specs
53
+ Thread.new do
54
+ loop do
55
+ sleep @config_sync_interval
56
+ download_config_specs
57
+ end
58
+ end
59
+ end
60
+
61
+ def sync_id_lists
62
+ Thread.new do
63
+ loop do
64
+ sleep @id_lists_sync_interval
65
+ download_id_lists
66
+ end
67
+ end
68
+ end
69
+
70
+ def download_config_specs
71
+ begin
72
+ response, e = @network.post_helper('download_config_specs', JSON.generate({'sinceTime' => @last_sync_time}))
73
+ if e.nil?
74
+ process(JSON.parse(response.body))
75
+ else
76
+ e
77
+ end
78
+ rescue StandardError => e
79
+ e
80
+ end
81
+ end
82
+
83
+ def process(specs_json)
84
+ if specs_json.nil?
85
+ return
86
+ end
87
+
88
+ @last_sync_time = specs_json['time'] || @last_sync_time
89
+ return unless specs_json['has_updates'] == true &&
90
+ !specs_json['feature_gates'].nil? &&
91
+ !specs_json['dynamic_configs'].nil?
92
+
93
+ new_gates = {}
94
+ new_configs = {}
95
+
96
+ specs_json['feature_gates'].map{|gate| new_gates[gate['name']] = gate }
97
+ specs_json['dynamic_configs'].map{|config| new_configs[config['name']] = config }
98
+ @store[:gates] = new_gates
99
+ @store[:configs] = new_configs
100
+
101
+ new_id_lists = specs_json['id_lists']
102
+ if new_id_lists.is_a? Hash
103
+ new_id_lists.each do |list_name, _|
104
+ unless @store[:id_lists].key?(list_name)
105
+ @store[:id_lists][list_name] = { :ids => {}, :time => 0 }
106
+ end
107
+ end
108
+
109
+ @store[:id_lists].each do |list_name, _|
110
+ unless new_id_lists.key?(list_name)
111
+ @store[:id_lists].delete(list_name)
112
+ end
113
+ end
114
+ end
115
+ end
116
+
117
+ def download_id_lists
118
+ if @store[:id_lists].is_a? Hash
119
+ threads = []
120
+ id_lists = @store[:id_lists]
121
+ id_lists.each do |list_name, list|
122
+ threads << Thread.new do
123
+ response, e = @network.post_helper('download_id_list', JSON.generate({'listName' => list_name, 'statsigMetadata' => Statsig.get_statsig_metadata, 'sinceTime' => list['time'] || 0 }))
124
+ if e.nil? && !response.nil?
125
+ begin
126
+ data = JSON.parse(response)
127
+ if data['add_ids'].is_a? Array
128
+ data['add_ids'].each do |id|
129
+ list[:ids][id] = true
130
+ end
131
+ end
132
+ if data['remove_ids'].is_a? Array
133
+ data['remove_ids'].each do |id|
134
+ list[:ids]&.delete(id)
135
+ end
136
+ end
137
+ if data['time'].is_a? Numeric
138
+ list[:time] = data['time']
139
+ end
140
+ rescue
141
+ # Ignored
142
+ end
143
+ end
144
+ end
145
+ end
146
+ threads.each(&:join)
147
+ end
148
+ end
149
+
51
150
  end
data/lib/statsig.rb CHANGED
@@ -1,32 +1,32 @@
1
1
  require 'statsig_driver'
2
2
 
3
3
  module Statsig
4
- def self.initialize(secret_key, options = nil)
4
+ def self.initialize(secret_key, options = nil, error_callback = nil)
5
5
  unless @shared_instance.nil?
6
6
  puts 'Statsig already initialized.'
7
7
  return @shared_instance
8
8
  end
9
9
 
10
- @shared_instance = StatsigDriver.new(secret_key, options)
10
+ @shared_instance = StatsigDriver.new(secret_key, options, error_callback)
11
11
  end
12
12
 
13
13
  def self.check_gate(user, gate_name)
14
- self.ensure_initialized
14
+ ensure_initialized
15
15
  @shared_instance&.check_gate(user, gate_name)
16
16
  end
17
17
 
18
18
  def self.get_config(user, dynamic_config_name)
19
- self.ensure_initialized
19
+ ensure_initialized
20
20
  @shared_instance&.get_config(user, dynamic_config_name)
21
21
  end
22
22
 
23
23
  def self.get_experiment(user, experiment_name)
24
- self.ensure_initialized
24
+ ensure_initialized
25
25
  @shared_instance&.get_config(user, experiment_name)
26
26
  end
27
27
 
28
28
  def self.log_event(user, event_name, value, metadata)
29
- self.ensure_initialized
29
+ ensure_initialized
30
30
  @shared_instance&.log_event(user, event_name, value, metadata)
31
31
  end
32
32
 
@@ -37,6 +37,13 @@ module Statsig
37
37
  @shared_instance = nil
38
38
  end
39
39
 
40
+ def self.get_statsig_metadata
41
+ {
42
+ 'sdkType' => 'ruby-server',
43
+ 'sdkVersion' => '1.8.0',
44
+ }
45
+ end
46
+
40
47
  private
41
48
 
42
49
  def self.ensure_initialized
@@ -8,7 +8,7 @@ require 'statsig_user'
8
8
  require 'spec_store'
9
9
 
10
10
  class StatsigDriver
11
- def initialize(secret_key, options = nil)
11
+ def initialize(secret_key, options = nil, error_callback = nil)
12
12
  super()
13
13
  if !secret_key.is_a?(String) || !secret_key.start_with?('secret-')
14
14
  raise 'Invalid secret key provided. Provide your project secret key from the Statsig console'
@@ -17,25 +17,12 @@ class StatsigDriver
17
17
  raise 'Invalid options provided. Either provide a valid StatsigOptions object or nil'
18
18
  end
19
19
 
20
- @options = options || StatsigOptions.new()
20
+ @options = options || StatsigOptions.new
21
21
  @shutdown = false
22
22
  @secret_key = secret_key
23
23
  @net = Network.new(secret_key, @options.api_url_base)
24
- @statsig_metadata = {
25
- 'sdkType' => 'ruby-server',
26
- 'sdkVersion' => Gem::Specification::load('statsig.gemspec')&.version,
27
- }
28
- @logger = StatsigLogger.new(@net, @statsig_metadata)
29
-
30
- downloaded_specs = @net.download_config_specs
31
- unless downloaded_specs.nil?
32
- @initialized = true
33
- end
34
-
35
- @store = SpecStore.new(downloaded_specs)
36
- @evaluator = Evaluator.new(@store)
37
-
38
- @polling_thread = @net.poll_for_changes(-> (config_specs) { @store.process(config_specs) })
24
+ @logger = StatsigLogger.new(@net)
25
+ @evaluator = Evaluator.new(@net, error_callback)
39
26
  end
40
27
 
41
28
  def check_gate(user, gate_name)
@@ -45,9 +32,6 @@ class StatsigDriver
45
32
  raise 'Invalid gate_name provided'
46
33
  end
47
34
  check_shutdown
48
- unless @initialized
49
- return false
50
- end
51
35
 
52
36
  res = @evaluator.check_gate(user, gate_name)
53
37
  if res.nil?
@@ -71,9 +55,6 @@ class StatsigDriver
71
55
  raise "Invalid dynamic_config_name provided"
72
56
  end
73
57
  check_shutdown
74
- unless @initialized
75
- return DynamicConfig.new(dynamic_config_name)
76
- end
77
58
 
78
59
  res = @evaluator.get_config(user, dynamic_config_name)
79
60
  if res.nil?
@@ -109,14 +90,14 @@ class StatsigDriver
109
90
  event.user = user
110
91
  event.value = value
111
92
  event.metadata = metadata
112
- event.statsig_metadata = @statsig_metadata
93
+ event.statsig_metadata = Statsig.get_statsig_metadata
113
94
  @logger.log_event(event)
114
95
  end
115
96
 
116
97
  def shutdown
117
98
  @shutdown = true
118
99
  @logger.flush(true)
119
- @polling_thread&.exit
100
+ @evaluator.shutdown
120
101
  end
121
102
 
122
103
  private
@@ -4,9 +4,8 @@ $gate_exposure_event = 'statsig::gate_exposure'
4
4
  $config_exposure_event = 'statsig::config_exposure'
5
5
 
6
6
  class StatsigLogger
7
- def initialize(network, statsig_metadata)
7
+ def initialize(network)
8
8
  @network = network
9
- @statsig_metadata = statsig_metadata
10
9
  @events = []
11
10
  @background_flush = Thread.new do
12
11
  sleep 60
@@ -29,7 +28,7 @@ class StatsigLogger
29
28
  'gateValue' => value.to_s,
30
29
  'ruleID' => rule_id
31
30
  }
32
- event.statsig_metadata = @statsig_metadata
31
+ event.statsig_metadata = Statsig.get_statsig_metadata
33
32
  event.secondary_exposures = secondary_exposures.is_a?(Array) ? secondary_exposures : []
34
33
  log_event(event)
35
34
  end
@@ -41,7 +40,7 @@ class StatsigLogger
41
40
  'config' => config_name,
42
41
  'ruleID' => rule_id
43
42
  }
44
- event.statsig_metadata = @statsig_metadata
43
+ event.statsig_metadata = Statsig.get_statsig_metadata
45
44
  event.secondary_exposures = secondary_exposures.is_a?(Array) ? secondary_exposures : []
46
45
  log_event(event)
47
46
  end
@@ -56,6 +55,6 @@ class StatsigLogger
56
55
  flush_events = @events.map { |e| e.serialize }
57
56
  @events = []
58
57
 
59
- @network.post_logs(flush_events, @statsig_metadata)
58
+ @network.post_logs(flush_events)
60
59
  end
61
60
  end
data/lib/statsig_user.rb CHANGED
@@ -7,6 +7,7 @@ class StatsigUser
7
7
  attr_accessor :locale
8
8
  attr_accessor :app_version
9
9
  attr_accessor :statsig_environment
10
+ attr_accessor :custom_ids
10
11
  attr_accessor :private_attributes
11
12
 
12
13
  def custom
@@ -28,9 +29,11 @@ class StatsigUser
28
29
  @country = user_hash['country']
29
30
  @locale = user_hash['locale']
30
31
  @app_version = user_hash['appVersion'] || user_hash['app_version']
31
- @custom = user_hash['custom']
32
+ @custom = user_hash['custom'] if user_hash['custom'].is_a? Hash
32
33
  @statsig_environment = user_hash['statsigEnvironment']
33
- @private_attributes = user_hash['privateAttributes']
34
+ @private_attributes = user_hash['privateAttributes'] if user_hash['privateAttributes'].is_a? Hash
35
+ custom_ids = user_hash['customIDs'] || user_hash['custom_ids']
36
+ @custom_ids = custom_ids if custom_ids.is_a? Hash
34
37
  end
35
38
  end
36
39
 
@@ -46,6 +49,7 @@ class StatsigUser
46
49
  'custom' => @custom,
47
50
  'statsigEnvironment' => @statsig_environment,
48
51
  'privateAttributes' => @private_attributes,
52
+ 'customIDs' => @custom_ids,
49
53
  }
50
54
  if for_logging
51
55
  hash.delete('privateAttributes')
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.6.3
4
+ version: 1.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Statsig, Inc
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-09-22 00:00:00.000000000 Z
11
+ date: 2021-12-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler