statsig 1.6.1 → 1.7.0.beta.2

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: 8dfdb320cf540cd81046ba96aae19d9d3f947ec557ad7fb7b405a35065eb67fa
4
- data.tar.gz: 0e8d15d9c3f9aa0245c2e834e55394479775d2a9de23208e54dfb41be89e0326
3
+ metadata.gz: 889dec377c0c563738b53a97153d54cde36d34518bfa67d1e56a1f2ac6b161b3
4
+ data.tar.gz: 9da0ba2bf4b241042e5e2a2ae0822700f2906f0454d5ddfda625b99b69c1fd2f
5
5
  SHA512:
6
- metadata.gz: e7a479f8ed9b2cbf49f4b567bdf16000b5ba23a88015593cec6e9806477419011a13b5ed64fd578208cec5f6633101c881e3fa0b98b0474a2faebc91aaf8be1e
7
- data.tar.gz: d731197be16b7ef43d6c9060f00254b3db3a205bef736243a326a382e9fca1aa72b1335102612ab13c044a03f4dc247e0b3443badfad147dffdd8be8391a038d
6
+ metadata.gz: fd0c026a5d93e99c8862fa36e215c185aef5704173c1f56fde1ff01765d276633880233e0d60b65f079b00f63d6618c447dfd10f1f037311b30804b5362db916
7
+ data.tar.gz: 4cf98a1164ce38977d05d682d94b761eda719e7cb1188a363c0b565e1ea9ad402f8719454363edf83dfefba4603666e2e1bd64573b04cbb899d008815e20b665
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
@@ -23,8 +23,6 @@ module EvaluationHelpers
23
23
  end
24
24
  end
25
25
 
26
- private
27
-
28
26
  def self.is_numeric(v)
29
27
  !(v.to_s =~ /\A[-+]?\d*\.?\d+\z/).nil?
30
28
  end
data/lib/evaluator.rb CHANGED
@@ -28,40 +28,54 @@ class Evaluator
28
28
  self.eval_spec(user, @spec_store.get_config(config_name))
29
29
  end
30
30
 
31
- private
32
-
33
31
  def eval_spec(user, config)
32
+ default_rule_id = 'default'
33
+ exposures = []
34
34
  if config['enabled']
35
35
  i = 0
36
36
  until i >= config['rules'].length do
37
37
  rule = config['rules'][i]
38
38
  result = self.eval_rule(user, rule)
39
39
  return $fetch_from_server if result == $fetch_from_server
40
- if result
40
+ exposures = exposures + result['exposures'] if result['exposures'].is_a? Array
41
+ if result['value']
41
42
  pass = self.eval_pass_percent(user, rule, config['salt'])
42
43
  return ConfigResult.new(
43
44
  config['name'],
44
45
  pass,
45
46
  pass ? rule['returnValue'] : config['defaultValue'],
46
47
  rule['id'],
48
+ exposures
47
49
  )
48
50
  end
49
51
 
50
52
  i += 1
51
53
  end
54
+ elsif (default_rule_id = 'disabled')
52
55
  end
53
56
 
54
- ConfigResult.new(config['name'], false, config['defaultValue'], 'default')
57
+ ConfigResult.new(config['name'], false, config['defaultValue'], default_rule_id, exposures)
55
58
  end
56
59
 
57
60
  def eval_rule(user, rule)
61
+ exposures = []
62
+ pass = true
58
63
  i = 0
59
64
  until i >= rule['conditions'].length do
60
65
  result = self.eval_condition(user, rule['conditions'][i])
61
- return result unless result == true
66
+ if result == $fetch_from_server
67
+ return $fetch_from_server
68
+ end
69
+
70
+ if result.is_a?(Hash)
71
+ exposures = exposures + result['exposures'] if result['exposures'].is_a? Array
72
+ pass = false if result['value'] == false
73
+ elsif result == false
74
+ pass = false
75
+ end
62
76
  i += 1
63
77
  end
64
- true
78
+ { 'value' => pass, 'exposures' => exposures }
65
79
  end
66
80
 
67
81
  def eval_condition(user, condition)
@@ -82,7 +96,18 @@ class Evaluator
82
96
  when 'fail_gate', 'pass_gate'
83
97
  other_gate_result = self.check_gate(user, target)
84
98
  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
99
+
100
+ gate_value = other_gate_result&.gate_value == true
101
+ new_exposure = {
102
+ 'gate' => target,
103
+ 'gateValue' => gate_value ? 'true' : 'false',
104
+ 'ruleID' => other_gate_result&.rule_id
105
+ }
106
+ exposures = other_gate_result&.secondary_exposures&.append(new_exposure)
107
+ return {
108
+ 'value' => type == 'pass_gate' ? gate_value : !gate_value,
109
+ 'exposures' => exposures
110
+ }
86
111
  when 'ip_based'
87
112
  value = get_value_from_user(user, field) || get_value_from_ip(user, field)
88
113
  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
@@ -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)
@@ -37,8 +37,6 @@ module Statsig
37
37
  @shared_instance = nil
38
38
  end
39
39
 
40
- private
41
-
42
40
  def self.ensure_initialized
43
41
  if @shared_instance.nil?
44
42
  raise 'Must call initialize first.'
@@ -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'
@@ -112,8 +121,6 @@ class StatsigDriver
112
121
  @polling_thread&.exit
113
122
  end
114
123
 
115
- private
116
-
117
124
  def validate_user(user)
118
125
  if user.nil? || !user.instance_of?(StatsigUser) || !user.user_id.is_a?(String)
119
126
  raise 'Must provide a valid StatsigUser with a user_id to use the server SDK. See https://docs.statsig.com/messages/serverRequiredUserID/ for more details.'
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.1
4
+ version: 1.7.0.beta.2
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-11 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
@@ -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: