kameleoon-client-ruby 1.0.8 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +4 -19
- data/lib/kameleoon/client.rb +195 -25
- data/lib/kameleoon/cookie.rb +9 -1
- data/lib/kameleoon/data.rb +14 -4
- data/lib/kameleoon/exceptions.rb +6 -1
- data/lib/kameleoon/query_graphql.rb +76 -0
- data/lib/kameleoon/request.rb +25 -1
- data/lib/kameleoon/targeting/condition_factory.rb +0 -1
- data/lib/kameleoon/targeting/conditions/custom_datum.rb +9 -10
- data/lib/kameleoon/targeting/models.rb +1 -1
- data/lib/kameleoon/targeting/tree_builder.rb +0 -1
- data/lib/kameleoon/version.rb +1 -1
- metadata +5 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 39f1a248856acac59bccf9df044f714962c44859ac9d253cfce7d4c963506d72
|
4
|
+
data.tar.gz: 9931a050f2b2de5e761a16b276a88a47a833b8f094f6e62be29794ff4dc56fd1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 238bbd2ccd8bd757dea1ee47326d412f5614bb031f5e6f2e27ac4f300a13455df1b9d7bcd70049324d50486db627f451ffdf46fe3805fe76cea44454e3d408af
|
7
|
+
data.tar.gz: 94660b0ffc6c5e04478362a28db232998ac38a848fa0974331f2dd9364a150eb07c8474c5b4d66b234c997de99330eda73f02e31a6be24f98b07127a7566b860
|
data/README.md
CHANGED
@@ -1,22 +1,7 @@
|
|
1
|
-
# Kameleoon
|
1
|
+
# Kameleoon Ruby SDK
|
2
2
|
|
3
|
-
|
3
|
+
## Getting Started
|
4
4
|
|
5
|
-
|
6
|
-
#### Prerequisite:
|
7
|
-
* [Install ruby](https://www.ruby-lang.org/en/documentation/installation)
|
5
|
+
Our SDK gives you the possibility of running experiments and activating feature flags on your Ruby platform. Integrating our SDK into your applications is easy, and its footprint (in terms of memory and network usage) is low.
|
8
6
|
|
9
|
-
|
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
|
-
#### Run Tests:
|
18
|
-
##### Unit
|
19
|
-
* Run `./test.sh -u`
|
20
|
-
##### Integration
|
21
|
-
Make sure you kill manually the server test app after the tests are done.
|
22
|
-
* Run `./test.sh -i`
|
7
|
+
You can refer to the [SDK reference](https://developers.kameleoon.com/ruby-sdk.html#reference) to check out all possible features of the SDK. Also make sure you check out our [Getting started tutorial](https://developers.kameleoon.com/ruby-sdk.html#getting-started) which we have prepared to walk you through the installation and implementation.
|
data/lib/kameleoon/client.rb
CHANGED
@@ -1,12 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'kameleoon/targeting/models'
|
2
4
|
require 'kameleoon/request'
|
3
5
|
require 'kameleoon/exceptions'
|
4
6
|
require 'kameleoon/cookie'
|
7
|
+
require 'kameleoon/query_graphql'
|
5
8
|
require 'rufus/scheduler'
|
6
9
|
require 'yaml'
|
7
10
|
require 'json'
|
8
11
|
require 'em-synchrony'
|
12
|
+
require 'em-synchrony/em-http'
|
13
|
+
require 'em-synchrony/fiber_iterator'
|
9
14
|
require 'objspace'
|
15
|
+
require 'time'
|
10
16
|
|
11
17
|
module Kameleoon
|
12
18
|
##
|
@@ -27,9 +33,11 @@ module Kameleoon
|
|
27
33
|
@default_timeout = config['default_timeout'] || default_timeout # in ms
|
28
34
|
@interval = config['actions_configuration_refresh_interval'].to_s + 'm' || interval
|
29
35
|
@tracking_url = config['tracking_url'] || "https://api-ssx.kameleoon.com"
|
36
|
+
@api_data_url = "https://api-data.kameleoon.com"
|
30
37
|
@client_id = client_id || config['client_id']
|
31
38
|
@client_secret = client_secret || config['client_secret']
|
32
39
|
@data_maximum_size = config['visitor_data_maximum_size'] || 500 # mb
|
40
|
+
@environment = config['environment'] || DEFAULT_ENVIRONMENT
|
33
41
|
@verbose_mode = config['verbose_mode'] || false
|
34
42
|
@experiments = []
|
35
43
|
@feature_flags = []
|
@@ -83,8 +91,10 @@ module Kameleoon
|
|
83
91
|
# @raise [Kameleoon::Exception::ExperimentConfigurationNotFound] Raise when experiment configuration is not found
|
84
92
|
# @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
|
85
93
|
# @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
|
94
|
+
# @raise [Kameleoon::Exception::VisitorCodeNotValid] If the visitor code is empty or longer than 255 chars
|
86
95
|
#
|
87
96
|
def trigger_experiment(visitor_code, experiment_id, timeout = @default_timeout)
|
97
|
+
check_visitor_code(visitor_code)
|
88
98
|
experiment = @experiments.find { |experiment| experiment['id'].to_s == experiment_id.to_s }
|
89
99
|
if experiment.nil?
|
90
100
|
raise Exception::ExperimentConfigurationNotFound.new(experiment_id)
|
@@ -115,6 +125,7 @@ module Kameleoon
|
|
115
125
|
end
|
116
126
|
variation_id.to_i
|
117
127
|
else
|
128
|
+
check_site_code_enable(experiment)
|
118
129
|
visitor_data = @data.select { |key, value| key.to_s == visitor_code }.values.flatten! || []
|
119
130
|
if experiment['targetingSegment'].nil? || experiment['targetingSegment'].check_tree(visitor_data)
|
120
131
|
threshold = obtain_hash_double(visitor_code, experiment['respoolTime'], experiment['id'])
|
@@ -143,7 +154,10 @@ module Kameleoon
|
|
143
154
|
# @param [String] visitor_code Visitor code
|
144
155
|
# @param [...Data] data Data to associate with the visitor code
|
145
156
|
#
|
157
|
+
# @raise [Kameleoon::Exception::VisitorCodeNotValid] If the visitor code is empty or longer than 255 chars
|
158
|
+
#
|
146
159
|
def add_data(visitor_code, *args)
|
160
|
+
check_visitor_code(visitor_code)
|
147
161
|
while ObjectSpace.memsize_of(@data) > @data_maximum_size * (2**20) do
|
148
162
|
@data.shift
|
149
163
|
end
|
@@ -167,7 +181,10 @@ module Kameleoon
|
|
167
181
|
# @param [Integer] goal_id Id of the goal
|
168
182
|
# @param [Float] revenue Optional - Revenue of the conversion.
|
169
183
|
#
|
184
|
+
# @raise [Kameleoon::Exception::VisitorCodeNotValid] If the visitor code is empty or longer than 255 chars
|
185
|
+
#
|
170
186
|
def track_conversion(visitor_code, goal_id, revenue = 0.0)
|
187
|
+
check_visitor_code(visitor_code)
|
171
188
|
add_data(visitor_code, Conversion.new(goal_id, revenue))
|
172
189
|
flush(visitor_code)
|
173
190
|
end
|
@@ -181,7 +198,10 @@ module Kameleoon
|
|
181
198
|
#
|
182
199
|
# @param [String] visitor_code Optional field - Visitor code, without visitor code it flush all of the data
|
183
200
|
#
|
201
|
+
# @raise [Kameleoon::Exception::VisitorCodeNotValid] If the visitor code is not null and empty or longer than 255 chars
|
202
|
+
#
|
184
203
|
def flush(visitor_code = nil)
|
204
|
+
check_visitor_code(visitor_code) unless visitor_code.nil?
|
185
205
|
track_data(visitor_code)
|
186
206
|
end
|
187
207
|
|
@@ -203,9 +223,9 @@ module Kameleoon
|
|
203
223
|
def obtain_variation_associated_data(variation_id)
|
204
224
|
variation = @experiments.map { |experiment| experiment['variations'] }.flatten.select { |variation| variation['id'].to_i == variation_id.to_i }.first
|
205
225
|
if variation.nil?
|
206
|
-
raise Exception::
|
226
|
+
raise Exception::VariationConfigurationNotFound.new(variation_id)
|
207
227
|
else
|
208
|
-
variation['customJson']
|
228
|
+
JSON.parse(variation['customJson'])
|
209
229
|
end
|
210
230
|
end
|
211
231
|
|
@@ -221,8 +241,10 @@ module Kameleoon
|
|
221
241
|
#
|
222
242
|
# @raise [Kameleoon::Exception::FeatureConfigurationNotFound]
|
223
243
|
# @raise [Kameleoon::Exception::NotTargeted]
|
244
|
+
# @raise [Kameleoon::Exception::VisitorCodeNotValid] If the visitor code is empty or longer than 255 chars
|
224
245
|
#
|
225
246
|
def activate_feature(visitor_code, feature_key, timeout = @default_timeout)
|
247
|
+
check_visitor_code(visitor_code)
|
226
248
|
feature_flag = get_feature_flag(feature_key)
|
227
249
|
id = feature_flag['id']
|
228
250
|
if @blocking
|
@@ -247,18 +269,23 @@ module Kameleoon
|
|
247
269
|
result.to_s != "null"
|
248
270
|
|
249
271
|
else
|
272
|
+
check_site_code_enable(feature_flag)
|
250
273
|
visitor_data = @data.select { |key, value| key.to_s == visitor_code }.values.flatten! || []
|
251
|
-
|
274
|
+
unless feature_flag['targetingSegment'].nil? || feature_flag['targetingSegment'].check_tree(visitor_data)
|
275
|
+
raise Exception::NotTargeted.new(visitor_code)
|
276
|
+
end
|
277
|
+
|
278
|
+
if is_feature_flag_scheduled(feature_flag, Time.now.to_i)
|
252
279
|
threshold = obtain_hash_double(visitor_code, {}, id)
|
253
|
-
if threshold
|
254
|
-
track_experiment(visitor_code, id, feature_flag["
|
255
|
-
|
280
|
+
if threshold >= 1 - feature_flag['expositionRate']
|
281
|
+
track_experiment(visitor_code, id, feature_flag["variations"].first['id'])
|
282
|
+
true
|
256
283
|
else
|
257
284
|
track_experiment(visitor_code, id, REFERENCE, true)
|
258
|
-
|
285
|
+
false
|
259
286
|
end
|
260
287
|
else
|
261
|
-
|
288
|
+
false
|
262
289
|
end
|
263
290
|
end
|
264
291
|
end
|
@@ -269,46 +296,83 @@ module Kameleoon
|
|
269
296
|
# A feature variable can be changed easily via our web application.
|
270
297
|
#
|
271
298
|
# @param [String | Integer] feature_key
|
272
|
-
# @param [String
|
299
|
+
# @param [String] variable_key
|
273
300
|
#
|
274
301
|
# @raise [Kameleoon::Exception::FeatureConfigurationNotFound]
|
275
302
|
# @raise [Kameleoon::Exception::FeatureVariableNotFound]
|
276
303
|
#
|
277
304
|
def obtain_feature_variable(feature_key, variable_key)
|
278
305
|
feature_flag = get_feature_flag(feature_key)
|
279
|
-
custom_json = feature_flag["variations"].first['customJson'][variable_key.to_s]
|
306
|
+
custom_json = JSON.parse(feature_flag["variations"].first['customJson'])[variable_key.to_s]
|
280
307
|
if custom_json.nil?
|
281
308
|
raise Exception::FeatureVariableNotFound.new("Feature variable not found")
|
282
309
|
end
|
283
310
|
case custom_json['type']
|
284
311
|
when "Boolean"
|
285
|
-
return
|
312
|
+
return custom_json['value'].downcase == "true"
|
286
313
|
when "String"
|
287
|
-
return custom_json['value'].to_s
|
288
|
-
when "Number"
|
289
|
-
return custom_json['value'].to_f
|
290
|
-
when "Json"
|
291
314
|
return custom_json['value']
|
315
|
+
when "Number"
|
316
|
+
return custom_json['value'].to_i
|
317
|
+
when "JSON"
|
318
|
+
return JSON.parse(custom_json['value'])
|
292
319
|
else
|
293
320
|
raise TypeError.new("Unknown type for feature variable")
|
294
321
|
end
|
295
322
|
end
|
296
323
|
|
324
|
+
##
|
325
|
+
# The retrieved_data_from_remote_source method allows you to retrieve data (according to a key passed as argument)
|
326
|
+
# stored on a remote Kameleoon server. Usually data will be stored on our remote servers via the use of our Data API.
|
327
|
+
# This method, along with the availability of our highly scalable servers for this purpose, provides a convenient way
|
328
|
+
# to quickly store massive amounts of data that can be later retrieved for each of your visitors / users.
|
329
|
+
#
|
330
|
+
# @param [String] key Key you want to retrieve data. This field is mandatory.
|
331
|
+
# @param [Int] timeout Timeout for request. Equals default_timeout in a config file. This field is optional.
|
332
|
+
#
|
333
|
+
# @return [Hash] Hash object of the json object.
|
334
|
+
#
|
335
|
+
#
|
336
|
+
def retrieve_data_from_remote_source(key, timeout = @default_timeout)
|
337
|
+
connexion_options = { connect_timeout: (timeout.to_f / 1000.0) }
|
338
|
+
path = get_api_data_request_url(key)
|
339
|
+
log "Retrieve API Data connexion: #{connexion_options.inspect}"
|
340
|
+
response = get_sync(@api_data_url + path, connexion_options)
|
341
|
+
if is_successful_sync(response)
|
342
|
+
JSON.parse(response.body) unless response.nil?
|
343
|
+
else
|
344
|
+
return nil
|
345
|
+
end
|
346
|
+
end
|
347
|
+
|
297
348
|
private
|
298
|
-
|
349
|
+
|
350
|
+
API_SSX_URL = 'https://api-ssx.kameleoon.com'
|
299
351
|
REFERENCE = 0
|
352
|
+
STATUS_ACTIVE = 'ACTIVE'
|
353
|
+
FEATURE_STATUS_DEACTIVATED = 'DEACTIVATED'
|
354
|
+
DEFAULT_ENVIRONMENT = 'production'
|
300
355
|
attr :site_code, :client_id, :client_secret, :access_token, :experiments, :feature_flags, :scheduler, :data,
|
301
356
|
:blocking, :tracking_url, :default_timeout, :interval, :memory_limit, :verbose_mode
|
302
357
|
|
303
358
|
def fetch_configuration
|
304
359
|
@scheduler = Rufus::Scheduler.singleton
|
305
360
|
@scheduler.every @interval do
|
306
|
-
log(
|
307
|
-
|
361
|
+
log('Scheduled job to fetch configuration is starting.')
|
362
|
+
fetch_configuration_job_graphql
|
308
363
|
end
|
309
364
|
@scheduler.schedule '0s' do
|
310
|
-
log(
|
311
|
-
|
365
|
+
log('Start-up, fetching is starting')
|
366
|
+
fetch_configuration_job_graphql
|
367
|
+
end
|
368
|
+
end
|
369
|
+
|
370
|
+
def fetch_configuration_job_graphql
|
371
|
+
EM.synchrony do
|
372
|
+
obtain_access_token
|
373
|
+
@experiments = obtain_experiments_graphql(@site_code) || @experiments
|
374
|
+
@feature_flags = obtain_feature_flags_graphql(@site_code, @environment) || @feature_flags
|
375
|
+
EM.stop
|
312
376
|
end
|
313
377
|
end
|
314
378
|
|
@@ -387,8 +451,37 @@ module Kameleoon
|
|
387
451
|
end
|
388
452
|
|
389
453
|
def complete_experiment(experiment)
|
390
|
-
unless experiment['
|
391
|
-
experiment['variations'] = experiment['
|
454
|
+
unless experiment['variations'].nil?
|
455
|
+
experiment['variations'] = experiment['variations'].map { |variationId| obtain_variation(variationId) }
|
456
|
+
end
|
457
|
+
unless experiment['targetingSegmentId'].nil?
|
458
|
+
experiment['targetingSegment'] = Kameleoon::Targeting::Segment.new(obtain_segment(experiment['targetingSegmentId']))
|
459
|
+
end
|
460
|
+
experiment
|
461
|
+
end
|
462
|
+
|
463
|
+
# fetching segment for both types: experiments and feature_flags (campaigns)
|
464
|
+
def complete_campaign_graphql(campaign)
|
465
|
+
campaign['id'] = campaign['id'].to_i
|
466
|
+
campaign['status'] = campaign['status']
|
467
|
+
unless campaign['deviations'].nil?
|
468
|
+
campaign['deviations'] = Hash[*campaign['deviations'].map { |it| [ it['variationId'] == '0' ? 'origin' : it['variationId'], it['value'] ] }.flatten]
|
469
|
+
end
|
470
|
+
unless campaign['respoolTime'].nil?
|
471
|
+
campaign['respoolTime'] = Hash[*campaign['respoolTime'].map { |it| [ it['variationId'] == '0' ? 'origin' : it['variationId'], it['value'] ] }.flatten]
|
472
|
+
end
|
473
|
+
unless campaign['variations'].nil?
|
474
|
+
campaign['variations'] = campaign['variations'].map { |it| {'id' => it['id'].to_i, 'customJson' => it['customJson']} }
|
475
|
+
end
|
476
|
+
unless campaign['segment'].nil?
|
477
|
+
campaign['targetingSegment'] = Kameleoon::Targeting::Segment.new((campaign['segment']))
|
478
|
+
end
|
479
|
+
campaign
|
480
|
+
end
|
481
|
+
|
482
|
+
def complete_experiment(experiment)
|
483
|
+
unless experiment['variations'].nil?
|
484
|
+
experiment['variations'] = experiment['variations'].map { |variationId| obtain_variation(variationId) }
|
392
485
|
end
|
393
486
|
unless experiment['targetingSegmentId'].nil?
|
394
487
|
experiment['targetingSegment'] = Kameleoon::Targeting::Segment.new(obtain_segment(experiment['targetingSegmentId']))
|
@@ -396,6 +489,15 @@ module Kameleoon
|
|
396
489
|
experiment
|
397
490
|
end
|
398
491
|
|
492
|
+
def obtain_experiments_graphql(site_code, per_page = -1)
|
493
|
+
log "Fetching experiments GraphQL"
|
494
|
+
experiments = fetch_all_graphql("v1/graphql?perPage=#{per_page}", Kameleoon::Query.query_experiments(site_code)).map { |it| JSON.parse(it.response)['data']['experiments']['edges'] }.flatten.map do |experiment|
|
495
|
+
complete_campaign_graphql(experiment['node'])
|
496
|
+
end
|
497
|
+
log "Experiment are fetched: " + experiments.inspect
|
498
|
+
experiments
|
499
|
+
end
|
500
|
+
|
399
501
|
def obtain_tests(site_id, per_page = -1)
|
400
502
|
log "Fetching experiments"
|
401
503
|
query_values = { 'perPage' => per_page }
|
@@ -411,12 +513,21 @@ module Kameleoon
|
|
411
513
|
experiments
|
412
514
|
end
|
413
515
|
|
516
|
+
def obtain_feature_flags_graphql(site_id, environment = @environment, per_page = -1)
|
517
|
+
log "Fetching feature flags GraphQL"
|
518
|
+
feature_flags = fetch_all_graphql("v1/graphql?perPage=#{per_page}", Kameleoon::Query.query_feature_flags(site_code, environment)).map { |it| JSON.parse(it.response)['data']['featureFlags']['edges'] }.flatten.map do |feature_flag|
|
519
|
+
complete_campaign_graphql(feature_flag['node'])
|
520
|
+
end
|
521
|
+
log "Feature flags are fetched: " + feature_flags.inspect
|
522
|
+
feature_flags
|
523
|
+
end
|
524
|
+
|
414
525
|
def obtain_feature_flags(site_id, per_page = -1)
|
415
526
|
log "Fetching feature flags"
|
416
527
|
query_values = { 'perPage' => per_page }
|
417
528
|
filters = [
|
418
529
|
hash_filter('siteId', 'EQUAL', [site_id]),
|
419
|
-
hash_filter('status', '
|
530
|
+
hash_filter('status', 'IN', ['ACTIVE', 'PAUSED'])
|
420
531
|
]
|
421
532
|
feature_flags = fetch_all('feature-flags', query_values, filters).map { |it| JSON.parse(it.response) }.flatten.map do |ff|
|
422
533
|
complete_experiment(ff)
|
@@ -425,6 +536,19 @@ module Kameleoon
|
|
425
536
|
feature_flags
|
426
537
|
end
|
427
538
|
|
539
|
+
def fetch_all_graphql(path, query_graphql = {})
|
540
|
+
results = []
|
541
|
+
current_page = 1
|
542
|
+
loop do
|
543
|
+
http = fetch_one_graphql(path, query_graphql)
|
544
|
+
break if http == false
|
545
|
+
results.push(http)
|
546
|
+
break if http.response_header["X-Pagination-Page-Count"].to_i <= current_page
|
547
|
+
current_page += 1
|
548
|
+
end
|
549
|
+
results
|
550
|
+
end
|
551
|
+
|
428
552
|
def fetch_all(path, query_values = {}, filters = [])
|
429
553
|
results = []
|
430
554
|
current_page = 1
|
@@ -439,6 +563,15 @@ module Kameleoon
|
|
439
563
|
results
|
440
564
|
end
|
441
565
|
|
566
|
+
def fetch_one_graphql(path, query_graphql = {})
|
567
|
+
request = EM::Synchrony.sync post({ :path => path, :body => query_graphql, :head => hash_headers })
|
568
|
+
unless is_successful(request)
|
569
|
+
log "Failed to fetch" + request.inspect
|
570
|
+
return false
|
571
|
+
end
|
572
|
+
request
|
573
|
+
end
|
574
|
+
|
442
575
|
def fetch_one(path, query_values = {}, filters = [])
|
443
576
|
unless filters.empty?
|
444
577
|
query_values['filter'] = filters.to_json
|
@@ -476,11 +609,21 @@ module Kameleoon
|
|
476
609
|
"/dataTracking?" + URI.encode_www_form(get_common_ssx_parameters(visitor_code))
|
477
610
|
end
|
478
611
|
|
612
|
+
def get_api_data_request_url(key)
|
613
|
+
mapKey = {
|
614
|
+
siteCode: site_code,
|
615
|
+
key: key
|
616
|
+
}
|
617
|
+
"/data?#{URI.encode_www_form(mapKey)}"
|
618
|
+
end
|
619
|
+
|
479
620
|
def get_feature_flag(feature_key)
|
480
621
|
if feature_key.is_a?(String)
|
481
622
|
feature_flag = @feature_flags.select { |ff| ff['identificationKey'] == feature_key}.first
|
482
623
|
elsif feature_key.is_a?(Integer)
|
483
624
|
feature_flag = @feature_flags.select { |ff| ff['id'].to_i == feature_key}.first
|
625
|
+
print "\nPassing `feature_key` with type of `int` to `activate_feature` or `obtain_feature_variable` "\
|
626
|
+
"is deprecated, it will be removed in next releases. This is necessary to support multi-environment feature\n"
|
484
627
|
else
|
485
628
|
raise TypeError.new("Feature key should be a String or an Integer.")
|
486
629
|
end
|
@@ -490,6 +633,20 @@ module Kameleoon
|
|
490
633
|
feature_flag
|
491
634
|
end
|
492
635
|
|
636
|
+
def is_feature_flag_scheduled(feature_flag, date)
|
637
|
+
current_status = feature_flag['status'] == STATUS_ACTIVE
|
638
|
+
if feature_flag['featureStatus'] == FEATURE_STATUS_DEACTIVATED || feature_flag['schedules'].empty?
|
639
|
+
return current_status
|
640
|
+
end
|
641
|
+
feature_flag['schedules'].each do |schedule|
|
642
|
+
if (schedule['dateStart'].nil? || Time.parse(schedule['dateStart']).to_i < date) &&
|
643
|
+
(schedule['dateEnd'].nil? || Time.parse(schedule['dateEnd']).to_i > date)
|
644
|
+
return true
|
645
|
+
end
|
646
|
+
end
|
647
|
+
false
|
648
|
+
end
|
649
|
+
|
493
650
|
def track_experiment(visitor_code, experiment_id, variation_id = nil, none_variation = false)
|
494
651
|
data_not_sent = data_not_sent(visitor_code)
|
495
652
|
options = {
|
@@ -498,6 +655,7 @@ module Kameleoon
|
|
498
655
|
:head => { "Content-Type" => "text/plain"}
|
499
656
|
}
|
500
657
|
trial = 0
|
658
|
+
success = false
|
501
659
|
log "Start post tracking experiment: " + data_not_sent.inspect
|
502
660
|
Thread.new do
|
503
661
|
EM.synchrony do
|
@@ -507,12 +665,18 @@ module Kameleoon
|
|
507
665
|
if is_successful(request)
|
508
666
|
(data_not_sent.values[0] || []).each { |it| it.sent = true }
|
509
667
|
EM.stop
|
668
|
+
success = true
|
669
|
+
break
|
510
670
|
end
|
511
671
|
trial += 1
|
512
672
|
end
|
513
673
|
EM.stop
|
514
674
|
end
|
515
|
-
|
675
|
+
if success
|
676
|
+
log "Post to experiment tracking is done after " + (trial + 1).to_s + " trials"
|
677
|
+
elsif
|
678
|
+
log "Post to experiment tracking is failed after " + trial.to_s + " trials"
|
679
|
+
end
|
516
680
|
Thread.exit
|
517
681
|
end
|
518
682
|
end
|
@@ -551,6 +715,12 @@ module Kameleoon
|
|
551
715
|
end
|
552
716
|
end
|
553
717
|
|
718
|
+
def check_site_code_enable(exp_or_ff)
|
719
|
+
unless exp_or_ff['site'].nil? || exp_or_ff['site']['isKameleoonEnabled']
|
720
|
+
raise Exception::SiteCodeDisabled.new(site_code)
|
721
|
+
end
|
722
|
+
end
|
723
|
+
|
554
724
|
def data_not_sent(visitor_code = nil)
|
555
725
|
if visitor_code.nil?
|
556
726
|
@data.select {|key, values| values.any? {|data| !data.sent}}
|
@@ -565,4 +735,4 @@ module Kameleoon
|
|
565
735
|
end
|
566
736
|
end
|
567
737
|
end
|
568
|
-
end
|
738
|
+
end
|
data/lib/kameleoon/cookie.rb
CHANGED
@@ -26,10 +26,18 @@ module Kameleoon
|
|
26
26
|
identifier = visitor_code.to_s
|
27
27
|
identifier += container_id.to_s
|
28
28
|
if !respool_times.nil? && !respool_times.empty?
|
29
|
-
identifier += respool_times.values.
|
29
|
+
identifier += respool_times.sort.to_h.values.join.to_s
|
30
30
|
end
|
31
31
|
(Digest::SHA256.hexdigest(identifier.encode('UTF-8')).to_i(16) / (BigDecimal("2") ** BigDecimal("256"))).round(16)
|
32
32
|
end
|
33
|
+
|
34
|
+
def check_visitor_code(visitor_code)
|
35
|
+
if visitor_code.nil?
|
36
|
+
check_default_visitor_code('')
|
37
|
+
elsif
|
38
|
+
check_default_visitor_code(visitor_code)
|
39
|
+
end
|
40
|
+
end
|
33
41
|
|
34
42
|
private
|
35
43
|
|
data/lib/kameleoon/data.rb
CHANGED
@@ -1,4 +1,3 @@
|
|
1
|
-
|
2
1
|
module Kameleoon
|
3
2
|
NONCE_LENGTH = 16
|
4
3
|
|
@@ -29,6 +28,17 @@ module Kameleoon
|
|
29
28
|
def obtain_full_post_text_line
|
30
29
|
raise KameleoonError.new("ToDo: implement this method.")
|
31
30
|
end
|
31
|
+
|
32
|
+
def encode(url)
|
33
|
+
encoded_url = CGI.escape(url)
|
34
|
+
encoded_url.gsub! "%27", "'"
|
35
|
+
encoded_url.gsub! "%21", "!"
|
36
|
+
encoded_url.gsub! "+", "%20"
|
37
|
+
encoded_url.gsub! "%2A", "*"
|
38
|
+
encoded_url.gsub! "%28", "("
|
39
|
+
encoded_url.gsub! "%29", ")"
|
40
|
+
encoded_url
|
41
|
+
end
|
32
42
|
end
|
33
43
|
|
34
44
|
class CustomData < Data
|
@@ -63,7 +73,7 @@ module Kameleoon
|
|
63
73
|
def obtain_full_post_text_line
|
64
74
|
to_encode = "[[\"" + @value.to_s.gsub("\"", "\\\"") + "\",1]]"
|
65
75
|
nonce = Kameleoon::Utils.generate_random_string(NONCE_LENGTH)
|
66
|
-
"eventType=customData&index=" + @id.to_s + "&valueToCount=" +
|
76
|
+
"eventType=customData&index=" + @id.to_s + "&valueToCount=" + encode(to_encode) + "&overwrite=true&nonce=" + nonce
|
67
77
|
end
|
68
78
|
end
|
69
79
|
|
@@ -101,9 +111,9 @@ module Kameleoon
|
|
101
111
|
nonce = Kameleoon::Utils.generate_random_string(NONCE_LENGTH)
|
102
112
|
referrer_text = ""
|
103
113
|
unless @referrer.nil?
|
104
|
-
referrer_text = "&referrers=[" + @referrer + "]"
|
114
|
+
referrer_text = "&referrers=[" + @referrer.to_s + "]"
|
105
115
|
end
|
106
|
-
"eventType=page&href=" + @url + "&title=" + @title + "&keyPages=[]" + referrer_text + "&nonce=" + nonce
|
116
|
+
"eventType=page&href=" + encode(@url) + "&title=" + @title + "&keyPages=[]" + referrer_text + "&nonce=" + nonce
|
107
117
|
end
|
108
118
|
end
|
109
119
|
|
data/lib/kameleoon/exceptions.rb
CHANGED
@@ -10,7 +10,7 @@ module Kameleoon
|
|
10
10
|
super(value.to_s + " not found.")
|
11
11
|
end
|
12
12
|
end
|
13
|
-
class
|
13
|
+
class VariationConfigurationNotFound < NotFound
|
14
14
|
def initialize(id = "")
|
15
15
|
super("Variation " + id.to_s)
|
16
16
|
end
|
@@ -50,5 +50,10 @@ module Kameleoon
|
|
50
50
|
super("Visitor code not valid: " + message)
|
51
51
|
end
|
52
52
|
end
|
53
|
+
class SiteCodeDisabled < KameleoonError
|
54
|
+
def initialize(message = "")
|
55
|
+
super("Site with siteCode '" + message + "' is disabled")
|
56
|
+
end
|
57
|
+
end
|
53
58
|
end
|
54
59
|
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
module Kameleoon
|
2
|
+
module Query
|
3
|
+
|
4
|
+
def self.query_experiments(site_code)
|
5
|
+
'{
|
6
|
+
"operationName": "getExperiments",
|
7
|
+
"query": "query getExperiments($first: Int, $after: String, $filter: FilteringExpression, $sort: [SortingParameter!]) { experiments(first: $first, after: $after, filter: $filter, sort: $sort) { edges { node { id name type site { id code isKameleoonEnabled } status variations { id customJson } deviations { variationId value } respoolTime {variationId value } segment { id name conditionsData { firstLevelOrOperators firstLevel { orOperators conditions { targetingType isInclude ... on CustomDataTargetingCondition { customDataIndex value valueMatchType } } } } } __typename } __typename } pageInfo { endCursor hasNextPage __typename } totalCount __typename } }",
|
8
|
+
"variables": {
|
9
|
+
"filter": {
|
10
|
+
"and": [{
|
11
|
+
"condition": {
|
12
|
+
"field": "status",
|
13
|
+
"operator": "IN",
|
14
|
+
"parameters": ["ACTIVE", "DEVIATED", "USED_AS_PERSONALIZATION"]
|
15
|
+
}
|
16
|
+
},
|
17
|
+
{
|
18
|
+
"condition": {
|
19
|
+
"field": "type",
|
20
|
+
"operator": "IN",
|
21
|
+
"parameters": ["SERVER_SIDE", "HYBRID"]
|
22
|
+
}
|
23
|
+
},
|
24
|
+
{
|
25
|
+
"condition": {
|
26
|
+
"field": "siteCode",
|
27
|
+
"operator": "IN",
|
28
|
+
"parameters": ["' + site_code + '"]
|
29
|
+
}
|
30
|
+
}]
|
31
|
+
},
|
32
|
+
"sort": [{
|
33
|
+
"field": "id",
|
34
|
+
"direction": "ASC"
|
35
|
+
}]
|
36
|
+
}
|
37
|
+
}'
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.query_feature_flags(site_code, environment)
|
41
|
+
'{
|
42
|
+
"operationName": "getFeatureFlags",
|
43
|
+
"query": "query getFeatureFlags($first: Int, $after: String, $filter: FilteringExpression, $sort: [SortingParameter!]) { featureFlags(first: $first, after: $after, filter: $filter, sort: $sort) { edges { node { id name site { id code isKameleoonEnabled } bypassDeviation status variations { id customJson } respoolTime { variationId value } expositionRate identificationKey featureFlagSdkLanguageType featureStatus schedules { dateStart dateEnd } segment { id name conditionsData { firstLevelOrOperators firstLevel { orOperators conditions { targetingType isInclude ... on CustomDataTargetingCondition { customDataIndex value valueMatchType } } } } } __typename } __typename } pageInfo { endCursor hasNextPage __typename } totalCount __typename } }",
|
44
|
+
"variables": {
|
45
|
+
"filter": {
|
46
|
+
"and": [{
|
47
|
+
"condition": {
|
48
|
+
"field": "featureStatus",
|
49
|
+
"operator": "IN",
|
50
|
+
"parameters": ["ACTIVATED", "SCHEDULED", "DEACTIVATED"]
|
51
|
+
}
|
52
|
+
},
|
53
|
+
{
|
54
|
+
"condition": {
|
55
|
+
"field": "siteCode",
|
56
|
+
"operator": "IN",
|
57
|
+
"parameters": ["' + site_code + '"]
|
58
|
+
}
|
59
|
+
},
|
60
|
+
{
|
61
|
+
"condition": {
|
62
|
+
"field": "environment.key",
|
63
|
+
"operator": "IN",
|
64
|
+
"parameters": ["' + environment + '"]
|
65
|
+
}
|
66
|
+
}]
|
67
|
+
},
|
68
|
+
"sort": [{
|
69
|
+
"field": "id",
|
70
|
+
"direction": "ASC"
|
71
|
+
}]
|
72
|
+
}
|
73
|
+
}'
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
data/lib/kameleoon/request.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
require "em-synchrony/em-http"
|
2
2
|
require "kameleoon/version"
|
3
|
+
require 'net/http'
|
3
4
|
|
4
5
|
module Kameleoon
|
5
6
|
# @api private
|
@@ -20,6 +21,10 @@ module Kameleoon
|
|
20
21
|
request(Method::POST, request_options, url, connexion_options)
|
21
22
|
end
|
22
23
|
|
24
|
+
def get_sync(url = API_URL, connexion_options = {})
|
25
|
+
request_sync(Method::GET, url, connexion_options)
|
26
|
+
end
|
27
|
+
|
23
28
|
private
|
24
29
|
|
25
30
|
def request(method, request_options, url, connexion_options)
|
@@ -29,7 +34,22 @@ module Kameleoon
|
|
29
34
|
when Method::POST then
|
30
35
|
return EventMachine::HttpRequest.new(url, connexion_options).apost request_options
|
31
36
|
when Method::GET then
|
32
|
-
return EventMachine::HttpRequest.new(url, connexion_options).
|
37
|
+
return EventMachine::HttpRequest.new(url, connexion_options).get request_options
|
38
|
+
else
|
39
|
+
print "Unknown request type"
|
40
|
+
return false
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def request_sync(method, url, connexion_options)
|
45
|
+
request_options = {} if request_options.nil?
|
46
|
+
add_user_agent(request_options)
|
47
|
+
case method
|
48
|
+
when Method::GET then
|
49
|
+
uri = URI(url)
|
50
|
+
request = Net::HTTP.new(url)
|
51
|
+
response = Net::HTTP.get_response(uri)
|
52
|
+
response
|
33
53
|
else
|
34
54
|
print "Unknown request type"
|
35
55
|
return false
|
@@ -40,6 +60,10 @@ module Kameleoon
|
|
40
60
|
!request.nil? && request != false && /20\d/.match(request.response_header.status.to_s)
|
41
61
|
end
|
42
62
|
|
63
|
+
def is_successful_sync(response)
|
64
|
+
!response.nil? && response != false && response.is_a?(Net::HTTPSuccess)
|
65
|
+
end
|
66
|
+
|
43
67
|
def add_user_agent(request_options)
|
44
68
|
if request_options[:head].nil?
|
45
69
|
request_options[:head] = {'Kameleoon-Client' => 'sdk/ruby/' + Kameleoon::VERSION}
|
@@ -1,6 +1,5 @@
|
|
1
1
|
require 'kameleoon/targeting/condition'
|
2
2
|
require 'kameleoon/exceptions'
|
3
|
-
require 'kameleoon/targeting/models'
|
4
3
|
|
5
4
|
module Kameleoon
|
6
5
|
#@api private
|
@@ -20,22 +19,22 @@ module Kameleoon
|
|
20
19
|
end
|
21
20
|
@operator = json_condition['valueMatchType']
|
22
21
|
|
23
|
-
if json_condition['value'].nil?
|
24
|
-
|
25
|
-
end
|
22
|
+
# if json_condition['value'].nil?
|
23
|
+
# raise Exception::NotFoundError.new('value')
|
24
|
+
# end
|
26
25
|
@value = json_condition['value']
|
27
26
|
|
28
27
|
@type = ConditionType::CUSTOM_DATUM
|
29
28
|
|
30
|
-
if json_condition['include'].nil?
|
31
|
-
raise Exception::NotFoundError.new('include')
|
29
|
+
if json_condition['include'].nil? && json_condition['isInclude'].nil?
|
30
|
+
raise Exception::NotFoundError.new('include / isInclude missed')
|
32
31
|
end
|
33
|
-
@include = json_condition['include']
|
32
|
+
@include = json_condition['include'] || json_condition['isInclude']
|
34
33
|
end
|
35
34
|
|
36
35
|
def check(datas)
|
37
36
|
is_targeted = false
|
38
|
-
custom_data = datas.select { |data| data.instance == DataType::CUSTOM && data.id == @index }.
|
37
|
+
custom_data = datas.select { |data| data.instance == DataType::CUSTOM && data.id == @index }.last
|
39
38
|
if custom_data.nil?
|
40
39
|
is_targeted = (@operator == Operator::UNDEFINED.to_s)
|
41
40
|
else
|
@@ -65,11 +64,11 @@ module Kameleoon
|
|
65
64
|
is_targeted = true
|
66
65
|
end
|
67
66
|
when Operator::IS_TRUE.to_s
|
68
|
-
if custom_data.value == true
|
67
|
+
if custom_data.value == 'true'
|
69
68
|
is_targeted = true
|
70
69
|
end
|
71
70
|
when Operator::IS_FALSE.to_s
|
72
|
-
if custom_data.value == false
|
71
|
+
if custom_data.value == 'false'
|
73
72
|
is_targeted = true
|
74
73
|
end
|
75
74
|
else
|
data/lib/kameleoon/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: kameleoon-client-ruby
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0
|
4
|
+
version: 1.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
|
-
- Kameleoon
|
7
|
+
- Kameleoon
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2022-04-12 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: em-http-request
|
@@ -54,7 +54,7 @@ dependencies:
|
|
54
54
|
version: '3.7'
|
55
55
|
description: Kameleoon Client Ruby Software Development Kit
|
56
56
|
email:
|
57
|
-
-
|
57
|
+
- sdk@kameleoon.com
|
58
58
|
executables: []
|
59
59
|
extensions: []
|
60
60
|
extra_rdoc_files: []
|
@@ -66,6 +66,7 @@ files:
|
|
66
66
|
- lib/kameleoon/data.rb
|
67
67
|
- lib/kameleoon/exceptions.rb
|
68
68
|
- lib/kameleoon/factory.rb
|
69
|
+
- lib/kameleoon/query_graphql.rb
|
69
70
|
- lib/kameleoon/request.rb
|
70
71
|
- lib/kameleoon/targeting/condition.rb
|
71
72
|
- lib/kameleoon/targeting/condition_factory.rb
|