statsig 1.25.2 → 1.33.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,46 @@
1
+ class FeatureGate
2
+
3
+ attr_accessor :name
4
+
5
+ attr_accessor :value
6
+
7
+ attr_accessor :rule_id
8
+
9
+ attr_accessor :group_name
10
+
11
+ attr_accessor :id_type
12
+
13
+ attr_accessor :evaluation_details
14
+
15
+ attr_accessor :target_app_ids
16
+
17
+ def initialize(
18
+ name,
19
+ value: false,
20
+ rule_id: '',
21
+ group_name: nil,
22
+ id_type: '',
23
+ evaluation_details: nil,
24
+ target_app_ids: nil
25
+ )
26
+ @name = name
27
+ @value = value
28
+ @rule_id = rule_id
29
+ @group_name = group_name
30
+ @id_type = id_type
31
+ @evaluation_details = evaluation_details
32
+ @target_app_ids = target_app_ids
33
+ end
34
+
35
+ def self.from_config_result(res)
36
+ new(
37
+ res.name,
38
+ value: res.gate_value,
39
+ rule_id: res.rule_id,
40
+ group_name: res.group_name,
41
+ id_type: res.id_type,
42
+ evaluation_details: res.evaluation_details,
43
+ target_app_ids: res.target_app_ids
44
+ )
45
+ end
46
+ end
data/lib/hash_utils.rb ADDED
@@ -0,0 +1,32 @@
1
+ require 'json'
2
+ module Statsig
3
+ class HashUtils
4
+ def self.djb2(input_str)
5
+ hash = 0
6
+ input_str.each_char.each do |c|
7
+ hash = (hash << 5) - hash + c.ord
8
+ hash &= hash
9
+ end
10
+ hash &= 0xFFFFFFFF # Convert to unsigned 32-bit integer
11
+ return hash.to_s
12
+ end
13
+
14
+ def self.djb2ForHash(input_hash)
15
+ return djb2(input_hash.to_json)
16
+ end
17
+
18
+ def self.sha256(input_str)
19
+ return Digest::SHA256.base64digest(input_str)
20
+ end
21
+
22
+ def self.sortHash(input_hash)
23
+ dictionary = input_hash.clone.sort_by { |key| key }.to_h;
24
+ input_hash.each do |key, value|
25
+ if value.is_a?(Hash)
26
+ dictionary[key] = self.sortHash(value)
27
+ end
28
+ end
29
+ return dictionary
30
+ end
31
+ end
32
+ end
data/lib/id_list.rb CHANGED
@@ -1,4 +1,4 @@
1
- # typed: true
1
+
2
2
  module Statsig
3
3
  class IDList
4
4
  attr_accessor :name
@@ -19,7 +19,7 @@ module Statsig
19
19
  end
20
20
 
21
21
  def self.new_empty(json)
22
- self.new(json)
22
+ new(json)
23
23
  @size = 0
24
24
  end
25
25
 
@@ -1,4 +1,4 @@
1
- # typed: true
1
+
2
2
  module Statsig
3
3
  module Interfaces
4
4
  class IDataStore
@@ -0,0 +1,12 @@
1
+
2
+ module Statsig
3
+ module Interfaces
4
+ class IUserPersistentStorage
5
+ def load(key)
6
+ nil
7
+ end
8
+
9
+ def save(key, data) end
10
+ end
11
+ end
12
+ end
data/lib/layer.rb CHANGED
@@ -1,6 +1,3 @@
1
- # typed: false
2
-
3
- require 'sorbet-runtime'
4
1
  ##
5
2
  # Contains the current values from Statsig.
6
3
  # Will contain layer default values for all shared parameters in that layer.
@@ -9,23 +6,22 @@ require 'sorbet-runtime'
9
6
  #
10
7
  # Layers Documentation: https://docs.statsig.com/layers
11
8
  class Layer
12
- extend T::Sig
13
9
 
14
- sig { returns(String) }
15
10
  attr_accessor :name
16
11
 
17
- sig { returns(String) }
18
12
  attr_accessor :rule_id
19
13
 
20
- sig { params(name: String, value: T::Hash[String, T.untyped], rule_id: String, exposure_log_func: T.any(Method, Proc, NilClass)).void }
21
- def initialize(name, value = {}, rule_id = '', exposure_log_func = nil)
14
+ attr_accessor :group_name
15
+
16
+ def initialize(name, value = {}, rule_id = '', group_name = nil, allocated_experiment = nil, exposure_log_func = nil)
22
17
  @name = name
23
- @value = value
18
+ @value = value || {}
24
19
  @rule_id = rule_id
20
+ @group_name = group_name
21
+ @allocated_experiment = allocated_experiment
25
22
  @exposure_log_func = exposure_log_func
26
23
  end
27
24
 
28
- sig { params(index: String, default_value: T.untyped).returns(T.untyped) }
29
25
  ##
30
26
  # Get the value for the given key (index), falling back to the default_value if it cannot be found.
31
27
  #
@@ -41,7 +37,6 @@ class Layer
41
37
  @value[index]
42
38
  end
43
39
 
44
- sig { params(index: String, default_value: T.untyped).returns(T.untyped) }
45
40
  ##
46
41
  # Get the value for the given key (index), falling back to the default_value if it cannot be found
47
42
  # or is found to have a different type from the default_value.
@@ -58,4 +53,4 @@ class Layer
58
53
 
59
54
  @value[index]
60
55
  end
61
- end
56
+ end
data/lib/network.rb CHANGED
@@ -1,12 +1,11 @@
1
- # typed: true
2
-
3
1
  require 'http'
4
2
  require 'json'
5
3
  require 'securerandom'
6
- require 'sorbet-runtime'
4
+
7
5
  require 'uri_helper'
6
+ require 'connection_pool'
8
7
 
9
- $retry_codes = [408, 500, 502, 503, 504, 522, 524, 599]
8
+ RETRY_CODES = [408, 500, 502, 503, 504, 522, 524, 599].freeze
10
9
 
11
10
  module Statsig
12
11
  class NetworkError < StandardError
@@ -19,9 +18,6 @@ module Statsig
19
18
  end
20
19
 
21
20
  class Network
22
- extend T::Sig
23
-
24
- sig { params(server_secret: String, options: StatsigOptions, backoff_mult: Integer).void }
25
21
 
26
22
  def initialize(server_secret, options, backoff_mult = 10)
27
23
  super()
@@ -33,29 +29,44 @@ module Statsig
33
29
  @post_logs_retry_backoff = options.post_logs_retry_backoff
34
30
  @post_logs_retry_limit = options.post_logs_retry_limit
35
31
  @session_id = SecureRandom.uuid
32
+ @connection_pool = ConnectionPool.new(size: 3) do
33
+ meta = Statsig.get_statsig_metadata
34
+ client = HTTP.use(:auto_inflate).headers(
35
+ {
36
+ 'STATSIG-API-KEY' => @server_secret,
37
+ 'STATSIG-SERVER-SESSION-ID' => @session_id,
38
+ 'Content-Type' => 'application/json; charset=UTF-8',
39
+ 'STATSIG-SDK-TYPE' => meta['sdkType'],
40
+ 'STATSIG-SDK-VERSION' => meta['sdkVersion'],
41
+ 'STATSIG-SDK-LANGUAGE-VERSION' => meta['languageVersion'],
42
+ 'Accept-Encoding' => 'gzip'
43
+ }
44
+ ).accept(:json)
45
+ if @timeout
46
+ client = client.timeout(@timeout)
47
+ end
48
+
49
+ client
50
+ end
36
51
  end
37
52
 
38
- sig { params(endpoint: String, body: String, retries: Integer, backoff: Integer)
39
- .returns([T.any(HTTP::Response, NilClass), T.any(StandardError, NilClass)]) }
53
+ def download_config_specs(since_time)
54
+ get("download_config_specs/#{@server_secret}.json?sinceTime=#{since_time}")
55
+ end
40
56
 
41
- def post_helper(endpoint, body, retries = 0, backoff = 1)
57
+ def get(endpoint, retries = 0, backoff = 1)
58
+ request(:GET, endpoint, nil, retries, backoff)
59
+ end
60
+
61
+ def post(endpoint, body, retries = 0, backoff = 1)
62
+ request(:POST, endpoint, body, retries, backoff)
63
+ end
64
+
65
+ def request(method, endpoint, body, retries = 0, backoff = 1)
42
66
  if @local_mode
43
67
  return nil, nil
44
68
  end
45
69
 
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
70
  backoff_adjusted = backoff > 10 ? backoff += Random.rand(10) : backoff # to deter overlap
60
71
  if @post_logs_retry_backoff
61
72
  if @post_logs_retry_backoff.is_a? Integer
@@ -66,48 +77,39 @@ module Statsig
66
77
  end
67
78
  url = URIHelper.build_url(endpoint)
68
79
  begin
69
- res = http.post(url, body: body)
80
+ res = @connection_pool.with do |conn|
81
+ request = conn.headers('STATSIG-CLIENT-TIME' => (Time.now.to_f * 1000).to_i.to_s)
82
+ case method
83
+ when :GET
84
+ request.get(url)
85
+ when :POST
86
+ request.post(url, body: body)
87
+ end
88
+ end
70
89
  rescue StandardError => e
71
90
  ## network error retry
72
- return nil, e unless retries > 0
91
+ return nil, e unless retries.positive?
92
+
73
93
  sleep backoff_adjusted
74
- return post_helper(endpoint, body, retries - 1, backoff * @backoff_multiplier)
94
+ return request(method, endpoint, body, retries - 1, backoff * @backoff_multiplier)
75
95
  end
76
96
  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)
78
- ## status code retry
79
- sleep backoff_adjusted
80
- post_helper(endpoint, body, retries - 1, backoff * @backoff_multiplier)
81
- end
82
97
 
83
- 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
98
+ unless retries.positive? && RETRY_CODES.include?(res.code)
99
+ return res, NetworkError.new("Got an exception when making request to #{url}: #{res.to_s}",
100
+ res.status.to_i)
91
101
  end
92
- end
93
102
 
94
- 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
103
+ ## status code retry
104
+ sleep backoff_adjusted
105
+ request(method, endpoint, body, retries - 1, backoff * @backoff_multiplier)
103
106
  end
104
107
 
105
108
  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
109
+ json_body = JSON.generate({ events: events, statsigMetadata: Statsig.get_statsig_metadata })
110
+ post('log_event', json_body, @post_logs_retry_limit)
111
+ rescue StandardError
112
+
111
113
  end
112
114
  end
113
- end
115
+ end