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 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,5 @@
1
+ #
2
+ # Kameleoon Ruby Client SDK
3
+ #
4
+ require "kameleoon/factory"
5
+ require "kameleoon/client"
@@ -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: []