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.
- checksums.yaml +4 -4
- data/Gemfile +6 -0
- data/devcycle-ruby-server-sdk.gemspec +9 -1
- data/lib/devcycle-ruby-server-sdk/api/devcycle_api.rb +128 -21
- data/lib/devcycle-ruby-server-sdk/api_client.rb +1 -1
- data/lib/devcycle-ruby-server-sdk/configuration.rb +5 -5
- data/lib/devcycle-ruby-server-sdk/localbucketing/bucketed_user_config.rb +21 -0
- data/lib/devcycle-ruby-server-sdk/localbucketing/bucketing-lib.release.wasm +0 -0
- data/lib/devcycle-ruby-server-sdk/localbucketing/config_manager.rb +97 -0
- data/lib/devcycle-ruby-server-sdk/localbucketing/dvc_options.rb +119 -0
- data/lib/devcycle-ruby-server-sdk/localbucketing/event_queue.rb +120 -0
- data/lib/devcycle-ruby-server-sdk/localbucketing/event_types.rb +8 -0
- data/lib/devcycle-ruby-server-sdk/localbucketing/events_payload.rb +23 -0
- data/lib/devcycle-ruby-server-sdk/localbucketing/local_bucketing.rb +257 -0
- data/lib/devcycle-ruby-server-sdk/localbucketing/platform_data.rb +29 -0
- data/lib/devcycle-ruby-server-sdk/models/event.rb +6 -6
- data/lib/devcycle-ruby-server-sdk/models/user_data.rb +78 -107
- data/lib/devcycle-ruby-server-sdk/models/variable.rb +0 -13
- data/lib/devcycle-ruby-server-sdk/version.rb +1 -1
- data/lib/devcycle-ruby-server-sdk.rb +9 -0
- data/spec/api/devcycle_api_spec.rb +26 -37
- metadata +77 -25
- data/Gemfile.lock +0 -70
- data/Rakefile +0 -10
- data/docs/DevcycleApi.md +0 -290
- data/docs/ErrorResponse.md +0 -20
- data/docs/Event.md +0 -26
- data/docs/Feature.md +0 -26
- data/docs/UserData.md +0 -48
- data/docs/Variable.md +0 -24
- data/examples/sinatra/Gemfile +0 -8
- data/examples/sinatra/Gemfile.lock +0 -51
- data/examples/sinatra/README.md +0 -14
- data/examples/sinatra/app.rb +0 -48
- data/git_push.sh +0 -57
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fc0fd57c7d492253c2a5aed803845d028057f5075f33ed40b7cd31d6862bd7b2
|
4
|
+
data.tar.gz: 3cf436a16d4ae282192e77687fccaf17d3346b1facb364a26f4ae932ed658140
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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 =
|
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
|
-
|
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
|
20
|
-
@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
|
-
|
40
|
-
|
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
|
-
|
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] ||
|
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
|
-
|
114
|
-
|
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
|
-
|
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] ||
|
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
|
-
|
200
|
-
|
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
|
-
|
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] ||
|
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
|
-
|
279
|
-
|
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
|
-
|
298
|
-
|
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
|
-
|
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 = "
|
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
|
-
|
141
|
-
|
142
|
-
|
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
|
-
|
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
|