devcycle-ruby-server-sdk 1.2.0 → 2.0.1
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 +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
|