optimizely-sdk 3.3.2 → 3.4.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.
- checksums.yaml +5 -5
- data/lib/optimizely.rb +53 -1
- data/lib/optimizely/config_manager/http_project_config_manager.rb +5 -2
- data/lib/optimizely/config_manager/static_project_config_manager.rb +6 -2
- data/lib/optimizely/event/batch_event_processor.rb +47 -39
- data/lib/optimizely/optimizely_config.rb +116 -0
- data/lib/optimizely/version.rb +1 -1
- metadata +4 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 00fc9a6599ce3b83c1696f788f5f4ad5e627c0063da459eb11353a7ae2872033
|
4
|
+
data.tar.gz: 36a84eb5593ce921103a7c1829e4c0677d95f82a287f37347ec8de3484fad082
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 26f809dd552d54670e9d2ffc00bacbbb91ba509b7e5ce98a2cc720dacf495e5fb232f50286d0373f18144bac8ed2c5a540a3435423b03cea347b2eb449dbd053
|
7
|
+
data.tar.gz: 67a123c84a058e5d398b39a3349266d9a2e484022c313ae63afc4f1ca97988a99e0285a9a4d6b42beb5df5c514e66cc4b54663f258bd602a80afbe8c28b175a2
|
data/lib/optimizely.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
#
|
4
|
-
# Copyright 2016-
|
4
|
+
# Copyright 2016-2020, Optimizely and contributors
|
5
5
|
#
|
6
6
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
7
7
|
# you may not use this file except in compliance with the License.
|
@@ -33,6 +33,7 @@ require_relative 'optimizely/helpers/validator'
|
|
33
33
|
require_relative 'optimizely/helpers/variable_type'
|
34
34
|
require_relative 'optimizely/logger'
|
35
35
|
require_relative 'optimizely/notification_center'
|
36
|
+
require_relative 'optimizely/optimizely_config'
|
36
37
|
|
37
38
|
module Optimizely
|
38
39
|
class Project
|
@@ -523,6 +524,57 @@ module Optimizely
|
|
523
524
|
@event_processor.stop! if @event_processor.respond_to?(:stop!)
|
524
525
|
end
|
525
526
|
|
527
|
+
def get_optimizely_config
|
528
|
+
# Get OptimizelyConfig object containing experiments and features data
|
529
|
+
# Returns Object
|
530
|
+
#
|
531
|
+
# OptimizelyConfig Object Schema
|
532
|
+
# {
|
533
|
+
# 'experimentsMap' => {
|
534
|
+
# 'my-fist-experiment' => {
|
535
|
+
# 'id' => '111111',
|
536
|
+
# 'key' => 'my-fist-experiment'
|
537
|
+
# 'variationsMap' => {
|
538
|
+
# 'variation_1' => {
|
539
|
+
# 'id' => '121212',
|
540
|
+
# 'key' => 'variation_1',
|
541
|
+
# 'variablesMap' => {
|
542
|
+
# 'age' => {
|
543
|
+
# 'id' => '222222',
|
544
|
+
# 'key' => 'age',
|
545
|
+
# 'type' => 'integer',
|
546
|
+
# 'value' => '0',
|
547
|
+
# }
|
548
|
+
# }
|
549
|
+
# }
|
550
|
+
# }
|
551
|
+
# }
|
552
|
+
# },
|
553
|
+
# 'featuresMap' => {
|
554
|
+
# 'awesome-feature' => {
|
555
|
+
# 'id' => '333333',
|
556
|
+
# 'key' => 'awesome-feature',
|
557
|
+
# 'experimentsMap' => Object,
|
558
|
+
# 'variablesMap' => Object,
|
559
|
+
# }
|
560
|
+
# },
|
561
|
+
# 'revision' => '13',
|
562
|
+
# }
|
563
|
+
#
|
564
|
+
unless is_valid
|
565
|
+
@logger.log(Logger::ERROR, InvalidProjectConfigError.new('get_optimizely_config').message)
|
566
|
+
return nil
|
567
|
+
end
|
568
|
+
|
569
|
+
# config_manager might not contain optimizely_config if its supplied by the consumer
|
570
|
+
# Generating a new OptimizelyConfig object in this case as a fallback
|
571
|
+
if @config_manager.respond_to?(:optimizely_config)
|
572
|
+
@config_manager.optimizely_config
|
573
|
+
else
|
574
|
+
OptimizelyConfig.new(project_config).config
|
575
|
+
end
|
576
|
+
end
|
577
|
+
|
526
578
|
private
|
527
579
|
|
528
580
|
def get_variation_with_config(experiment_key, user_id, attributes, config)
|
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
#
|
4
|
-
# Copyright 2019, Optimizely and contributors
|
4
|
+
# Copyright 2019-2020, Optimizely and contributors
|
5
5
|
#
|
6
6
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
7
7
|
# you may not use this file except in compliance with the License.
|
@@ -22,6 +22,7 @@ require_relative '../helpers/constants'
|
|
22
22
|
require_relative '../logger'
|
23
23
|
require_relative '../notification_center'
|
24
24
|
require_relative '../project_config'
|
25
|
+
require_relative '../optimizely_config'
|
25
26
|
require_relative 'project_config_manager'
|
26
27
|
require_relative 'async_scheduler'
|
27
28
|
require 'httparty'
|
@@ -30,7 +31,7 @@ module Optimizely
|
|
30
31
|
class HTTPProjectConfigManager < ProjectConfigManager
|
31
32
|
# Config manager that polls for the datafile and updated ProjectConfig based on an update interval.
|
32
33
|
|
33
|
-
attr_reader :stopped
|
34
|
+
attr_reader :stopped, :optimizely_config
|
34
35
|
|
35
36
|
# Initialize config manager. One of sdk_key or url has to be set to be able to use.
|
36
37
|
#
|
@@ -73,6 +74,7 @@ module Optimizely
|
|
73
74
|
@skip_json_validation = skip_json_validation
|
74
75
|
@notification_center = notification_center.is_a?(Optimizely::NotificationCenter) ? notification_center : NotificationCenter.new(@logger, @error_handler)
|
75
76
|
@config = datafile.nil? ? nil : DatafileProjectConfig.create(datafile, @logger, @error_handler, @skip_json_validation)
|
77
|
+
@optimizely_config = @config.nil? ? nil : OptimizelyConfig.new(@config).config
|
76
78
|
@mutex = Mutex.new
|
77
79
|
@resource = ConditionVariable.new
|
78
80
|
@async_scheduler = AsyncScheduler.new(method(:fetch_datafile_config), @polling_interval, auto_update, @logger)
|
@@ -192,6 +194,7 @@ module Optimizely
|
|
192
194
|
end
|
193
195
|
|
194
196
|
@config = config
|
197
|
+
@optimizely_config = OptimizelyConfig.new(config).config
|
195
198
|
|
196
199
|
@notification_center.send_notifications(NotificationCenter::NOTIFICATION_TYPES[:OPTIMIZELY_CONFIG_UPDATE])
|
197
200
|
|
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
#
|
4
|
-
# Copyright 2019, Optimizely and contributors
|
4
|
+
# Copyright 2019-2020, Optimizely and contributors
|
5
5
|
#
|
6
6
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
7
7
|
# you may not use this file except in compliance with the License.
|
@@ -17,11 +17,13 @@
|
|
17
17
|
#
|
18
18
|
|
19
19
|
require_relative '../config/datafile_project_config'
|
20
|
+
require_relative '../optimizely_config'
|
20
21
|
require_relative 'project_config_manager'
|
22
|
+
|
21
23
|
module Optimizely
|
22
24
|
class StaticProjectConfigManager < ProjectConfigManager
|
23
25
|
# Implementation of ProjectConfigManager interface.
|
24
|
-
attr_reader :config
|
26
|
+
attr_reader :config, :optimizely_config
|
25
27
|
|
26
28
|
def initialize(datafile, logger, error_handler, skip_json_validation)
|
27
29
|
# Looks up and sets datafile and config based on response body.
|
@@ -38,6 +40,8 @@ module Optimizely
|
|
38
40
|
error_handler,
|
39
41
|
skip_json_validation
|
40
42
|
)
|
43
|
+
|
44
|
+
@optimizely_config = @config.nil? ? nil : OptimizelyConfig.new(@config).config
|
41
45
|
end
|
42
46
|
end
|
43
47
|
end
|
@@ -31,7 +31,6 @@ module Optimizely
|
|
31
31
|
DEFAULT_BATCH_INTERVAL = 30_000 # interval in milliseconds
|
32
32
|
DEFAULT_QUEUE_CAPACITY = 1000
|
33
33
|
DEFAULT_TIMEOUT_INTERVAL = 5 # interval in seconds
|
34
|
-
MAX_NIL_COUNT = 3
|
35
34
|
|
36
35
|
FLUSH_SIGNAL = 'FLUSH_SIGNAL'
|
37
36
|
SHUTDOWN_SIGNAL = 'SHUTDOWN_SIGNAL'
|
@@ -62,7 +61,7 @@ module Optimizely
|
|
62
61
|
@notification_center = notification_center
|
63
62
|
@current_batch = []
|
64
63
|
@started = false
|
65
|
-
|
64
|
+
@stopped = false
|
66
65
|
end
|
67
66
|
|
68
67
|
def start!
|
@@ -72,26 +71,37 @@ module Optimizely
|
|
72
71
|
end
|
73
72
|
@flushing_interval_deadline = Helpers::DateTimeUtils.create_timestamp + @flush_interval
|
74
73
|
@logger.log(Logger::INFO, 'Starting scheduler.')
|
75
|
-
@
|
74
|
+
if @wait_mutex.nil?
|
75
|
+
@wait_mutex = Mutex.new
|
76
|
+
@resource = ConditionVariable.new
|
77
|
+
end
|
78
|
+
@thread = Thread.new { run_queue }
|
76
79
|
@started = true
|
80
|
+
@stopped = false
|
77
81
|
end
|
78
82
|
|
79
83
|
def flush
|
80
84
|
@event_queue << FLUSH_SIGNAL
|
85
|
+
@wait_mutex.synchronize { @resource.signal }
|
81
86
|
end
|
82
87
|
|
83
88
|
def process(user_event)
|
84
89
|
@logger.log(Logger::DEBUG, "Received userEvent: #{user_event}")
|
85
90
|
|
86
|
-
if
|
91
|
+
# if the processor has been explicitly stopped. Don't accept tasks
|
92
|
+
if @stopped
|
87
93
|
@logger.log(Logger::WARN, 'Executor shutdown, not accepting tasks.')
|
88
94
|
return
|
89
95
|
end
|
90
96
|
|
97
|
+
# start if the processor hasn't been started
|
98
|
+
start! unless @started
|
99
|
+
|
91
100
|
begin
|
92
101
|
@event_queue.push(user_event, true)
|
93
|
-
|
94
|
-
|
102
|
+
@wait_mutex.synchronize { @resource.signal }
|
103
|
+
rescue => e
|
104
|
+
@logger.log(Logger::WARN, 'Payload not accepted by the queue: ' + e.message)
|
95
105
|
return
|
96
106
|
end
|
97
107
|
end
|
@@ -101,42 +111,20 @@ module Optimizely
|
|
101
111
|
|
102
112
|
@logger.log(Logger::INFO, 'Stopping scheduler.')
|
103
113
|
@event_queue << SHUTDOWN_SIGNAL
|
114
|
+
@wait_mutex.synchronize { @resource.signal }
|
104
115
|
@thread.join(DEFAULT_TIMEOUT_INTERVAL)
|
105
116
|
@started = false
|
117
|
+
@stopped = true
|
106
118
|
end
|
107
119
|
|
108
120
|
private
|
109
121
|
|
110
|
-
def
|
111
|
-
|
112
|
-
|
113
|
-
@nil_count = 0
|
114
|
-
# hang on pop if true
|
115
|
-
@use_pop = false
|
116
|
-
loop do
|
117
|
-
if Helpers::DateTimeUtils.create_timestamp >= @flushing_interval_deadline
|
118
|
-
@logger.log(Logger::DEBUG, 'Deadline exceeded flushing current batch.')
|
119
|
-
flush_queue!
|
120
|
-
@flushing_interval_deadline = Helpers::DateTimeUtils.create_timestamp + @flush_interval
|
121
|
-
@use_pop = true if @nil_count > MAX_NIL_COUNT
|
122
|
-
end
|
123
|
-
|
124
|
-
item = @event_queue.pop if @event_queue.length.positive? || @use_pop
|
125
|
-
|
126
|
-
if item.nil?
|
127
|
-
# when nil count is greater than MAX_NIL_COUNT, we hang on the pop until there is an item available.
|
128
|
-
# this avoids to much spinning of the loop.
|
129
|
-
@nil_count += 1
|
130
|
-
next
|
131
|
-
end
|
132
|
-
|
133
|
-
# reset nil_count and use_pop if we have received an item.
|
134
|
-
@nil_count = 0
|
135
|
-
@use_pop = false
|
136
|
-
|
122
|
+
def process_queue
|
123
|
+
while @event_queue.length.positive?
|
124
|
+
item = @event_queue.pop
|
137
125
|
if item == SHUTDOWN_SIGNAL
|
138
126
|
@logger.log(Logger::DEBUG, 'Received shutdown signal.')
|
139
|
-
|
127
|
+
return false
|
140
128
|
end
|
141
129
|
|
142
130
|
if item == FLUSH_SIGNAL
|
@@ -147,15 +135,35 @@ module Optimizely
|
|
147
135
|
|
148
136
|
add_to_batch(item) if item.is_a? Optimizely::UserEvent
|
149
137
|
end
|
138
|
+
true
|
139
|
+
end
|
140
|
+
|
141
|
+
def run_queue
|
142
|
+
loop do
|
143
|
+
if Helpers::DateTimeUtils.create_timestamp >= @flushing_interval_deadline
|
144
|
+
@logger.log(Logger::DEBUG, 'Deadline exceeded flushing current batch.')
|
145
|
+
|
146
|
+
break unless process_queue
|
147
|
+
|
148
|
+
flush_queue!
|
149
|
+
@flushing_interval_deadline = Helpers::DateTimeUtils.create_timestamp + @flush_interval
|
150
|
+
end
|
151
|
+
|
152
|
+
break unless process_queue
|
153
|
+
|
154
|
+
# what is the current interval to flush in seconds
|
155
|
+
interval = (@flushing_interval_deadline - Helpers::DateTimeUtils.create_timestamp) * 0.001
|
156
|
+
|
157
|
+
next unless interval.positive?
|
158
|
+
|
159
|
+
@wait_mutex.synchronize { @resource.wait(@wait_mutex, interval) }
|
160
|
+
end
|
150
161
|
rescue SignalException
|
151
162
|
@logger.log(Logger::ERROR, 'Interrupted while processing buffer.')
|
152
|
-
rescue
|
163
|
+
rescue => e
|
153
164
|
@logger.log(Logger::ERROR, "Uncaught exception processing buffer. #{e.message}")
|
154
165
|
ensure
|
155
|
-
@logger.log(
|
156
|
-
Logger::INFO,
|
157
|
-
'Exiting processing loop. Attempting to flush pending events.'
|
158
|
-
)
|
166
|
+
@logger.log(Logger::INFO, 'Exiting processing loop. Attempting to flush pending events.')
|
159
167
|
flush_queue!
|
160
168
|
end
|
161
169
|
|
@@ -0,0 +1,116 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Copyright 2019, Optimizely and contributors
|
4
|
+
#
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
6
|
+
# you may not use this file except in compliance with the License.
|
7
|
+
# You may obtain a copy of the License at
|
8
|
+
#
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
10
|
+
#
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
14
|
+
# See the License for the specific language governing permissions and
|
15
|
+
# limitations under the License.
|
16
|
+
#
|
17
|
+
|
18
|
+
module Optimizely
|
19
|
+
class OptimizelyConfig
|
20
|
+
def initialize(project_config)
|
21
|
+
@project_config = project_config
|
22
|
+
end
|
23
|
+
|
24
|
+
def config
|
25
|
+
experiments_map_object = experiments_map
|
26
|
+
features_map = get_features_map(experiments_map_object)
|
27
|
+
{
|
28
|
+
'experimentsMap' => experiments_map_object,
|
29
|
+
'featuresMap' => features_map,
|
30
|
+
'revision' => @project_config.revision
|
31
|
+
}
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def experiments_map
|
37
|
+
feature_variables_map = @project_config.feature_flags.reduce({}) do |result_map, feature|
|
38
|
+
result_map.update(feature['id'] => feature['variables'])
|
39
|
+
end
|
40
|
+
@project_config.experiments.reduce({}) do |experiments_map, experiment|
|
41
|
+
experiments_map.update(
|
42
|
+
experiment['key'] => {
|
43
|
+
'id' => experiment['id'],
|
44
|
+
'key' => experiment['key'],
|
45
|
+
'variationsMap' => experiment['variations'].reduce({}) do |variations_map, variation|
|
46
|
+
variation_object = {
|
47
|
+
'id' => variation['id'],
|
48
|
+
'key' => variation['key'],
|
49
|
+
'variablesMap' => get_merged_variables_map(variation, experiment['id'], feature_variables_map)
|
50
|
+
}
|
51
|
+
variation_object['featureEnabled'] = variation['featureEnabled'] if @project_config.feature_experiment?(experiment['id'])
|
52
|
+
variations_map.update(variation['key'] => variation_object)
|
53
|
+
end
|
54
|
+
}
|
55
|
+
)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# Merges feature key and type from feature variables to variation variables.
|
60
|
+
def get_merged_variables_map(variation, experiment_id, feature_variables_map)
|
61
|
+
feature_ids = @project_config.experiment_feature_map[experiment_id]
|
62
|
+
return {} unless feature_ids
|
63
|
+
|
64
|
+
experiment_feature_variables = feature_variables_map[feature_ids[0]]
|
65
|
+
# temporary variation variables map to get values to merge.
|
66
|
+
temp_variables_id_map = {}
|
67
|
+
if variation['variables']
|
68
|
+
temp_variables_id_map = variation['variables'].reduce({}) do |variables_map, variable|
|
69
|
+
variables_map.update(
|
70
|
+
variable['id'] => {
|
71
|
+
'id' => variable['id'],
|
72
|
+
'value' => variable['value']
|
73
|
+
}
|
74
|
+
)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
experiment_feature_variables.reduce({}) do |variables_map, feature_variable|
|
78
|
+
variation_variable = temp_variables_id_map[feature_variable['id']]
|
79
|
+
variable_value = variation['featureEnabled'] && variation_variable ? variation_variable['value'] : feature_variable['defaultValue']
|
80
|
+
variables_map.update(
|
81
|
+
feature_variable['key'] => {
|
82
|
+
'id' => feature_variable['id'],
|
83
|
+
'key' => feature_variable['key'],
|
84
|
+
'type' => feature_variable['type'],
|
85
|
+
'value' => variable_value
|
86
|
+
}
|
87
|
+
)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def get_features_map(all_experiments_map)
|
92
|
+
@project_config.feature_flags.reduce({}) do |features_map, feature|
|
93
|
+
features_map.update(
|
94
|
+
feature['key'] => {
|
95
|
+
'id' => feature['id'],
|
96
|
+
'key' => feature['key'],
|
97
|
+
'experimentsMap' => feature['experimentIds'].reduce({}) do |experiments_map, experiment_id|
|
98
|
+
experiment_key = @project_config.experiment_id_map[experiment_id]['key']
|
99
|
+
experiments_map.update(experiment_key => all_experiments_map[experiment_key])
|
100
|
+
end,
|
101
|
+
'variablesMap' => feature['variables'].reduce({}) do |variables, variable|
|
102
|
+
variables.update(
|
103
|
+
variable['key'] => {
|
104
|
+
'id' => variable['id'],
|
105
|
+
'key' => variable['key'],
|
106
|
+
'type' => variable['type'],
|
107
|
+
'value' => variable['defaultValue']
|
108
|
+
}
|
109
|
+
)
|
110
|
+
end
|
111
|
+
}
|
112
|
+
)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
data/lib/optimizely/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: optimizely-sdk
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 3.
|
4
|
+
version: 3.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Optimizely
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2020-01-23 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -182,6 +182,7 @@ files:
|
|
182
182
|
- lib/optimizely/helpers/variable_type.rb
|
183
183
|
- lib/optimizely/logger.rb
|
184
184
|
- lib/optimizely/notification_center.rb
|
185
|
+
- lib/optimizely/optimizely_config.rb
|
185
186
|
- lib/optimizely/optimizely_factory.rb
|
186
187
|
- lib/optimizely/params.rb
|
187
188
|
- lib/optimizely/project_config.rb
|
@@ -206,8 +207,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
206
207
|
- !ruby/object:Gem::Version
|
207
208
|
version: '0'
|
208
209
|
requirements: []
|
209
|
-
|
210
|
-
rubygems_version: 2.5.2.3
|
210
|
+
rubygems_version: 3.0.3
|
211
211
|
signing_key:
|
212
212
|
specification_version: 4
|
213
213
|
summary: Ruby SDK for Optimizely's testing framework
|