kameleoon-client-ruby 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +18 -0
- data/lib/kameleoon.rb +5 -0
- data/lib/kameleoon/client.rb +503 -0
- data/lib/kameleoon/cookie.rb +65 -0
- data/lib/kameleoon/data.rb +145 -0
- data/lib/kameleoon/exceptions.rb +44 -0
- data/lib/kameleoon/factory.rb +24 -0
- data/lib/kameleoon/request.rb +43 -0
- data/lib/kameleoon/targeting/condition.rb +16 -0
- data/lib/kameleoon/targeting/condition_factory.rb +17 -0
- data/lib/kameleoon/targeting/conditions/custom_datum.rb +83 -0
- data/lib/kameleoon/targeting/models.rb +161 -0
- data/lib/kameleoon/targeting/tree_builder.rb +73 -0
- data/lib/kameleoon/utils.rb +13 -0
- metadata +99 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: f0d398647ef17fa8d33046f2f591973a57a00fde17fef994d955f23ea474538d
|
4
|
+
data.tar.gz: fc466cc6b762e1b4759dee2de2914c33ce27274b90bfe00394db5a80017f186e
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: ad96e42e9c7b75436dca8a6529d2118693b94be8edc7a6d7ee35bfa5cd0909235885f383a313b306912bb79a43aac6b8aee0c6e44d55135c79cca158113fba50
|
7
|
+
data.tar.gz: d2e75d812cf12088420984120337be790c7b3c60574181572d995f9ac613cf5b884820fddc4e7e2b4938ed3444f27d298e883f1560da34d83150d4b71207077a
|
data/README.md
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
# Kameleoon RUBY SDK
|
2
|
+
|
3
|
+
This is the repository for the Kameleoon Ruby SDK.
|
4
|
+
|
5
|
+
### How to build and install Kameleoon Gem locally
|
6
|
+
#### Prerequisite:
|
7
|
+
* [Install ruby](https://www.ruby-lang.org/en/documentation/installation)
|
8
|
+
|
9
|
+
#### Build and install:
|
10
|
+
* Run `./buildAndInstallGem.sh`
|
11
|
+
|
12
|
+
### How to run tests
|
13
|
+
#### Prerequisite:
|
14
|
+
* Build and install Kameleoon Gem locally (infos above).
|
15
|
+
* Install rake: `gem install rake`
|
16
|
+
|
17
|
+
#### Test:
|
18
|
+
* Run `rake test`
|
data/lib/kameleoon.rb
ADDED
@@ -0,0 +1,503 @@
|
|
1
|
+
require 'kameleoon/targeting/models'
|
2
|
+
require 'kameleoon/request'
|
3
|
+
require 'kameleoon/exceptions'
|
4
|
+
require 'kameleoon/cookie'
|
5
|
+
require 'rufus/scheduler'
|
6
|
+
require 'yaml'
|
7
|
+
require 'json'
|
8
|
+
require 'em-synchrony'
|
9
|
+
require 'objspace'
|
10
|
+
|
11
|
+
module Kameleoon
|
12
|
+
##
|
13
|
+
# Client for Kameleoon
|
14
|
+
#
|
15
|
+
class Client
|
16
|
+
include Request
|
17
|
+
include Cookie
|
18
|
+
include Exception
|
19
|
+
|
20
|
+
##
|
21
|
+
# You should create Client with the Client Factory only.
|
22
|
+
def initialize(site_code, path_config_file, blocking, interval, default_timeout, client_id = nil, client_secret = nil)
|
23
|
+
config = YAML.load_file(path_config_file)
|
24
|
+
@site_code = site_code
|
25
|
+
@blocking = blocking
|
26
|
+
@default_timeout = config['default_timeout'] || default_timeout
|
27
|
+
@interval = config['actions_configuration_refresh_interval'] || interval
|
28
|
+
@tracking_url = config['tracking_url'] || "https://api-ssx.kameleoon.com"
|
29
|
+
@client_id = client_id || config['client_id']
|
30
|
+
@client_secret = client_secret || config['client_secret']
|
31
|
+
@data_maximum_size = config['visitor_data_maximum_size'] || 500
|
32
|
+
@experiments = []
|
33
|
+
@feature_flags = []
|
34
|
+
@data = {}
|
35
|
+
end
|
36
|
+
|
37
|
+
##
|
38
|
+
# Obtain a visitor code.
|
39
|
+
#
|
40
|
+
#
|
41
|
+
# This method should be called to obtain the Kameleoon visitor_code for the current visitor.
|
42
|
+
# This is especially important when using Kameleoon in a mixed front-end and back-end environment,
|
43
|
+
# where user identification consistency must be guaranteed.
|
44
|
+
# @note The implementation logic is described here:
|
45
|
+
# First we check if a kameleoonVisitorCode cookie or query parameter associated with the current HTTP request can be
|
46
|
+
# found. If so, we will use this as the visitor identifier. If no cookie / parameter is found in the current
|
47
|
+
# request, we either randomly generate a new identifier, or use the defaultVisitorCode argument as identifier if it
|
48
|
+
# is passed. This allows our customers to use their own identifiers as visitor codes, should they wish to.
|
49
|
+
# This can have the added benefit of matching Kameleoon visitors with their own users without any additional
|
50
|
+
# look-ups in a matching table.
|
51
|
+
# In any case, the server-side (via HTTP header) kameleoonVisitorCode cookie is set with the value. Then this
|
52
|
+
# identifier value is finally returned by the method.
|
53
|
+
#
|
54
|
+
#
|
55
|
+
# @param [Hash] cookies Cookies of the request.
|
56
|
+
# @param [String] top_level_domain Top level domain of your website, settled while writing cookie.
|
57
|
+
# @param [String] default_visitor_vode - Optional - Define your default visitor_code (maximum length 100 chars).
|
58
|
+
#
|
59
|
+
# @return [String] visitor code
|
60
|
+
#
|
61
|
+
# @example
|
62
|
+
# cookies = {'kameleoonVisitorCode' => '1234asdf4321fdsa'}
|
63
|
+
# visitor_code = obtain_visitor_code(cookies, 'my-domaine.com')
|
64
|
+
#
|
65
|
+
def obtain_visitor_code(cookies, top_level_domain, default_visitor_code = nil)
|
66
|
+
read_and_write(cookies, top_level_domain, cookies, default_visitor_code)
|
67
|
+
end
|
68
|
+
|
69
|
+
##
|
70
|
+
# Trigger an experiment.
|
71
|
+
# If such a visitor_code has never been associated with any variation, the SDK returns a randomly selected variation.
|
72
|
+
# If a user with a given visitor_code is already registered with a variation, it will detect the previously
|
73
|
+
# registered variation and return the variation_id.
|
74
|
+
# You have to make sure that proper error handling is set up in your code as shown in the example to the right to
|
75
|
+
# catch potential exceptions.
|
76
|
+
#
|
77
|
+
# @param [String] visitor_code Visitor code
|
78
|
+
# @param [Integer] experiment_id Id of the experiment you want to trigger.
|
79
|
+
#
|
80
|
+
# @return [Integer] Variation id
|
81
|
+
#
|
82
|
+
# @raise [Kameleoon::Exception::ExperimentNotFound] Raise when experiment configuration is not found
|
83
|
+
# @raise [Kameleoon::Exception::NotActivated] The visitor triggered the experiment, but did not activate it. Usually, this happens because the user has been associated with excluded traffic
|
84
|
+
# @raise [Kameleoon::Exception::NotTargeted] The visitor is not targeted by the experiment, as the associated targeting segment conditions were not fulfilled. He should see the reference variation
|
85
|
+
def trigger_experiment(visitor_code, experiment_id, timeout = @default_timeout)
|
86
|
+
experiment = @experiments.find { |experiment| experiment['id'].to_s == experiment_id.to_s }
|
87
|
+
if experiment.nil?
|
88
|
+
raise Exception::ExperimentNotFound.new(experiment_id)
|
89
|
+
end
|
90
|
+
if @blocking
|
91
|
+
variation_id = nil
|
92
|
+
EM.synchrony do
|
93
|
+
connexion_options = { :connect_timeout => timeout }
|
94
|
+
body = @data.values.flatten.select { |data| !data.sent }.map { |data| data.obtain_full_post_text_line }
|
95
|
+
.join("\n") || ""
|
96
|
+
path = get_experiment_register_url(visitor_code, experiment_id)
|
97
|
+
request_options = { :path => path, :body => body }
|
98
|
+
request = EM::Synchrony.sync post(request_options, @tracking_url, connexion_options)
|
99
|
+
if is_successful(request)
|
100
|
+
variation_id = request.response
|
101
|
+
else
|
102
|
+
raise Exception::ExperimentNotFound.new(experiment_id) if variation_id.nil?
|
103
|
+
end
|
104
|
+
EM.stop
|
105
|
+
end
|
106
|
+
if variation_id.nil? || variation_id.to_s == "null" || variation_id.to_s == ""
|
107
|
+
raise Exception::NotTargeted.new(visitor_code)
|
108
|
+
elsif variation_id.to_s == "0"
|
109
|
+
raise Exception::NotActivated.new(visitor_code)
|
110
|
+
end
|
111
|
+
variation_id.to_i
|
112
|
+
else
|
113
|
+
visitor_data = @data.select { |key, value| key.to_s == visitor_code }.values.flatten! || []
|
114
|
+
if experiment['targetingSegment'].check_tree(visitor_data)
|
115
|
+
threshold = obtain_hash_double(visitor_code, experiment['respoolTime'], experiment['id'])
|
116
|
+
experiment['deviations'].each do |key, value|
|
117
|
+
threshold -= value
|
118
|
+
if threshold < 0
|
119
|
+
post_beacon("experimentTracking", visitor_code, experiment_id, key)
|
120
|
+
return key.to_s.to_i
|
121
|
+
end
|
122
|
+
end
|
123
|
+
post_beacon("experimentTracking", visitor_code, experiment_id, REFERENCE, true)
|
124
|
+
raise Exception::NotActivated.new(visitor_code)
|
125
|
+
end
|
126
|
+
raise Exception::NotTargeted.new(visitor_code)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
##
|
131
|
+
# Associate various data to a visitor. Note that this method doesn't return any value and doesn't interact with the
|
132
|
+
# Kameleoon back-end servers by itself. Instead, the declared data is saved for future sending via the flush method.
|
133
|
+
# This reduces the number of server calls made, as data is usually grouped into a single server call triggered by
|
134
|
+
# the execution of the flush method.
|
135
|
+
#
|
136
|
+
# @param [String] visitor_code Visitor code
|
137
|
+
# @param [...Data] data Data to associate with the visitor code
|
138
|
+
#
|
139
|
+
def add_data(visitor_code, *args)
|
140
|
+
while ObjectSpace.memsize_of(@data) > @data_maximum_size * (2**20) do
|
141
|
+
@data.shift
|
142
|
+
end
|
143
|
+
unless args.empty?
|
144
|
+
if @data.key?(visitor_code)
|
145
|
+
@data[visitor_code].push(*args)
|
146
|
+
else
|
147
|
+
@data[visitor_code] = args
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
##
|
153
|
+
# Track conversions on a particular goal
|
154
|
+
# This method requires visitor_code and goal_id to track conversion on this particular goal.
|
155
|
+
# In addition, this method also accepts revenue as a third optional argument to track revenue. The visitor_code usually is identical to the one that was used when triggering the experiment.
|
156
|
+
# The track_conversion method doesn't return any value. This method is non-blocking as the server call is made asynchronously.
|
157
|
+
#
|
158
|
+
# @param [String] visitor_code Visitor code
|
159
|
+
# @param [Integer] goal_id Id of the goal
|
160
|
+
# @param [Float] revenue Revenue of the conversion. This field is optional
|
161
|
+
#
|
162
|
+
def track_conversion(visitor_code, goal_id, revenue = 0.0)
|
163
|
+
add_data(visitor_code, Conversion.new(goal_id, revenue))
|
164
|
+
flush(visitor_code)
|
165
|
+
end
|
166
|
+
|
167
|
+
##
|
168
|
+
# Flush the associated data.
|
169
|
+
# The data added with the method add_data, is not directly sent to the kameleoon servers.
|
170
|
+
# It's stored and accumulated until it is sent automatically by the trigger_experiment or track_conversion methods.
|
171
|
+
# With this method you can manually send it.
|
172
|
+
#
|
173
|
+
# @param [String] visitor_code Optional field - Visitor code, without visitor code it flush all of the data
|
174
|
+
#
|
175
|
+
def flush(visitor_code = nil)
|
176
|
+
post_beacon("dataTracking", visitor_code)
|
177
|
+
end
|
178
|
+
|
179
|
+
##
|
180
|
+
# Obtain variation associated data.
|
181
|
+
# To retrieve JSON data associated with a variation, call the obtain_variation_associated_data method of our SDK.
|
182
|
+
# The JSON data usually represents some metadata of the variation, and can be configured on our web application
|
183
|
+
# interface or via our Automation API.
|
184
|
+
# This method takes the variationID as a parameter and will return the data as a json string.
|
185
|
+
# It will throw an exception () if the variation ID is wrong or corresponds to an experiment that is not yet online.
|
186
|
+
#
|
187
|
+
# @param [Integer] variation_id
|
188
|
+
#
|
189
|
+
# @return [String] json string of the variation data.
|
190
|
+
#
|
191
|
+
# @raise [Kameleoon::Exception::VariationNotFound] Raise exception if the variation is not found.
|
192
|
+
def obtain_variation_associated_data(variation_id)
|
193
|
+
variation = @experiments.map { |experiment| experiment['variations'] }.flatten.select { |variation| variation['id'].to_i == variation_id.to_i }.first
|
194
|
+
if variation.nil?
|
195
|
+
raise Exception::VariationNotFound.new(variation_id)
|
196
|
+
else
|
197
|
+
variation['customJson'].to_json
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
#
|
202
|
+
# Method to activate a feature toggle.
|
203
|
+
#
|
204
|
+
# This method takes a visitor_code and feature_key (or feature_id) as mandatory arguments to check if the specified feature will be active for a given user.
|
205
|
+
# If such a user has never been associated with this feature flag, the SDK returns a boolean value randomly (true if the user should have this feature or false if not). If a user with a given visitorCode is already registered with this feature flag, it will detect the previous featureFlag value.
|
206
|
+
# You have to make sure that proper error handling is set up in your code as shown in the example to the right to catch potential exceptions.
|
207
|
+
#
|
208
|
+
# @param [String] visitor_code
|
209
|
+
# @param [String | Integer] feature_key
|
210
|
+
#
|
211
|
+
# @raise [Kameleoon::Exception::FeatureFlagNotFound]
|
212
|
+
# @raise [Kameleoon::Exception::NotTargeted]
|
213
|
+
def activate_feature(visitor_code, feature_key, timeout = @default_timeout)
|
214
|
+
feature_flag = get_feature_flag(feature_key)
|
215
|
+
id = feature_flag['id']
|
216
|
+
if @blocking
|
217
|
+
result = nil
|
218
|
+
EM.synchrony do
|
219
|
+
connexion_options = { :connect_timeout => timeout }
|
220
|
+
request_options = {
|
221
|
+
:path => get_experiment_register_url(visitor_code, id),
|
222
|
+
:body => (select_data_to_sent(visitor_code).values.map { |data| data.obtain_full_post_text_line }.join("\n") || "").encode("UTF-8")
|
223
|
+
}
|
224
|
+
request = EM::Synchrony.sync post(request_options, @tracking_url, connexion_options)
|
225
|
+
if is_successful(request)
|
226
|
+
result = request.response
|
227
|
+
end
|
228
|
+
EM.stop
|
229
|
+
end
|
230
|
+
raise Exception::FeatureFlagNotFound.new(id) if result.nil?
|
231
|
+
result.to_s != "null"
|
232
|
+
|
233
|
+
else
|
234
|
+
visitor_data = @data.select { |key, value| key.to_s == visitor_code }.values.flatten! || []
|
235
|
+
if feature_flag['targetingSegment'].nil? || feature_flag['targetingSegment'].check_tree(visitor_data)
|
236
|
+
threshold = obtain_hash_double(visitor_code, {}, id)
|
237
|
+
if threshold <= feature_flag['expositionRate']
|
238
|
+
post_beacon("experimentTracking", visitor_code, id, feature_flag["variationsId"].first)
|
239
|
+
return true
|
240
|
+
else
|
241
|
+
post_beacon("experimentTracking", visitor_code, id, REFERENCE, true)
|
242
|
+
return false
|
243
|
+
end
|
244
|
+
else
|
245
|
+
raise Exception::NotTargeted.new(visitor_code)
|
246
|
+
end
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
##
|
251
|
+
# Method To retrieve a feature variable. A feature variable can be changed easily via our web application.
|
252
|
+
#
|
253
|
+
# @param [String | Integer] feature_key
|
254
|
+
# @param [String ] variable_key
|
255
|
+
#
|
256
|
+
# @raise [Kameleoon::Exception::FeatureFlagNotFound]
|
257
|
+
def obtain_feature_variable(feature_key, variable_key)
|
258
|
+
feature_flag = get_feature_flag(feature_key)
|
259
|
+
custom_json = feature_flag["variations"].first['customJson'][variable_key.to_s]
|
260
|
+
if custom_json.nil?
|
261
|
+
raise Exception::FeatureFlagNotFound.new("Feature variable not found")
|
262
|
+
end
|
263
|
+
case custom_json['type']
|
264
|
+
when "Boolean"
|
265
|
+
return !!custom_json['value']
|
266
|
+
when "String"
|
267
|
+
return custom_json['value'].to_s
|
268
|
+
when "Number"
|
269
|
+
return custom_json['value'].to_f
|
270
|
+
when "Json"
|
271
|
+
return custom_json['value'].to_json
|
272
|
+
else
|
273
|
+
raise Exception::FeatureFlagNotFound.new("Unknown type for feature variable")
|
274
|
+
end
|
275
|
+
end
|
276
|
+
|
277
|
+
private
|
278
|
+
API_SSX_URL = "https://api-ssx.kameleoon.com"
|
279
|
+
REFERENCE = 0
|
280
|
+
attr :site_code, :client_id, :client_secret, :access_token, :experiments, :feature_flags, :scheduler, :data,
|
281
|
+
:blocking, :tracking_url, :default_timeout, :interval, :memory_limit
|
282
|
+
|
283
|
+
def fetch_configuration
|
284
|
+
print "=> Starting Scheduler"
|
285
|
+
@scheduler = Rufus::Scheduler.singleton
|
286
|
+
@scheduler.every @interval do
|
287
|
+
fetch_configuration_job
|
288
|
+
end
|
289
|
+
@scheduler.schedule '0s' do
|
290
|
+
fetch_configuration_job
|
291
|
+
end
|
292
|
+
end
|
293
|
+
|
294
|
+
def fetch_configuration_job
|
295
|
+
EM.synchrony do
|
296
|
+
obtain_access_token
|
297
|
+
site = obtain_site
|
298
|
+
if site.nil? || site.empty?
|
299
|
+
@experiments ||= []
|
300
|
+
@feature_flags ||= []
|
301
|
+
else
|
302
|
+
site_id = site.first['id']
|
303
|
+
@experiments = obtain_tests(site_id)
|
304
|
+
@feature_flags = obtain_feature_flags(site_id)
|
305
|
+
end
|
306
|
+
EM.stop
|
307
|
+
end
|
308
|
+
end
|
309
|
+
|
310
|
+
def hash_headers
|
311
|
+
if @access_token.nil?
|
312
|
+
CredentialsNotFound.new
|
313
|
+
end
|
314
|
+
{
|
315
|
+
'Authorization' => 'Bearer ' + @access_token.to_s,
|
316
|
+
'Content-Type' => 'application/json'
|
317
|
+
}
|
318
|
+
end
|
319
|
+
|
320
|
+
def hash_filter(field, operator, parameters)
|
321
|
+
{ 'field' => field, 'operator' => operator, 'parameters' => parameters }
|
322
|
+
end
|
323
|
+
|
324
|
+
def obtain_site
|
325
|
+
query_params = { 'perPage' => 1 }
|
326
|
+
filters = [hash_filter('code', 'EQUAL', [@site_code])]
|
327
|
+
request = fetch_one('sites', query_params, filters)
|
328
|
+
if request != false
|
329
|
+
JSON.parse(request.response)
|
330
|
+
end
|
331
|
+
end
|
332
|
+
|
333
|
+
def obtain_access_token
|
334
|
+
body = {
|
335
|
+
'grant_type' => 'client_credentials',
|
336
|
+
'client_id' => @client_id,
|
337
|
+
'client_secret' => @client_secret
|
338
|
+
}
|
339
|
+
header = { 'Content-Type' => 'application/x-www-form-urlencoded' }
|
340
|
+
|
341
|
+
request = EM::Synchrony.sync post({ :path => '/oauth/token', :body => body, :head => header })
|
342
|
+
if is_successful(request)
|
343
|
+
@access_token = JSON.parse(request.response)['access_token']
|
344
|
+
end
|
345
|
+
end
|
346
|
+
|
347
|
+
def obtain_variation(variation_id)
|
348
|
+
request = fetch_one('variations/' + variation_id.to_s)
|
349
|
+
if request != false
|
350
|
+
JSON.parse(request.response)
|
351
|
+
end
|
352
|
+
end
|
353
|
+
|
354
|
+
def obtain_segment(segment_id)
|
355
|
+
request = fetch_one('segments/' + segment_id.to_s)
|
356
|
+
if request != false
|
357
|
+
JSON.parse(request.response)
|
358
|
+
end
|
359
|
+
end
|
360
|
+
|
361
|
+
def complete_experiment(experiment)
|
362
|
+
unless experiment['variationsId'].nil?
|
363
|
+
experiment['variations'] = experiment['variationsId'].map { |variationId| obtain_variation(variationId) }
|
364
|
+
end
|
365
|
+
unless experiment['targetingSegmentId'].nil?
|
366
|
+
experiment['targetingSegment'] = Kameleoon::Targeting::Segment.new(obtain_segment(experiment['targetingSegmentId']))
|
367
|
+
end
|
368
|
+
experiment
|
369
|
+
end
|
370
|
+
|
371
|
+
def obtain_tests(site_id, per_page = -1)
|
372
|
+
query_values = { 'perPage' => per_page }
|
373
|
+
filters = [
|
374
|
+
hash_filter('siteId', 'EQUAL', [site_id]),
|
375
|
+
hash_filter('status', 'EQUAL', ['ACTIVE']),
|
376
|
+
hash_filter('type', 'IN', ['SERVER_SIDE', 'HYBRID'])
|
377
|
+
]
|
378
|
+
fetch_all('experiments', query_values, filters).map { |it| JSON.parse(it.response) }.flatten.map do |test|
|
379
|
+
complete_experiment(test)
|
380
|
+
end
|
381
|
+
end
|
382
|
+
|
383
|
+
def obtain_feature_flags(site_id, per_page = -1)
|
384
|
+
query_values = { 'perPage' => per_page }
|
385
|
+
filters = [
|
386
|
+
hash_filter('siteId', 'EQUAL', [site_id]),
|
387
|
+
hash_filter('status', 'EQUAL', ['ACTIVE'])
|
388
|
+
]
|
389
|
+
fetch_all('feature-flags', query_values, filters).map { |it| JSON.parse(it.response) }.flatten.map do |ff|
|
390
|
+
complete_experiment(ff)
|
391
|
+
end
|
392
|
+
end
|
393
|
+
|
394
|
+
def fetch_all(path, query_values = {}, filters = [])
|
395
|
+
results = []
|
396
|
+
current_page = 1
|
397
|
+
loop do
|
398
|
+
query_values['page'] = current_page
|
399
|
+
http = fetch_one(path, query_values, filters)
|
400
|
+
break if http == false
|
401
|
+
results.push(http)
|
402
|
+
break if http.response_header["X-Pagination-Page-Count"].to_i <= current_page
|
403
|
+
current_page += 1
|
404
|
+
end
|
405
|
+
results
|
406
|
+
end
|
407
|
+
|
408
|
+
def fetch_one(path, query_values = {}, filters = [])
|
409
|
+
unless filters.empty?
|
410
|
+
query_values['filter'] = filters.to_json
|
411
|
+
end
|
412
|
+
request = EM::Synchrony.sync get({ :path => path, :query => query_values, :head => hash_headers })
|
413
|
+
unless is_successful(request)
|
414
|
+
return false
|
415
|
+
end
|
416
|
+
request
|
417
|
+
end
|
418
|
+
|
419
|
+
def get_common_ssx_parameters(visitor_code)
|
420
|
+
{
|
421
|
+
:nonce => Kameleoon::Utils.generate_random_string(16),
|
422
|
+
:siteCode => @site_code,
|
423
|
+
:visitorCode => visitor_code
|
424
|
+
}
|
425
|
+
end
|
426
|
+
|
427
|
+
def get_experiment_register_url(visitor_code, experiment_id, variation_id = nil, none_variation = false)
|
428
|
+
url = "/experimentTracking?" + URI.encode_www_form(get_common_ssx_parameters(visitor_code))
|
429
|
+
url += "&experimentId=" + experiment_id.to_s
|
430
|
+
if variation_id.nil?
|
431
|
+
return url
|
432
|
+
end
|
433
|
+
url += "&variationId=" + variation_id.to_s
|
434
|
+
if none_variation
|
435
|
+
url += "&noneVariation=true"
|
436
|
+
end
|
437
|
+
url
|
438
|
+
end
|
439
|
+
|
440
|
+
def get_feature_flag(feature_key)
|
441
|
+
if feature_key.is_a?(String)
|
442
|
+
feature_flag = @feature_flags.select { |ff| ff['identificationKey'] == feature_key}.first
|
443
|
+
elsif feature_key.is_a?(Integer)
|
444
|
+
feature_flag = @feature_flags.select { |ff| ff['id'].to_i == feature_key}.first
|
445
|
+
else
|
446
|
+
raise TypeError.new("Feature key should be a String or an Integer.")
|
447
|
+
end
|
448
|
+
if feature_flag.nil?
|
449
|
+
raise Exception::FeatureFlagNotFound.new(feature_key)
|
450
|
+
end
|
451
|
+
feature_flag
|
452
|
+
end
|
453
|
+
|
454
|
+
def post_beacon(type = "dataTracking", visitor_code = nil, experiment_id = nil, variation_id = nil, none_variation = false)
|
455
|
+
Thread.new do
|
456
|
+
EM.synchrony do
|
457
|
+
entries = select_data_to_sent(visitor_code)
|
458
|
+
trials = 10
|
459
|
+
concurrency = 1
|
460
|
+
while !entries.empty? && trials > 0
|
461
|
+
EM::Synchrony::Iterator.new(entries, concurrency).map do |entry, iter|
|
462
|
+
options = {
|
463
|
+
:path => build_beacon_path(type, entry.first || visitor_code, experiment_id, variation_id, none_variation),
|
464
|
+
:body => (entry.last.map { |data| data.obtain_full_post_text_line }.join("\n") || "").encode("UTF-8"),
|
465
|
+
:head => { "Content-Type" => "text/plain" }
|
466
|
+
}
|
467
|
+
request = post(options, @tracking_url)
|
468
|
+
request.callback {
|
469
|
+
if is_successful(request)
|
470
|
+
entry.last.each { |data| data.sent = true }
|
471
|
+
end
|
472
|
+
iter.return(request)
|
473
|
+
}
|
474
|
+
request.errback { iter.return(request) }
|
475
|
+
end
|
476
|
+
entries = select_data_to_sent(visitor_code)
|
477
|
+
trials -= 1
|
478
|
+
end
|
479
|
+
EM.stop
|
480
|
+
end
|
481
|
+
Thread.exit
|
482
|
+
end
|
483
|
+
end
|
484
|
+
|
485
|
+
def select_data_to_sent(visitor_code)
|
486
|
+
if visitor_code.nil?
|
487
|
+
@data.select {|key, values| values.any? {|data| !data.sent}}
|
488
|
+
else
|
489
|
+
@data.select { |key, values| key == visitor_code && values.any? {|data| !data.sent} }
|
490
|
+
end
|
491
|
+
end
|
492
|
+
|
493
|
+
def build_beacon_path(type, visitor_code, experiment_id = nil, variation_id = nil, none_variation = nil)
|
494
|
+
if type == "dataTracking"
|
495
|
+
return "/dataTracking?" + URI.encode_www_form(get_common_ssx_parameters(visitor_code))
|
496
|
+
elsif type == "experimentTracking"
|
497
|
+
return get_experiment_register_url(visitor_code, experiment_id, variation_id, none_variation)
|
498
|
+
else
|
499
|
+
raise TypeError("Unknown type for post_beacon: " + type.to_s)
|
500
|
+
end
|
501
|
+
end
|
502
|
+
end
|
503
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
require 'fiber'
|
2
|
+
require 'em-http'
|
3
|
+
require 'eventmachine'
|
4
|
+
require 'bigdecimal'
|
5
|
+
require 'kameleoon/utils'
|
6
|
+
|
7
|
+
module Kameleoon
|
8
|
+
# @api private
|
9
|
+
module Cookie
|
10
|
+
# @return [String] visitor code
|
11
|
+
def read_and_write(request_cookies, top_level_domain, response_cookies, default_visitor_code = nil)
|
12
|
+
kameleoon_visitor_code = read(request_cookies)
|
13
|
+
if kameleoon_visitor_code.nil?
|
14
|
+
kameleoon_visitor_code = check_default_visitor_code(default_visitor_code) || Kameleoon::Utils.generate_random_string(KAMELEOON_COOKIE_VALUE_LENGTH)
|
15
|
+
end
|
16
|
+
cookie = { :value => kameleoon_visitor_code, :expires => Time.now + Kameleoon::Utils.in_seconds(EXPIRE_DAYS), :path => '/' }
|
17
|
+
unless top_level_domain.nil?
|
18
|
+
cookie[:domain] = top_level_domain
|
19
|
+
end
|
20
|
+
response_cookies[KAMELEOON_COOKIE_NAME] = cookie
|
21
|
+
kameleoon_visitor_code
|
22
|
+
end
|
23
|
+
|
24
|
+
def obtain_hash_double(visitor_code, respool_times = {}, container_id = '')
|
25
|
+
identifier = visitor_code.to_s
|
26
|
+
identifier += container_id.to_s
|
27
|
+
if !respool_times.nil? && !respool_times.empty?
|
28
|
+
identifier += respool_times.values.sort.join.to_s
|
29
|
+
end
|
30
|
+
(Digest::SHA256.hexdigest(identifier.encode('UTF-8')).to_i(16) / (BigDecimal("2") ** BigDecimal("256"))).round(16)
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
KAMELEOON_KEY_JS_COOKIE = "_js_"
|
36
|
+
KAMELEOON_COOKIE_VALUE_LENGTH = 16
|
37
|
+
KAMELEOON_VISITOR_CODE_LENGTH = 100
|
38
|
+
KAMELEOON_COOKIE_NAME = 'kameleoonVisitorCode'
|
39
|
+
EXPIRE_DAYS = 380
|
40
|
+
|
41
|
+
def check_default_visitor_code(default_visitor_code)
|
42
|
+
if default_visitor_code.nil?
|
43
|
+
return nil
|
44
|
+
end
|
45
|
+
default_visitor_code[0..(KAMELEOON_VISITOR_CODE_LENGTH - 1)]
|
46
|
+
end
|
47
|
+
|
48
|
+
def read(cookies)
|
49
|
+
value = cookies[KAMELEOON_COOKIE_NAME]
|
50
|
+
if value.nil?
|
51
|
+
return
|
52
|
+
end
|
53
|
+
if value.start_with?(KAMELEOON_KEY_JS_COOKIE)
|
54
|
+
start_index = KAMELEOON_KEY_JS_COOKIE.length
|
55
|
+
value = value[start_index..-1]
|
56
|
+
end
|
57
|
+
if value.length < KAMELEOON_COOKIE_VALUE_LENGTH
|
58
|
+
return nil
|
59
|
+
end
|
60
|
+
value[0..(KAMELEOON_COOKIE_VALUE_LENGTH - 1)]
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
|
@@ -0,0 +1,145 @@
|
|
1
|
+
|
2
|
+
module Kameleoon
|
3
|
+
NONCE_LENGTH = 16
|
4
|
+
|
5
|
+
module DataType
|
6
|
+
CUSTOM = "CUSTOM"
|
7
|
+
BROWSER = "BROWSER"
|
8
|
+
CONVERSION = "CONVERSION"
|
9
|
+
INTEREST = "INTEREST"
|
10
|
+
PAGE_VIEW = "PAGE_VIEW"
|
11
|
+
end
|
12
|
+
|
13
|
+
module BrowserType
|
14
|
+
CHROME = 0
|
15
|
+
INTERNET_EXPLORER = 1
|
16
|
+
FIREFOX = 2
|
17
|
+
SAFARI = 3
|
18
|
+
OPERA = 4
|
19
|
+
OTHER = 5
|
20
|
+
end
|
21
|
+
|
22
|
+
class Data
|
23
|
+
attr_accessor :instance, :sent
|
24
|
+
|
25
|
+
def initialize(*args)
|
26
|
+
raise KameleoonError.new("Cannot initialise interface.")
|
27
|
+
end
|
28
|
+
|
29
|
+
def obtain_full_post_text_line
|
30
|
+
raise KameleoonError.new("ToDo: implement this method.")
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
class CustomData < Data
|
35
|
+
attr_accessor :id, :value
|
36
|
+
|
37
|
+
# @param [Integer] id Id of the custom data
|
38
|
+
# @param [String] value Value of the custom data
|
39
|
+
#
|
40
|
+
# @overload
|
41
|
+
# @param [Hash] hash Json value encoded in a hash.
|
42
|
+
def initialize(*args)
|
43
|
+
@instance = DataType::CUSTOM
|
44
|
+
@sent = false
|
45
|
+
unless args.empty?
|
46
|
+
if args.length == 1
|
47
|
+
hash = args.first
|
48
|
+
if hash["id"].nil?
|
49
|
+
raise NotFoundError.new("id")
|
50
|
+
end
|
51
|
+
@id = hash["id"].to_s
|
52
|
+
if hash["value"].nil?
|
53
|
+
raise NotFoundError.new(hash["value"])
|
54
|
+
end
|
55
|
+
@value = hash["value"]
|
56
|
+
elsif args.length == 2
|
57
|
+
@id = args[0].to_s
|
58
|
+
@value = args[1]
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def obtain_full_post_text_line
|
64
|
+
to_encode = "[[\"" + @value.to_s.gsub("\"", "\\\"") + "\",1]]"
|
65
|
+
nonce = Kameleoon::Utils.generate_random_string(NONCE_LENGTH)
|
66
|
+
"eventType=customData&index=" + @id.to_s + "&valueToCount=" + URI.encode_www_form_component(to_encode) + "&overwrite=true&nonce=" + nonce
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
class Browser < Data
|
71
|
+
attr_accessor :browser
|
72
|
+
|
73
|
+
# @param [BrowserType] browser_type Browser type, can be: CHROME, INTERNET_EXPLORER, FIREFOX, SAFARI, OPERA, OTHER
|
74
|
+
def initialize(browser_type)
|
75
|
+
@instance = DataType::BROWSER
|
76
|
+
@sent = false
|
77
|
+
@browser = browser_type
|
78
|
+
end
|
79
|
+
|
80
|
+
def obtain_full_post_text_line
|
81
|
+
nonce = Kameleoon::Utils.generate_random_string(NONCE_LENGTH)
|
82
|
+
"eventType=staticData&browser=" + @browser.to_s + "&nonce=" + nonce
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
class PageView < Data
|
87
|
+
attr_accessor :url, :title, :referrer
|
88
|
+
|
89
|
+
# @param [String] url Url of the page
|
90
|
+
# @param [String] title Title of the page
|
91
|
+
# @param [Integer] referrer Optional field - Referrer id
|
92
|
+
def initialize(url, title, referrer = nil)
|
93
|
+
@instance = DataType::PAGE_VIEW
|
94
|
+
@sent = false
|
95
|
+
@url = url
|
96
|
+
@title = title
|
97
|
+
@referrer = referrer
|
98
|
+
end
|
99
|
+
|
100
|
+
def obtain_full_post_text_line
|
101
|
+
nonce = Kameleoon::Utils.generate_random_string(NONCE_LENGTH)
|
102
|
+
referrer_text = ""
|
103
|
+
unless @referrer.nil?
|
104
|
+
referrer_text = "&referrers=[" + @referrer + "]"
|
105
|
+
end
|
106
|
+
"eventType=page&href=" + @url + "&title=" + @title + "&keyPages=[]" + referrer_text + "&nonce=" + nonce
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
class Conversion < Data
|
111
|
+
attr_accessor :goal_id, :revenue, :negative
|
112
|
+
|
113
|
+
# @param [Integer] goal_id Id of the goal associated to the conversion
|
114
|
+
# @param [Float] revenue Optional field - Revenue associated to the conversion.
|
115
|
+
# @param [Boolean] negative Optional field - If the revenue is negative. By default it's positive.
|
116
|
+
def initialize(goal_id, revenue = 0.0, negative = false)
|
117
|
+
@instance = DataType::CONVERSION
|
118
|
+
@sent = false
|
119
|
+
@goal_id = goal_id
|
120
|
+
@revenue = revenue
|
121
|
+
@negative = negative
|
122
|
+
end
|
123
|
+
|
124
|
+
def obtain_full_post_text_line
|
125
|
+
nonce = Kameleoon::Utils.generate_random_string(NONCE_LENGTH)
|
126
|
+
"eventType=conversion&goalId=" + @goal_id.to_s + "&revenue=" + @revenue.to_s + "&negative=" + @negative.to_s + "&nonce=" + nonce
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
class Interest < Data
|
131
|
+
attr_accessor :index
|
132
|
+
|
133
|
+
# @param [Integer] index Index of the interest
|
134
|
+
def initialize(index)
|
135
|
+
@instance = DataType::INTEREST
|
136
|
+
@sent = false
|
137
|
+
@index = index
|
138
|
+
end
|
139
|
+
|
140
|
+
def obtain_full_post_text_line
|
141
|
+
nonce = Kameleoon::Utils.generate_random_string(NONCE_LENGTH)
|
142
|
+
"eventType=interests&indexes=[" + @index.to_s + "]&fresh=true&nonce=" + nonce
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module Kameleoon
|
2
|
+
module Exception
|
3
|
+
class KameleoonError < ::StandardError
|
4
|
+
def initialize(message = nil)
|
5
|
+
super("Kameleoon error: " + message)
|
6
|
+
end
|
7
|
+
end
|
8
|
+
class NotFound < KameleoonError
|
9
|
+
def initialize(value = "")
|
10
|
+
super(value.to_s + " not found.")
|
11
|
+
end
|
12
|
+
end
|
13
|
+
class VariationNotFound < NotFound
|
14
|
+
def initialize(id = "")
|
15
|
+
super("Variation " + id.to_s)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
class ExperimentNotFound < NotFound
|
19
|
+
def initialize(id = "")
|
20
|
+
super("Experiment " + id.to_s)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
class FeatureFlagNotFound < NotFound
|
24
|
+
def initialize(id = "")
|
25
|
+
super("Feature flag " + id.to_s)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
class CredentialsNotFound < NotFound
|
29
|
+
def initialize
|
30
|
+
super("Credentials")
|
31
|
+
end
|
32
|
+
end
|
33
|
+
class NotTargeted < KameleoonError
|
34
|
+
def initialize(visitor_code = "")
|
35
|
+
super("Visitor " + visitor_code + " is not targeted.")
|
36
|
+
end
|
37
|
+
end
|
38
|
+
class NotActivated < KameleoonError
|
39
|
+
def initialize(visitor_code = "")
|
40
|
+
super("Visitor " + visitor_code + " is not activated.")
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'kameleoon/client'
|
2
|
+
|
3
|
+
module Kameleoon
|
4
|
+
module ClientFactory
|
5
|
+
CONFIGURATION_UPDATE_INTERVAL= "60m"
|
6
|
+
CONFIG_PATH = "/etc/kameleoon/client-ruby.yaml"
|
7
|
+
DEFAULT_TIMEOUT = 2 #seconds
|
8
|
+
|
9
|
+
##
|
10
|
+
# Create a kameleoon client object, each call create a new client.
|
11
|
+
# The starting point for using the SDK is the initialization step. All interaction with the SDK is done through an object of the Kameleoon::Client class, therefore you need to create this object via Kameleoon::ClientFactory create static method.
|
12
|
+
#
|
13
|
+
# @param [String] site_code Site code
|
14
|
+
# @param [Boolean] blocking - optional, default is false
|
15
|
+
#
|
16
|
+
# @return [Kameleoon::Client]
|
17
|
+
#
|
18
|
+
def self.create(site_code, blocking = false, config_path = CONFIG_PATH, client_id = nil, client_secret = nil)
|
19
|
+
client = Client.new(site_code, config_path, blocking, CONFIGURATION_UPDATE_INTERVAL, DEFAULT_TIMEOUT, client_id, client_secret)
|
20
|
+
client.send(:fetch_configuration)
|
21
|
+
client
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require "em-synchrony/em-http"
|
2
|
+
|
3
|
+
module Kameleoon
|
4
|
+
# @api private
|
5
|
+
module Request
|
6
|
+
protected
|
7
|
+
API_URL = "https://api.kameleoon.com"
|
8
|
+
|
9
|
+
module Method
|
10
|
+
GET = "get"
|
11
|
+
POST = "post"
|
12
|
+
end
|
13
|
+
|
14
|
+
def get(request_options, url = API_URL, connexion_options = {})
|
15
|
+
request(Method::GET, request_options, url, connexion_options)
|
16
|
+
end
|
17
|
+
|
18
|
+
def post(request_options, url = API_URL, connexion_options = {})
|
19
|
+
request(Method::POST, request_options, url, connexion_options)
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def request(method, request_options, url, connexion_options)
|
25
|
+
request_options[:tls] = {verify_peer: true}
|
26
|
+
case method
|
27
|
+
when Method::POST then
|
28
|
+
return EventMachine::HttpRequest.new(url, connexion_options).apost request_options
|
29
|
+
when Method::GET then
|
30
|
+
return EventMachine::HttpRequest.new(url, connexion_options).aget request_options
|
31
|
+
else
|
32
|
+
print "Unknown request type"
|
33
|
+
return false
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def is_successful(request)
|
38
|
+
!request.nil? && request != false && /20\d/.match(request.response_header.status.to_s)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Kameleoon
|
2
|
+
#@api private
|
3
|
+
module Targeting
|
4
|
+
class Condition
|
5
|
+
attr_accessor :type, :include
|
6
|
+
|
7
|
+
def initialize(json_conditions)
|
8
|
+
raise "Abstract class cannot be instantiate"
|
9
|
+
end
|
10
|
+
|
11
|
+
def check(conditions)
|
12
|
+
raise "Todo: Implement check method in condition"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'kameleoon/targeting/models'
|
2
|
+
require 'kameleoon/targeting/conditions/custom_datum'
|
3
|
+
|
4
|
+
module Kameleoon
|
5
|
+
#@api private
|
6
|
+
module Targeting
|
7
|
+
module ConditionFactory
|
8
|
+
def get_condition(condition_json)
|
9
|
+
condition = nil
|
10
|
+
if condition_json['targetingType'] == ConditionType::CUSTOM_DATUM.to_s
|
11
|
+
condition = CustomDatum.new(condition_json)
|
12
|
+
end
|
13
|
+
condition
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
require 'kameleoon/targeting/condition'
|
2
|
+
require 'kameleoon/exceptions'
|
3
|
+
require 'kameleoon/targeting/models'
|
4
|
+
|
5
|
+
module Kameleoon
|
6
|
+
#@api private
|
7
|
+
module Targeting
|
8
|
+
class CustomDatum < Condition
|
9
|
+
include Kameleoon::Exception
|
10
|
+
|
11
|
+
attr_accessor :index, :operator, :value
|
12
|
+
def initialize(json_condition)
|
13
|
+
if json_condition['customDataIndex'].nil?
|
14
|
+
raise Exception::NotFoundError.new('customDataIndex')
|
15
|
+
end
|
16
|
+
@index = json_condition['customDataIndex']
|
17
|
+
|
18
|
+
if json_condition['valueMatchType'].nil?
|
19
|
+
raise Exception::NotFoundError.new('valueMatchType')
|
20
|
+
end
|
21
|
+
@operator = json_condition['valueMatchType']
|
22
|
+
|
23
|
+
if json_condition['value'].nil?
|
24
|
+
raise Exception::NotFoundError.new('value')
|
25
|
+
end
|
26
|
+
@value = json_condition['value']
|
27
|
+
|
28
|
+
@type = ConditionType::CUSTOM_DATUM
|
29
|
+
|
30
|
+
if json_condition['include'].nil?
|
31
|
+
raise Exception::NotFoundError.new('include')
|
32
|
+
end
|
33
|
+
@include = json_condition['include']
|
34
|
+
end
|
35
|
+
|
36
|
+
def check(datas)
|
37
|
+
is_targeted = false
|
38
|
+
custom_data = datas.select { |data| data.instance == DataType::CUSTOM && data.id == @index }.first
|
39
|
+
if custom_data.nil?
|
40
|
+
is_targeted = (@operator == Operator::UNDEFINED.to_s)
|
41
|
+
else
|
42
|
+
case @operator
|
43
|
+
when Operator::MATCH.to_s
|
44
|
+
if Regexp.new(@value.to_s).match(custom_data.value.to_s)
|
45
|
+
is_targeted = true
|
46
|
+
end
|
47
|
+
when Operator::CONTAINS.to_s
|
48
|
+
if custom_data.value.to_s.include? @value
|
49
|
+
is_targeted = true
|
50
|
+
end
|
51
|
+
when Operator::EXACT.to_s
|
52
|
+
if custom_data.value.to_s == @value.to_s
|
53
|
+
is_targeted = true
|
54
|
+
end
|
55
|
+
when Operator::EQUAL.to_s
|
56
|
+
if custom_data.value.to_f == @value.to_f
|
57
|
+
is_targeted = true
|
58
|
+
end
|
59
|
+
when Operator::GREATER.to_s
|
60
|
+
if custom_data.value.to_f > @value.to_f
|
61
|
+
is_targeted = true
|
62
|
+
end
|
63
|
+
when Operator::LOWER.to_s
|
64
|
+
if custom_data.value.to_f < @value.to_f
|
65
|
+
is_targeted = true
|
66
|
+
end
|
67
|
+
when Operator::IS_TRUE.to_s
|
68
|
+
if custom_data.value == true
|
69
|
+
is_targeted = true
|
70
|
+
end
|
71
|
+
when Operator::IS_FALSE.to_s
|
72
|
+
if custom_data.value == false
|
73
|
+
is_targeted = true
|
74
|
+
end
|
75
|
+
else
|
76
|
+
raise KameleoonError.new("Undefined operator " + @operator.to_s)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
is_targeted
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,161 @@
|
|
1
|
+
require 'kameleoon/targeting/tree_builder'
|
2
|
+
require 'kameleoon/exceptions'
|
3
|
+
|
4
|
+
module Kameleoon
|
5
|
+
#@api private
|
6
|
+
module Targeting
|
7
|
+
class Segment
|
8
|
+
include TreeBuilder
|
9
|
+
attr_accessor :id, :tree
|
10
|
+
def to_s
|
11
|
+
print("\nSegment id: " + @id.to_s)
|
12
|
+
print("\n")
|
13
|
+
@tree.to_s
|
14
|
+
end
|
15
|
+
|
16
|
+
def initialize(*args)
|
17
|
+
unless args.empty?
|
18
|
+
if args.length == 1
|
19
|
+
hash = args.first
|
20
|
+
if hash.nil?
|
21
|
+
raise Kameleoon::Exception::NotFound.new("arguments for segment")
|
22
|
+
end
|
23
|
+
if hash["id"].nil?
|
24
|
+
raise Kameleoon::Exception::NotFound.new("id")
|
25
|
+
end
|
26
|
+
@id = hash["id"]
|
27
|
+
if hash["conditionsData"].nil?
|
28
|
+
raise Kameleoon::Exception::NotFound.new(hash["conditionsData"])
|
29
|
+
end
|
30
|
+
@tree = create_tree(hash["conditionsData"])
|
31
|
+
elsif args.length == 2
|
32
|
+
@id = args[0]
|
33
|
+
@tree = args[1]
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def check_tree(targeting_data)
|
39
|
+
if @tree.nil?
|
40
|
+
is_targeted = true
|
41
|
+
else
|
42
|
+
is_targeted = @tree.check(targeting_data)
|
43
|
+
end
|
44
|
+
is_targeted == true
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
class Tree
|
49
|
+
attr_accessor :or_operator, :left_child, :right_child, :condition
|
50
|
+
|
51
|
+
def to_s
|
52
|
+
print("or_operator: " + @or_operator.to_s)
|
53
|
+
print("\n")
|
54
|
+
print("condition: " + @condition.to_s)
|
55
|
+
unless @left_child.nil?
|
56
|
+
print("\n")
|
57
|
+
print("Left child:\n ")
|
58
|
+
@left_child.to_s
|
59
|
+
end
|
60
|
+
unless @right_child.nil?
|
61
|
+
print("\n")
|
62
|
+
print("right child:\n ")
|
63
|
+
@right_child.to_s
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def initialize(or_operator = nil, left_child = nil, right_child = nil, condition = nil)
|
68
|
+
@or_operator = Marshal.load(Marshal.dump(or_operator))
|
69
|
+
@left_child = left_child
|
70
|
+
@right_child = right_child
|
71
|
+
@condition = condition
|
72
|
+
end
|
73
|
+
|
74
|
+
def check(datas)
|
75
|
+
unless @condition.nil?
|
76
|
+
is_targeted = check_condition(datas)
|
77
|
+
else
|
78
|
+
if @left_child.nil?
|
79
|
+
is_left_child_targeted = true
|
80
|
+
else
|
81
|
+
is_left_child_targeted = @left_child.check(datas)
|
82
|
+
end
|
83
|
+
|
84
|
+
if is_left_child_targeted.nil?
|
85
|
+
has_to_compute_right_child = true
|
86
|
+
else
|
87
|
+
has_to_compute_right_child = (is_left_child_targeted != @or_operator)
|
88
|
+
end
|
89
|
+
|
90
|
+
# Compute right child tree
|
91
|
+
is_right_child_targeted = nil
|
92
|
+
if has_to_compute_right_child
|
93
|
+
if @right_child.nil?
|
94
|
+
is_right_child_targeted = true
|
95
|
+
else
|
96
|
+
is_right_child_targeted = @right_child.check(datas)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# Computing results
|
101
|
+
if is_left_child_targeted.nil?
|
102
|
+
if is_right_child_targeted == @or_operator
|
103
|
+
is_targeted = Marshal.load(Marshal.dump(@or_operator)) #Deep copy
|
104
|
+
else
|
105
|
+
is_targeted = nil
|
106
|
+
end
|
107
|
+
else
|
108
|
+
if is_left_child_targeted == @or_operator
|
109
|
+
is_targeted = Marshal.load(Marshal.dump(@or_operator)) #Deep copy
|
110
|
+
else
|
111
|
+
if is_right_child_targeted == true
|
112
|
+
is_targeted = true
|
113
|
+
elsif is_right_child_targeted == false
|
114
|
+
is_targeted = false
|
115
|
+
else
|
116
|
+
is_targeted = nil
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
Marshal.load(Marshal.dump(is_targeted)) #Deep copy
|
122
|
+
end
|
123
|
+
|
124
|
+
def check_condition(datas, condition = @condition)
|
125
|
+
if condition.nil?
|
126
|
+
is_targeted = true
|
127
|
+
else
|
128
|
+
is_targeted = condition.check(datas)
|
129
|
+
unless condition.include
|
130
|
+
if is_targeted.nil?
|
131
|
+
is_targeted = true
|
132
|
+
else
|
133
|
+
is_targeted = !is_targeted
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
Marshal.load(Marshal.dump(is_targeted)) #Deep copy
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
module DataType
|
142
|
+
CUSTOM = "CUSTOM"
|
143
|
+
end
|
144
|
+
|
145
|
+
module ConditionType
|
146
|
+
CUSTOM_DATUM = "CUSTOM_DATUM"
|
147
|
+
end
|
148
|
+
|
149
|
+
module Operator
|
150
|
+
UNDEFINED = "UNDEFINED"
|
151
|
+
CONTAINS = "CONTAINS"
|
152
|
+
EXACT = "EXACT"
|
153
|
+
MATCH = "REGULAR_EXPRESSION"
|
154
|
+
LOWER = "LOWER"
|
155
|
+
EQUAL = "EQUAL"
|
156
|
+
GREATER = "GREATER"
|
157
|
+
IS_TRUE = "TRUE"
|
158
|
+
IS_FALSE = "FALSE"
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
require 'kameleoon/targeting/condition_factory'
|
2
|
+
require 'kameleoon/targeting/models'
|
3
|
+
|
4
|
+
module Kameleoon
|
5
|
+
#@api private
|
6
|
+
module Targeting
|
7
|
+
module TreeBuilder
|
8
|
+
|
9
|
+
def create_tree(conditions_data_json)
|
10
|
+
if conditions_data_json.nil?
|
11
|
+
return nil
|
12
|
+
end
|
13
|
+
if conditions_data_json['firstLevel'].empty?
|
14
|
+
conditions_data_json['firstLevelOrOperators'] = []
|
15
|
+
end
|
16
|
+
create_first_level(conditions_data_json)
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
include ConditionFactory
|
21
|
+
|
22
|
+
def create_first_level(conditions_data_json)
|
23
|
+
unless conditions_data_json['firstLevel'].empty?
|
24
|
+
left_first_level = conditions_data_json['firstLevel'].shift
|
25
|
+
left_child = create_second_level(left_first_level)
|
26
|
+
|
27
|
+
unless conditions_data_json['firstLevel'].empty?
|
28
|
+
or_operator = conditions_data_json['firstLevelOrOperators'].shift
|
29
|
+
if or_operator
|
30
|
+
return Tree.new(or_operator, left_child, create_first_level(conditions_data_json))
|
31
|
+
end
|
32
|
+
|
33
|
+
right_first_level = conditions_data_json['firstLevel'].shift
|
34
|
+
right_child = create_second_level(right_first_level)
|
35
|
+
tree = Tree.new(or_operator, left_child, right_child)
|
36
|
+
|
37
|
+
unless conditions_data_json['firstLevel'].empty?
|
38
|
+
return Tree.new(conditions_data_json['firstLevelOrOperators'].shift, tree, create_first_level(conditions_data_json))
|
39
|
+
end
|
40
|
+
return tree
|
41
|
+
end
|
42
|
+
return left_child
|
43
|
+
end
|
44
|
+
nil
|
45
|
+
end
|
46
|
+
|
47
|
+
def create_second_level(conditions_data_json)
|
48
|
+
|
49
|
+
unless conditions_data_json.nil? or conditions_data_json['conditions'].empty?
|
50
|
+
left_child = Tree.new
|
51
|
+
left_child.condition = get_condition(conditions_data_json['conditions'].shift)
|
52
|
+
|
53
|
+
unless conditions_data_json['conditions'].empty?
|
54
|
+
or_operator = conditions_data_json['orOperators'].shift
|
55
|
+
if or_operator
|
56
|
+
return Tree.new(or_operator, left_child, create_second_level(conditions_data_json))
|
57
|
+
end
|
58
|
+
|
59
|
+
right_child = Tree.new
|
60
|
+
right_child.condition = get_condition(conditions_data_json['conditions'].shift)
|
61
|
+
tree = Tree.new(or_operator, left_child, right_child)
|
62
|
+
unless conditions_data_json['conditions'].empty?
|
63
|
+
return Tree.new(conditions_data_json['orOperators'].shift, tree, create_second_level(conditions_data_json))
|
64
|
+
end
|
65
|
+
return tree
|
66
|
+
end
|
67
|
+
return left_child
|
68
|
+
end
|
69
|
+
nil
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module Kameleoon
|
2
|
+
module Utils
|
3
|
+
ALPHA_NUMERIC_CHARS = 'abcdef0123456789'
|
4
|
+
|
5
|
+
def self.in_seconds(days)
|
6
|
+
days * 60 * 60 * 24
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.generate_random_string(length)
|
10
|
+
(1..length).map { ALPHA_NUMERIC_CHARS[rand(ALPHA_NUMERIC_CHARS.length)] }.join
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
metadata
ADDED
@@ -0,0 +1,99 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: kameleoon-client-ruby
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Kameleoon - Guillaume Grandjean
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2021-04-14 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: em-http-request
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 1.1.5
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 1.1.5
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: em-synchrony
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 1.0.6
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 1.0.6
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rufus-scheduler
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '3.7'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '3.7'
|
55
|
+
description: Kameleoon Client Ruby Software Development Kit
|
56
|
+
email:
|
57
|
+
- ggrandjean@kameleoon.com
|
58
|
+
executables: []
|
59
|
+
extensions: []
|
60
|
+
extra_rdoc_files: []
|
61
|
+
files:
|
62
|
+
- README.md
|
63
|
+
- lib/kameleoon.rb
|
64
|
+
- lib/kameleoon/client.rb
|
65
|
+
- lib/kameleoon/cookie.rb
|
66
|
+
- lib/kameleoon/data.rb
|
67
|
+
- lib/kameleoon/exceptions.rb
|
68
|
+
- lib/kameleoon/factory.rb
|
69
|
+
- lib/kameleoon/request.rb
|
70
|
+
- lib/kameleoon/targeting/condition.rb
|
71
|
+
- lib/kameleoon/targeting/condition_factory.rb
|
72
|
+
- lib/kameleoon/targeting/conditions/custom_datum.rb
|
73
|
+
- lib/kameleoon/targeting/models.rb
|
74
|
+
- lib/kameleoon/targeting/tree_builder.rb
|
75
|
+
- lib/kameleoon/utils.rb
|
76
|
+
homepage: https://developers.kameleoon.com/ruby-sdk.html
|
77
|
+
licenses:
|
78
|
+
- GPL-3.0
|
79
|
+
metadata: {}
|
80
|
+
post_install_message:
|
81
|
+
rdoc_options: []
|
82
|
+
require_paths:
|
83
|
+
- lib
|
84
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
85
|
+
requirements:
|
86
|
+
- - ">="
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: '0'
|
89
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
90
|
+
requirements:
|
91
|
+
- - ">="
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: '0'
|
94
|
+
requirements: []
|
95
|
+
rubygems_version: 3.0.3
|
96
|
+
signing_key:
|
97
|
+
specification_version: 4
|
98
|
+
summary: Kameleoon Client Ruby SDK
|
99
|
+
test_files: []
|