statsig 1.6.2 → 1.7.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: 325d5430e48a3d71b68d2e284e20b66b0052bf1a4d9e99e7a92a84069b680ab8
4
- data.tar.gz: b227e1fede44928035b1a03ebc8e8c006425466b50b007f89c452f6a47b51cd6
3
+ metadata.gz: d24b945d9500781cda02ab594f003360d81c5a1bca99f366a2ceb95013b53dad
4
+ data.tar.gz: 63c9ea1a9d6347ecec9bff74949665b2af20bfe774e21a6851e96ba9b7eea397
5
5
  SHA512:
6
- metadata.gz: 28017886b1a5d3f5c37e36680f0dfbd300cf5a0f6e0678430af69631486bc88689a622d74a63da1687ef7d610d612227d68cb58b85a7572b7cc5cfcaee0794dd
7
- data.tar.gz: 10a66e7674df116a65351b97b8d27055bacd46f021309c3256bc5605fe3a972416e2c595135262fc9d18117e447a5038db3ebe1b89cb71495f308430fe4d2a32
6
+ metadata.gz: e47350b9ba8e20f659b65a909955d40e66bf2397306aa798aaab60e73fb3b58fd1d3cade5ea0077fd37f847c5f6e976f7ecbfc978db4830884e463bfa6656574
7
+ data.tar.gz: 8e8b11dd34de6f35585c916d3125be3a36f6f897b69facb1788364702bcd15d1729109d8ad8aadfda98f9909a7f053077433ba6221f3ca7c27d2b5686214fda6
data/lib/config_result.rb CHANGED
@@ -3,11 +3,13 @@ class ConfigResult
3
3
  attr_accessor :gate_value
4
4
  attr_accessor :json_value
5
5
  attr_accessor :rule_id
6
+ attr_accessor :secondary_exposures
6
7
 
7
- def initialize(name, gate_value = false, json_value = {}, rule_id = '')
8
+ def initialize(name, gate_value = false, json_value = {}, rule_id = '', secondary_exposures = [])
8
9
  @name = name
9
10
  @gate_value = gate_value
10
11
  @json_value = json_value
11
12
  @rule_id = rule_id
13
+ @secondary_exposures = secondary_exposures.is_a?(Array) ? secondary_exposures : []
12
14
  end
13
15
  end
@@ -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
@@ -20,48 +20,64 @@ class Evaluator
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
29
  end
30
30
 
31
31
  private
32
32
 
33
33
  def eval_spec(user, config)
34
+ default_rule_id = 'default'
35
+ exposures = []
34
36
  if config['enabled']
35
37
  i = 0
36
38
  until i >= config['rules'].length do
37
39
  rule = config['rules'][i]
38
- result = self.eval_rule(user, rule)
40
+ result = eval_rule(user, rule)
39
41
  return $fetch_from_server if result == $fetch_from_server
40
- if result
41
- pass = self.eval_pass_percent(user, rule, config['salt'])
42
+ exposures = exposures + result['exposures'] if result['exposures'].is_a? Array
43
+ if result['value']
44
+ pass = eval_pass_percent(user, rule, config['salt'])
42
45
  return ConfigResult.new(
43
46
  config['name'],
44
47
  pass,
45
48
  pass ? rule['returnValue'] : config['defaultValue'],
46
49
  rule['id'],
50
+ exposures
47
51
  )
48
52
  end
49
53
 
50
54
  i += 1
51
55
  end
56
+ elsif (default_rule_id = 'disabled')
52
57
  end
53
58
 
54
- ConfigResult.new(config['name'], false, config['defaultValue'], 'default')
59
+ ConfigResult.new(config['name'], false, config['defaultValue'], default_rule_id, exposures)
55
60
  end
56
61
 
57
62
  def eval_rule(user, rule)
63
+ exposures = []
64
+ pass = true
58
65
  i = 0
59
66
  until i >= rule['conditions'].length do
60
- result = self.eval_condition(user, rule['conditions'][i])
61
- return result unless result == true
67
+ result = eval_condition(user, rule['conditions'][i])
68
+ if result == $fetch_from_server
69
+ return $fetch_from_server
70
+ end
71
+
72
+ if result.is_a?(Hash)
73
+ exposures = exposures + result['exposures'] if result['exposures'].is_a? Array
74
+ pass = false if result['value'] == false
75
+ elsif result == false
76
+ pass = false
77
+ end
62
78
  i += 1
63
79
  end
64
- true
80
+ { 'value' => pass, 'exposures' => exposures }
65
81
  end
66
82
 
67
83
  def eval_condition(user, condition)
@@ -80,9 +96,20 @@ class Evaluator
80
96
  when 'public'
81
97
  return true
82
98
  when 'fail_gate', 'pass_gate'
83
- other_gate_result = self.check_gate(user, target)
99
+ other_gate_result = check_gate(user, target)
84
100
  return $fetch_from_server if other_gate_result == $fetch_from_server
85
- return type == 'pass_gate' ? other_gate_result.gate_value : !other_gate_result.gate_value
101
+
102
+ gate_value = other_gate_result&.gate_value == true
103
+ new_exposure = {
104
+ 'gate' => target,
105
+ 'gateValue' => gate_value ? 'true' : 'false',
106
+ 'ruleID' => other_gate_result&.rule_id
107
+ }
108
+ exposures = other_gate_result&.secondary_exposures&.append(new_exposure)
109
+ return {
110
+ 'value' => type == 'pass_gate' ? gate_value : !gate_value,
111
+ 'exposures' => exposures
112
+ }
86
113
  when 'ip_based'
87
114
  value = get_value_from_user(user, field) || get_value_from_ip(user, field)
88
115
  return $fetch_from_server if value == $fetch_from_server
data/lib/network.rb CHANGED
@@ -24,14 +24,14 @@ class Network
24
24
  }).accept(:json)
25
25
  begin
26
26
  res = http.post(@api + endpoint, body: body)
27
- rescue
27
+ rescue StandardError => e
28
28
  ## network error retry
29
- return nil unless retries > 0
29
+ return nil, e unless retries > 0
30
30
  sleep backoff
31
31
  return post_helper(endpoint, body, retries - 1, backoff * @backoff_multiplier)
32
32
  end
33
- return res unless !res.status.success?
34
- return nil unless retries > 0 && $retry_codes.include?(res.code)
33
+ return res, nil unless !res.status.success?
34
+ 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
35
  ## status code retry
36
36
  sleep backoff
37
37
  post_helper(endpoint, body, retries - 1, backoff * @backoff_multiplier)
@@ -40,7 +40,7 @@ class Network
40
40
  def check_gate(user, gate_name)
41
41
  begin
42
42
  request_body = JSON.generate({'user' => user&.serialize(false), 'gateName' => gate_name})
43
- response = post_helper('check_gate', request_body)
43
+ response, _ = post_helper('check_gate', request_body)
44
44
  return JSON.parse(response.body) unless response.nil?
45
45
  false
46
46
  rescue
@@ -51,7 +51,7 @@ class Network
51
51
  def get_config(user, dynamic_config_name)
52
52
  begin
53
53
  request_body = JSON.generate({'user' => user&.serialize(false), 'configName' => dynamic_config_name})
54
- response = post_helper('get_config', request_body)
54
+ response, _ = post_helper('get_config', request_body)
55
55
  return JSON.parse(response.body) unless response.nil?
56
56
  nil
57
57
  rescue
@@ -61,13 +61,13 @@ class Network
61
61
 
62
62
  def download_config_specs
63
63
  begin
64
- response = post_helper('download_config_specs', JSON.generate({'sinceTime' => @last_sync_time}))
65
- return nil unless !response.nil?
64
+ response, e = post_helper('download_config_specs', JSON.generate({'sinceTime' => @last_sync_time}))
65
+ return nil, e if response.nil?
66
66
  json_body = JSON.parse(response.body)
67
67
  @last_sync_time = json_body['time']
68
- return json_body
69
- rescue
70
- return nil
68
+ return json_body, nil
69
+ rescue StandardError => e
70
+ return nil, e
71
71
  end
72
72
  end
73
73
 
@@ -75,7 +75,7 @@ class Network
75
75
  Thread.new do
76
76
  loop do
77
77
  sleep 10
78
- specs = download_config_specs
78
+ specs, _ = download_config_specs
79
79
  unless specs.nil?
80
80
  callback.call(specs)
81
81
  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
 
@@ -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,7 +17,7 @@ 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)
@@ -27,7 +27,7 @@ class StatsigDriver
27
27
  }
28
28
  @logger = StatsigLogger.new(@net, @statsig_metadata)
29
29
 
30
- downloaded_specs = @net.download_config_specs
30
+ downloaded_specs, e = @net.download_config_specs
31
31
  unless downloaded_specs.nil?
32
32
  @initialized = true
33
33
  end
@@ -36,6 +36,8 @@ class StatsigDriver
36
36
  @evaluator = Evaluator.new(@store)
37
37
 
38
38
  @polling_thread = @net.poll_for_changes(-> (config_specs) { @store.process(config_specs) })
39
+
40
+ error_callback.call(e) unless error_callback.nil?
39
41
  end
40
42
 
41
43
  def check_gate(user, gate_name)
@@ -58,7 +60,7 @@ class StatsigDriver
58
60
  res = check_gate_fallback(user, gate_name)
59
61
  # exposure logged by the server
60
62
  else
61
- @logger.log_gate_exposure(user, res.name, res.gate_value, res.rule_id)
63
+ @logger.log_gate_exposure(user, res.name, res.gate_value, res.rule_id, res.secondary_exposures)
62
64
  end
63
65
 
64
66
  res.gate_value
@@ -84,12 +86,19 @@ class StatsigDriver
84
86
  res = get_config_fallback(user, dynamic_config_name)
85
87
  # exposure logged by the server
86
88
  else
87
- @logger.log_config_exposure(user, res.name, res.rule_id)
89
+ @logger.log_config_exposure(user, res.name, res.rule_id, res.secondary_exposures)
88
90
  end
89
91
 
90
92
  DynamicConfig.new(res.name, res.json_value, res.rule_id)
91
93
  end
92
94
 
95
+ def get_experiment(user, experiment_name)
96
+ if !experiment_name.is_a?(String) || experiment_name.empty?
97
+ raise "Invalid experiment_name provided"
98
+ end
99
+ get_config(user, experiment_name)
100
+ end
101
+
93
102
  def log_event(user, event_name, value = nil, metadata = nil)
94
103
  if !user.nil? && !user.instance_of?(StatsigUser)
95
104
  raise 'Must provide a valid StatsigUser or nil'
data/lib/statsig_event.rb CHANGED
@@ -2,6 +2,7 @@ class StatsigEvent
2
2
  attr_accessor :value
3
3
  attr_accessor :metadata
4
4
  attr_accessor :statsig_metadata
5
+ attr_accessor :secondary_exposures
5
6
  attr_reader :user
6
7
 
7
8
  def initialize(event_name)
@@ -23,6 +24,7 @@ class StatsigEvent
23
24
  'user' => @user,
24
25
  'time' => @time,
25
26
  'statsigMetadata' => @statsig_metadata,
27
+ 'secondaryExposures' => @secondary_exposures
26
28
  }
27
29
  end
28
30
  end
@@ -21,7 +21,7 @@ class StatsigLogger
21
21
  end
22
22
  end
23
23
 
24
- def log_gate_exposure(user, gate_name, value, rule_id)
24
+ def log_gate_exposure(user, gate_name, value, rule_id, secondary_exposures)
25
25
  event = StatsigEvent.new($gate_exposure_event)
26
26
  event.user = user
27
27
  event.metadata = {
@@ -30,10 +30,11 @@ class StatsigLogger
30
30
  'ruleID' => rule_id
31
31
  }
32
32
  event.statsig_metadata = @statsig_metadata
33
+ event.secondary_exposures = secondary_exposures.is_a?(Array) ? secondary_exposures : []
33
34
  log_event(event)
34
35
  end
35
36
 
36
- def log_config_exposure(user, config_name, rule_id)
37
+ def log_config_exposure(user, config_name, rule_id, secondary_exposures)
37
38
  event = StatsigEvent.new($config_exposure_event)
38
39
  event.user = user
39
40
  event.metadata = {
@@ -41,6 +42,7 @@ class StatsigLogger
41
42
  'ruleID' => rule_id
42
43
  }
43
44
  event.statsig_metadata = @statsig_metadata
45
+ event.secondary_exposures = secondary_exposures.is_a?(Array) ? secondary_exposures : []
44
46
  log_event(event)
45
47
  end
46
48
 
@@ -51,7 +53,7 @@ class StatsigLogger
51
53
  if @events.length == 0
52
54
  return
53
55
  end
54
- flush_events = @events.map { |e| e.serialize() }
56
+ flush_events = @events.map { |e| e.serialize }
55
57
  @events = []
56
58
 
57
59
  @network.post_logs(flush_events, @statsig_metadata)
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.2
4
+ version: 1.7.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-14 00:00:00.000000000 Z
11
+ date: 2021-10-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler