statsig 1.5.1 → 1.6.3

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: a7fc845aabeb9119437279ba79db3970aecf676229ea36c0fd5af08186cb87af
4
- data.tar.gz: d81e0e6b0ae19959b95bfbd56d0dfb55c8baaab0bfa393a099e84226bb5a8fb5
3
+ metadata.gz: 9192f1c9886d3a3b18c0a2e70599e998b4784b23245e9064018ec31c98881b25
4
+ data.tar.gz: eb329d5f742ce2f77391f3f33ba4229852c2c68827643159b51fd870c1fb21d9
5
5
  SHA512:
6
- metadata.gz: 1d9d50f2f01599da2d6a5694c049e4a9ff1caca997563d69849588068e8505878678c5bbc7d8be2b7f887eb1ce36acea6e0f465246f4ce77117860ce77c34f7c
7
- data.tar.gz: e3da88258f99e6579a9b07cd0a5d715587a56dc78b0c99cc490a52d0a12962bb7a74c783458b8cf85f497cf92d74b75a5eca9ae3053a37a84cfb5609b8252067
6
+ metadata.gz: e8a090dc289fe6994b08578b2d2884f9196f37f4104120c546dd11b3036bc939b84a343a643f15f8e12ca01a494f920206ee01a2302de5bcb71a94efb3048b76
7
+ data.tar.gz: 995f91cb599608d0691e8f598d7bc2aa50f0f1b652ad1fac83f3afdb9e950132e21a34aaee44c57797849fb99005cc9dea9d4664f3f4f0ba13afb75dd120c251
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
@@ -32,18 +32,21 @@ class Evaluator
32
32
 
33
33
  def eval_spec(user, config)
34
34
  if config['enabled']
35
+ exposures = []
35
36
  i = 0
36
37
  until i >= config['rules'].length do
37
38
  rule = config['rules'][i]
38
39
  result = self.eval_rule(user, rule)
39
40
  return $fetch_from_server if result == $fetch_from_server
40
- if result
41
+ exposures = exposures + result["exposures"] if result["exposures"].is_a? Array
42
+ if result['value']
41
43
  pass = self.eval_pass_percent(user, rule, config['salt'])
42
44
  return ConfigResult.new(
43
45
  config['name'],
44
46
  pass,
45
47
  pass ? rule['returnValue'] : config['defaultValue'],
46
48
  rule['id'],
49
+ exposures
47
50
  )
48
51
  end
49
52
 
@@ -51,17 +54,28 @@ class Evaluator
51
54
  end
52
55
  end
53
56
 
54
- ConfigResult.new(config['name'], false, config['defaultValue'], 'default')
57
+ ConfigResult.new(config['name'], false, config['defaultValue'], 'default', [])
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
@@ -138,13 +163,13 @@ class Evaluator
138
163
 
139
164
  # array operations
140
165
  when 'any'
141
- return EvaluationHelpers::array_contains(target, value, true)
166
+ return EvaluationHelpers::match_string_in_array(target, value, true, ->(a, b) { a == b })
142
167
  when 'none'
143
- return !EvaluationHelpers::array_contains(target, value, true)
168
+ return !EvaluationHelpers::match_string_in_array(target, value, true, ->(a, b) { a == b })
144
169
  when 'any_case_sensitive'
145
- return EvaluationHelpers::array_contains(target, value, false)
170
+ return EvaluationHelpers::match_string_in_array(target, value, false, ->(a, b) { a == b })
146
171
  when 'none_case_sensitive'
147
- return !EvaluationHelpers::array_contains(target, value, false)
172
+ return !EvaluationHelpers::match_string_in_array(target, value, false, ->(a, b) { a == b })
148
173
 
149
174
  #string
150
175
  when 'str_starts_with_any'
@@ -153,6 +178,8 @@ class Evaluator
153
178
  return EvaluationHelpers::match_string_in_array(target, value, true, ->(a, b) { a.end_with?(b) })
154
179
  when 'str_contains_any'
155
180
  return EvaluationHelpers::match_string_in_array(target, value, true, ->(a, b) { a.include?(b) })
181
+ when 'str_contains_none'
182
+ return !EvaluationHelpers::match_string_in_array(target, value, true, ->(a, b) { a.include?(b) })
156
183
  when 'str_matches'
157
184
  return (value.is_a?(String) && !(value =~ Regexp.new(target)).nil? rescue false)
158
185
  when 'eq'
data/lib/network.rb CHANGED
@@ -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
@@ -56,9 +56,11 @@ class StatsigDriver
56
56
 
57
57
  if res == $fetch_from_server
58
58
  res = check_gate_fallback(user, gate_name)
59
+ # exposure logged by the server
60
+ else
61
+ @logger.log_gate_exposure(user, res.name, res.gate_value, res.rule_id, res.secondary_exposures)
59
62
  end
60
63
 
61
- @logger.log_gate_exposure(user, res.name, res.gate_value, res.rule_id)
62
64
  res.gate_value
63
65
  end
64
66
 
@@ -80,11 +82,19 @@ class StatsigDriver
80
82
 
81
83
  if res == $fetch_from_server
82
84
  res = get_config_fallback(user, dynamic_config_name)
85
+ # exposure logged by the server
86
+ else
87
+ @logger.log_config_exposure(user, res.name, res.rule_id, res.secondary_exposures)
83
88
  end
84
89
 
85
- result_config = DynamicConfig.new(res.name, res.json_value, res.rule_id)
86
- @logger.log_config_exposure(user, result_config.name, result_config.rule_id)
87
- result_config
90
+ DynamicConfig.new(res.name, res.json_value, res.rule_id)
91
+ end
92
+
93
+ def get_experiment(user, experiment_name)
94
+ if !experiment_name.is_a?(String) || experiment_name.empty?
95
+ raise "Invalid experiment_name provided"
96
+ end
97
+ get_config(user, experiment_name)
88
98
  end
89
99
 
90
100
  def log_event(user, event_name, value = nil, metadata = 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.5.1
4
+ version: 1.6.3
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-01 00:00:00.000000000 Z
11
+ date: 2021-09-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler