statsig 1.6.3 → 1.8.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 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