statsig 1.6.0 → 1.7.0.beta.1

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: a1985e842f0301387596fbd3d8db298ce485c013a502c09a1aff00916882afc0
4
- data.tar.gz: 9cf814e0def7dc3fefd2db9d80f102ec318a41703e6638f66b8a8d844c751959
3
+ metadata.gz: cf5fda9f980ecae86129f6ea0bd4052b6af45cba14c83e38062b495cd06cfeb1
4
+ data.tar.gz: 5b1a231a8af145fec2303fc89998f3e9acf05323cbc55d9fea3850ba841ecd1e
5
5
  SHA512:
6
- metadata.gz: ba2661bcd29ca31b3c458f8046add904029b03ecc0eeeb0c358760467389e58c5f48b5bc39456df1ea2e30051df951359a37e1045283a5d77f911291b0f7bfcd
7
- data.tar.gz: 2631c284b8b792101558e90689353c79aa043a22cbf2f995f045efc4333f74d3d80fcca639ece7f11d734cc4a39b459edd08ce14f6603f86f09ad57afadf2c08
6
+ metadata.gz: 7f1c7cf9a5a549021f02c16bac3d1095143ee9231a3f4165db9040efbf9b16cef083b6955dc5f748dd5fcfe12953064e8c48874a1eb8f477c24f61d061a57890
7
+ data.tar.gz: a5f291bd14edb61286416c293eae1c14f3c519dc1c6548356fe7147667cc33ed6b327488412e2f0b5516139d71d06bacf0db3f6ee907685be9c7e8ca2cc6b119
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
@@ -6,19 +6,11 @@ module EvaluationHelpers
6
6
  func.call(a.to_f, b.to_f) rescue false
7
7
  end
8
8
 
9
- # returns true if array contains value, ignoring case when comparing strings
10
- def self.array_contains(array, value, ignore_case)
11
- return false unless array.is_a?(Array) && !value.nil?
12
- if value.is_a?(String) && match_string_in_array(array, value, ignore_case, ->(a, b) { a == b })
13
- return true
14
- end
15
- return array.include?(value)
16
- end
17
-
18
9
  # returns true if array has any element that evaluates to true with value using func lambda, ignoring case
19
10
  def self.match_string_in_array(array, value, ignore_case, func)
20
- return false unless array.is_a?(Array) && value.is_a?(String)
21
- array.any?{ |s| s.is_a?(String) && ((ignore_case && func.call(value.downcase, s.downcase)) || func.call(value, s)) } rescue false
11
+ return false unless array.is_a?(Array) && !value.nil?
12
+ str_value = value.to_s
13
+ array.any?{ |s| !s.nil? && ((ignore_case && func.call(str_value.downcase, s.to_s.downcase)) || func.call(str_value, s.to_s)) } rescue false
22
14
  end
23
15
 
24
16
  def self.compare_times(a, b, func)
data/lib/evaluator.rb CHANGED
@@ -31,37 +31,53 @@ class Evaluator
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
40
  result = self.eval_rule(user, rule)
39
41
  return $fetch_from_server if result == $fetch_from_server
40
- if result
42
+ exposures = exposures + result['exposures'] if result['exposures'].is_a? Array
43
+ if result['value']
41
44
  pass = self.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
67
  result = self.eval_condition(user, rule['conditions'][i])
61
- return result unless result == true
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)
@@ -82,7 +98,18 @@ class Evaluator
82
98
  when 'fail_gate', 'pass_gate'
83
99
  other_gate_result = self.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
@@ -138,13 +165,13 @@ class Evaluator
138
165
 
139
166
  # array operations
140
167
  when 'any'
141
- return EvaluationHelpers::array_contains(target, value, true)
168
+ return EvaluationHelpers::match_string_in_array(target, value, true, ->(a, b) { a == b })
142
169
  when 'none'
143
- return !EvaluationHelpers::array_contains(target, value, true)
170
+ return !EvaluationHelpers::match_string_in_array(target, value, true, ->(a, b) { a == b })
144
171
  when 'any_case_sensitive'
145
- return EvaluationHelpers::array_contains(target, value, false)
172
+ return EvaluationHelpers::match_string_in_array(target, value, false, ->(a, b) { a == b })
146
173
  when 'none_case_sensitive'
147
- return !EvaluationHelpers::array_contains(target, value, false)
174
+ return !EvaluationHelpers::match_string_in_array(target, value, false, ->(a, b) { a == b })
148
175
 
149
176
  #string
150
177
  when 'str_starts_with_any'
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
@@ -86,7 +86,7 @@ class Network
86
86
  def post_logs(events, statsig_metadata)
87
87
  begin
88
88
  json_body = JSON.generate({'events' => events, 'statsigMetadata' => statsig_metadata})
89
- post_helper('log_event', body: json_body, retries: 5)
89
+ post_helper('log_event', json_body, retries: 5)
90
90
  rescue
91
91
  end
92
92
  end
data/lib/statsig.rb CHANGED
@@ -1,13 +1,13 @@
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)
@@ -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,11 +53,9 @@ 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
- Thread.new do
58
- @network.post_logs(flush_events, @statsig_metadata)
59
- end
59
+ @network.post_logs(flush_events, @statsig_metadata)
60
60
  end
61
61
  end
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.0
4
+ version: 1.7.0.beta.1
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-09 00:00:00.000000000 Z
11
+ date: 2021-10-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -141,9 +141,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
141
141
  version: '0'
142
142
  required_rubygems_version: !ruby/object:Gem::Requirement
143
143
  requirements:
144
- - - ">="
144
+ - - ">"
145
145
  - !ruby/object:Gem::Version
146
- version: '0'
146
+ version: 1.3.1
147
147
  requirements: []
148
148
  rubygems_version: 3.2.3
149
149
  signing_key: