statsig 1.25.2 → 1.27.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: c7f3c8522d7d8062daf248fd667fd0d8152a4194339fde799814a5da44812eda
4
- data.tar.gz: 1fd5e67d4a75ab3b5b6c92c780841252a4610e18bc13310f9b11dbb583aaf16a
3
+ metadata.gz: f4d0e32be6be9e6110bf6237eb4ba33728dfde66385272bb9a15412662455873
4
+ data.tar.gz: '03408269143b235e02fc8be9f650ed2d4fc778ddf3cc7ffb7242ed9b9494ab73'
5
5
  SHA512:
6
- metadata.gz: 8f516ca6d221c9d789f1f358b550edeedf4203c01ead45374b97a3b81f4205d09f1c7c3e0214516cdcad314bacbdbde39a5df2053779679fe4df593c974c8300
7
- data.tar.gz: a90846ef3c050c7c0b8bddbabd19ed7bff6236a15ed941f55d1a305f4c9cb5ff35f595a3d7ea4ab1560d7f7bae11c669c29d17f8041da9125524669703dc5370
6
+ metadata.gz: dac08983696816714e346ff1381fa2e83847375d5cee5bd33eb533a83e92be53f33a8fc5217a63b6856f723ca9063a631d4beee260bff95673329f6bfcdd5097
7
+ data.tar.gz: 8ad5c492697b2b8cb7c06eac670b03230190dbbe9210f4f8ab9bd951bd73ed0c3c91f45353fc4466d2d1295c09366ae95cae109521d98d1041c629d61202c092
@@ -1,4 +1,7 @@
1
1
  # typed: true
2
+
3
+ require_relative 'hash_utils'
4
+
2
5
  $empty_eval_result = {
3
6
  :gate_value => false,
4
7
  :json_value => {},
@@ -9,10 +12,12 @@ $empty_eval_result = {
9
12
 
10
13
  module ClientInitializeHelpers
11
14
  class ResponseFormatter
12
- def initialize(evaluator, user)
15
+ def initialize(evaluator, user, hash, client_sdk_key)
13
16
  @evaluator = evaluator
14
17
  @user = user
15
18
  @specs = evaluator.spec_store.get_raw_specs
19
+ @hash = hash
20
+ @client_sdk_key = client_sdk_key
16
21
  end
17
22
 
18
23
  def get_responses(key)
@@ -24,6 +29,13 @@ module ClientInitializeHelpers
24
29
  private
25
30
 
26
31
  def to_response(config_name, config_spec)
32
+ target_app_id = @evaluator.spec_store.get_app_id_for_sdk_key(@client_sdk_key)
33
+ config_target_apps = config_spec['targetAppIDs']
34
+
35
+ unless target_app_id.nil? || config_target_apps.nil? || config_target_apps.include?(target_app_id)
36
+ return nil
37
+ end
38
+
27
39
  eval_result = @evaluator.eval_spec(@user, config_spec)
28
40
  if eval_result.nil?
29
41
  return nil
@@ -33,6 +45,8 @@ module ClientInitializeHelpers
33
45
  :gate_value => eval_result.gate_value,
34
46
  :json_value => eval_result.json_value,
35
47
  :rule_id => eval_result.rule_id,
48
+ :group_name => eval_result.group_name,
49
+ :id_type => eval_result.id_type,
36
50
  :config_delegate => eval_result.config_delegate,
37
51
  :is_experiment_group => eval_result.is_experiment_group,
38
52
  :secondary_exposures => eval_result.secondary_exposures,
@@ -52,10 +66,14 @@ module ClientInitializeHelpers
52
66
  end
53
67
 
54
68
  result['value'] = safe_eval_result[:gate_value]
69
+ result["group_name"] = safe_eval_result[:group_name]
70
+ result["id_type"] = safe_eval_result[:id_type]
55
71
  when 'dynamic_config'
56
72
  id_type = config_spec['idType']
57
73
  result['value'] = safe_eval_result[:json_value]
58
74
  result["group"] = safe_eval_result[:rule_id]
75
+ result["group_name"] = safe_eval_result[:group_name]
76
+ result["id_type"] = safe_eval_result[:id_type]
59
77
  result["is_device_based"] = id_type.is_a?(String) && id_type.downcase == 'stableid'
60
78
  else
61
79
  return nil
@@ -67,6 +85,7 @@ module ClientInitializeHelpers
67
85
 
68
86
  if entity_type == 'layer'
69
87
  populate_layer_fields(config_spec, safe_eval_result, result)
88
+ result.delete('id_type') # not exposed for layer configs in /initialize
70
89
  end
71
90
 
72
91
  hashed_name = hash_name(config_name)
@@ -126,7 +145,14 @@ module ClientInitializeHelpers
126
145
  end
127
146
 
128
147
  def hash_name(name)
129
- Digest::SHA256.base64digest(name)
148
+ case @hash
149
+ when 'none'
150
+ return name
151
+ when 'sha256'
152
+ return Statsig::HashUtils.sha256(name)
153
+ when 'djb2'
154
+ return Statsig::HashUtils.djb2(name)
155
+ end
130
156
  end
131
157
  end
132
- end
158
+ end
data/lib/diagnostics.rb CHANGED
@@ -7,14 +7,18 @@ module Statsig
7
7
  extend T::Sig
8
8
 
9
9
  sig { returns(String) }
10
- attr_reader :context
10
+ attr_accessor :context
11
11
 
12
12
  sig { returns(T::Array[T::Hash[Symbol, T.untyped]]) }
13
13
  attr_reader :markers
14
14
 
15
+ sig { returns(T::Hash[String, Numeric]) }
16
+ attr_accessor :sample_rates
17
+
15
18
  def initialize(context)
16
19
  @context = context
17
20
  @markers = []
21
+ @sample_rates = {}
18
22
  end
19
23
 
20
24
  sig do
@@ -22,33 +26,37 @@ module Statsig
22
26
  key: String,
23
27
  action: String,
24
28
  step: T.any(String, NilClass),
25
- value: T.any(String, Integer, T::Boolean, NilClass),
26
- metadata: T.any(T::Hash[Symbol, T.untyped], NilClass)
29
+ tags: T::Hash[Symbol, T.untyped]
27
30
  ).void
28
31
  end
29
32
 
30
- def mark(key, action, step = nil, value = nil, metadata = nil)
31
- @markers.push({
32
- key: key,
33
- step: step,
34
- action: action,
35
- value: value,
36
- metadata: metadata,
37
- timestamp: (Time.now.to_f * 1000).to_i
38
- })
33
+ def mark(key, action, step, tags)
34
+ marker = {
35
+ key: key,
36
+ action: action,
37
+ timestamp: (Time.now.to_f * 1000).to_i
38
+ }
39
+ if !step.nil?
40
+ marker[:step] = step
41
+ end
42
+ tags.each do |key, val|
43
+ unless val.nil?
44
+ marker[key] = val
45
+ end
46
+ end
47
+ @markers.push(marker)
39
48
  end
40
49
 
41
50
  sig do
42
51
  params(
43
52
  key: String,
44
53
  step: T.any(String, NilClass),
45
- value: T.any(String, Integer, T::Boolean, NilClass),
46
- metadata: T.any(T::Hash[Symbol, T.untyped], NilClass)
54
+ tags: T::Hash[Symbol, T.untyped]
47
55
  ).returns(Tracker)
48
56
  end
49
- def track(key, step = nil, value = nil, metadata = nil)
50
- tracker = Tracker.new(self, key, step, metadata)
51
- tracker.start(value)
57
+ def track(key, step = nil, tags = {})
58
+ tracker = Tracker.new(self, key, step, tags)
59
+ tracker.start(**tags)
52
60
  tracker
53
61
  end
54
62
 
@@ -61,10 +69,29 @@ module Statsig
61
69
  }
62
70
  end
63
71
 
72
+ def serialize_with_sampling
73
+ marker_keys = @markers.map { |e| e[:key] }
74
+ unique_marker_keys = marker_keys.uniq { |e| e }
75
+ sampled_marker_keys = unique_marker_keys.select do |key|
76
+ @sample_rates.key?(key) && !self.class.sample(@sample_rates[key])
77
+ end
78
+ final_markers = @markers.select do |marker|
79
+ !sampled_marker_keys.include?(marker[:key])
80
+ end
81
+ {
82
+ context: @context.clone,
83
+ markers: final_markers
84
+ }
85
+ end
86
+
64
87
  def clear_markers
65
88
  @markers.clear
66
89
  end
67
90
 
91
+ def self.sample(rate_over_ten_thousand)
92
+ rand * 10_000 < rate_over_ten_thousand
93
+ end
94
+
68
95
  class Context
69
96
  INITIALIZE = 'initialize'.freeze
70
97
  CONFIG_SYNC = 'config_sync'.freeze
@@ -81,22 +108,22 @@ module Statsig
81
108
  diagnostics: Diagnostics,
82
109
  key: String,
83
110
  step: T.any(String, NilClass),
84
- metadata: T.any(T::Hash[Symbol, T.untyped], NilClass)
111
+ tags: T::Hash[Symbol, T.untyped]
85
112
  ).void
86
113
  end
87
- def initialize(diagnostics, key, step, metadata)
114
+ def initialize(diagnostics, key, step, tags = {})
88
115
  @diagnostics = diagnostics
89
116
  @key = key
90
117
  @step = step
91
- @metadata = metadata
118
+ @tags = tags
92
119
  end
93
120
 
94
- def start(value = nil)
95
- @diagnostics.mark(@key, 'start', @step, value, @metadata)
121
+ def start(**tags)
122
+ @diagnostics.mark(@key, 'start', @step, tags.nil? ? {} : tags.merge(@tags))
96
123
  end
97
124
 
98
- def end(value = nil)
99
- @diagnostics.mark(@key, 'end', @step, value, @metadata)
125
+ def end(**tags)
126
+ @diagnostics.mark(@key, 'end', @step, tags.nil? ? {} : tags.merge(@tags))
100
127
  end
101
128
  end
102
129
  end
@@ -9,70 +9,56 @@ module Statsig
9
9
  class ErrorBoundary
10
10
  extend T::Sig
11
11
 
12
- sig { returns(T.any(StatsigLogger, NilClass)) }
13
- attr_accessor :logger
14
-
15
12
  sig { params(sdk_key: String).void }
16
13
  def initialize(sdk_key)
17
14
  @sdk_key = sdk_key
18
15
  @seen = Set.new
19
16
  end
20
17
 
21
- def sample_diagnostics
22
- rand(10_000).zero?
23
- end
24
-
25
18
  def capture(task:, recover: -> {}, caller: nil)
26
- if !caller.nil? && Diagnostics::API_CALL_KEYS.include?(caller) && sample_diagnostics
27
- diagnostics = Diagnostics.new('api_call')
28
- tracker = diagnostics.track(caller)
29
- end
30
19
  begin
31
20
  res = task.call
32
- tracker&.end(true)
33
- rescue StandardError => e
34
- tracker&.end(false)
35
- if e.is_a?(Statsig::UninitializedError) or e.is_a?(Statsig::ValueError)
21
+ rescue StandardError, SystemStackError => e
22
+ if e.is_a?(Statsig::UninitializedError) || e.is_a?(Statsig::ValueError)
36
23
  raise e
37
24
  end
25
+
38
26
  puts '[Statsig]: An unexpected exception occurred.'
39
- log_exception(e)
27
+ log_exception(e, tag: caller)
40
28
  res = recover.call
41
29
  end
42
- @logger&.log_diagnostics_event(diagnostics)
43
30
  return res
44
31
  end
45
32
 
46
33
  private
47
34
 
48
- def log_exception(exception)
49
- begin
50
- name = exception.class.name
51
- if @seen.include?(name)
52
- return
53
- end
54
-
55
- @seen << name
56
- meta = Statsig.get_statsig_metadata
57
- http = HTTP.headers(
58
- {
59
- 'STATSIG-API-KEY' => @sdk_key,
60
- 'STATSIG-SDK-TYPE' => meta['sdkType'],
61
- 'STATSIG-SDK-VERSION' => meta['sdkVersion'],
62
- 'Content-Type' => 'application/json; charset=UTF-8'
63
- }).accept(:json)
64
- body = {
65
- 'exception' => name,
66
- 'info' => {
67
- 'trace' => exception.backtrace.to_s,
68
- 'message' => exception.message
69
- }.to_s,
70
- 'statsigMetadata' => meta
71
- }
72
- http.post($endpoint, body: JSON.generate(body))
73
- rescue
35
+ def log_exception(exception, tag: nil)
36
+ name = exception.class.name
37
+ if @seen.include?(name)
74
38
  return
75
39
  end
40
+
41
+ @seen << name
42
+ meta = Statsig.get_statsig_metadata
43
+ http = HTTP.headers(
44
+ {
45
+ 'STATSIG-API-KEY' => @sdk_key,
46
+ 'STATSIG-SDK-TYPE' => meta['sdkType'],
47
+ 'STATSIG-SDK-VERSION' => meta['sdkVersion'],
48
+ 'Content-Type' => 'application/json; charset=UTF-8'
49
+ }).accept(:json)
50
+ body = {
51
+ 'exception' => name,
52
+ 'info' => {
53
+ 'trace' => exception.backtrace.to_s,
54
+ 'message' => exception.message
55
+ }.to_s,
56
+ 'statsigMetadata' => meta,
57
+ 'tag' => tag
58
+ }
59
+ http.post($endpoint, body: JSON.generate(body))
60
+ rescue StandardError
61
+ return
76
62
  end
77
63
  end
78
- end
64
+ end
data/lib/evaluator.rb CHANGED
@@ -17,8 +17,8 @@ module Statsig
17
17
  class Evaluator
18
18
  attr_accessor :spec_store
19
19
 
20
- def initialize(network, options, error_callback, diagnostics)
21
- @spec_store = Statsig::SpecStore.new(network, options, error_callback, diagnostics)
20
+ def initialize(network, options, error_callback, diagnostics, error_boundary, logger)
21
+ @spec_store = Statsig::SpecStore.new(network, options, error_callback, diagnostics, error_boundary, logger)
22
22
  UAParser.initialize_async
23
23
  CountryLookup.initialize_async
24
24
 
@@ -98,12 +98,12 @@ module Statsig
98
98
  eval_spec(user, @spec_store.get_layer(layer_name))
99
99
  end
100
100
 
101
- def get_client_initialize_response(user)
101
+ def get_client_initialize_response(user, hash, client_sdk_key)
102
102
  if @spec_store.is_ready_for_checks == false
103
103
  return nil
104
104
  end
105
105
 
106
- formatter = ClientInitializeHelpers::ResponseFormatter.new(self, user)
106
+ formatter = ClientInitializeHelpers::ResponseFormatter.new(self, user, hash, client_sdk_key)
107
107
 
108
108
  evaluated_keys = {}
109
109
  if user.user_id.nil? == false
@@ -123,6 +123,7 @@ module Statsig
123
123
  "generator" => "statsig-ruby-sdk",
124
124
  "evaluated_keys" => evaluated_keys,
125
125
  "time" => 0,
126
+ "hash_used" => hash
126
127
  }
127
128
  end
128
129
 
@@ -199,7 +200,7 @@ module Statsig
199
200
  @spec_store.initial_config_sync_time,
200
201
  @spec_store.init_reason
201
202
  ),
202
- group_name: nil,
203
+ group_name: 'default',
203
204
  id_type: config['idType']
204
205
  )
205
206
  end
data/lib/hash_utils.rb ADDED
@@ -0,0 +1,17 @@
1
+ module Statsig
2
+ class HashUtils
3
+ def self.djb2(input_str)
4
+ hash = 0
5
+ input_str.each_char.each do |c|
6
+ hash = (hash << 5) - hash + c.ord
7
+ hash &= hash
8
+ end
9
+ hash &= 0xFFFFFFFF # Convert to unsigned 32-bit integer
10
+ return hash.to_s
11
+ end
12
+
13
+ def self.sha256(input_str)
14
+ return Digest::SHA256.base64digest(input_str)
15
+ end
16
+ end
17
+ end
data/lib/network.rb CHANGED
@@ -5,8 +5,9 @@ require 'json'
5
5
  require 'securerandom'
6
6
  require 'sorbet-runtime'
7
7
  require 'uri_helper'
8
+ require 'connection_pool'
8
9
 
9
- $retry_codes = [408, 500, 502, 503, 504, 522, 524, 599]
10
+ RETRY_CODES = [408, 500, 502, 503, 504, 522, 524, 599].freeze
10
11
 
11
12
  module Statsig
12
13
  class NetworkError < StandardError
@@ -22,7 +23,6 @@ module Statsig
22
23
  extend T::Sig
23
24
 
24
25
  sig { params(server_secret: String, options: StatsigOptions, backoff_mult: Integer).void }
25
-
26
26
  def initialize(server_secret, options, backoff_mult = 10)
27
27
  super()
28
28
  URIHelper.initialize(options)
@@ -33,29 +33,35 @@ module Statsig
33
33
  @post_logs_retry_backoff = options.post_logs_retry_backoff
34
34
  @post_logs_retry_limit = options.post_logs_retry_limit
35
35
  @session_id = SecureRandom.uuid
36
- end
36
+ @connection_pool = ConnectionPool.new(size: 3) do
37
+ meta = Statsig.get_statsig_metadata
38
+ client = HTTP.use(:auto_inflate).headers(
39
+ {
40
+ 'STATSIG-API-KEY' => @server_secret,
41
+ 'STATSIG-SERVER-SESSION-ID' => @session_id,
42
+ 'Content-Type' => 'application/json; charset=UTF-8',
43
+ 'STATSIG-SDK-TYPE' => meta['sdkType'],
44
+ 'STATSIG-SDK-VERSION' => meta['sdkVersion'],
45
+ 'Accept-Encoding' => 'gzip'
46
+ }
47
+ ).accept(:json)
48
+ if @timeout
49
+ client = client.timeout(@timeout)
50
+ end
37
51
 
38
- sig { params(endpoint: String, body: String, retries: Integer, backoff: Integer)
39
- .returns([T.any(HTTP::Response, NilClass), T.any(StandardError, NilClass)]) }
52
+ client
53
+ end
54
+ end
40
55
 
56
+ sig do
57
+ params(endpoint: String, body: String, retries: Integer, backoff: Integer)
58
+ .returns([T.any(HTTP::Response, NilClass), T.any(StandardError, NilClass)])
59
+ end
41
60
  def post_helper(endpoint, body, retries = 0, backoff = 1)
42
61
  if @local_mode
43
62
  return nil, nil
44
63
  end
45
64
 
46
- meta = Statsig.get_statsig_metadata
47
- http = HTTP.headers(
48
- {
49
- "STATSIG-API-KEY" => @server_secret,
50
- "STATSIG-CLIENT-TIME" => (Time.now.to_f * 1000).to_i.to_s,
51
- "STATSIG-SERVER-SESSION-ID" => @session_id,
52
- "Content-Type" => "application/json; charset=UTF-8",
53
- "STATSIG-SDK-TYPE" => meta['sdkType'],
54
- "STATSIG-SDK-VERSION" => meta['sdkVersion'],
55
- }).accept(:json)
56
- if @timeout
57
- http = http.timeout(@timeout)
58
- end
59
65
  backoff_adjusted = backoff > 10 ? backoff += Random.rand(10) : backoff # to deter overlap
60
66
  if @post_logs_retry_backoff
61
67
  if @post_logs_retry_backoff.is_a? Integer
@@ -66,48 +72,53 @@ module Statsig
66
72
  end
67
73
  url = URIHelper.build_url(endpoint)
68
74
  begin
69
- res = http.post(url, body: body)
75
+ res = @connection_pool.with do |conn|
76
+ conn.headers('STATSIG-CLIENT-TIME' => (Time.now.to_f * 1000).to_i.to_s).post(url, body: body)
77
+ end
70
78
  rescue StandardError => e
71
79
  ## network error retry
72
- return nil, e unless retries > 0
80
+ return nil, e unless retries.positive?
81
+
73
82
  sleep backoff_adjusted
74
83
  return post_helper(endpoint, body, retries - 1, backoff * @backoff_multiplier)
75
84
  end
76
85
  return res, nil if res.status.success?
77
- return nil, NetworkError.new("Got an exception when making request to #{url}: #{res.to_s}", res.status.to_i) unless retries > 0 && $retry_codes.include?(res.code)
86
+
87
+ unless retries.positive? && RETRY_CODES.include?(res.code)
88
+ return res, NetworkError.new("Got an exception when making request to #{url}: #{res.to_s}",
89
+ res.status.to_i)
90
+ end
91
+
78
92
  ## status code retry
79
93
  sleep backoff_adjusted
80
94
  post_helper(endpoint, body, retries - 1, backoff * @backoff_multiplier)
81
95
  end
82
96
 
83
97
  def check_gate(user, gate_name)
84
- begin
85
- request_body = JSON.generate({ 'user' => user&.serialize(false), 'gateName' => gate_name })
86
- response, _ = post_helper('check_gate', request_body)
87
- return JSON.parse(response.body) unless response.nil?
88
- false
89
- rescue
90
- return false
91
- end
98
+ request_body = JSON.generate({ 'user' => user&.serialize(false), 'gateName' => gate_name })
99
+ response, = post_helper('check_gate', request_body)
100
+ return JSON.parse(response.body) unless response.nil?
101
+
102
+ false
103
+ rescue StandardError
104
+ false
92
105
  end
93
106
 
94
107
  def get_config(user, dynamic_config_name)
95
- begin
96
- request_body = JSON.generate({ 'user' => user&.serialize(false), 'configName' => dynamic_config_name })
97
- response, _ = post_helper('get_config', request_body)
98
- return JSON.parse(response.body) unless response.nil?
99
- nil
100
- rescue
101
- return nil
102
- end
108
+ request_body = JSON.generate({ 'user' => user&.serialize(false), 'configName' => dynamic_config_name })
109
+ response, = post_helper('get_config', request_body)
110
+ return JSON.parse(response.body) unless response.nil?
111
+
112
+ nil
113
+ rescue StandardError
114
+ nil
103
115
  end
104
116
 
105
117
  def post_logs(events)
106
- begin
107
- json_body = JSON.generate({ 'events' => events, 'statsigMetadata' => Statsig.get_statsig_metadata })
108
- post_helper('log_event', json_body, @post_logs_retry_limit)
109
- rescue
110
- end
118
+ json_body = JSON.generate({ 'events' => events, 'statsigMetadata' => Statsig.get_statsig_metadata })
119
+ post_helper('log_event', json_body, @post_logs_retry_limit)
120
+ rescue StandardError
121
+
111
122
  end
112
123
  end
113
- end
124
+ end