statsig 1.10.0 → 1.20.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.
@@ -1,50 +1,69 @@
1
+ # typed: true
1
2
  require 'statsig_event'
3
+ require 'concurrent-ruby'
2
4
 
3
5
  $gate_exposure_event = 'statsig::gate_exposure'
4
6
  $config_exposure_event = 'statsig::config_exposure'
5
7
  $layer_exposure_event = 'statsig::layer_exposure'
8
+ $diagnostics_event = 'statsig::diagnostics'
6
9
 
7
10
  module Statsig
8
11
  class StatsigLogger
9
- def initialize(network)
12
+ def initialize(network, options)
10
13
  @network = network
11
14
  @events = []
15
+ @options = options
16
+
17
+ @logging_pool = Concurrent::ThreadPoolExecutor.new(
18
+ min_threads: [2, Concurrent.processor_count].min,
19
+ max_threads: [2, Concurrent.processor_count].max,
20
+ # max jobs pending before we start dropping
21
+ max_queue: [2, Concurrent.processor_count].max * 5,
22
+ fallback_policy: :discard,
23
+ )
24
+
12
25
  @background_flush = periodic_flush
13
26
  end
14
27
 
15
28
  def log_event(event)
16
29
  @events.push(event)
17
- if @events.length >= 1000
18
- flush
30
+ if @events.length >= @options.logging_max_buffer_size
31
+ flush_async
19
32
  end
20
33
  end
21
34
 
22
- def log_gate_exposure(user, gate_name, value, rule_id, secondary_exposures)
35
+ def log_gate_exposure(user, gate_name, value, rule_id, secondary_exposures, eval_details, context = nil)
23
36
  event = StatsigEvent.new($gate_exposure_event)
24
37
  event.user = user
25
38
  event.metadata = {
26
39
  'gate' => gate_name,
27
40
  'gateValue' => value.to_s,
28
- 'ruleID' => rule_id
41
+ 'ruleID' => rule_id,
29
42
  }
30
43
  event.statsig_metadata = Statsig.get_statsig_metadata
31
44
  event.secondary_exposures = secondary_exposures.is_a?(Array) ? secondary_exposures : []
45
+
46
+ safe_add_eval_details(eval_details, event)
47
+ safe_add_exposure_context(context, event)
32
48
  log_event(event)
33
49
  end
34
50
 
35
- def log_config_exposure(user, config_name, rule_id, secondary_exposures)
51
+ def log_config_exposure(user, config_name, rule_id, secondary_exposures, eval_details, context = nil)
36
52
  event = StatsigEvent.new($config_exposure_event)
37
53
  event.user = user
38
54
  event.metadata = {
39
55
  'config' => config_name,
40
- 'ruleID' => rule_id
56
+ 'ruleID' => rule_id,
41
57
  }
42
58
  event.statsig_metadata = Statsig.get_statsig_metadata
43
59
  event.secondary_exposures = secondary_exposures.is_a?(Array) ? secondary_exposures : []
60
+
61
+ safe_add_eval_details(eval_details, event)
62
+ safe_add_exposure_context(context, event)
44
63
  log_event(event)
45
64
  end
46
65
 
47
- def log_layer_exposure(user, layer, parameter_name, config_evaluation)
66
+ def log_layer_exposure(user, layer, parameter_name, config_evaluation, context = nil)
48
67
  exposures = config_evaluation.undelegated_sec_exps
49
68
  allocated_experiment = ''
50
69
  is_explicit = (config_evaluation.explicit_parameters&.include? parameter_name) || false
@@ -60,26 +79,46 @@ module Statsig
60
79
  'ruleID' => layer.rule_id,
61
80
  'allocatedExperiment' => allocated_experiment,
62
81
  'parameterName' => parameter_name,
63
- 'isExplicitParameter' => String(is_explicit)
82
+ 'isExplicitParameter' => String(is_explicit),
64
83
  }
65
84
  event.statsig_metadata = Statsig.get_statsig_metadata
66
85
  event.secondary_exposures = exposures.is_a?(Array) ? exposures : []
86
+
87
+ safe_add_eval_details(config_evaluation.evaluation_details, event)
88
+ safe_add_exposure_context(context, event)
89
+ log_event(event)
90
+ end
91
+
92
+ def log_diagnostics_event(diagnostics, user = nil)
93
+ event = StatsigEvent.new($diagnostics_event)
94
+ event.user = user
95
+ event.metadata = diagnostics.serialize
67
96
  log_event(event)
68
97
  end
69
98
 
70
99
  def periodic_flush
71
100
  Thread.new do
72
101
  loop do
73
- sleep 60
102
+ sleep @options.logging_interval_seconds
74
103
  flush
75
104
  end
76
105
  end
77
106
  end
78
107
 
79
- def flush(closing = false)
80
- if closing
81
- @background_flush&.exit
108
+ def shutdown
109
+ @background_flush&.exit
110
+ @logging_pool.shutdown
111
+ @logging_pool.wait_for_termination(timeout = 3)
112
+ flush
113
+ end
114
+
115
+ def flush_async
116
+ @logging_pool.post do
117
+ flush
82
118
  end
119
+ end
120
+
121
+ def flush
83
122
  if @events.length == 0
84
123
  return
85
124
  end
@@ -89,5 +128,34 @@ module Statsig
89
128
 
90
129
  @network.post_logs(flush_events)
91
130
  end
131
+
132
+ def maybe_restart_background_threads
133
+ if @background_flush.nil? or !@background_flush.alive?
134
+ @background_flush = periodic_flush
135
+ end
136
+ end
137
+
138
+ private
139
+
140
+ def safe_add_eval_details(eval_details, event)
141
+ if eval_details.nil?
142
+ return
143
+ end
144
+
145
+ event.metadata['reason'] = eval_details.reason
146
+ event.metadata['configSyncTime'] = eval_details.config_sync_time
147
+ event.metadata['initTime'] = eval_details.init_time
148
+ event.metadata['serverTime'] = eval_details.server_time
149
+ end
150
+
151
+ def safe_add_exposure_context(context, event)
152
+ if context.nil?
153
+ return
154
+ end
155
+
156
+ if context['is_manual_exposure']
157
+ event.metadata['isManualExposure'] = 'true'
158
+ end
159
+ end
92
160
  end
93
161
  end
@@ -1,17 +1,124 @@
1
+ # typed: true
2
+
3
+ require 'sorbet-runtime'
4
+ require_relative 'interfaces/data_store'
5
+
6
+ ##
7
+ # Configuration options for the Statsig SDK.
1
8
  class StatsigOptions
2
- attr_reader :environment
3
- attr_reader :api_url_base
4
- attr_reader :rulesets_sync_interval
5
- attr_reader :idlists_sync_interval
9
+ extend T::Sig
10
+
11
+ sig { returns(T.any(T::Hash[String, String], NilClass)) }
12
+ # Hash you can use to set environment variables that apply to all of your users in
13
+ # the same session and will be used for targeting purposes.
14
+ # eg. { "tier" => "development" }
15
+ attr_accessor :environment
16
+
17
+ sig { returns(String) }
18
+ # The base url used to make network calls to Statsig.
19
+ # default: https://statsigapi.net/v1
20
+ attr_accessor :api_url_base
21
+
22
+ sig { returns(T.any(Float, Integer)) }
23
+ # The interval (in seconds) to poll for changes to your Statsig configuration
24
+ # default: 10s
25
+ attr_accessor :rulesets_sync_interval
26
+
27
+ sig { returns(T.any(Float, Integer)) }
28
+ # The interval (in seconds) to poll for changes to your id lists
29
+ # default: 60s
30
+ attr_accessor :idlists_sync_interval
31
+
32
+ sig { returns(T.any(Float, Integer)) }
33
+ # How often to flush logs to Statsig
34
+ # default: 60s
35
+ attr_accessor :logging_interval_seconds
36
+
37
+ sig { returns(Integer) }
38
+ # The maximum number of events to batch before flushing logs to the server
39
+ # default: 1000
40
+ attr_accessor :logging_max_buffer_size
41
+
42
+ sig { returns(T::Boolean) }
43
+ # Restricts the SDK to not issue any network requests and only respond with default values (or local overrides)
44
+ # default: false
45
+ attr_accessor :local_mode
46
+
47
+ sig { returns(T.any(String, NilClass)) }
48
+ # A string that represents all rules for all feature gates, dynamic configs and experiments.
49
+ # It can be provided to bootstrap the Statsig server SDK at initialization in case your server runs
50
+ # into network issue or Statsig is down temporarily.
51
+ attr_accessor :bootstrap_values
52
+
53
+ sig { returns(T.any(Method, Proc, NilClass)) }
54
+ # A callback function that will be called anytime the rulesets are updated.
55
+ attr_accessor :rules_updated_callback
56
+
57
+ sig { returns(T.any(Statsig::Interfaces::IDataStore, NilClass)) }
58
+ # A class that extends IDataStore. Can be used to provide values from a
59
+ # common data store (like Redis) to initialize the Statsig SDK.
60
+ attr_accessor :data_store
61
+
62
+ sig { returns(Integer) }
63
+ # The number of threads allocated to syncing IDLists.
64
+ # default: 3
65
+ attr_accessor :idlist_threadpool_size
66
+
67
+ sig { returns(T::Boolean) }
68
+ # Should diagnostics be logged. These include performance metrics for initialize.
69
+ # default: false
70
+ attr_accessor :disable_diagnostics_logging
71
+
72
+ sig { returns(T::Boolean) }
73
+ # Statsig utilizes Sorbet (https://sorbet.org) to ensure type safety of the SDK. This includes logging
74
+ # to console when errors are detected. You can disable this logging by setting this flag to true.
75
+ # default: false
76
+ attr_accessor :disable_sorbet_logging_handlers
77
+
78
+ sig do
79
+ params(
80
+ environment: T.any(T::Hash[String, String], NilClass),
81
+ api_url_base: String,
82
+ rulesets_sync_interval: T.any(Float, Integer),
83
+ idlists_sync_interval: T.any(Float, Integer),
84
+ logging_interval_seconds: T.any(Float, Integer),
85
+ logging_max_buffer_size: Integer,
86
+ local_mode: T::Boolean,
87
+ bootstrap_values: T.any(String, NilClass),
88
+ rules_updated_callback: T.any(Method, Proc, NilClass),
89
+ data_store: T.any(Statsig::Interfaces::IDataStore, NilClass),
90
+ idlist_threadpool_size: Integer,
91
+ disable_diagnostics_logging: T::Boolean,
92
+ disable_sorbet_logging_handlers: T::Boolean
93
+ ).void
94
+ end
6
95
 
7
96
  def initialize(
8
- environment=nil,
9
- api_url_base='https://statsigapi.net/v1',
97
+ environment = nil,
98
+ api_url_base = 'https://statsigapi.net/v1',
10
99
  rulesets_sync_interval: 10,
11
- idlists_sync_interval: 60)
100
+ idlists_sync_interval: 60,
101
+ logging_interval_seconds: 60,
102
+ logging_max_buffer_size: 1000,
103
+ local_mode: false,
104
+ bootstrap_values: nil,
105
+ rules_updated_callback: nil,
106
+ data_store: nil,
107
+ idlist_threadpool_size: 3,
108
+ disable_diagnostics_logging: false,
109
+ disable_sorbet_logging_handlers: false)
12
110
  @environment = environment.is_a?(Hash) ? environment : nil
13
111
  @api_url_base = api_url_base
14
112
  @rulesets_sync_interval = rulesets_sync_interval
15
113
  @idlists_sync_interval = idlists_sync_interval
114
+ @logging_interval_seconds = logging_interval_seconds
115
+ @logging_max_buffer_size = [logging_max_buffer_size, 1000].min
116
+ @local_mode = local_mode
117
+ @bootstrap_values = bootstrap_values
118
+ @rules_updated_callback = rules_updated_callback
119
+ @data_store = data_store
120
+ @idlist_threadpool_size = idlist_threadpool_size
121
+ @disable_diagnostics_logging = disable_diagnostics_logging
122
+ @disable_sorbet_logging_handlers = disable_sorbet_logging_handlers
16
123
  end
17
124
  end
data/lib/statsig_user.rb CHANGED
@@ -1,40 +1,78 @@
1
+ # typed: true
2
+
3
+ require 'sorbet-runtime'
4
+
5
+ ##
6
+ # The user object to be evaluated against your Statsig configurations (gates/experiments/dynamic configs).
1
7
  class StatsigUser
8
+ extend T::Sig
9
+
10
+ sig { returns(T.any(String, NilClass)) }
11
+ # An identifier for this user. Evaluated against the User ID criteria. (https://docs.statsig.com/feature-gates/conditions#userid)
2
12
  attr_accessor :user_id
13
+
14
+ sig { returns(T.any(String, NilClass)) }
15
+ # An identifier for this user. Evaluated against the Email criteria. (https://docs.statsig.com/feature-gates/conditions#email)
3
16
  attr_accessor :email
17
+
18
+ sig { returns(T.any(String, NilClass)) }
19
+ # An IP address associated with this user. Evaluated against the IP Address criteria. (https://docs.statsig.com/feature-gates/conditions#ip)
4
20
  attr_accessor :ip
21
+
22
+ sig { returns(T.any(String, NilClass)) }
23
+ # A user agent string associated with this user. Evaluated against Browser Version and Name (https://docs.statsig.com/feature-gates/conditions#browser-version)
5
24
  attr_accessor :user_agent
25
+
26
+ sig { returns(T.any(String, NilClass)) }
27
+ # The country code associated with this user (e.g New Zealand => NZ). Evaluated against the Country criteria. (https://docs.statsig.com/feature-gates/conditions#country)
6
28
  attr_accessor :country
29
+
30
+ sig { returns(T.any(String, NilClass)) }
31
+ # An locale for this user.
7
32
  attr_accessor :locale
33
+
34
+ sig { returns(T.any(String, NilClass)) }
35
+ # The current app version the user is interacting with. Evaluated against the App Version criteria. (https://docs.statsig.com/feature-gates/conditions#app-version)
8
36
  attr_accessor :app_version
37
+
38
+ sig { returns(T.any(T::Hash[String, String], NilClass)) }
39
+ # A Hash you can use to set environment variables that apply to this user. e.g. { "tier" => "development" }
9
40
  attr_accessor :statsig_environment
41
+
42
+ sig { returns(T.any(T::Hash[String, String], NilClass)) }
43
+ # Any Custom IDs to associated with the user. (See https://docs.statsig.com/guides/experiment-on-custom-id-types)
10
44
  attr_accessor :custom_ids
45
+
46
+ sig { returns(T.any(T::Hash[String, String], NilClass)) }
47
+ # Any value you wish to use in evaluation, but do not want logged with events, can be stored in this field.
11
48
  attr_accessor :private_attributes
12
49
 
50
+ sig { returns(T.any(T::Hash[String, T.untyped], NilClass)) }
51
+
13
52
  def custom
14
53
  @custom
15
54
  end
16
55
 
56
+ sig { params(value: T.any(T::Hash[String, T.untyped], NilClass)).void }
57
+ # Any custom fields for this user. Evaluated against the Custom criteria. (https://docs.statsig.com/feature-gates/conditions#custom)
17
58
  def custom=(value)
18
59
  @custom = value.is_a?(Hash) ? value : Hash.new
19
60
  end
20
61
 
62
+ sig { params(user_hash: T.any(T::Hash[T.any(String, Symbol), T.untyped], NilClass)).void }
63
+
21
64
  def initialize(user_hash)
22
- @statsig_environment = Hash.new
23
- if user_hash.is_a?(Hash)
24
- @user_id = user_hash['userID'] || user_hash['user_id']
25
- @user_id = @user_id.to_s unless @user_id.nil?
26
- @email = user_hash['email']
27
- @ip = user_hash['ip']
28
- @user_agent = user_hash['userAgent'] || user_hash['user_agent']
29
- @country = user_hash['country']
30
- @locale = user_hash['locale']
31
- @app_version = user_hash['appVersion'] || user_hash['app_version']
32
- @custom = user_hash['custom'] if user_hash['custom'].is_a? Hash
33
- @statsig_environment = user_hash['statsigEnvironment']
34
- @private_attributes = user_hash['privateAttributes'] if user_hash['privateAttributes'].is_a? Hash
35
- custom_ids = user_hash['customIDs'] || user_hash['custom_ids']
36
- @custom_ids = custom_ids if custom_ids.is_a? Hash
37
- end
65
+ @user_id = from_hash(user_hash, [:user_id, :userID], String)
66
+ @email = from_hash(user_hash, [:email], String)
67
+ @ip = from_hash(user_hash, [:ip], String)
68
+ @user_agent = from_hash(user_hash, [:user_agent, :userAgent], String)
69
+ @country = from_hash(user_hash, [:country], String)
70
+ @locale = from_hash(user_hash, [:locale], String)
71
+ @app_version = from_hash(user_hash, [:app_version, :appVersion], String)
72
+ @custom = from_hash(user_hash, [:custom], Hash)
73
+ @private_attributes = from_hash(user_hash, [:private_attributes, :privateAttributes], Hash)
74
+ @custom_ids = from_hash(user_hash, [:custom_ids, :customIDs], Hash)
75
+ @statsig_environment = from_hash(user_hash, [:statsig_environment, :statsigEnvironment], Hash)
38
76
  end
39
77
 
40
78
  def serialize(for_logging)
@@ -76,4 +114,29 @@ class StatsigUser
76
114
  'privateAttributes' => @private_attributes,
77
115
  }
78
116
  end
117
+
118
+ private
119
+
120
+ sig {
121
+ params(user_hash: T.any(T::Hash[T.any(String, Symbol), T.untyped], NilClass),
122
+ keys: T::Array[Symbol],
123
+ type: T.untyped)
124
+ .returns(T.untyped)
125
+ }
126
+ # Pulls fields from the user hash via Symbols and Strings
127
+ def from_hash(user_hash, keys, type)
128
+ if user_hash.nil?
129
+ return nil
130
+ end
131
+
132
+ keys.each do |key|
133
+ val = user_hash[key] || user_hash[key.to_s]
134
+ if not val.nil? and val.is_a? type
135
+ return val
136
+ end
137
+ end
138
+
139
+ nil
140
+ end
141
+
79
142
  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.10.0
4
+ version: 1.20.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Statsig, Inc
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-07-07 00:00:00.000000000 Z
11
+ date: 2023-01-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '2.1'
19
+ version: '2.0'
20
20
  type: :development
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: '2.1'
26
+ version: '2.0'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: webmock
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -66,6 +66,34 @@ dependencies:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
68
  version: '1.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: sorbet
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - '='
74
+ - !ruby/object:Gem::Version
75
+ version: 0.5.10461
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - '='
81
+ - !ruby/object:Gem::Version
82
+ version: 0.5.10461
83
+ - !ruby/object:Gem::Dependency
84
+ name: tapioca
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: 0.4.27
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: 0.4.27
69
97
  - !ruby/object:Gem::Dependency
70
98
  name: user_agent_parser
71
99
  requirement: !ruby/object:Gem::Requirement
@@ -102,34 +130,68 @@ dependencies:
102
130
  version: '6.0'
103
131
  - !ruby/object:Gem::Dependency
104
132
  name: ip3country
133
+ requirement: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - '='
136
+ - !ruby/object:Gem::Version
137
+ version: 0.1.1
138
+ type: :runtime
139
+ prerelease: false
140
+ version_requirements: !ruby/object:Gem::Requirement
141
+ requirements:
142
+ - - '='
143
+ - !ruby/object:Gem::Version
144
+ version: 0.1.1
145
+ - !ruby/object:Gem::Dependency
146
+ name: sorbet-runtime
147
+ requirement: !ruby/object:Gem::Requirement
148
+ requirements:
149
+ - - "~>"
150
+ - !ruby/object:Gem::Version
151
+ version: 0.5.10461
152
+ type: :runtime
153
+ prerelease: false
154
+ version_requirements: !ruby/object:Gem::Requirement
155
+ requirements:
156
+ - - "~>"
157
+ - !ruby/object:Gem::Version
158
+ version: 0.5.10461
159
+ - !ruby/object:Gem::Dependency
160
+ name: concurrent-ruby
105
161
  requirement: !ruby/object:Gem::Requirement
106
162
  requirements:
107
163
  - - "~>"
108
164
  - !ruby/object:Gem::Version
109
- version: '0.1'
165
+ version: '1.1'
110
166
  type: :runtime
111
167
  prerelease: false
112
168
  version_requirements: !ruby/object:Gem::Requirement
113
169
  requirements:
114
170
  - - "~>"
115
171
  - !ruby/object:Gem::Version
116
- version: '0.1'
172
+ version: '1.1'
117
173
  description: Statsig server SDK for feature gates and experimentation in Ruby
118
174
  email: support@statsig.com
119
175
  executables: []
120
176
  extensions: []
121
177
  extra_rdoc_files: []
122
178
  files:
179
+ - lib/client_initialize_helpers.rb
123
180
  - lib/config_result.rb
181
+ - lib/diagnostics.rb
124
182
  - lib/dynamic_config.rb
183
+ - lib/error_boundary.rb
184
+ - lib/evaluation_details.rb
125
185
  - lib/evaluation_helpers.rb
126
186
  - lib/evaluator.rb
127
187
  - lib/id_list.rb
188
+ - lib/interfaces/data_store.rb
128
189
  - lib/layer.rb
129
190
  - lib/network.rb
130
191
  - lib/spec_store.rb
131
192
  - lib/statsig.rb
132
193
  - lib/statsig_driver.rb
194
+ - lib/statsig_errors.rb
133
195
  - lib/statsig_event.rb
134
196
  - lib/statsig_logger.rb
135
197
  - lib/statsig_options.rb
@@ -146,14 +208,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
146
208
  requirements:
147
209
  - - ">="
148
210
  - !ruby/object:Gem::Version
149
- version: '0'
211
+ version: 2.5.0
150
212
  required_rubygems_version: !ruby/object:Gem::Requirement
151
213
  requirements:
152
214
  - - ">="
153
215
  - !ruby/object:Gem::Version
154
216
  version: '0'
155
217
  requirements: []
156
- rubygems_version: 3.2.3
218
+ rubygems_version: 3.3.26
157
219
  signing_key:
158
220
  specification_version: 4
159
221
  summary: Statsig server SDK for Ruby