devcycle-ruby-server-sdk 1.2.0 → 2.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +6 -0
  3. data/devcycle-ruby-server-sdk.gemspec +9 -1
  4. data/lib/devcycle-ruby-server-sdk/api/devcycle_api.rb +128 -21
  5. data/lib/devcycle-ruby-server-sdk/api_client.rb +1 -1
  6. data/lib/devcycle-ruby-server-sdk/configuration.rb +5 -5
  7. data/lib/devcycle-ruby-server-sdk/localbucketing/bucketed_user_config.rb +21 -0
  8. data/lib/devcycle-ruby-server-sdk/localbucketing/bucketing-lib.release.wasm +0 -0
  9. data/lib/devcycle-ruby-server-sdk/localbucketing/config_manager.rb +97 -0
  10. data/lib/devcycle-ruby-server-sdk/localbucketing/dvc_options.rb +119 -0
  11. data/lib/devcycle-ruby-server-sdk/localbucketing/event_queue.rb +120 -0
  12. data/lib/devcycle-ruby-server-sdk/localbucketing/event_types.rb +8 -0
  13. data/lib/devcycle-ruby-server-sdk/localbucketing/events_payload.rb +23 -0
  14. data/lib/devcycle-ruby-server-sdk/localbucketing/local_bucketing.rb +257 -0
  15. data/lib/devcycle-ruby-server-sdk/localbucketing/platform_data.rb +29 -0
  16. data/lib/devcycle-ruby-server-sdk/models/event.rb +6 -6
  17. data/lib/devcycle-ruby-server-sdk/models/user_data.rb +78 -107
  18. data/lib/devcycle-ruby-server-sdk/models/variable.rb +0 -13
  19. data/lib/devcycle-ruby-server-sdk/version.rb +1 -1
  20. data/lib/devcycle-ruby-server-sdk.rb +9 -0
  21. data/spec/api/devcycle_api_spec.rb +26 -37
  22. metadata +77 -25
  23. data/Gemfile.lock +0 -70
  24. data/Rakefile +0 -10
  25. data/docs/DevcycleApi.md +0 -290
  26. data/docs/ErrorResponse.md +0 -20
  27. data/docs/Event.md +0 -26
  28. data/docs/Feature.md +0 -26
  29. data/docs/UserData.md +0 -48
  30. data/docs/Variable.md +0 -24
  31. data/examples/sinatra/Gemfile +0 -8
  32. data/examples/sinatra/Gemfile.lock +0 -51
  33. data/examples/sinatra/README.md +0 -14
  34. data/examples/sinatra/app.rb +0 -48
  35. data/git_push.sh +0 -57
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b37103165cc47272d718527f01a20fd41e5a4fa851329a1b9ae472d04c561e87
4
- data.tar.gz: dc33b920301e236e1f4829c3c5b2bfa811c4c7649146d59719aefb81111bded1
3
+ metadata.gz: fc0fd57c7d492253c2a5aed803845d028057f5075f33ed40b7cd31d6862bd7b2
4
+ data.tar.gz: 3cf436a16d4ae282192e77687fccaf17d3346b1facb364a26f4ae932ed658140
5
5
  SHA512:
6
- metadata.gz: bd9a35095127bfd7e51a9af0894b7d0244e9d8f494566a85b95b71179c1090f79cf4d757d427a98553aaccbc90bbcb1cd8606f0001b3b09f36dedc3629286588
7
- data.tar.gz: 5ca612fe98bad4539f5b5d87ac1b3512d0eca2fbb285a3482d5675c1b4cfd52643bd94ff3dbebca585cf5f288e8bb08d7bee5ccee869b45d27e44c08bd472116
6
+ metadata.gz: 8e463d4849b50cfd4a346e252909d968f2a41ed31e3bac91de33ad968ecca3afd0ceefb1243cb3326049853894502f8979cd8574fa81bc12cbc9cf66ea34edfb
7
+ data.tar.gz: f9467f2c7ad852e18b9de9bba1a96a73b55febe9ab25d64571e09e19ab9dac394a965d543f77c3aed44a4a1719368c677151d016ca8ba0c0772d2592a78489cf
data/Gemfile CHANGED
@@ -1,8 +1,14 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
3
  gemspec
4
+ gem 'sorbet-runtime'
5
+ gem 'oj'
6
+ gem 'wasmtime'
7
+ gem 'concurrent-ruby'
4
8
 
5
9
  group :development, :test do
10
+ gem 'sorbet'
11
+ gem 'tapioca', require: false
6
12
  gem 'rake', '~> 13.0.1'
7
13
  gem 'pry-byebug'
8
14
  gem 'rubocop', '~> 0.66.0'
@@ -24,10 +24,18 @@ Gem::Specification.new do |s|
24
24
  s.required_ruby_version = ">= 2.4"
25
25
 
26
26
  s.add_runtime_dependency 'typhoeus', '~> 1.0', '>= 1.0.1'
27
+ s.add_runtime_dependency 'wasmtime', '5.0.0'
28
+ s.add_runtime_dependency 'concurrent-ruby', '1.2.0'
29
+ s.add_runtime_dependency 'sorbet-runtime', '0.5.10648'
30
+ s.add_runtime_dependency 'oj', '3.13.2'
31
+
27
32
 
28
33
  s.add_development_dependency 'rspec', '~> 3.6', '>= 3.6.0'
29
34
 
30
- s.files = `find *`.split("\n").uniq.sort.select { |f| !f.empty? }
35
+ s.files = Dir['README.md', 'LICENSE',
36
+ 'lib/**/*',
37
+ 'devcycle-ruby-server-sdk.gemspec',
38
+ 'Gemfile']
31
39
  s.test_files = `find spec/*`.split("\n")
32
40
  s.executables = []
33
41
  s.require_paths = ["lib"]
@@ -11,13 +11,58 @@ OpenAPI Generator version: 5.3.0
11
11
  =end
12
12
 
13
13
  require 'cgi'
14
+ require 'logger'
14
15
 
15
16
  module DevCycle
16
17
  class DVCClient
17
- attr_accessor :api_client
18
+ def initialize(sdkKey, dvc_options = DVCOptions.new, wait_for_init = false)
19
+ if sdkKey.nil?
20
+ raise ArgumentError('Missing SDK key!')
21
+ elsif !sdkKey.start_with?('server') && !sdkKey.start_with?('dvc_server')
22
+ raise ArgumentError('Invalid SDK key!')
23
+ end
24
+
25
+ @sdkKey = sdkKey
26
+ @dvc_options = dvc_options
27
+ @logger = dvc_options.logger
28
+
29
+ if @dvc_options.enable_cloud_bucketing
30
+ @api_client = ApiClient.default
31
+ @api_client.config.api_key['bearerAuth'] = @sdkKey
32
+ @api_client.config.enable_edge_db = @dvc_options.enable_edge_db
33
+ @api_client.config.logger = @logger
34
+ else
35
+ @localbucketing = LocalBucketing.new(@sdkKey, dvc_options, wait_for_init)
36
+ @event_queue = EventQueue.new(@sdkKey, dvc_options.event_queue_options, @localbucketing)
37
+ end
38
+ end
39
+
40
+ def close
41
+ if @dvc_options.enable_cloud_bucketing
42
+ @logger.info("Cloud Bucketing does not require closing.")
43
+ return
44
+ end
45
+ if @localbucketing != nil
46
+ if !@localbucketing.initialized
47
+ @logger.info("Awaiting client initialization before closing")
48
+ while !@localbucketing.initialized
49
+ sleep(0.5)
50
+ end
51
+ end
52
+ @localbucketing.close
53
+ @localbucketing = nil
54
+ @logger.info("Closed DevCycle Local Bucketing Engine.")
55
+ end
56
+
57
+ @event_queue.close
58
+ @logger.info("Closed DevCycle Client.")
59
+ end
18
60
 
19
- def initialize(api_client = ApiClient.default)
20
- @api_client = api_client
61
+ def set_client_custom_data(customdata)
62
+ if @api_client.config.enable_cloud_bucketing
63
+ fail ArgumentError("Client Custom Data is only available in Local bucketing mode.")
64
+ end
65
+ @localbucketing.set_client_custom_data(customdata)
21
66
  end
22
67
 
23
68
  def validate_model(model)
@@ -36,8 +81,17 @@ module DevCycle
36
81
 
37
82
  validate_model(user_data)
38
83
 
39
- data, _status_code, _headers = all_features_with_http_info(user_data, opts)
40
- data
84
+ if @dvc_options.enable_cloud_bucketing
85
+ data, _status_code, _headers = all_features_with_http_info(user_data, opts)
86
+ return data
87
+ end
88
+
89
+ if local_bucketing_initialized?
90
+ bucketed_config = @localbucketing.generate_bucketed_config(user_data)
91
+ bucketed_config.features
92
+ else
93
+ {}
94
+ end
41
95
  end
42
96
 
43
97
  # Get all features by key for user data
@@ -65,14 +119,14 @@ module DevCycle
65
119
  # HTTP header 'Content-Type'
66
120
  content_type = @api_client.select_header_content_type(['application/json'])
67
121
  if !content_type.nil?
68
- header_params['Content-Type'] = content_type
122
+ header_params['Content-Type'] = content_type
69
123
  end
70
124
 
71
125
  # form parameters
72
126
  form_params = opts[:form_params] || {}
73
127
 
74
128
  # http body (model)
75
- post_body = opts[:debug_body] || @api_client.object_to_http_body(user_data)
129
+ post_body = opts[:debug_body] || user_data.to_json
76
130
 
77
131
  # return_type
78
132
  return_type = opts[:debug_return_type] || 'Hash<String, Feature>'
@@ -110,8 +164,36 @@ module DevCycle
110
164
 
111
165
  validate_model(user_data)
112
166
 
113
- data, _status_code, _headers = variable_with_http_info(key, user_data, default, opts)
114
- data
167
+ if @dvc_options.enable_cloud_bucketing
168
+ data, _status_code, _headers = variable_with_http_info(key, user_data, default, opts)
169
+ return data
170
+ end
171
+
172
+ if local_bucketing_initialized?
173
+ bucketed_config = @localbucketing.generate_bucketed_config(user_data)
174
+ variable_json = bucketed_config.variables[key]
175
+ if variable_json == nil
176
+ variable_event = Event.new({ type: DevCycle::EventTypes[:agg_variable_defaulted], target: key })
177
+ @event_queue.queue_aggregate_event(variable_event, bucketed_config)
178
+
179
+ return Variable.new({ key: key, value: default, isDefaulted: true })
180
+ end
181
+
182
+ variable_event = Event.new({ type: DevCycle::EventTypes[:agg_variable_evaluated], target: key })
183
+ @event_queue.queue_aggregate_event(variable_event, bucketed_config)
184
+
185
+ Variable.new({
186
+ key: key,
187
+ type: variable_json['type'],
188
+ value: variable_json['value'],
189
+ isDefaulted: false
190
+ })
191
+ else
192
+ variable_event = Event.new({ type: DevCycle::EventTypes[:agg_variable_defaulted], target: key })
193
+ @event_queue.queue_aggregate_event(variable_event, bucketed_config)
194
+
195
+ Variable.new({ key: key, value: default, isDefaulted: true })
196
+ end
115
197
  end
116
198
 
117
199
  # Get variable by key for user data
@@ -145,14 +227,14 @@ module DevCycle
145
227
  # HTTP header 'Content-Type'
146
228
  content_type = @api_client.select_header_content_type(['application/json'])
147
229
  if !content_type.nil?
148
- header_params['Content-Type'] = content_type
230
+ header_params['Content-Type'] = content_type
149
231
  end
150
232
 
151
233
  # form parameters
152
234
  form_params = opts[:form_params] || {}
153
235
 
154
236
  # http body (model)
155
- post_body = opts[:debug_body] || @api_client.object_to_http_body(user_data)
237
+ post_body = opts[:debug_body] || user_data.to_json
156
238
 
157
239
  # return_type
158
240
  return_type = opts[:debug_return_type] || 'Variable'
@@ -196,8 +278,17 @@ module DevCycle
196
278
 
197
279
  validate_model(user_data)
198
280
 
199
- data, _status_code, _headers = all_variables_with_http_info(user_data, opts)
200
- data
281
+ if @dvc_options.enable_cloud_bucketing
282
+ data, _status_code, _headers = all_variables_with_http_info(user_data, opts)
283
+ return data
284
+ end
285
+
286
+ if local_bucketing_initialized?
287
+ bucketed_config = @localbucketing.generate_bucketed_config(user_data)
288
+ bucketed_config.variables
289
+ else
290
+ {}
291
+ end
201
292
  end
202
293
 
203
294
  # Get all variables by key for user data
@@ -225,14 +316,14 @@ module DevCycle
225
316
  # HTTP header 'Content-Type'
226
317
  content_type = @api_client.select_header_content_type(['application/json'])
227
318
  if !content_type.nil?
228
- header_params['Content-Type'] = content_type
319
+ header_params['Content-Type'] = content_type
229
320
  end
230
321
 
231
322
  # form parameters
232
323
  form_params = opts[:form_params] || {}
233
324
 
234
325
  # http body (model)
235
- post_body = opts[:debug_body] || @api_client.object_to_http_body(user_data)
326
+ post_body = opts[:debug_body] || user_data.to_json
236
327
 
237
328
  # return_type
238
329
  return_type = opts[:debug_return_type] || 'Hash<String, Variable>'
@@ -275,8 +366,16 @@ module DevCycle
275
366
 
276
367
  validate_model(event_data)
277
368
 
278
- data, _status_code, _headers = track_with_http_info(user_data, event_data, opts)
279
- data
369
+ if @dvc_options.enable_cloud_bucketing
370
+ track_with_http_info(user_data, event_data, opts)
371
+ return
372
+ end
373
+
374
+ if local_bucketing_initialized?
375
+ @event_queue.queue_event(user_data, event_data)
376
+ else
377
+ @logger.warn('track called before DVCClient initialized, event will not be tracked')
378
+ end
280
379
  end
281
380
 
282
381
  # Post events to DevCycle for user
@@ -294,9 +393,9 @@ module DevCycle
294
393
  end
295
394
 
296
395
  user_data_and_events_body = DevCycle::UserDataAndEventsBody.new({
297
- user: user_data,
298
- events: [event_data]
299
- })
396
+ user: user_data,
397
+ events: [event_data]
398
+ })
300
399
 
301
400
  # resource path
302
401
  local_var_path = '/v1/track'
@@ -311,7 +410,7 @@ module DevCycle
311
410
  # HTTP header 'Content-Type'
312
411
  content_type = @api_client.select_header_content_type(['application/json'])
313
412
  if !content_type.nil?
314
- header_params['Content-Type'] = content_type
413
+ header_params['Content-Type'] = content_type
315
414
  end
316
415
 
317
416
  # form parameters
@@ -345,5 +444,13 @@ module DevCycle
345
444
  end
346
445
  return data, status_code, headers
347
446
  end
447
+
448
+ def flush_events
449
+ @event_queue.flush_events
450
+ end
451
+
452
+ def local_bucketing_initialized?
453
+ !@localbucketing.nil? && @localbucketing.initialized
454
+ end
348
455
  end
349
456
  end
@@ -31,7 +31,7 @@ module DevCycle
31
31
  # @option config [Configuration] Configuration for initializing the object, default to Configuration.default
32
32
  def initialize(config = Configuration.default)
33
33
  @config = config
34
- @user_agent = "OpenAPI-Generator/#{VERSION}/ruby"
34
+ @user_agent = "DevCycle-Ruby:#{VERSION}"
35
35
  @default_headers = {
36
36
  'Content-Type' => 'application/json',
37
37
  'User-Agent' => @user_agent
@@ -137,9 +137,9 @@ module DevCycle
137
137
 
138
138
  attr_accessor :force_ending_format
139
139
 
140
- # Define if EdgeDB is Enabled (Boolean)
141
- # Default to false
142
- attr_accessor :enable_edge_db
140
+ # Define if EdgeDB is Enabled (Boolean)
141
+ # Default to false
142
+ attr_accessor :enable_edge_db
143
143
 
144
144
  def initialize
145
145
  @scheme = 'https'
@@ -169,7 +169,7 @@ module DevCycle
169
169
 
170
170
  # The default Configuration object.
171
171
  def self.default
172
- @@default ||= Configuration.new
172
+ @default ||= Configuration.new
173
173
  end
174
174
 
175
175
  def configure
@@ -280,7 +280,7 @@ module DevCycle
280
280
  end
281
281
 
282
282
  def enable_edge_db=(enable_edge_db = false)
283
- if(enable_edge_db)
283
+ if (enable_edge_db)
284
284
  @enable_edge_db = true
285
285
  end
286
286
  end
@@ -0,0 +1,21 @@
1
+ module DevCycle
2
+ class BucketedUserConfig
3
+ attr_accessor :project
4
+ attr_accessor :environment
5
+ attr_accessor :features
6
+ attr_accessor :feature_variation_map
7
+ attr_accessor :variable_variation_map
8
+ attr_accessor :variables
9
+ attr_accessor :known_variable_keys
10
+
11
+ def initialize(project, environment, features, feature_var_map, variable_var_map, variables, known_variable_keys)
12
+ @project = project
13
+ @environment = environment
14
+ @features = features
15
+ @feature_variation_map = feature_var_map
16
+ @variable_variation_map = variable_var_map
17
+ @variables = variables
18
+ @known_variable_keys = known_variable_keys
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'sorbet-runtime'
4
+ require 'concurrent-ruby'
5
+ require 'typhoeus'
6
+ require 'json'
7
+
8
+ module DevCycle
9
+ class ConfigManager
10
+ extend T::Sig
11
+ sig { params(
12
+ sdkKey: String,
13
+ local_bucketing: LocalBucketing,
14
+ wait_for_init: T::Boolean
15
+ ).void }
16
+ def initialize(sdkKey, local_bucketing, wait_for_init)
17
+ @first_load = true
18
+ @config_version = "v1"
19
+ @local_bucketing = local_bucketing
20
+ @sdkKey = sdkKey
21
+ @config_e_tag = ""
22
+ @logger = local_bucketing.options.logger
23
+
24
+ @config_poller = Concurrent::TimerTask.new(
25
+ {
26
+ execution_interval: @local_bucketing.options.config_polling_interval_ms.fdiv(1000),
27
+ run_now: true
28
+ }) do |task|
29
+ fetch_config(false, task)
30
+ end
31
+
32
+ t = Thread.new { fetch_config(false, nil) }
33
+ t.join if wait_for_init
34
+ end
35
+
36
+ def fetch_config(retrying, task)
37
+ req = Typhoeus::Request.new(
38
+ get_config_url,
39
+ headers: {
40
+ Accept: "application/json",
41
+ })
42
+
43
+ if @config_e_tag != ""
44
+ req.options[:headers]['If-None-Match'] = @config_e_tag
45
+ end
46
+
47
+ resp = req.run
48
+
49
+ case resp.code
50
+ when 304
51
+ return nil
52
+ when 200
53
+ return set_config(resp.body, resp.headers['Etag'])
54
+ when 403
55
+ raise("Failed to download DevCycle config; Invalid SDK Key.")
56
+ when 500...599
57
+ if !retrying
58
+ return fetch_config(true, task)
59
+ end
60
+ @logger.warn("Failed to download DevCycle config. Status: #{resp.code}")
61
+ else
62
+ if task != nil
63
+ task.shutdown
64
+ end
65
+ raise("Unexpected response code - DevCycle Response: #{Oj.dump(resp)}")
66
+ end
67
+
68
+ nil
69
+ end
70
+
71
+ def set_config(config, etag)
72
+ if !JSON.parse(config).is_a?(Hash)
73
+ raise("Invalid JSON body parsed from Config Response")
74
+ end
75
+
76
+ @local_bucketing.store_config(@sdkKey, config)
77
+ @config_e_tag = etag
78
+
79
+ if @first_load
80
+ @logger.info("Config Set. Client Initialized.")
81
+ @first_load = false
82
+ @local_bucketing.initialized = true
83
+ @config_poller.execute
84
+ end
85
+ end
86
+
87
+ def get_config_url
88
+ configBasePath = @local_bucketing.options.config_cdn_uri
89
+ "#{configBasePath}/config/#{@config_version}/server/#{@sdkKey}.json"
90
+ end
91
+
92
+ def close
93
+ @config_poller.shutdown
94
+ nil
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,119 @@
1
+ require 'oj'
2
+
3
+ module DevCycle
4
+ class DVCOptions
5
+ attr_reader :config_polling_interval_ms
6
+ attr_reader :enable_edge_db
7
+ attr_reader :enable_cloud_bucketing
8
+ attr_reader :config_cdn_uri
9
+ attr_reader :events_api_uri
10
+ attr_reader :bucketing_api_uri
11
+ attr_reader :logger
12
+
13
+ def initialize(
14
+ enable_cloud_bucketing: false,
15
+ event_flush_interval_ms: 10 * 1000,
16
+ disable_custom_event_logging: false,
17
+ disable_automatic_event_logging: false,
18
+ config_polling_interval_ms: 10000,
19
+ request_timeout_ms: 5000,
20
+ max_event_queue_size: 2000,
21
+ flush_event_queue_size: 1000,
22
+ event_request_chunk_size: 100,
23
+ logger: nil,
24
+ events_api_uri: 'https://events.devcycle.com',
25
+ enable_edge_db: false
26
+ )
27
+ @logger = logger || defined?(Rails) ? Rails.logger : Logger.new(STDOUT)
28
+ @enable_cloud_bucketing = enable_cloud_bucketing
29
+
30
+ @enable_edge_db = enable_edge_db
31
+
32
+ if !@enable_cloud_bucketing && @enable_edge_db
33
+ raise ArgumentError.new('Cannot enable edgedb without enabling cloud bucketing.')
34
+ end
35
+
36
+ if config_polling_interval_ms < 1000
37
+ @logger.warn('config_polling_interval cannot be less than 1000ms, defaulting to 10000ms')
38
+ config_polling_interval_ms = 10000
39
+ end
40
+ @config_polling_interval_ms = config_polling_interval_ms
41
+
42
+ if request_timeout_ms <= 5000
43
+ request_timeout_ms = 5000
44
+ end
45
+ @request_timeout_ms = request_timeout_ms
46
+
47
+
48
+ if event_flush_interval_ms < 500 || event_flush_interval_ms > (60 * 1000)
49
+ raise ArgumentError.new('event_flush_interval_ms must be between 500ms and 1 minute')
50
+ end
51
+ @event_flush_interval_ms = event_flush_interval_ms
52
+
53
+ if flush_event_queue_size >= max_event_queue_size
54
+ raise ArgumentError.new("flush_event_queue_size: #{flush_event_queue_size} must be " +
55
+ "smaller than max_event_queue_size: #{@max_event_queue_size}")
56
+ elsif flush_event_queue_size < event_request_chunk_size || max_event_queue_size < event_request_chunk_size
57
+ throw ArgumentError.new("flush_event_queue_size: #{flush_event_queue_size} and " +
58
+ "max_event_queue_size: #{max_event_queue_size} " +
59
+ "must be larger than event_request_chunk_size: #{event_request_chunk_size}")
60
+ elsif flush_event_queue_size > 20000 || max_event_queue_size > 20000
61
+ raise ArgumentError.new("flush_event_queue_size: #{flush_event_queue_size} or " +
62
+ "max_event_queue_size: #{max_event_queue_size} must be smaller than 20,000")
63
+ end
64
+ @flush_event_queue_size = flush_event_queue_size
65
+ @max_event_queue_size = max_event_queue_size
66
+ @event_request_chunk_size = event_request_chunk_size
67
+
68
+ @disable_custom_event_logging = disable_custom_event_logging
69
+ @disable_automatic_event_logging = disable_automatic_event_logging
70
+ @config_cdn_uri = "https://config-cdn.devcycle.com"
71
+ @events_api_uri = events_api_uri
72
+ @bucketing_api_uri = "https://bucketing-api.devcyle.com"
73
+ end
74
+
75
+ def event_queue_options
76
+ EventQueueOptions.new(
77
+ @event_flush_interval_ms,
78
+ @disable_automatic_event_logging,
79
+ @disable_custom_event_logging,
80
+ @max_event_queue_size,
81
+ @flush_event_queue_size,
82
+ @events_api_uri,
83
+ @event_request_chunk_size,
84
+ @logger
85
+ )
86
+ end
87
+ end
88
+
89
+ class EventQueueOptions
90
+ attr_reader :event_flush_interval_ms
91
+ attr_reader :disable_automatic_event_logging
92
+ attr_reader :disable_custom_event_logging
93
+ attr_reader :max_event_queue_size
94
+ attr_reader :flush_event_queue_size
95
+ attr_reader :events_api_uri
96
+ attr_reader :event_request_chunk_size
97
+ attr_reader :logger
98
+
99
+ def initialize (
100
+ event_flush_interval_ms,
101
+ disable_automatic_event_logging,
102
+ disable_custom_event_logging,
103
+ max_event_queue_size,
104
+ flush_event_queue_size,
105
+ events_api_uri,
106
+ event_request_chunk_size,
107
+ logger
108
+ )
109
+ @event_flush_interval_ms = event_flush_interval_ms
110
+ @disable_automatic_event_logging = disable_automatic_event_logging
111
+ @disable_custom_event_logging = disable_custom_event_logging
112
+ @max_event_queue_size = max_event_queue_size
113
+ @flush_event_queue_size = flush_event_queue_size
114
+ @events_api_uri = events_api_uri
115
+ @event_request_chunk_size = event_request_chunk_size
116
+ @logger = logger
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,120 @@
1
+ require 'typhoeus'
2
+ require 'sorbet-runtime'
3
+ require 'concurrent-ruby'
4
+
5
+ module DevCycle
6
+ class EventQueue
7
+ extend T::Sig
8
+
9
+ sig { params(sdkKey: String, options: EventQueueOptions, local_bucketing: LocalBucketing).void }
10
+ def initialize(sdkKey, options, local_bucketing)
11
+ @sdkKey = sdkKey
12
+ @events_api_uri = options.events_api_uri
13
+ @logger = options.logger
14
+ @event_flush_interval_ms = options.event_flush_interval_ms
15
+ @flush_event_queue_size = options.flush_event_queue_size
16
+ @max_event_queue_size = options.max_event_queue_size
17
+
18
+ @flush_timer_task = Concurrent::TimerTask.new(
19
+ execution_interval: @event_flush_interval_ms.fdiv(1000),
20
+ run_now: true
21
+ ) {
22
+ flush_events
23
+ }
24
+ @flush_timer_task.execute
25
+ @flush_timer_task.add_observer(FlushTimerTaskObserver.new)
26
+
27
+ @local_bucketing = local_bucketing
28
+ @local_bucketing.init_event_queue(options)
29
+
30
+ @flush_mutex = Mutex.new
31
+ nil
32
+ end
33
+
34
+ def close
35
+ @flush_timer_task.shutdown
36
+ flush_events
37
+ end
38
+
39
+ sig { returns(NilClass) }
40
+ def flush_events
41
+ @flush_mutex.synchronize do
42
+ payloads = @local_bucketing.flush_event_queue
43
+ if payloads.length == 0
44
+ return
45
+ end
46
+ eventCount = payloads.reduce(0) { |sum, payload| sum + payload.eventCount }
47
+ @logger.debug("DVC: Flushing #{eventCount} event(s) for #{payloads.length} user(s)")
48
+
49
+ payloads.each do |payload|
50
+ begin
51
+ response = Typhoeus.post(
52
+ @events_api_uri + '/v1/events/batch',
53
+ headers: { 'Authorization': @sdkKey },
54
+ body: { 'batch': payload.records }.to_json
55
+ )
56
+ if response.code != 201
57
+ @logger.error("Error publishing events, status: #{response.code}, body: #{response.return_message}")
58
+ @local_bucketing.on_payload_failure(payload.payloadId, response.code >= 500)
59
+ else
60
+ @logger.debug("DVC: Flushed #{eventCount} event(s), for #{payload.records.length} user(s)")
61
+ @local_bucketing.on_payload_success(payload.payloadId)
62
+ end
63
+ rescue => e
64
+ @logger.error("DVC Error Flushing Events response message: #{e.message}")
65
+ @local_bucketing.on_payload_failure(payload.payloadId, false)
66
+ end
67
+ end
68
+ end
69
+ nil
70
+ end
71
+
72
+ # Todo: implement PopulatedUser
73
+ sig { params(user: UserData, event: Event).returns(NilClass) }
74
+ def queue_event(user, event)
75
+ if max_event_queue_size_reached?
76
+ @logger.warn("Max event queue size reached, dropping event: #{event}")
77
+ return
78
+ end
79
+
80
+ @local_bucketing.queue_event(user, event)
81
+ nil
82
+ end
83
+
84
+ sig { params(event: Event, bucketed_config: T.nilable(BucketedUserConfig)).returns(NilClass) }
85
+ def queue_aggregate_event(event, bucketed_config)
86
+ if max_event_queue_size_reached?
87
+ @logger.warn("Max event queue size reached, dropping event: #{event}")
88
+ return
89
+ end
90
+
91
+ @local_bucketing.queue_aggregate_event(event, bucketed_config)
92
+ nil
93
+ end
94
+
95
+ sig { returns(T::Boolean) }
96
+ def max_event_queue_size_reached?
97
+ queue_size = @local_bucketing.check_event_queue_size()
98
+ if queue_size >= @flush_event_queue_size
99
+ flush_events
100
+ if queue_size >= @max_event_queue_size
101
+ return true
102
+ end
103
+ end
104
+ false
105
+ end
106
+ end
107
+
108
+ # Todo: remove when done testing
109
+ class FlushTimerTaskObserver
110
+ def update(time, result, ex)
111
+ return if ex.nil?
112
+
113
+ if ex.is_a?(Concurrent::TimeoutError)
114
+ print("DVC FlushTimerTaskObserver: Execution timed out")
115
+ else
116
+ print("DVC FlushTimerTaskObserver: Execution failed with error: #{ex}")
117
+ end
118
+ end
119
+ end
120
+ end