statsig 1.6.2 → 1.7.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: 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