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 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: []