kameleoon-client-ruby 2.2.0 → 3.0.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 +4 -4
- data/lib/kameleoon/client_readiness.rb +40 -0
- data/lib/kameleoon/configuration/data_file.rb +41 -0
- data/lib/kameleoon/configuration/feature_flag.rb +3 -2
- data/lib/kameleoon/configuration/rule.rb +18 -3
- data/lib/kameleoon/configuration/settings.rb +3 -1
- data/lib/kameleoon/configuration/variable.rb +0 -2
- data/lib/kameleoon/configuration/variation_exposition.rb +0 -2
- data/lib/kameleoon/data/browser.rb +1 -1
- data/lib/kameleoon/data/conversion.rb +1 -1
- data/lib/kameleoon/data/custom_data.rb +10 -14
- data/lib/kameleoon/data/data.rb +22 -3
- data/lib/kameleoon/data/device.rb +2 -1
- data/lib/kameleoon/data/manager/assigned_variation.rb +38 -0
- data/lib/kameleoon/data/manager/data_array_storage.rb +43 -0
- data/lib/kameleoon/data/manager/data_map_storage.rb +43 -0
- data/lib/kameleoon/data/manager/page_view_visit.rb +19 -0
- data/lib/kameleoon/data/manager/visitor.rb +142 -0
- data/lib/kameleoon/data/manager/visitor_manager.rb +71 -0
- data/lib/kameleoon/data/page_view.rb +2 -1
- data/lib/kameleoon/exceptions.rb +30 -35
- data/lib/kameleoon/hybrid/manager.rb +13 -31
- data/lib/kameleoon/{client.rb → kameleoon_client.rb} +165 -333
- data/lib/kameleoon/kameleoon_client_config.rb +91 -0
- data/lib/kameleoon/kameleoon_client_factory.rb +42 -0
- data/lib/kameleoon/network/activity_event.rb +6 -3
- data/lib/kameleoon/network/cookie/cookie_manager.rb +84 -0
- data/lib/kameleoon/network/net_provider.rb +5 -37
- data/lib/kameleoon/network/network_manager.rb +8 -7
- data/lib/kameleoon/network/request.rb +3 -2
- data/lib/kameleoon/network/response.rb +0 -8
- data/lib/kameleoon/network/url_provider.rb +5 -3
- data/lib/kameleoon/targeting/conditions/browser_condition.rb +2 -3
- data/lib/kameleoon/targeting/conditions/conversion_condition.rb +12 -4
- data/lib/kameleoon/targeting/conditions/custom_datum.rb +19 -13
- data/lib/kameleoon/targeting/conditions/device_condition.rb +3 -4
- data/lib/kameleoon/targeting/conditions/exclusive_experiment.rb +2 -1
- data/lib/kameleoon/targeting/conditions/page_title_condition.rb +11 -4
- data/lib/kameleoon/targeting/conditions/page_url_condition.rb +18 -4
- data/lib/kameleoon/targeting/conditions/string_value_condition.rb +2 -0
- data/lib/kameleoon/targeting/conditions/target_experiment.rb +11 -6
- data/lib/kameleoon/utils.rb +41 -4
- data/lib/kameleoon/version.rb +1 -1
- data/lib/kameleoon.rb +4 -2
- metadata +14 -9
- data/lib/kameleoon/configuration/experiment.rb +0 -42
- data/lib/kameleoon/cookie.rb +0 -88
- data/lib/kameleoon/factory.rb +0 -29
- data/lib/kameleoon/network/experiment_event.rb +0 -35
- data/lib/kameleoon/storage/variation_storage.rb +0 -42
- data/lib/kameleoon/storage/visitor_variation.rb +0 -20
|
@@ -1,27 +1,25 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'kameleoon/client_readiness'
|
|
3
4
|
require 'kameleoon/targeting/models'
|
|
4
5
|
require 'kameleoon/exceptions'
|
|
5
|
-
require 'kameleoon/cookie'
|
|
6
6
|
require 'kameleoon/data/custom_data'
|
|
7
7
|
require 'kameleoon/data/user_agent'
|
|
8
|
+
require 'kameleoon/data/manager/assigned_variation'
|
|
9
|
+
require 'kameleoon/data/manager/visitor_manager'
|
|
8
10
|
require 'kameleoon/configuration/feature_flag'
|
|
9
11
|
require 'kameleoon/configuration/variation'
|
|
10
|
-
require 'kameleoon/configuration/
|
|
12
|
+
require 'kameleoon/configuration/data_file'
|
|
11
13
|
require 'kameleoon/network/activity_event'
|
|
12
|
-
require 'kameleoon/network/experiment_event'
|
|
13
14
|
require 'kameleoon/network/url_provider'
|
|
14
15
|
require 'kameleoon/network/network_manager'
|
|
16
|
+
require 'kameleoon/network/cookie/cookie_manager'
|
|
15
17
|
require 'kameleoon/real_time/real_time_configuration_service'
|
|
16
|
-
require 'kameleoon/storage/variation_storage'
|
|
17
18
|
require 'kameleoon/hybrid/manager'
|
|
18
19
|
require 'kameleoon/storage/cache_factory'
|
|
19
20
|
require 'rufus/scheduler'
|
|
20
21
|
require 'yaml'
|
|
21
22
|
require 'json'
|
|
22
|
-
require 'em-synchrony'
|
|
23
|
-
require 'em-synchrony/em-http'
|
|
24
|
-
require 'em-synchrony/fiber_iterator'
|
|
25
23
|
require 'objspace'
|
|
26
24
|
require 'time'
|
|
27
25
|
require 'ostruct'
|
|
@@ -30,45 +28,37 @@ module Kameleoon
|
|
|
30
28
|
##
|
|
31
29
|
# Client for Kameleoon
|
|
32
30
|
#
|
|
33
|
-
class
|
|
34
|
-
include Cookie
|
|
31
|
+
class KameleoonClient
|
|
35
32
|
include Exception
|
|
36
33
|
|
|
34
|
+
attr_reader :site_code
|
|
35
|
+
|
|
37
36
|
##
|
|
38
|
-
# You should create
|
|
37
|
+
# You should create KameleoonClient with the Client Factory only.
|
|
39
38
|
#
|
|
40
|
-
def initialize(site_code,
|
|
41
|
-
|
|
39
|
+
def initialize(site_code, config)
|
|
40
|
+
raise Exception::SiteCodeIsEmpty, 'Provided site_sode is empty' if site_code&.empty? != false
|
|
41
|
+
|
|
42
42
|
@site_code = site_code
|
|
43
|
-
@
|
|
44
|
-
refresh_interval = config['actions_configuration_refresh_interval']
|
|
45
|
-
@interval = refresh_interval.nil? ? interval : "#{refresh_interval}m"
|
|
43
|
+
@config = config
|
|
46
44
|
@real_time_configuration_service = nil
|
|
47
45
|
@update_configuration_handler = nil
|
|
48
46
|
@fetch_configuration_update_job = nil
|
|
49
|
-
@
|
|
50
|
-
@
|
|
51
|
-
@
|
|
52
|
-
@environment = config['environment'] || DEFAULT_ENVIRONMENT
|
|
53
|
-
@settings = Kameleoon::Configuration::Settings.new
|
|
54
|
-
@verbose_mode = config['verbose_mode'] || false
|
|
55
|
-
@experiments = []
|
|
56
|
-
@feature_flags = []
|
|
57
|
-
@data = {}
|
|
58
|
-
@user_agents = {}
|
|
59
|
-
@variation_storage = Kameleoon::Storage::VariationStorage.new
|
|
60
|
-
@hybrid_manager = Kameleoon::Hybrid::ManagerImpl.new(
|
|
61
|
-
CACHE_EXPIRATION_TIMEOUT,
|
|
62
|
-
CACHE_EXPIRATION_TIMEOUT * 3,
|
|
63
|
-
Kameleoon::Storage::CacheFactoryImpl.new,
|
|
64
|
-
method(:log)
|
|
65
|
-
)
|
|
47
|
+
@data_file = Configuration::DataFile.new(config.environment)
|
|
48
|
+
@visitor_manager = Kameleoon::DataManager::VisitorManager.new(config.session_duration_second)
|
|
49
|
+
@hybrid_manager = Kameleoon::Hybrid::ManagerImpl.new(HYBRID_EXPIRATION_TIME, method(:log))
|
|
66
50
|
@network_manager = Network::NetworkManager.new(
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
Network::UrlProvider.new(
|
|
51
|
+
config.environment,
|
|
52
|
+
config.default_timeout_millisecond,
|
|
53
|
+
Network::UrlProvider.new(site_code, Network::UrlProvider::DEFAULT_DATA_API_URL),
|
|
70
54
|
method(:log)
|
|
71
55
|
)
|
|
56
|
+
@cookie_manager = Network::Cookie::CookieManager.new(config.top_level_domain)
|
|
57
|
+
@readiness = ClientReadiness.new
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def wait_init
|
|
61
|
+
@readiness.wait
|
|
72
62
|
end
|
|
73
63
|
|
|
74
64
|
##
|
|
@@ -80,8 +70,8 @@ module Kameleoon
|
|
|
80
70
|
# @note The implementation logic is described here:
|
|
81
71
|
# First we check if a kameleoonVisitorCode cookie or query parameter associated with the current HTTP request can be
|
|
82
72
|
# found. If so, we will use this as the visitor identifier. If no cookie / parameter is found in the current
|
|
83
|
-
# request, we either randomly generate a new identifier, or use the
|
|
84
|
-
# is passed. This allows our customers to use their own identifiers as visitor codes, should they wish to.
|
|
73
|
+
# request, we either randomly generate a new identifier, or use the default_visitor_code argument as identifier if
|
|
74
|
+
# it is passed. This allows our customers to use their own identifiers as visitor codes, should they wish to.
|
|
85
75
|
# This can have the added benefit of matching Kameleoon visitors with their own users without any additional
|
|
86
76
|
# look-ups in a matching table.
|
|
87
77
|
# In any case, the server-side (via HTTP header) kameleoonVisitorCode cookie is set with the value. Then this
|
|
@@ -97,61 +87,15 @@ module Kameleoon
|
|
|
97
87
|
# cookies = {'kameleoonVisitorCode' => '1234asdf4321fdsa'}
|
|
98
88
|
# visitor_code = get_visitor_code(cookies, 'my-domaine.com')
|
|
99
89
|
#
|
|
100
|
-
def get_visitor_code(cookies,
|
|
101
|
-
|
|
90
|
+
def get_visitor_code(cookies, default_visitor_code = nil)
|
|
91
|
+
@cookie_manager.get_or_add(cookies, default_visitor_code)
|
|
102
92
|
end
|
|
103
93
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
##
|
|
111
|
-
# Trigger an experiment.
|
|
112
|
-
#
|
|
113
|
-
# If such a visitor_code has never been associated with any variation, the SDK returns a randomly selected variation
|
|
114
|
-
# If a user with a given visitor_code is already registered with a variation, it will detect the previously
|
|
115
|
-
# registered variation and return the variation_id.
|
|
116
|
-
# You have to make sure that proper error handling is set up in your code as shown in the example to the right to
|
|
117
|
-
# catch potential exceptions.
|
|
118
|
-
#
|
|
119
|
-
# @param [String] visitor_code Visitor code
|
|
120
|
-
# @param [Integer] experiment_id Id of the experiment you want to trigger.
|
|
121
|
-
#
|
|
122
|
-
# @return [Integer] Id of the variation
|
|
123
|
-
#
|
|
124
|
-
# @raise [Kameleoon::Exception::ExperimentConfigurationNotFound] Raise when experiment configuration is not found
|
|
125
|
-
# @raise [Kameleoon::Exception::NotAllocated] The visitor triggered the experiment, but did not activate it.
|
|
126
|
-
# Usually, this happens because the user has been associated with excluded traffic
|
|
127
|
-
# @raise [Kameleoon::Exception::NotTargeted] The visitor is not targeted by the experiment, as the
|
|
128
|
-
# associated targeting segment conditions were not fulfilled. He should see the reference variation
|
|
129
|
-
# @raise [Kameleoon::Exception::VisitorCodeNotValid] If the visitor code is empty or longer than 255 chars
|
|
130
|
-
#
|
|
131
|
-
def trigger_experiment(visitor_code, experiment_id)
|
|
132
|
-
check_visitor_code(visitor_code)
|
|
133
|
-
experiment = @experiments.find { |exp| exp.id.to_s == experiment_id.to_s }
|
|
134
|
-
if experiment.nil?
|
|
135
|
-
raise Exception::ExperimentConfigurationNotFound.new(experiment_id),
|
|
136
|
-
"Experiment #{experiment_id} is not found"
|
|
137
|
-
end
|
|
138
|
-
check_site_code_enable(experiment)
|
|
139
|
-
targeted = check_targeting(visitor_code, experiment_id, experiment)
|
|
140
|
-
if targeted
|
|
141
|
-
# saved_variation = get_valid_saved_variation(visitor_code, experiment)
|
|
142
|
-
variation_id = calculate_variation_for_experiment(visitor_code, experiment)
|
|
143
|
-
save_variation(visitor_code, experiment_id, variation_id)
|
|
144
|
-
end
|
|
145
|
-
_send_tracking_request(visitor_code, experiment_id, variation_id)
|
|
146
|
-
unless targeted
|
|
147
|
-
raise Exception::NotTargeted.new(visitor_code),
|
|
148
|
-
"Experiment #{experiment_id} is not targeted for visitor #{visitor_code}"
|
|
149
|
-
end
|
|
150
|
-
if variation_id.nil?
|
|
151
|
-
raise Exception::NotAllocated.new(visitor_code),
|
|
152
|
-
"Experiment #{experiment_id} is not active for visitor #{visitor_code}"
|
|
153
|
-
end
|
|
154
|
-
variation_id
|
|
94
|
+
def set_legal_consent(visitor_code, consent, cookies = nil)
|
|
95
|
+
Utils::VisitorCode.validate(visitor_code)
|
|
96
|
+
visitor = @visitor_manager.get_or_create_visitor(visitor_code)
|
|
97
|
+
visitor.legal_consent = consent
|
|
98
|
+
@cookie_manager.update(visitor_code, consent, cookies)
|
|
155
99
|
end
|
|
156
100
|
|
|
157
101
|
##
|
|
@@ -165,19 +109,12 @@ module Kameleoon
|
|
|
165
109
|
# @param [String] visitor_code Visitor code
|
|
166
110
|
# @param [...Data] data Data to associate with the visitor code
|
|
167
111
|
#
|
|
168
|
-
# @raise [Kameleoon::Exception::
|
|
112
|
+
# @raise [Kameleoon::Exception::VisitorCodeInvalid] If the visitor code is empty or longer than 255 chars
|
|
169
113
|
#
|
|
170
114
|
def add_data(visitor_code, *args)
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
if data_element.is_a?(UserAgent)
|
|
175
|
-
add_user_agent_data(visitor_code, data_element)
|
|
176
|
-
next
|
|
177
|
-
end
|
|
178
|
-
@data[visitor_code] = [] unless @data.key?(visitor_code)
|
|
179
|
-
@data[visitor_code].push(data_element)
|
|
180
|
-
end
|
|
115
|
+
Utils::VisitorCode.validate(visitor_code)
|
|
116
|
+
visitor = @visitor_manager.get_or_create_visitor(visitor_code)
|
|
117
|
+
visitor.add_data(method(:log), *args)
|
|
181
118
|
end
|
|
182
119
|
|
|
183
120
|
##
|
|
@@ -193,10 +130,10 @@ module Kameleoon
|
|
|
193
130
|
# @param [Integer] goal_id Id of the goal
|
|
194
131
|
# @param [Float] revenue Optional - Revenue of the conversion.
|
|
195
132
|
#
|
|
196
|
-
# @raise [Kameleoon::Exception::
|
|
133
|
+
# @raise [Kameleoon::Exception::VisitorCodeInvalid] If the visitor code is empty or longer than 255 chars
|
|
197
134
|
#
|
|
198
135
|
def track_conversion(visitor_code, goal_id, revenue = 0.0)
|
|
199
|
-
|
|
136
|
+
Utils::VisitorCode.validate(visitor_code)
|
|
200
137
|
add_data(visitor_code, Conversion.new(goal_id, revenue))
|
|
201
138
|
flush(visitor_code)
|
|
202
139
|
end
|
|
@@ -210,74 +147,17 @@ module Kameleoon
|
|
|
210
147
|
#
|
|
211
148
|
# @param [String] visitor_code Optional field - Visitor code, without visitor code it flush all of the data
|
|
212
149
|
#
|
|
213
|
-
# @raise [Kameleoon::Exception::
|
|
150
|
+
# @raise [Kameleoon::Exception::VisitorCodeInvalid] If the visitor code is not nil and is empty or longer than 255 chars
|
|
214
151
|
#
|
|
215
152
|
def flush(visitor_code = nil)
|
|
216
|
-
|
|
217
|
-
if
|
|
218
|
-
_send_tracking_request(visitor_code)
|
|
219
|
-
else
|
|
220
|
-
@data.select { |_, values| values.any? { |data| !data.sent } }.each_key { |key| flush(key) }
|
|
221
|
-
end
|
|
222
|
-
end
|
|
223
|
-
|
|
224
|
-
##
|
|
225
|
-
# Obtain variation associated data.
|
|
226
|
-
#
|
|
227
|
-
# To retrieve JSON data associated with a variation, call the get_variation_associated_data method of our SDK.
|
|
228
|
-
# The JSON data usually represents some metadata of the variation, and can be configured on our web application
|
|
229
|
-
# interface or via our Automation API.
|
|
230
|
-
# This method takes the variationID as a parameter and will return the data as a json string.
|
|
231
|
-
# It will throw an exception () if the variation ID is wrong or corresponds to an experiment that is not yet online.
|
|
232
|
-
#
|
|
233
|
-
# @param [Integer] variation_id
|
|
234
|
-
#
|
|
235
|
-
# @return [Hash] Hash object of the json object.
|
|
236
|
-
#
|
|
237
|
-
# @raise [Kameleoon::Exception::VariationNotFound] Raise exception if the variation is not found.
|
|
238
|
-
#
|
|
239
|
-
def get_variation_associated_data(variation_id)
|
|
240
|
-
variation = @experiments.map(&:variations).flatten.select { |var| var['id'].to_i == variation_id.to_i }.first
|
|
241
|
-
if variation.nil?
|
|
242
|
-
raise Exception::VariationConfigurationNotFound.new(variation_id),
|
|
243
|
-
"Variation key #{variation_id} not found"
|
|
153
|
+
Utils::VisitorCode.validate(visitor_code) unless visitor_code.nil?
|
|
154
|
+
if visitor_code.nil?
|
|
155
|
+
@visitor_manager.enumerate { |visitor_code, visitor| _send_tracking_request(visitor_code, visitor, false) }
|
|
244
156
|
else
|
|
245
|
-
|
|
157
|
+
_send_tracking_request(visitor_code, nil, true)
|
|
246
158
|
end
|
|
247
159
|
end
|
|
248
160
|
|
|
249
|
-
# DEPRECATED. Please use `get_variation_associated_data` instead.
|
|
250
|
-
def obtain_variation_associated_data(variation_id)
|
|
251
|
-
warn '[DEPRECATION] `obtain_variation_associated_data` is deprecated.
|
|
252
|
-
Please use `get_variation_associated_data` instead.'
|
|
253
|
-
get_variation_associated_data(variation_id)
|
|
254
|
-
end
|
|
255
|
-
|
|
256
|
-
# #
|
|
257
|
-
# Activate a feature toggle.
|
|
258
|
-
|
|
259
|
-
# This method takes a visitor_code and feature_key (or feature_id) as mandatory arguments to check if the specified
|
|
260
|
-
# feature will be active for a given user.
|
|
261
|
-
# If such a user has never been associated with this feature flag, the SDK returns a boolean value randomly
|
|
262
|
-
# (true if the user should have this feature or false if not). If a user with a given visitorCode is already
|
|
263
|
-
# registered with this feature flag, it will detect the previous featureFlag value.
|
|
264
|
-
# You have to make sure that proper error handling is set up in your code as shown in the example
|
|
265
|
-
# to the right to catch potential exceptions.
|
|
266
|
-
|
|
267
|
-
# @param [String] visitor_code
|
|
268
|
-
# @param [String | Integer] feature_key
|
|
269
|
-
|
|
270
|
-
# @raise [Kameleoon::Exception::FeatureConfigurationNotFound] Feature Flag isn't found in this configuration
|
|
271
|
-
# @raise [Kameleoon::Exception::NotTargeted] The visitor is not targeted by the experiment, as the
|
|
272
|
-
# associated targeting segment conditions were not fulfilled. He should see the reference variation
|
|
273
|
-
# @raise [Kameleoon::Exception::VisitorCodeNotValid] If the visitor code is empty or longer than 255 chars
|
|
274
|
-
#
|
|
275
|
-
# DEPRECATED. Please use `feature_active?` instead.
|
|
276
|
-
def activate_feature(visitor_code, feature_key)
|
|
277
|
-
warn '[DEPRECATION] `activate_feature` is deprecated. Please use `feature_active?` instead.'
|
|
278
|
-
feature_active?(visitor_code, feature_key)
|
|
279
|
-
end
|
|
280
|
-
|
|
281
161
|
##
|
|
282
162
|
# Check if feature is active for a given visitor code
|
|
283
163
|
#
|
|
@@ -292,12 +172,14 @@ module Kameleoon
|
|
|
292
172
|
# @param [String] visitor_code Unique identifier of the user. This field is mandatory.
|
|
293
173
|
# @param [String] feature_key Key of the feature flag you want to expose to a user. This field is mandatory.
|
|
294
174
|
#
|
|
295
|
-
# @raise [Kameleoon::Exception::
|
|
296
|
-
# @raise [Kameleoon::Exception::
|
|
175
|
+
# @raise [Kameleoon::Exception::FeatureNotFound] Feature Flag isn't found in this configuration
|
|
176
|
+
# @raise [Kameleoon::Exception::VisitorCodeInvalid] If the visitor code is empty or longer than 255 chars
|
|
297
177
|
#
|
|
298
178
|
def feature_active?(visitor_code, feature_key)
|
|
299
179
|
_, variation_key = _get_feature_variation_key(visitor_code, feature_key)
|
|
300
180
|
variation_key != Kameleoon::Configuration::VariationType::VARIATION_OFF
|
|
181
|
+
rescue Exception::FeatureEnvironmentDisabled
|
|
182
|
+
false
|
|
301
183
|
end
|
|
302
184
|
|
|
303
185
|
#
|
|
@@ -312,8 +194,10 @@ module Kameleoon
|
|
|
312
194
|
# @param [String] visitor_code
|
|
313
195
|
# @param [String] feature_key
|
|
314
196
|
#
|
|
315
|
-
# @raise [Kameleoon::Exception::
|
|
316
|
-
# @raise [Kameleoon::Exception::
|
|
197
|
+
# @raise [Kameleoon::Exception::FeatureNotFound] Feature Flag isn't found in this configuration
|
|
198
|
+
# @raise [Kameleoon::Exception::VisitorCodeInvalid] If the visitor code is empty or longer than 255 chars
|
|
199
|
+
# @raise [Kameleoon::Exception::FeatureEnvironmentDisabled] If the requested feature flag is disabled for
|
|
200
|
+
# the current environment
|
|
317
201
|
#
|
|
318
202
|
def get_feature_variation_key(visitor_code, feature_key)
|
|
319
203
|
_, variation_key = _get_feature_variation_key(visitor_code, feature_key)
|
|
@@ -329,9 +213,11 @@ module Kameleoon
|
|
|
329
213
|
# @param [String] feature_key
|
|
330
214
|
# @param [String] variable_name
|
|
331
215
|
#
|
|
332
|
-
# @raise [Kameleoon::Exception::
|
|
216
|
+
# @raise [Kameleoon::Exception::FeatureNotFound] Feature Flag isn't found in this configuration
|
|
333
217
|
# @raise [Kameleoon::Exception::FeatureVariableNotFound]
|
|
334
|
-
# @raise [Kameleoon::Exception::
|
|
218
|
+
# @raise [Kameleoon::Exception::VisitorCodeInvalid] If the visitor code is empty or longer than 255 chars
|
|
219
|
+
# @raise [Kameleoon::Exception::FeatureEnvironmentDisabled] If the requested feature flag is disabled for
|
|
220
|
+
# the current environment
|
|
335
221
|
#
|
|
336
222
|
def get_feature_variable(visitor_code, feature_key, variable_name)
|
|
337
223
|
feature_flag, variation_key = _get_feature_variation_key(visitor_code, feature_key)
|
|
@@ -354,14 +240,15 @@ module Kameleoon
|
|
|
354
240
|
# @param [String] feature_key
|
|
355
241
|
# @param [String] variation_key
|
|
356
242
|
#
|
|
357
|
-
# @raise [Kameleoon::Exception::
|
|
358
|
-
# @raise [Kameleoon::Exception::
|
|
243
|
+
# @raise [Kameleoon::Exception::FeatureNotFound] Feature Flag isn't found in this configuration
|
|
244
|
+
# @raise [Kameleoon::Exception::FeatureVariationNotFound]
|
|
245
|
+
# @raise [Kameleoon::Exception::FeatureEnvironmentDisabled]
|
|
359
246
|
#
|
|
360
|
-
def
|
|
361
|
-
feature_flag =
|
|
247
|
+
def get_feature_variation_variables(feature_key, variation_key)
|
|
248
|
+
feature_flag = @data_file.get_feature_flag(feature_key)
|
|
362
249
|
variation = feature_flag.get_variation_key(variation_key)
|
|
363
250
|
if variation.nil?
|
|
364
|
-
raise Exception::
|
|
251
|
+
raise Exception::FeatureVariationNotFound.new(variation_key),
|
|
365
252
|
"Variation key #{variation_key} not found"
|
|
366
253
|
end
|
|
367
254
|
variables = {}
|
|
@@ -369,27 +256,6 @@ module Kameleoon
|
|
|
369
256
|
variables
|
|
370
257
|
end
|
|
371
258
|
|
|
372
|
-
##
|
|
373
|
-
# Retrieve a feature variable.
|
|
374
|
-
#
|
|
375
|
-
# A feature variable can be changed easily via our web application.
|
|
376
|
-
#
|
|
377
|
-
# @param [String | Integer] feature_key
|
|
378
|
-
# @param [String] variable_key
|
|
379
|
-
#
|
|
380
|
-
# @raise [Kameleoon::Exception::FeatureConfigurationNotFound] Feature Flag isn't found in this configuration
|
|
381
|
-
# @raise [Kameleoon::Exception::FeatureVariableNotFound]
|
|
382
|
-
#
|
|
383
|
-
# DEPRECATED. Please use `get_feature_variable` instead.
|
|
384
|
-
def obtain_feature_variable(feature_key, variable_key)
|
|
385
|
-
warn '[DEPRECATION] `obtain_feature_variable` is deprecated. Please use `get_feature_variable` instead.'
|
|
386
|
-
all_variables = get_feature_all_variables(
|
|
387
|
-
feature_key,
|
|
388
|
-
Configuration::VariationType::VARIATION_OFF
|
|
389
|
-
)
|
|
390
|
-
all_variables[variable_key]
|
|
391
|
-
end
|
|
392
|
-
|
|
393
259
|
##
|
|
394
260
|
# The get_remote_data method allows you to retrieve data (according to a key passed as argument)
|
|
395
261
|
# stored on a remote Kameleoon server. Usually data will be stored on our remote
|
|
@@ -407,13 +273,6 @@ module Kameleoon
|
|
|
407
273
|
JSON.parse(response) if response
|
|
408
274
|
end
|
|
409
275
|
|
|
410
|
-
##
|
|
411
|
-
# DEPRECATED. Please use `get_feature_variable` instead.
|
|
412
|
-
def retrieve_data_from_remote_source(key, timeout = @default_timeout)
|
|
413
|
-
warn '[DEPRECATION] `retrieve_data_from_remote_source` is deprecated. Please use `get_remote_date` instead.'
|
|
414
|
-
get_remote_data(key, timeout)
|
|
415
|
-
end
|
|
416
|
-
|
|
417
276
|
##
|
|
418
277
|
# The get_remote_visitor_data is a method for retrieving custom data for
|
|
419
278
|
# the latest visit of `visitor_code` from Kameleoon Data API and optionally adding it
|
|
@@ -434,55 +293,27 @@ module Kameleoon
|
|
|
434
293
|
data_array
|
|
435
294
|
end
|
|
436
295
|
|
|
437
|
-
##
|
|
438
|
-
# Returns a list of all experiment ids
|
|
439
|
-
#
|
|
440
|
-
# @return [Array] array of all experiment ids
|
|
441
|
-
def get_experiment_list # rubocop:disable Naming/AccessorMethodName
|
|
442
|
-
@experiments.map { |it| it.id.to_i }
|
|
443
|
-
end
|
|
444
|
-
|
|
445
|
-
##
|
|
446
|
-
# Returns a list of all experiment ids targeted for a visitor
|
|
447
|
-
# if only_allocated is `true` returns a list of allocated experiments for a visitor
|
|
448
|
-
#
|
|
449
|
-
# @raise [Kameleoon::Exception::VisitorCodeNotValid] If the visitor code is empty or longer than 255 chars
|
|
450
|
-
#
|
|
451
|
-
# @return [Array] array of all experiment ids accorging to a only_allocated parameter
|
|
452
|
-
def get_experiment_list_for_visitor(visitor_code, only_allocated: true)
|
|
453
|
-
list_ids = []
|
|
454
|
-
@experiments.each do |experiment|
|
|
455
|
-
next unless check_targeting(visitor_code, experiment.id.to_i, experiment)
|
|
456
|
-
next if only_allocated && calculate_variation_for_experiment(visitor_code, experiment).nil?
|
|
457
|
-
|
|
458
|
-
list_ids.push(experiment.id.to_i)
|
|
459
|
-
end
|
|
460
|
-
list_ids
|
|
461
|
-
end
|
|
462
|
-
|
|
463
296
|
##
|
|
464
297
|
# Returns a list of all feature flag keys
|
|
465
298
|
#
|
|
466
299
|
# @return [Array] array of all feature flag keys
|
|
467
300
|
def get_feature_list # rubocop:disable Naming/AccessorMethodName
|
|
468
|
-
@feature_flags.
|
|
301
|
+
@data_file.feature_flags.keys
|
|
469
302
|
end
|
|
470
303
|
|
|
471
304
|
##
|
|
472
305
|
# Returns a list of active feature flag keys for a visitor
|
|
473
306
|
#
|
|
474
|
-
# @raise [Kameleoon::Exception::
|
|
307
|
+
# @raise [Kameleoon::Exception::VisitorCodeInvalid] If the visitor code is empty or longer than 255 chars
|
|
475
308
|
#
|
|
476
309
|
# @return [Array] array of active feature flag keys for a visitor
|
|
477
310
|
def get_active_feature_list_for_visitor(visitor_code)
|
|
478
|
-
|
|
311
|
+
Utils::VisitorCode.validate(visitor_code)
|
|
479
312
|
list_keys = []
|
|
480
|
-
@feature_flags.each do |feature_flag|
|
|
313
|
+
@data_file.feature_flags.each do |feature_key, feature_flag|
|
|
481
314
|
variation, rule, = _calculate_variation_key_for_feature(visitor_code, feature_flag)
|
|
482
315
|
variation_key = _get_variation_key(variation, rule, feature_flag)
|
|
483
|
-
if variation_key != Kameleoon::Configuration::VariationType::VARIATION_OFF
|
|
484
|
-
list_keys.push(feature_flag.feature_key)
|
|
485
|
-
end
|
|
316
|
+
list_keys.push(feature_key) if variation_key != Kameleoon::Configuration::VariationType::VARIATION_OFF
|
|
486
317
|
end
|
|
487
318
|
list_keys
|
|
488
319
|
end
|
|
@@ -507,44 +338,49 @@ module Kameleoon
|
|
|
507
338
|
# @return [String] JavasScript code to be inserted in your page to send automatically
|
|
508
339
|
# the exposure events to the analytics solution you are using.
|
|
509
340
|
def get_engine_tracking_code(visitor_code)
|
|
510
|
-
@
|
|
341
|
+
visitor_variations = @visitor_manager.get_visitor(visitor_code)&.variations
|
|
342
|
+
@hybrid_manager.get_engine_tracking_code(visitor_variations)
|
|
511
343
|
end
|
|
512
344
|
|
|
513
345
|
private
|
|
514
346
|
|
|
515
|
-
|
|
516
|
-
DEFAULT_ENVIRONMENT = 'production'
|
|
517
|
-
CACHE_EXPIRATION_TIMEOUT = 5
|
|
518
|
-
attr :site_code, :client_id, :client_secret, :access_token, :experiments, :feature_flags, :scheduler, :data,
|
|
519
|
-
:tracking_url, :default_timeout, :interval, :memory_limit, :verbose_mode
|
|
347
|
+
HYBRID_EXPIRATION_TIME = 5
|
|
520
348
|
|
|
521
|
-
def
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
349
|
+
def fetch_configuration_initially
|
|
350
|
+
log('Initial configuration fetch is started.')
|
|
351
|
+
Thread.new do
|
|
352
|
+
ok = false
|
|
353
|
+
begin
|
|
354
|
+
ok = obtain_configuration
|
|
355
|
+
log('Initial configuration fetch failed') unless ok
|
|
356
|
+
rescue StandardError => e
|
|
357
|
+
log("Initial configuration fetch failed: #{e}")
|
|
358
|
+
end
|
|
359
|
+
@readiness.set(ok)
|
|
360
|
+
manage_configuration_update(@data_file.settings.real_time_update) if ok
|
|
525
361
|
end
|
|
526
362
|
end
|
|
527
363
|
|
|
528
364
|
def fetch_configuration_job(time_stamp = nil)
|
|
529
|
-
|
|
365
|
+
Thread.new do
|
|
366
|
+
ok = false
|
|
530
367
|
begin
|
|
531
368
|
ok = obtain_configuration(time_stamp)
|
|
532
|
-
if !ok && @settings.real_time_update
|
|
533
|
-
@settings.real_time_update = false
|
|
534
|
-
log('Switching to polling mode due to failed fetch')
|
|
535
|
-
end
|
|
536
369
|
rescue StandardError => e
|
|
537
370
|
log("Error occurred during configuration fetching: #{e}")
|
|
538
371
|
end
|
|
539
|
-
|
|
540
|
-
|
|
372
|
+
if !ok && @data_file.settings.real_time_update
|
|
373
|
+
@data_file.settings.real_time_update = false
|
|
374
|
+
log('Switching to polling mode due to failed fetch')
|
|
375
|
+
end
|
|
376
|
+
manage_configuration_update(@data_file.settings.real_time_update)
|
|
541
377
|
end
|
|
542
378
|
end
|
|
543
379
|
|
|
544
380
|
def start_configuration_update_job_if_needed
|
|
545
381
|
return unless @fetch_configuration_update_job.nil?
|
|
546
382
|
|
|
547
|
-
@fetch_configuration_update_job = Rufus::Scheduler.singleton.schedule_every @
|
|
383
|
+
@fetch_configuration_update_job = Rufus::Scheduler.singleton.schedule_every @config.refresh_interval_second do
|
|
548
384
|
log('Scheduled job to fetch configuration is started.')
|
|
549
385
|
fetch_configuration_job
|
|
550
386
|
end
|
|
@@ -604,12 +440,10 @@ module Kameleoon
|
|
|
604
440
|
return false unless response
|
|
605
441
|
|
|
606
442
|
configuration = JSON.parse(response)
|
|
607
|
-
@
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
)
|
|
612
|
-
@settings.update(configuration['configuration'])
|
|
443
|
+
@data_file = Configuration::DataFile.new(@config.environment).init(configuration)
|
|
444
|
+
@cookie_manager.consent_required =
|
|
445
|
+
@data_file.settings.is_consent_required && !@data_file.has_any_targeted_delivery_rule
|
|
446
|
+
|
|
613
447
|
call_update_handler_if_needed(!time_stamp.nil?)
|
|
614
448
|
log "Feature flags are fetched: #{response.inspect}"
|
|
615
449
|
true
|
|
@@ -625,49 +459,16 @@ module Kameleoon
|
|
|
625
459
|
@update_configuration_handler.call
|
|
626
460
|
end
|
|
627
461
|
|
|
628
|
-
def
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
return key.to_s.to_i if threshold.negative?
|
|
633
|
-
end
|
|
634
|
-
nil
|
|
635
|
-
end
|
|
636
|
-
|
|
637
|
-
def find_feature_flag(feature_key)
|
|
638
|
-
if feature_key.is_a?(String)
|
|
639
|
-
feature_flag = @feature_flags.select { |ff| ff.feature_key == feature_key }.first
|
|
640
|
-
else
|
|
641
|
-
raise TypeError.new('Feature key should be a String or an Integer.'),
|
|
642
|
-
'Feature key should be a String or an Integer.'
|
|
643
|
-
end
|
|
644
|
-
error_message = "Feature #{feature_key} not found"
|
|
645
|
-
raise Exception::FeatureConfigurationNotFound.new(feature_key), error_message if feature_flag.nil?
|
|
646
|
-
|
|
647
|
-
feature_flag
|
|
648
|
-
end
|
|
649
|
-
|
|
650
|
-
def check_site_code_enable(campaign)
|
|
651
|
-
raise Exception::SiteCodeDisabled.new(site_code), site_code unless campaign.site_enabled
|
|
652
|
-
end
|
|
653
|
-
|
|
654
|
-
def data_not_sent(visitor_code)
|
|
655
|
-
@data.key?(visitor_code) ? @data[visitor_code].reject(&:sent) : []
|
|
462
|
+
def dispose
|
|
463
|
+
stop_configuration_update_job_if_needed
|
|
464
|
+
stop_real_time_configuration_service_if_needed
|
|
465
|
+
@visitor_manager.stop
|
|
656
466
|
end
|
|
657
467
|
|
|
658
468
|
def log(text)
|
|
659
469
|
print "Kameleoon SDK Log: #{text}\n" if @verbose_mode
|
|
660
470
|
end
|
|
661
471
|
|
|
662
|
-
def add_user_agent_data(visitor_code, user_agent)
|
|
663
|
-
@user_agents[visitor_code] = user_agent
|
|
664
|
-
end
|
|
665
|
-
|
|
666
|
-
def set_user_agent_to_headers(visitor_code, headers)
|
|
667
|
-
user_agent = @user_agents[visitor_code]
|
|
668
|
-
headers['User-Agent'] = user_agent.value unless user_agent.nil?
|
|
669
|
-
end
|
|
670
|
-
|
|
671
472
|
# Uncomment when using storage
|
|
672
473
|
# def get_valid_saved_variation(visitor_code, experiment)
|
|
673
474
|
# variation_id = @variation_storage.get_variation_id(visitor_code, experiment.id.to_i)
|
|
@@ -681,29 +482,38 @@ module Kameleoon
|
|
|
681
482
|
|
|
682
483
|
def check_targeting(visitor_code, campaign_id, exp_ff_rule)
|
|
683
484
|
segment = exp_ff_rule.targeting_segment
|
|
684
|
-
segment.nil?
|
|
485
|
+
return true if segment.nil?
|
|
486
|
+
|
|
487
|
+
visitor = @visitor_manager.get_visitor(visitor_code)
|
|
488
|
+
segment.check_tree(->(type) { get_condition_data(type, visitor, visitor_code, campaign_id) })
|
|
685
489
|
end
|
|
686
490
|
|
|
687
|
-
def get_condition_data(type, visitor_code, campaign_id)
|
|
491
|
+
def get_condition_data(type, visitor, visitor_code, campaign_id)
|
|
688
492
|
condition_data = nil
|
|
689
493
|
case type
|
|
690
|
-
when Kameleoon::Targeting::ConditionType::CUSTOM_DATUM
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
Kameleoon::Targeting::ConditionType::
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
condition_data =
|
|
494
|
+
when Kameleoon::Targeting::ConditionType::CUSTOM_DATUM
|
|
495
|
+
condition_data = visitor.custom_data unless visitor.nil?
|
|
496
|
+
when Kameleoon::Targeting::ConditionType::PAGE_TITLE,
|
|
497
|
+
Kameleoon::Targeting::ConditionType::PAGE_URL
|
|
498
|
+
condition_data = visitor.page_view_visits unless visitor.nil?
|
|
499
|
+
when Kameleoon::Targeting::ConditionType::DEVICE_TYPE
|
|
500
|
+
condition_data = visitor.device unless visitor.nil?
|
|
501
|
+
when Kameleoon::Targeting::ConditionType::BROWSER
|
|
502
|
+
condition_data = visitor.browser unless visitor.nil?
|
|
503
|
+
when Kameleoon::Targeting::ConditionType::CONVERSIONS
|
|
504
|
+
condition_data = visitor.conversions unless visitor.nil?
|
|
697
505
|
when Kameleoon::Targeting::ConditionType::SDK_LANGUAGE
|
|
698
506
|
condition_data = Kameleoon::Targeting::SdkInfo.new(Kameleoon::SDK_NAME, Kameleoon::SDK_VERSION)
|
|
699
507
|
when Kameleoon::Targeting::ConditionType::VISITOR_CODE
|
|
700
508
|
condition_data = visitor_code
|
|
701
509
|
when Kameleoon::Targeting::ConditionType::TARGET_EXPERIMENT
|
|
702
|
-
condition_data =
|
|
510
|
+
condition_data = visitor.variations unless visitor.nil?
|
|
703
511
|
when Kameleoon::Targeting::ConditionType::EXCLUSIVE_EXPERIMENT
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
512
|
+
unless visitor.nil?
|
|
513
|
+
condition_data = OpenStruct.new
|
|
514
|
+
condition_data.experiment_id = campaign_id
|
|
515
|
+
condition_data.storage = visitor.variations
|
|
516
|
+
end
|
|
707
517
|
end
|
|
708
518
|
condition_data
|
|
709
519
|
end
|
|
@@ -711,16 +521,21 @@ module Kameleoon
|
|
|
711
521
|
##
|
|
712
522
|
# helper method for getting variation key for feature flag
|
|
713
523
|
def _get_feature_variation_key(visitor_code, feature_key)
|
|
714
|
-
|
|
715
|
-
feature_flag =
|
|
524
|
+
Utils::VisitorCode.validate(visitor_code)
|
|
525
|
+
feature_flag = @data_file.get_feature_flag(feature_key)
|
|
716
526
|
variation, rule = _calculate_variation_key_for_feature(visitor_code, feature_flag)
|
|
717
527
|
variation_key = _get_variation_key(variation, rule, feature_flag)
|
|
528
|
+
visitor = nil
|
|
718
529
|
unless rule.nil?
|
|
719
530
|
experiment_id = rule.experiment_id
|
|
720
531
|
variation_id = variation.variation_id unless variation.nil?
|
|
721
|
-
|
|
532
|
+
visitor = @visitor_manager.get_or_create_visitor(visitor_code)
|
|
533
|
+
unless experiment_id.nil? || variation_id.nil?
|
|
534
|
+
as_variation = Kameleoon::DataManager::AssignedVariation.new(experiment_id, variation_id, rule.type)
|
|
535
|
+
visitor.assign_variation(as_variation)
|
|
536
|
+
end
|
|
722
537
|
end
|
|
723
|
-
_send_tracking_request(visitor_code,
|
|
538
|
+
_send_tracking_request(visitor_code, visitor)
|
|
724
539
|
[feature_flag, variation_key]
|
|
725
540
|
end
|
|
726
541
|
|
|
@@ -733,13 +548,13 @@ module Kameleoon
|
|
|
733
548
|
next unless check_targeting(visitor_code, feature_flag.id, rule)
|
|
734
549
|
|
|
735
550
|
# uses for rule exposition
|
|
736
|
-
hash_rule =
|
|
551
|
+
hash_rule = Utils::HashDouble.obtain_rule(visitor_code, rule.id, rule.respool_time)
|
|
737
552
|
# check main expostion for rule with hashRule
|
|
738
553
|
if hash_rule <= rule.exposition
|
|
739
554
|
return [rule.variation_by_exposition[0], rule] if rule.targeted_delivery_type?
|
|
740
555
|
|
|
741
556
|
# uses for variation's expositions
|
|
742
|
-
hash_variation =
|
|
557
|
+
hash_variation = Utils::HashDouble.obtain_rule(visitor_code, rule.experiment_id, rule.respool_time)
|
|
743
558
|
# get variation key with new hashVariation
|
|
744
559
|
variation = rule.get_variation(hash_variation)
|
|
745
560
|
return [variation, rule] unless variation.nil?
|
|
@@ -751,16 +566,8 @@ module Kameleoon
|
|
|
751
566
|
[nil, nil]
|
|
752
567
|
end
|
|
753
568
|
|
|
754
|
-
def save_variation(visitor_code, experiment_id, variation_id)
|
|
755
|
-
return if experiment_id.nil? || variation_id.nil?
|
|
756
|
-
|
|
757
|
-
@variation_storage.update_variation(visitor_code, experiment_id, variation_id)
|
|
758
|
-
@hybrid_manager.add_variation(visitor_code, experiment_id, variation_id)
|
|
759
|
-
end
|
|
760
|
-
|
|
761
569
|
def _get_variation_key(var_by_exp, rule, feature_flag)
|
|
762
570
|
return var_by_exp.variation_key unless var_by_exp.nil?
|
|
763
|
-
|
|
764
571
|
return Kameleoon::Configuration::VariationType::VARIATION_OFF if !rule.nil? && rule.experimentation_type?
|
|
765
572
|
|
|
766
573
|
feature_flag.default_variation_key
|
|
@@ -768,16 +575,37 @@ module Kameleoon
|
|
|
768
575
|
|
|
769
576
|
##
|
|
770
577
|
# helper method for sending tracking requests for new FF
|
|
771
|
-
def _send_tracking_request(visitor_code,
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
578
|
+
def _send_tracking_request(visitor_code, visitor = nil, force_request = true)
|
|
579
|
+
if visitor.nil?
|
|
580
|
+
visitor = @visitor_manager.get_visitor(visitor_code)
|
|
581
|
+
return if visitor.nil? && @data_file.settings.is_consent_required
|
|
582
|
+
end
|
|
583
|
+
consent = consent_provided?(visitor)
|
|
584
|
+
user_agent = visitor&.user_agent
|
|
585
|
+
unsent = visitor.nil? ? [] : select_unsent_data(visitor, consent)
|
|
586
|
+
if unsent.empty?
|
|
587
|
+
return unless force_request && consent
|
|
588
|
+
|
|
589
|
+
unsent.push(Network::ActivityEvent.new)
|
|
590
|
+
end
|
|
591
|
+
log "Start post tracking: #{unsent.inspect}"
|
|
592
|
+
@network_manager.send_tracking_data(visitor_code, unsent, user_agent)
|
|
593
|
+
end
|
|
594
|
+
|
|
595
|
+
def select_unsent_data(visitor, consent)
|
|
596
|
+
unsent = []
|
|
597
|
+
if consent
|
|
598
|
+
visitor.enumerate_sendable_data { |data| unsent.push(data) unless data.sent }
|
|
776
599
|
else
|
|
777
|
-
|
|
600
|
+
visitor.conversions.enumerate { |conversion| unsent.push(conversion) unless conversion.sent }
|
|
601
|
+
if @data_file.has_any_targeted_delivery_rule
|
|
602
|
+
visitor.variations.enumerate do |variation|
|
|
603
|
+
unsent.push(variation) unless
|
|
604
|
+
variation.sent || (variation.rule_type != Configuration::RuleType::TARGETED_DELIVERY)
|
|
605
|
+
end
|
|
606
|
+
end
|
|
778
607
|
end
|
|
779
|
-
|
|
780
|
-
@network_manager.send_tracking_data(visitor_code, data_not_sent, user_agent)
|
|
608
|
+
unsent
|
|
781
609
|
end
|
|
782
610
|
|
|
783
611
|
##
|
|
@@ -816,5 +644,9 @@ module Kameleoon
|
|
|
816
644
|
log("Parsing of visitor data of '#{visitor_code}' failed: #{e}")
|
|
817
645
|
[]
|
|
818
646
|
end
|
|
647
|
+
|
|
648
|
+
def consent_provided?(visitor)
|
|
649
|
+
!@data_file.settings.is_consent_required || visitor&.legal_consent
|
|
650
|
+
end
|
|
819
651
|
end
|
|
820
652
|
end
|