kameleoon-client-ruby 1.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 +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: []
|