optimizely-sdk 3.4.0 → 3.8.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/optimizely.rb +383 -49
- data/lib/optimizely/audience.rb +31 -43
- data/lib/optimizely/bucketer.rb +36 -33
- data/lib/optimizely/config/datafile_project_config.rb +19 -3
- data/lib/optimizely/config/proxy_config.rb +34 -0
- data/lib/optimizely/config_manager/async_scheduler.rb +6 -2
- data/lib/optimizely/config_manager/http_project_config_manager.rb +40 -23
- data/lib/optimizely/custom_attribute_condition_evaluator.rb +133 -37
- data/lib/optimizely/decide/optimizely_decide_option.rb +28 -0
- data/lib/optimizely/decide/optimizely_decision.rb +60 -0
- data/lib/optimizely/decide/optimizely_decision_message.rb +26 -0
- data/lib/optimizely/decision_service.rb +163 -139
- data/lib/optimizely/event/entity/decision.rb +6 -4
- data/lib/optimizely/event/entity/impression_event.rb +4 -2
- data/lib/optimizely/event/event_factory.rb +4 -3
- data/lib/optimizely/event/user_event_factory.rb +4 -3
- data/lib/optimizely/event_dispatcher.rb +8 -14
- data/lib/optimizely/exceptions.rb +17 -9
- data/lib/optimizely/helpers/constants.rb +19 -5
- data/lib/optimizely/helpers/http_utils.rb +64 -0
- data/lib/optimizely/helpers/variable_type.rb +8 -1
- data/lib/optimizely/optimizely_config.rb +2 -1
- data/lib/optimizely/optimizely_factory.rb +54 -5
- data/lib/optimizely/optimizely_user_context.rb +107 -0
- data/lib/optimizely/project_config.rb +5 -1
- data/lib/optimizely/semantic_version.rb +166 -0
- data/lib/optimizely/version.rb +1 -1
- metadata +9 -16
data/lib/optimizely/audience.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
#
|
4
|
-
# Copyright 2016-2017, 2019, Optimizely and contributors
|
4
|
+
# Copyright 2016-2017, 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.
|
@@ -24,38 +24,37 @@ module Optimizely
|
|
24
24
|
module Audience
|
25
25
|
module_function
|
26
26
|
|
27
|
-
def
|
28
|
-
# Determine for given experiment if user satisfies the
|
27
|
+
def user_meets_audience_conditions?(config, experiment, attributes, logger, logging_hash = nil, logging_key = nil)
|
28
|
+
# Determine for given experiment/rollout rule if user satisfies the audience conditions.
|
29
29
|
#
|
30
30
|
# config - Representation of the Optimizely project config.
|
31
|
-
# experiment - Experiment
|
31
|
+
# experiment - Experiment/Rollout rule in which user is to be bucketed.
|
32
32
|
# attributes - Hash representing user attributes which will be used in determining if
|
33
33
|
# the audience conditions are met.
|
34
|
+
# logger - Provides a logger instance.
|
35
|
+
# logging_hash - Optional string representing logs hash inside Helpers::Constants.
|
36
|
+
# This defaults to 'EXPERIMENT_AUDIENCE_EVALUATION_LOGS'.
|
37
|
+
# logging_key - Optional string to be logged as an identifier of experiment under evaluation.
|
38
|
+
# This defaults to experiment['key'].
|
34
39
|
#
|
35
40
|
# Returns boolean representing if user satisfies audience conditions for the audiences or not.
|
41
|
+
decide_reasons = []
|
42
|
+
logging_hash ||= 'EXPERIMENT_AUDIENCE_EVALUATION_LOGS'
|
43
|
+
logging_key ||= experiment['key']
|
44
|
+
|
45
|
+
logs_hash = Object.const_get "Optimizely::Helpers::Constants::#{logging_hash}"
|
36
46
|
|
37
47
|
audience_conditions = experiment['audienceConditions'] || experiment['audienceIds']
|
38
48
|
|
39
|
-
|
40
|
-
|
41
|
-
format(
|
42
|
-
Helpers::Constants::AUDIENCE_EVALUATION_LOGS['EVALUATING_AUDIENCES_COMBINED'],
|
43
|
-
experiment['key'],
|
44
|
-
audience_conditions
|
45
|
-
)
|
46
|
-
)
|
49
|
+
message = format(logs_hash['EVALUATING_AUDIENCES_COMBINED'], logging_key, audience_conditions)
|
50
|
+
logger.log(Logger::DEBUG, message)
|
47
51
|
|
48
52
|
# Return true if there are no audiences
|
49
53
|
if audience_conditions.empty?
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
experiment['key'],
|
55
|
-
'TRUE'
|
56
|
-
)
|
57
|
-
)
|
58
|
-
return true
|
54
|
+
message = format(logs_hash['AUDIENCE_EVALUATION_RESULT_COMBINED'], logging_key, 'TRUE')
|
55
|
+
logger.log(Logger::INFO, message)
|
56
|
+
decide_reasons.push(message)
|
57
|
+
return true, decide_reasons
|
59
58
|
end
|
60
59
|
|
61
60
|
attributes ||= {}
|
@@ -71,39 +70,28 @@ module Optimizely
|
|
71
70
|
return nil unless audience
|
72
71
|
|
73
72
|
audience_conditions = audience['conditions']
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
Helpers::Constants::AUDIENCE_EVALUATION_LOGS['EVALUATING_AUDIENCE'],
|
78
|
-
audience_id,
|
79
|
-
audience_conditions
|
80
|
-
)
|
81
|
-
)
|
73
|
+
message = format(logs_hash['EVALUATING_AUDIENCE'], audience_id, audience_conditions)
|
74
|
+
logger.log(Logger::DEBUG, message)
|
75
|
+
decide_reasons.push(message)
|
82
76
|
|
83
77
|
audience_conditions = JSON.parse(audience_conditions) if audience_conditions.is_a?(String)
|
84
78
|
result = ConditionTreeEvaluator.evaluate(audience_conditions, evaluate_custom_attr)
|
85
79
|
result_str = result.nil? ? 'UNKNOWN' : result.to_s.upcase
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
80
|
+
message = format(logs_hash['AUDIENCE_EVALUATION_RESULT'], audience_id, result_str)
|
81
|
+
logger.log(Logger::DEBUG, message)
|
82
|
+
decide_reasons.push(message)
|
83
|
+
|
90
84
|
result
|
91
85
|
end
|
92
86
|
|
93
87
|
eval_result = ConditionTreeEvaluator.evaluate(audience_conditions, evaluate_audience)
|
94
|
-
|
95
88
|
eval_result ||= false
|
96
89
|
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
Helpers::Constants::AUDIENCE_EVALUATION_LOGS['AUDIENCE_EVALUATION_RESULT_COMBINED'],
|
101
|
-
experiment['key'],
|
102
|
-
eval_result.to_s.upcase
|
103
|
-
)
|
104
|
-
)
|
90
|
+
message = format(logs_hash['AUDIENCE_EVALUATION_RESULT_COMBINED'], logging_key, eval_result.to_s.upcase)
|
91
|
+
logger.log(Logger::INFO, message)
|
92
|
+
decide_reasons.push(message)
|
105
93
|
|
106
|
-
eval_result
|
94
|
+
[eval_result, decide_reasons]
|
107
95
|
end
|
108
96
|
end
|
109
97
|
end
|
data/lib/optimizely/bucketer.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
#
|
4
|
-
# Copyright 2016-2017, 2019 Optimizely and contributors
|
4
|
+
# Copyright 2016-2017, 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.
|
@@ -39,14 +39,17 @@ module Optimizely
|
|
39
39
|
# Determines ID of variation to be shown for a given experiment key and user ID.
|
40
40
|
#
|
41
41
|
# project_config - Instance of ProjectConfig
|
42
|
-
# experiment - Experiment for which visitor is to be bucketed.
|
42
|
+
# experiment - Experiment or Rollout rule for which visitor is to be bucketed.
|
43
43
|
# bucketing_id - String A customer-assigned value used to generate the bucketing key
|
44
44
|
# user_id - String ID for user.
|
45
45
|
#
|
46
46
|
# Returns variation in which visitor with ID user_id has been placed. Nil if no variation.
|
47
|
-
return nil if experiment.nil?
|
47
|
+
return nil, [] if experiment.nil?
|
48
|
+
|
49
|
+
decide_reasons = []
|
48
50
|
|
49
51
|
# check if experiment is in a group; if so, check if user is bucketed into specified experiment
|
52
|
+
# this will not affect evaluation of rollout rules.
|
50
53
|
experiment_id = experiment['id']
|
51
54
|
experiment_key = experiment['key']
|
52
55
|
group_id = experiment['groupId']
|
@@ -54,52 +57,49 @@ module Optimizely
|
|
54
57
|
group = project_config.group_id_map.fetch(group_id)
|
55
58
|
if Helpers::Group.random_policy?(group)
|
56
59
|
traffic_allocations = group.fetch('trafficAllocation')
|
57
|
-
bucketed_experiment_id = find_bucket(bucketing_id, user_id, group_id, traffic_allocations)
|
60
|
+
bucketed_experiment_id, find_bucket_reasons = find_bucket(bucketing_id, user_id, group_id, traffic_allocations)
|
61
|
+
decide_reasons.push(*find_bucket_reasons)
|
62
|
+
|
58
63
|
# return if the user is not bucketed into any experiment
|
59
64
|
unless bucketed_experiment_id
|
60
|
-
|
61
|
-
|
65
|
+
message = "User '#{user_id}' is in no experiment."
|
66
|
+
@logger.log(Logger::INFO, message)
|
67
|
+
decide_reasons.push(message)
|
68
|
+
return nil, decide_reasons
|
62
69
|
end
|
63
70
|
|
64
71
|
# return if the user is bucketed into a different experiment than the one specified
|
65
72
|
if bucketed_experiment_id != experiment_id
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
return nil
|
73
|
+
message = "User '#{user_id}' is not in experiment '#{experiment_key}' of group #{group_id}."
|
74
|
+
@logger.log(Logger::INFO, message)
|
75
|
+
decide_reasons.push(message)
|
76
|
+
return nil, decide_reasons
|
71
77
|
end
|
72
78
|
|
73
79
|
# continue bucketing if the user is bucketed into the experiment specified
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
)
|
80
|
+
message = "User '#{user_id}' is in experiment '#{experiment_key}' of group #{group_id}."
|
81
|
+
@logger.log(Logger::INFO, message)
|
82
|
+
decide_reasons.push(message)
|
78
83
|
end
|
79
84
|
end
|
80
85
|
|
81
86
|
traffic_allocations = experiment['trafficAllocation']
|
82
|
-
variation_id = find_bucket(bucketing_id, user_id, experiment_id, traffic_allocations)
|
87
|
+
variation_id, find_bucket_reasons = find_bucket(bucketing_id, user_id, experiment_id, traffic_allocations)
|
88
|
+
decide_reasons.push(*find_bucket_reasons)
|
89
|
+
|
83
90
|
if variation_id && variation_id != ''
|
84
91
|
variation = project_config.get_variation_from_id(experiment_key, variation_id)
|
85
|
-
|
86
|
-
@logger.log(
|
87
|
-
Logger::INFO,
|
88
|
-
"User '#{user_id}' is in variation '#{variation_key}' of experiment '#{experiment_key}'."
|
89
|
-
)
|
90
|
-
return variation
|
92
|
+
return variation, decide_reasons
|
91
93
|
end
|
92
94
|
|
93
95
|
# Handle the case when the traffic range is empty due to sticky bucketing
|
94
96
|
if variation_id == ''
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
)
|
97
|
+
message = 'Bucketed into an empty traffic range. Returning nil.'
|
98
|
+
@logger.log(Logger::DEBUG, message)
|
99
|
+
decide_reasons.push(message)
|
99
100
|
end
|
100
101
|
|
101
|
-
|
102
|
-
nil
|
102
|
+
[nil, decide_reasons]
|
103
103
|
end
|
104
104
|
|
105
105
|
def find_bucket(bucketing_id, user_id, parent_id, traffic_allocations)
|
@@ -110,21 +110,24 @@ module Optimizely
|
|
110
110
|
# parent_id - String entity ID to use for bucketing ID
|
111
111
|
# traffic_allocations - Array of traffic allocations
|
112
112
|
#
|
113
|
-
# Returns entity ID corresponding to the provided bucket value
|
113
|
+
# Returns and array of two values where first value is the entity ID corresponding to the provided bucket value
|
114
|
+
# or nil if no match is found. The second value contains the array of reasons stating how the deicision was taken
|
115
|
+
decide_reasons = []
|
114
116
|
bucketing_key = format(BUCKETING_ID_TEMPLATE, bucketing_id: bucketing_id, entity_id: parent_id)
|
115
117
|
bucket_value = generate_bucket_value(bucketing_key)
|
116
|
-
|
117
|
-
|
118
|
+
|
119
|
+
message = "Assigned bucket #{bucket_value} to user '#{user_id}' with bucketing ID: '#{bucketing_id}'."
|
120
|
+
@logger.log(Logger::DEBUG, message)
|
118
121
|
|
119
122
|
traffic_allocations.each do |traffic_allocation|
|
120
123
|
current_end_of_range = traffic_allocation['endOfRange']
|
121
124
|
if bucket_value < current_end_of_range
|
122
125
|
entity_id = traffic_allocation['entityId']
|
123
|
-
return entity_id
|
126
|
+
return entity_id, decide_reasons
|
124
127
|
end
|
125
128
|
end
|
126
129
|
|
127
|
-
nil
|
130
|
+
[nil, decide_reasons]
|
128
131
|
end
|
129
132
|
|
130
133
|
private
|
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
# Copyright 2019, Optimizely and contributors
|
3
|
+
# Copyright 2019-2020, Optimizely and contributors
|
4
4
|
#
|
5
5
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
6
6
|
# you may not use this file except in compliance with the License.
|
@@ -24,6 +24,7 @@ module Optimizely
|
|
24
24
|
RUNNING_EXPERIMENT_STATUS = ['Running'].freeze
|
25
25
|
RESERVED_ATTRIBUTE_PREFIX = '$opt_'
|
26
26
|
|
27
|
+
attr_reader :datafile
|
27
28
|
attr_reader :account_id
|
28
29
|
attr_reader :attributes
|
29
30
|
attr_reader :audiences
|
@@ -39,6 +40,7 @@ module Optimizely
|
|
39
40
|
attr_reader :revision
|
40
41
|
attr_reader :rollouts
|
41
42
|
attr_reader :version
|
43
|
+
attr_reader :send_flag_decisions
|
42
44
|
|
43
45
|
attr_reader :attribute_key_map
|
44
46
|
attr_reader :audience_id_map
|
@@ -62,6 +64,7 @@ module Optimizely
|
|
62
64
|
|
63
65
|
config = JSON.parse(datafile)
|
64
66
|
|
67
|
+
@datafile = datafile
|
65
68
|
@error_handler = error_handler
|
66
69
|
@logger = logger
|
67
70
|
@version = config['version']
|
@@ -81,6 +84,18 @@ module Optimizely
|
|
81
84
|
@bot_filtering = config['botFiltering']
|
82
85
|
@revision = config['revision']
|
83
86
|
@rollouts = config.fetch('rollouts', [])
|
87
|
+
@send_flag_decisions = config.fetch('sendFlagDecisions', false)
|
88
|
+
|
89
|
+
# Json type is represented in datafile as a subtype of string for the sake of backwards compatibility.
|
90
|
+
# Converting it to a first-class json type while creating Project Config
|
91
|
+
@feature_flags.each do |feature_flag|
|
92
|
+
feature_flag['variables'].each do |variable|
|
93
|
+
if variable['type'] == 'string' && variable['subType'] == 'json'
|
94
|
+
variable['type'] = 'json'
|
95
|
+
variable.delete('subType')
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
84
99
|
|
85
100
|
# Utility maps for quick lookup
|
86
101
|
@attribute_key_map = generate_key_map(@attributes, 'key')
|
@@ -159,8 +174,9 @@ module Optimizely
|
|
159
174
|
config = new(datafile, logger, error_handler)
|
160
175
|
rescue StandardError => e
|
161
176
|
default_logger = SimpleLogger.new
|
162
|
-
|
163
|
-
|
177
|
+
error_to_handle = e.class == InvalidDatafileVersionError ? e : InvalidInputError.new('datafile')
|
178
|
+
error_msg = error_to_handle.message
|
179
|
+
|
164
180
|
default_logger.log(Logger::ERROR, error_msg)
|
165
181
|
error_handler.handle_error error_to_handle
|
166
182
|
return nil
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Copyright 2020, 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
|
+
|
19
|
+
module Optimizely
|
20
|
+
class ProxyConfig
|
21
|
+
attr_reader :host, :port, :username, :password
|
22
|
+
|
23
|
+
def initialize(host, port = nil, username = nil, password = nil)
|
24
|
+
# host - DNS name or IP address of proxy
|
25
|
+
# port - port to use to acess the proxy
|
26
|
+
# username - username if authorization is required
|
27
|
+
# password - password if authorization is required
|
28
|
+
@host = host
|
29
|
+
@port = port
|
30
|
+
@username = username
|
31
|
+
@password = password
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -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.
|
@@ -19,13 +19,14 @@ module Optimizely
|
|
19
19
|
class AsyncScheduler
|
20
20
|
attr_reader :running
|
21
21
|
|
22
|
-
def initialize(callback, interval, auto_update, logger = nil)
|
22
|
+
def initialize(callback, interval, auto_update, logger = nil, error_handler = nil)
|
23
23
|
# Sets up AsyncScheduler to execute a callback periodically.
|
24
24
|
#
|
25
25
|
# callback - Main function to be executed periodically.
|
26
26
|
# interval - How many seconds to wait between executions.
|
27
27
|
# auto_update - boolean indicates to run infinitely or only once.
|
28
28
|
# logger - Optional Provides a logger instance.
|
29
|
+
# error_handler - Optional Provides a handle_error method to handle exceptions.
|
29
30
|
|
30
31
|
@callback = callback
|
31
32
|
@interval = interval
|
@@ -33,6 +34,7 @@ module Optimizely
|
|
33
34
|
@running = false
|
34
35
|
@thread = nil
|
35
36
|
@logger = logger || NoOpLogger.new
|
37
|
+
@error_handler = error_handler || NoOpErrorHandler.new
|
36
38
|
end
|
37
39
|
|
38
40
|
def start!
|
@@ -54,6 +56,7 @@ module Optimizely
|
|
54
56
|
Logger::ERROR,
|
55
57
|
"Couldn't create a new thread for async scheduler. #{e.message}"
|
56
58
|
)
|
59
|
+
@error_handler.handle_error(e)
|
57
60
|
end
|
58
61
|
end
|
59
62
|
|
@@ -80,6 +83,7 @@ module Optimizely
|
|
80
83
|
Logger::ERROR,
|
81
84
|
"Something went wrong when executing passed callback. #{e.message}"
|
82
85
|
)
|
86
|
+
@error_handler.handle_error(e)
|
83
87
|
stop!
|
84
88
|
end
|
85
89
|
break unless @auto_update
|
@@ -19,14 +19,16 @@ require_relative '../config/datafile_project_config'
|
|
19
19
|
require_relative '../error_handler'
|
20
20
|
require_relative '../exceptions'
|
21
21
|
require_relative '../helpers/constants'
|
22
|
+
require_relative '../helpers/http_utils'
|
22
23
|
require_relative '../logger'
|
23
24
|
require_relative '../notification_center'
|
24
25
|
require_relative '../project_config'
|
25
26
|
require_relative '../optimizely_config'
|
26
27
|
require_relative 'project_config_manager'
|
27
28
|
require_relative 'async_scheduler'
|
28
|
-
|
29
|
+
|
29
30
|
require 'json'
|
31
|
+
|
30
32
|
module Optimizely
|
31
33
|
class HTTPProjectConfigManager < ProjectConfigManager
|
32
34
|
# Config manager that polls for the datafile and updated ProjectConfig based on an update interval.
|
@@ -49,6 +51,8 @@ module Optimizely
|
|
49
51
|
# error_handler - Provides a handle_error method to handle exceptions.
|
50
52
|
# skip_json_validation - Optional boolean param which allows skipping JSON schema
|
51
53
|
# validation upon object invocation. By default JSON schema validation will be performed.
|
54
|
+
# datafile_access_token - access token used to fetch private datafiles
|
55
|
+
# proxy_config - Optional proxy config instancea to configure making web requests through a proxy server.
|
52
56
|
def initialize(
|
53
57
|
sdk_key: nil,
|
54
58
|
url: nil,
|
@@ -61,10 +65,13 @@ module Optimizely
|
|
61
65
|
logger: nil,
|
62
66
|
error_handler: nil,
|
63
67
|
skip_json_validation: false,
|
64
|
-
notification_center: nil
|
68
|
+
notification_center: nil,
|
69
|
+
datafile_access_token: nil,
|
70
|
+
proxy_config: nil
|
65
71
|
)
|
66
72
|
@logger = logger || NoOpLogger.new
|
67
73
|
@error_handler = error_handler || NoOpErrorHandler.new
|
74
|
+
@access_token = datafile_access_token
|
68
75
|
@datafile_url = get_datafile_url(sdk_key, url, url_template)
|
69
76
|
@polling_interval = nil
|
70
77
|
polling_interval(polling_interval)
|
@@ -81,6 +88,7 @@ module Optimizely
|
|
81
88
|
# Start async scheduler in the end to avoid race condition where scheduler executes
|
82
89
|
# callback which makes use of variables not yet initialized by the main thread.
|
83
90
|
@async_scheduler.start! if start_by_default == true
|
91
|
+
@proxy_config = proxy_config
|
84
92
|
@stopped = false
|
85
93
|
end
|
86
94
|
|
@@ -143,21 +151,20 @@ module Optimizely
|
|
143
151
|
end
|
144
152
|
|
145
153
|
def request_config
|
146
|
-
@logger.log(
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
headers = {
|
152
|
-
'Content-Type' => 'application/json'
|
153
|
-
}
|
154
|
+
@logger.log(Logger::DEBUG, "Fetching datafile from #{@datafile_url}")
|
155
|
+
headers = {}
|
156
|
+
headers['Content-Type'] = 'application/json'
|
157
|
+
headers['If-Modified-Since'] = @last_modified if @last_modified
|
158
|
+
headers['Authorization'] = "Bearer #{@access_token}" unless @access_token.nil?
|
154
159
|
|
155
|
-
|
160
|
+
# Cleaning headers before logging to avoid exposing authorization token
|
161
|
+
cleansed_headers = {}
|
162
|
+
headers.each { |key, value| cleansed_headers[key] = key == 'Authorization' ? '********' : value }
|
163
|
+
@logger.log(Logger::DEBUG, "Datafile request headers: #{cleansed_headers}")
|
156
164
|
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
timeout: Helpers::Constants::CONFIG_MANAGER['REQUEST_TIMEOUT']
|
165
|
+
begin
|
166
|
+
response = Helpers::HttpUtils.make_request(
|
167
|
+
@datafile_url, :get, nil, headers, Helpers::Constants::CONFIG_MANAGER['REQUEST_TIMEOUT'], @proxy_config
|
161
168
|
)
|
162
169
|
rescue StandardError => e
|
163
170
|
@logger.log(
|
@@ -167,6 +174,9 @@ module Optimizely
|
|
167
174
|
return nil
|
168
175
|
end
|
169
176
|
|
177
|
+
response_code = response.code.to_i
|
178
|
+
@logger.log(Logger::DEBUG, "Datafile response status code #{response_code}")
|
179
|
+
|
170
180
|
# Leave datafile and config unchanged if it has not been modified.
|
171
181
|
if response.code == '304'
|
172
182
|
@logger.log(
|
@@ -176,9 +186,14 @@ module Optimizely
|
|
176
186
|
return
|
177
187
|
end
|
178
188
|
|
179
|
-
|
180
|
-
|
181
|
-
|
189
|
+
if response_code >= 200 && response_code < 400
|
190
|
+
@logger.log(Logger::DEBUG, 'Successfully fetched datafile, generating Project config')
|
191
|
+
config = DatafileProjectConfig.create(response.body, @logger, @error_handler, @skip_json_validation)
|
192
|
+
@last_modified = response[Helpers::Constants::HTTP_HEADERS['LAST_MODIFIED']]
|
193
|
+
@logger.log(Logger::DEBUG, "Saved last modified header value from response: #{@last_modified}.")
|
194
|
+
else
|
195
|
+
@logger.log(Logger::DEBUG, "Datafile fetch failed, status: #{response.code}, message: #{response.message}")
|
196
|
+
end
|
182
197
|
|
183
198
|
config
|
184
199
|
end
|
@@ -284,17 +299,19 @@ module Optimizely
|
|
284
299
|
# SDK key to determine URL from which to fetch the datafile.
|
285
300
|
# Returns String representing URL to fetch datafile from.
|
286
301
|
if sdk_key.nil? && url.nil?
|
287
|
-
|
288
|
-
@
|
302
|
+
error_msg = 'Must provide at least one of sdk_key or url.'
|
303
|
+
@logger.log(Logger::ERROR, error_msg)
|
304
|
+
@error_handler.handle_error(InvalidInputsError.new(error_msg))
|
289
305
|
end
|
290
306
|
|
291
307
|
unless url
|
292
|
-
url_template ||= Helpers::Constants::CONFIG_MANAGER['DATAFILE_URL_TEMPLATE']
|
308
|
+
url_template ||= @access_token.nil? ? Helpers::Constants::CONFIG_MANAGER['DATAFILE_URL_TEMPLATE'] : Helpers::Constants::CONFIG_MANAGER['AUTHENTICATED_DATAFILE_URL_TEMPLATE']
|
293
309
|
begin
|
294
310
|
return (url_template % sdk_key)
|
295
311
|
rescue
|
296
|
-
|
297
|
-
@
|
312
|
+
error_msg = "Invalid url_template #{url_template} provided."
|
313
|
+
@logger.log(Logger::ERROR, error_msg)
|
314
|
+
@error_handler.handle_error(InvalidInputsError.new(error_msg))
|
298
315
|
end
|
299
316
|
end
|
300
317
|
|