optimizely-sdk 3.6.0 → 3.9.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/audience.rb +18 -39
- data/lib/optimizely/bucketer.rb +35 -27
- data/lib/optimizely/condition_tree_evaluator.rb +2 -0
- data/lib/optimizely/config/datafile_project_config.rb +97 -14
- 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 +164 -141
- 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/helpers/constants.rb +1 -0
- data/lib/optimizely/optimizely_config.rb +180 -25
- data/lib/optimizely/optimizely_user_context.rb +107 -0
- data/lib/optimizely/project_config.rb +14 -2
- data/lib/optimizely/version.rb +1 -1
- data/lib/optimizely.rb +245 -18
- metadata +7 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3502b43c8fae064fda3e1b237786a0285103d4ce9383a9d64cfc7b34f3f8c849
|
4
|
+
data.tar.gz: 9b843fc3a7999e640f329812aeeb8e26474b5b85f29d27c0647ba7c3685cb73f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a244307de60cf4a7e9b4e90bd0282e071e310701c09b4314a5f011f9f97a1f638205dc9e3de59611c247315314bfd58a0521690bbc3fa876575f3ef8fa917306
|
7
|
+
data.tar.gz: b68df42eef86c3a4e1fd22f2b1b4cd3060df4ade0c9125862443164735778a16415af958a4002fba7adcdf078d1552a5ffef446bac22e7a269a5984445d1fd50
|
data/lib/optimizely/audience.rb
CHANGED
@@ -38,6 +38,7 @@ module Optimizely
|
|
38
38
|
# This defaults to experiment['key'].
|
39
39
|
#
|
40
40
|
# Returns boolean representing if user satisfies audience conditions for the audiences or not.
|
41
|
+
decide_reasons = []
|
41
42
|
logging_hash ||= 'EXPERIMENT_AUDIENCE_EVALUATION_LOGS'
|
42
43
|
logging_key ||= experiment['key']
|
43
44
|
|
@@ -45,26 +46,15 @@ module Optimizely
|
|
45
46
|
|
46
47
|
audience_conditions = experiment['audienceConditions'] || experiment['audienceIds']
|
47
48
|
|
48
|
-
|
49
|
-
|
50
|
-
format(
|
51
|
-
logs_hash['EVALUATING_AUDIENCES_COMBINED'],
|
52
|
-
logging_key,
|
53
|
-
audience_conditions
|
54
|
-
)
|
55
|
-
)
|
49
|
+
message = format(logs_hash['EVALUATING_AUDIENCES_COMBINED'], logging_key, audience_conditions)
|
50
|
+
logger.log(Logger::DEBUG, message)
|
56
51
|
|
57
52
|
# Return true if there are no audiences
|
58
53
|
if audience_conditions.empty?
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
logging_key,
|
64
|
-
'TRUE'
|
65
|
-
)
|
66
|
-
)
|
67
|
-
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
|
68
58
|
end
|
69
59
|
|
70
60
|
attributes ||= {}
|
@@ -80,39 +70,28 @@ module Optimizely
|
|
80
70
|
return nil unless audience
|
81
71
|
|
82
72
|
audience_conditions = audience['conditions']
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
logs_hash['EVALUATING_AUDIENCE'],
|
87
|
-
audience_id,
|
88
|
-
audience_conditions
|
89
|
-
)
|
90
|
-
)
|
73
|
+
message = format(logs_hash['EVALUATING_AUDIENCE'], audience_id, audience_conditions)
|
74
|
+
logger.log(Logger::DEBUG, message)
|
75
|
+
decide_reasons.push(message)
|
91
76
|
|
92
77
|
audience_conditions = JSON.parse(audience_conditions) if audience_conditions.is_a?(String)
|
93
78
|
result = ConditionTreeEvaluator.evaluate(audience_conditions, evaluate_custom_attr)
|
94
79
|
result_str = result.nil? ? 'UNKNOWN' : result.to_s.upcase
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
80
|
+
message = format(logs_hash['AUDIENCE_EVALUATION_RESULT'], audience_id, result_str)
|
81
|
+
logger.log(Logger::DEBUG, message)
|
82
|
+
decide_reasons.push(message)
|
83
|
+
|
99
84
|
result
|
100
85
|
end
|
101
86
|
|
102
87
|
eval_result = ConditionTreeEvaluator.evaluate(audience_conditions, evaluate_audience)
|
103
|
-
|
104
88
|
eval_result ||= false
|
105
89
|
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
logs_hash['AUDIENCE_EVALUATION_RESULT_COMBINED'],
|
110
|
-
logging_key,
|
111
|
-
eval_result.to_s.upcase
|
112
|
-
)
|
113
|
-
)
|
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)
|
114
93
|
|
115
|
-
eval_result
|
94
|
+
[eval_result, decide_reasons]
|
116
95
|
end
|
117
96
|
end
|
118
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-
|
4
|
+
# Copyright 2016-2017, 2019-2021 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.
|
@@ -44,7 +44,9 @@ module Optimizely
|
|
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
|
50
52
|
# this will not affect evaluation of rollout rules.
|
@@ -55,46 +57,49 @@ module Optimizely
|
|
55
57
|
group = project_config.group_id_map.fetch(group_id)
|
56
58
|
if Helpers::Group.random_policy?(group)
|
57
59
|
traffic_allocations = group.fetch('trafficAllocation')
|
58
|
-
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
|
+
|
59
63
|
# return if the user is not bucketed into any experiment
|
60
64
|
unless bucketed_experiment_id
|
61
|
-
|
62
|
-
|
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
|
63
69
|
end
|
64
70
|
|
65
71
|
# return if the user is bucketed into a different experiment than the one specified
|
66
72
|
if bucketed_experiment_id != experiment_id
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
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
|
72
77
|
end
|
73
78
|
|
74
79
|
# continue bucketing if the user is bucketed into the experiment specified
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
)
|
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)
|
79
83
|
end
|
80
84
|
end
|
81
85
|
|
82
86
|
traffic_allocations = experiment['trafficAllocation']
|
83
|
-
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
|
+
|
84
90
|
if variation_id && variation_id != ''
|
85
|
-
variation = project_config.
|
86
|
-
return variation
|
91
|
+
variation = project_config.get_variation_from_id_by_experiment_id(experiment_id, variation_id)
|
92
|
+
return variation, decide_reasons
|
87
93
|
end
|
88
94
|
|
89
95
|
# Handle the case when the traffic range is empty due to sticky bucketing
|
90
96
|
if variation_id == ''
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
)
|
97
|
+
message = 'Bucketed into an empty traffic range. Returning nil.'
|
98
|
+
@logger.log(Logger::DEBUG, message)
|
99
|
+
decide_reasons.push(message)
|
95
100
|
end
|
96
101
|
|
97
|
-
nil
|
102
|
+
[nil, decide_reasons]
|
98
103
|
end
|
99
104
|
|
100
105
|
def find_bucket(bucketing_id, user_id, parent_id, traffic_allocations)
|
@@ -105,21 +110,24 @@ module Optimizely
|
|
105
110
|
# parent_id - String entity ID to use for bucketing ID
|
106
111
|
# traffic_allocations - Array of traffic allocations
|
107
112
|
#
|
108
|
-
# 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 = []
|
109
116
|
bucketing_key = format(BUCKETING_ID_TEMPLATE, bucketing_id: bucketing_id, entity_id: parent_id)
|
110
117
|
bucket_value = generate_bucket_value(bucketing_key)
|
111
|
-
|
112
|
-
|
118
|
+
|
119
|
+
message = "Assigned bucket #{bucket_value} to user '#{user_id}' with bucketing ID: '#{bucketing_id}'."
|
120
|
+
@logger.log(Logger::DEBUG, message)
|
113
121
|
|
114
122
|
traffic_allocations.each do |traffic_allocation|
|
115
123
|
current_end_of_range = traffic_allocation['endOfRange']
|
116
124
|
if bucket_value < current_end_of_range
|
117
125
|
entity_id = traffic_allocation['entityId']
|
118
|
-
return entity_id
|
126
|
+
return entity_id, decide_reasons
|
119
127
|
end
|
120
128
|
end
|
121
129
|
|
122
|
-
nil
|
130
|
+
[nil, decide_reasons]
|
123
131
|
end
|
124
132
|
|
125
133
|
private
|
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
# Copyright 2019-
|
3
|
+
# Copyright 2019-2021, 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.
|
@@ -38,8 +38,11 @@ module Optimizely
|
|
38
38
|
attr_reader :anonymize_ip
|
39
39
|
attr_reader :bot_filtering
|
40
40
|
attr_reader :revision
|
41
|
+
attr_reader :sdk_key
|
42
|
+
attr_reader :environment_key
|
41
43
|
attr_reader :rollouts
|
42
44
|
attr_reader :version
|
45
|
+
attr_reader :send_flag_decisions
|
43
46
|
|
44
47
|
attr_reader :attribute_key_map
|
45
48
|
attr_reader :audience_id_map
|
@@ -51,10 +54,12 @@ module Optimizely
|
|
51
54
|
attr_reader :feature_variable_key_map
|
52
55
|
attr_reader :group_id_map
|
53
56
|
attr_reader :rollout_id_map
|
54
|
-
attr_reader :
|
57
|
+
attr_reader :rollout_experiment_id_map
|
55
58
|
attr_reader :variation_id_map
|
56
59
|
attr_reader :variation_id_to_variable_usage_map
|
57
60
|
attr_reader :variation_key_map
|
61
|
+
attr_reader :variation_id_map_by_experiment_id
|
62
|
+
attr_reader :variation_key_map_by_experiment_id
|
58
63
|
|
59
64
|
def initialize(datafile, logger, error_handler)
|
60
65
|
# ProjectConfig init method to fetch and set project config data
|
@@ -82,7 +87,10 @@ module Optimizely
|
|
82
87
|
@anonymize_ip = config.key?('anonymizeIP') ? config['anonymizeIP'] : false
|
83
88
|
@bot_filtering = config['botFiltering']
|
84
89
|
@revision = config['revision']
|
90
|
+
@sdk_key = config.fetch('sdkKey', '')
|
91
|
+
@environment_key = config.fetch('environmentKey', '')
|
85
92
|
@rollouts = config.fetch('rollouts', [])
|
93
|
+
@send_flag_decisions = config.fetch('sendFlagDecisions', false)
|
86
94
|
|
87
95
|
# Json type is represented in datafile as a subtype of string for the sake of backwards compatibility.
|
88
96
|
# Converting it to a first-class json type while creating Project Config
|
@@ -111,9 +119,11 @@ module Optimizely
|
|
111
119
|
@audience_id_map = @audience_id_map.merge(generate_key_map(@typed_audiences, 'id')) unless @typed_audiences.empty?
|
112
120
|
@variation_id_map = {}
|
113
121
|
@variation_key_map = {}
|
122
|
+
@variation_id_map_by_experiment_id = {}
|
123
|
+
@variation_key_map_by_experiment_id = {}
|
114
124
|
@variation_id_to_variable_usage_map = {}
|
115
125
|
@variation_id_to_experiment_map = {}
|
116
|
-
@
|
126
|
+
@experiment_id_map.each_value do |exp|
|
117
127
|
# Excludes experiments from rollouts
|
118
128
|
variations = exp.fetch('variations')
|
119
129
|
variations.each do |variation|
|
@@ -123,13 +133,13 @@ module Optimizely
|
|
123
133
|
end
|
124
134
|
@rollout_id_map = generate_key_map(@rollouts, 'id')
|
125
135
|
# split out the experiment key map for rollouts
|
126
|
-
@
|
136
|
+
@rollout_experiment_id_map = {}
|
127
137
|
@rollout_id_map.each_value do |rollout|
|
128
138
|
exps = rollout.fetch('experiments')
|
129
|
-
@
|
139
|
+
@rollout_experiment_id_map = @rollout_experiment_id_map.merge(generate_key_map(exps, 'id'))
|
130
140
|
end
|
131
|
-
@all_experiments = @
|
132
|
-
@all_experiments.each do |
|
141
|
+
@all_experiments = @experiment_id_map.merge(@rollout_experiment_id_map)
|
142
|
+
@all_experiments.each do |id, exp|
|
133
143
|
variations = exp.fetch('variations')
|
134
144
|
variations.each do |variation|
|
135
145
|
variation_id = variation['id']
|
@@ -139,8 +149,10 @@ module Optimizely
|
|
139
149
|
|
140
150
|
@variation_id_to_variable_usage_map[variation_id] = generate_key_map(variation_variables, 'id')
|
141
151
|
end
|
142
|
-
@variation_id_map[key] = generate_key_map(variations, 'id')
|
143
|
-
@variation_key_map[key] = generate_key_map(variations, 'key')
|
152
|
+
@variation_id_map[exp['key']] = generate_key_map(variations, 'id')
|
153
|
+
@variation_key_map[exp['key']] = generate_key_map(variations, 'key')
|
154
|
+
@variation_id_map_by_experiment_id[id] = generate_key_map(variations, 'id')
|
155
|
+
@variation_key_map_by_experiment_id[id] = generate_key_map(variations, 'key')
|
144
156
|
end
|
145
157
|
@feature_flag_key_map = generate_key_map(@feature_flags, 'key')
|
146
158
|
@experiment_feature_map = {}
|
@@ -207,6 +219,21 @@ module Optimizely
|
|
207
219
|
nil
|
208
220
|
end
|
209
221
|
|
222
|
+
def get_experiment_from_id(experiment_id)
|
223
|
+
# Retrieves experiment ID for a given key
|
224
|
+
#
|
225
|
+
# experiment_id - String id representing the experiment
|
226
|
+
#
|
227
|
+
# Returns Experiment or nil if not found
|
228
|
+
|
229
|
+
experiment = @experiment_id_map[experiment_id]
|
230
|
+
return experiment if experiment
|
231
|
+
|
232
|
+
@logger.log Logger::ERROR, "Experiment id '#{experiment_id}' is not in datafile."
|
233
|
+
@error_handler.handle_error InvalidExperimentError
|
234
|
+
nil
|
235
|
+
end
|
236
|
+
|
210
237
|
def get_experiment_key(experiment_id)
|
211
238
|
# Retrieves experiment key for a given ID.
|
212
239
|
#
|
@@ -275,6 +302,52 @@ module Optimizely
|
|
275
302
|
nil
|
276
303
|
end
|
277
304
|
|
305
|
+
def get_variation_from_id_by_experiment_id(experiment_id, variation_id)
|
306
|
+
# Get variation given experiment ID and variation ID
|
307
|
+
#
|
308
|
+
# experiment_id - ID representing parent experiment of variation
|
309
|
+
# variation_id - ID of the variation
|
310
|
+
#
|
311
|
+
# Returns the variation or nil if not found
|
312
|
+
|
313
|
+
variation_id_map_by_experiment_id = @variation_id_map_by_experiment_id[experiment_id]
|
314
|
+
if variation_id_map_by_experiment_id
|
315
|
+
variation = variation_id_map_by_experiment_id[variation_id]
|
316
|
+
return variation if variation
|
317
|
+
|
318
|
+
@logger.log Logger::ERROR, "Variation id '#{variation_id}' is not in datafile."
|
319
|
+
@error_handler.handle_error InvalidVariationError
|
320
|
+
return nil
|
321
|
+
end
|
322
|
+
|
323
|
+
@logger.log Logger::ERROR, "Experiment id '#{experiment_id}' is not in datafile."
|
324
|
+
@error_handler.handle_error InvalidExperimentError
|
325
|
+
nil
|
326
|
+
end
|
327
|
+
|
328
|
+
def get_variation_id_from_key_by_experiment_id(experiment_id, variation_key)
|
329
|
+
# Get variation given experiment ID and variation key
|
330
|
+
#
|
331
|
+
# experiment_id - ID representing parent experiment of variation
|
332
|
+
# variation_key - Key of the variation
|
333
|
+
#
|
334
|
+
# Returns the variation or nil if not found
|
335
|
+
|
336
|
+
variation_key_map = @variation_key_map_by_experiment_id[experiment_id]
|
337
|
+
if variation_key_map
|
338
|
+
variation = variation_key_map[variation_key]
|
339
|
+
return variation['id'] if variation
|
340
|
+
|
341
|
+
@logger.log Logger::ERROR, "Variation key '#{variation_key}' is not in datafile."
|
342
|
+
@error_handler.handle_error InvalidVariationError
|
343
|
+
return nil
|
344
|
+
end
|
345
|
+
|
346
|
+
@logger.log Logger::ERROR, "Experiment id '#{experiment_id}' is not in datafile."
|
347
|
+
@error_handler.handle_error InvalidExperimentError
|
348
|
+
nil
|
349
|
+
end
|
350
|
+
|
278
351
|
def get_variation_id_from_key(experiment_key, variation_key)
|
279
352
|
# Get variation ID given experiment key and variation key
|
280
353
|
#
|
@@ -298,17 +371,17 @@ module Optimizely
|
|
298
371
|
nil
|
299
372
|
end
|
300
373
|
|
301
|
-
def get_whitelisted_variations(
|
302
|
-
# Retrieves whitelisted variations for a given experiment
|
374
|
+
def get_whitelisted_variations(experiment_id)
|
375
|
+
# Retrieves whitelisted variations for a given experiment id
|
303
376
|
#
|
304
|
-
#
|
377
|
+
# experiment_id - String id representing the experiment
|
305
378
|
#
|
306
379
|
# Returns whitelisted variations for the experiment or nil
|
307
380
|
|
308
|
-
experiment = @
|
381
|
+
experiment = @experiment_id_map[experiment_id]
|
309
382
|
return experiment['forcedVariations'] if experiment
|
310
383
|
|
311
|
-
@logger.log Logger::ERROR, "Experiment
|
384
|
+
@logger.log Logger::ERROR, "Experiment ID '#{experiment_id}' is not in datafile."
|
312
385
|
@error_handler.handle_error InvalidExperimentError
|
313
386
|
end
|
314
387
|
|
@@ -409,6 +482,16 @@ module Optimizely
|
|
409
482
|
@experiment_feature_map.key?(experiment_id)
|
410
483
|
end
|
411
484
|
|
485
|
+
def rollout_experiment?(experiment_id)
|
486
|
+
# Determines if given experiment is a rollout test.
|
487
|
+
#
|
488
|
+
# experiment_id - String experiment ID
|
489
|
+
#
|
490
|
+
# Returns true if experiment belongs to any rollout,
|
491
|
+
# false otherwise.
|
492
|
+
@rollout_experiment_id_map.key?(experiment_id)
|
493
|
+
end
|
494
|
+
|
412
495
|
private
|
413
496
|
|
414
497
|
def generate_key_map(array, key)
|
@@ -0,0 +1,28 @@
|
|
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
|
+
module Optimizely
|
19
|
+
module Decide
|
20
|
+
module OptimizelyDecideOption
|
21
|
+
DISABLE_DECISION_EVENT = 'DISABLE_DECISION_EVENT'
|
22
|
+
ENABLED_FLAGS_ONLY = 'ENABLED_FLAGS_ONLY'
|
23
|
+
IGNORE_USER_PROFILE_SERVICE = 'IGNORE_USER_PROFILE_SERVICE'
|
24
|
+
INCLUDE_REASONS = 'INCLUDE_REASONS'
|
25
|
+
EXCLUDE_VARIABLES = 'EXCLUDE_VARIABLES'
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|