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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0b1daddd63c5a75a2eadf683c4055b70a748b9aaff7326c4ec22913eca3b733a
4
- data.tar.gz: 128608fb9a5060581a884ac46f36e2e73cca216f27acf97f2163947bfbc07477
3
+ metadata.gz: 39f1a248856acac59bccf9df044f714962c44859ac9d253cfce7d4c963506d72
4
+ data.tar.gz: 9931a050f2b2de5e761a16b276a88a47a833b8f094f6e62be29794ff4dc56fd1
5
5
  SHA512:
6
- metadata.gz: e33eeba8b980325174d203c2d0e5ea11ad99565f8e14f6e9be48e962ec1038b972347c5d34f9171e47bf7999cf8b5cafad128a541da2a82e367a84b02c2aedbc
7
- data.tar.gz: 4135c9bd59d70db3c67cc3d887d02b41fb890b1d8a31dfafd25a5e46946c27cab1c8de7249156e483ad7b224c109146d0b33f79a80e9bc0db6ca54bc094a6c3e
6
+ metadata.gz: 238bbd2ccd8bd757dea1ee47326d412f5614bb031f5e6f2e27ac4f300a13455df1b9d7bcd70049324d50486db627f451ffdf46fe3805fe76cea44454e3d408af
7
+ data.tar.gz: 94660b0ffc6c5e04478362a28db232998ac38a848fa0974331f2dd9364a150eb07c8474c5b4d66b234c997de99330eda73f02e31a6be24f98b07127a7566b860
data/README.md CHANGED
@@ -1,22 +1,7 @@
1
- # Kameleoon RUBY SDK
1
+ # Kameleoon Ruby SDK
2
2
 
3
- This is the repository for the Kameleoon Ruby SDK.
3
+ ## Getting Started
4
4
 
5
- ### How to build and install Kameleoon Gem locally
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
- #### Build and install the gem:
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.
@@ -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::VariationNotFound.new(variation_id)
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
- if feature_flag['targetingSegment'].nil? || feature_flag['targetingSegment'].check_tree(visitor_data)
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 <= feature_flag['expositionRate']
254
- track_experiment(visitor_code, id, feature_flag["variationsId"].first)
255
- return true
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
- return false
285
+ false
259
286
  end
260
287
  else
261
- raise Exception::NotTargeted.new(visitor_code)
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 ] variable_key
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 !!custom_json['value']
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
- API_SSX_URL = "https://api-ssx.kameleoon.com"
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("Scheduled job to fetch configuration is starting.")
307
- fetch_configuration_job
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("Start-up, fetching is starting")
311
- fetch_configuration_job
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['variationsId'].nil?
391
- experiment['variations'] = experiment['variationsId'].map { |variationId| obtain_variation(variationId) }
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', 'EQUAL', ['ACTIVE'])
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
- log "Post to experiment tracking is done after " + trial.to_s + " trials"
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
@@ -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.sort.join.to_s
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
 
@@ -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=" + URI.encode_www_form_component(to_encode) + "&overwrite=true&nonce=" + nonce
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
 
@@ -10,7 +10,7 @@ module Kameleoon
10
10
  super(value.to_s + " not found.")
11
11
  end
12
12
  end
13
- class VariationNotFound < NotFound
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
@@ -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).aget request_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,4 +1,3 @@
1
- require 'kameleoon/targeting/models'
2
1
  require 'kameleoon/targeting/conditions/custom_datum'
3
2
 
4
3
  module Kameleoon
@@ -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
- raise Exception::NotFoundError.new('value')
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 }.first
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
@@ -23,7 +23,7 @@ module Kameleoon
23
23
  if hash["id"].nil?
24
24
  raise Kameleoon::Exception::NotFound.new("id")
25
25
  end
26
- @id = hash["id"]
26
+ @id = hash["id"].to_i
27
27
  if hash["conditionsData"].nil?
28
28
  raise Kameleoon::Exception::NotFound.new(hash["conditionsData"])
29
29
  end
@@ -1,5 +1,4 @@
1
1
  require 'kameleoon/targeting/condition_factory'
2
- require 'kameleoon/targeting/models'
3
2
 
4
3
  module Kameleoon
5
4
  #@api private
@@ -1,3 +1,3 @@
1
1
  module Kameleoon
2
- VERSION = '1.0.8'
2
+ VERSION = '1.1.0'
3
3
  end
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.8
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
- - Kameleoon - Guillaume Grandjean
7
+ - Kameleoon
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-07-27 00:00:00.000000000 Z
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
- - ggrandjean@kameleoon.com
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