kameleoon-client-ruby 2.0.0 → 2.1.1
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 +4 -4
- data/lib/kameleoon/client.rb +277 -260
- data/lib/kameleoon/configuration/feature_flag.rb +13 -24
- data/lib/kameleoon/configuration/rule.rb +17 -5
- data/lib/kameleoon/configuration/settings.rb +20 -0
- data/lib/kameleoon/cookie.rb +3 -3
- data/lib/kameleoon/data/browser.rb +33 -0
- data/lib/kameleoon/data/conversion.rb +26 -0
- data/lib/kameleoon/data/custom_data.rb +53 -0
- data/lib/kameleoon/data/data.rb +35 -0
- data/lib/kameleoon/data/device.rb +26 -0
- data/lib/kameleoon/data/page_view.rb +31 -0
- data/lib/kameleoon/data/user_agent.rb +14 -0
- data/lib/kameleoon/hybrid/manager.rb +60 -0
- data/lib/kameleoon/network/activity_event.rb +31 -0
- data/lib/kameleoon/network/experiment_event.rb +35 -0
- data/lib/kameleoon/network/uri_helper.rb +36 -0
- data/lib/kameleoon/network/url_provider.rb +71 -0
- data/lib/kameleoon/real_time/real_time_configuration_service.rb +98 -0
- data/lib/kameleoon/real_time/real_time_event.rb +22 -0
- data/lib/kameleoon/real_time/sse_client.rb +111 -0
- data/lib/kameleoon/real_time/sse_message.rb +23 -0
- data/lib/kameleoon/real_time/sse_request.rb +59 -0
- data/lib/kameleoon/request.rb +5 -19
- data/lib/kameleoon/storage/cache.rb +84 -0
- data/lib/kameleoon/storage/cache_factory.rb +23 -0
- data/lib/kameleoon/targeting/condition.rb +41 -12
- data/lib/kameleoon/targeting/condition_factory.rb +35 -12
- data/lib/kameleoon/targeting/conditions/browser_condition.rb +71 -0
- data/lib/kameleoon/targeting/conditions/conversion_condition.rb +21 -0
- data/lib/kameleoon/targeting/conditions/custom_datum.rb +64 -34
- data/lib/kameleoon/targeting/conditions/device_condition.rb +21 -0
- data/lib/kameleoon/targeting/conditions/exclusive_experiment.rb +0 -12
- data/lib/kameleoon/targeting/conditions/page_title_condition.rb +21 -0
- data/lib/kameleoon/targeting/conditions/page_url_condition.rb +21 -0
- data/lib/kameleoon/targeting/conditions/sdk_language_condition.rb +65 -0
- data/lib/kameleoon/targeting/conditions/string_value_condition.rb +40 -0
- data/lib/kameleoon/targeting/conditions/target_experiment.rb +5 -9
- data/lib/kameleoon/targeting/conditions/unknown_condition.rb +15 -0
- data/lib/kameleoon/targeting/conditions/visitor_code_condition.rb +16 -0
- data/lib/kameleoon/targeting/models.rb +0 -24
- data/lib/kameleoon/utils.rb +1 -1
- data/lib/kameleoon/version.rb +28 -1
- metadata +45 -4
- data/lib/kameleoon/configuration/feature_flag_v2.rb +0 -30
- data/lib/kameleoon/data.rb +0 -169
data/lib/kameleoon/client.rb
CHANGED
@@ -4,10 +4,17 @@ require 'kameleoon/targeting/models'
|
|
4
4
|
require 'kameleoon/request'
|
5
5
|
require 'kameleoon/exceptions'
|
6
6
|
require 'kameleoon/cookie'
|
7
|
+
require 'kameleoon/data/user_agent'
|
7
8
|
require 'kameleoon/configuration/feature_flag'
|
8
9
|
require 'kameleoon/configuration/variation'
|
9
|
-
require 'kameleoon/configuration/
|
10
|
+
require 'kameleoon/configuration/settings'
|
11
|
+
require 'kameleoon/network/activity_event'
|
12
|
+
require 'kameleoon/network/experiment_event'
|
13
|
+
require 'kameleoon/network/url_provider'
|
14
|
+
require 'kameleoon/real_time/real_time_configuration_service'
|
10
15
|
require 'kameleoon/storage/variation_storage'
|
16
|
+
require 'kameleoon/hybrid/manager'
|
17
|
+
require 'kameleoon/storage/cache_factory'
|
11
18
|
require 'rufus/scheduler'
|
12
19
|
require 'yaml'
|
13
20
|
require 'json'
|
@@ -36,19 +43,28 @@ module Kameleoon
|
|
36
43
|
@default_timeout = config['default_timeout'] || default_timeout # in ms
|
37
44
|
refresh_interval = config['actions_configuration_refresh_interval']
|
38
45
|
@interval = refresh_interval.nil? ? interval : "#{refresh_interval}m"
|
39
|
-
|
40
|
-
@
|
46
|
+
data_api_url = config['tracking_url'] || Network::UrlProvider::DEFAULT_DATA_API_URL
|
47
|
+
@url_provider = Network::UrlProvider.new(@site_code, data_api_url)
|
48
|
+
@real_time_configuration_service = nil
|
49
|
+
@update_configuration_handler = nil
|
50
|
+
@fetch_configuration_update_job = nil
|
41
51
|
@client_id = client_id || config['client_id']
|
42
52
|
@client_secret = client_secret || config['client_secret']
|
43
53
|
@data_maximum_size = config['visitor_data_maximum_size'] || 500 # mb
|
44
54
|
@environment = config['environment'] || DEFAULT_ENVIRONMENT
|
55
|
+
@settings = Kameleoon::Configuration::Settings.new
|
45
56
|
@verbose_mode = config['verbose_mode'] || false
|
46
57
|
@experiments = []
|
47
58
|
@feature_flags = []
|
48
|
-
@feature_flags_v2 = []
|
49
59
|
@data = {}
|
50
60
|
@user_agents = {}
|
51
61
|
@variation_storage = Kameleoon::Storage::VariationStorage.new
|
62
|
+
@hybrid_manager = Kameleoon::Hybrid::ManagerImpl.new(
|
63
|
+
CACHE_EXPIRATION_TIMEOUT,
|
64
|
+
CACHE_EXPIRATION_TIMEOUT * 3,
|
65
|
+
Kameleoon::Storage::CacheFactoryImpl.new,
|
66
|
+
method(:log)
|
67
|
+
)
|
52
68
|
end
|
53
69
|
|
54
70
|
##
|
@@ -112,25 +128,26 @@ module Kameleoon
|
|
112
128
|
check_visitor_code(visitor_code)
|
113
129
|
experiment = @experiments.find { |exp| exp.id.to_s == experiment_id.to_s }
|
114
130
|
if experiment.nil?
|
115
|
-
raise Exception::ExperimentConfigurationNotFound.new(experiment_id)
|
131
|
+
raise Exception::ExperimentConfigurationNotFound.new(experiment_id),
|
132
|
+
"Experiment #{experiment_id} is not found"
|
116
133
|
end
|
117
134
|
check_site_code_enable(experiment)
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
track_experiment(visitor_code, experiment_id, REFERENCE, true)
|
127
|
-
raise Exception::NotAllocated.new(visitor_code),
|
128
|
-
"Experiment #{experiment_id} is not active for visitor #{visitor_code}"
|
129
|
-
end
|
130
|
-
else
|
135
|
+
targeted = check_targeting(visitor_code, experiment_id, experiment)
|
136
|
+
if targeted
|
137
|
+
# saved_variation = get_valid_saved_variation(visitor_code, experiment)
|
138
|
+
variation_id = calculate_variation_for_experiment(visitor_code, experiment)
|
139
|
+
save_variation(visitor_code, experiment_id, variation_id)
|
140
|
+
end
|
141
|
+
_send_tracking_request(visitor_code, experiment_id, variation_id)
|
142
|
+
unless targeted
|
131
143
|
raise Exception::NotTargeted.new(visitor_code),
|
132
144
|
"Experiment #{experiment_id} is not targeted for visitor #{visitor_code}"
|
133
145
|
end
|
146
|
+
if variation_id.nil?
|
147
|
+
raise Exception::NotAllocated.new(visitor_code),
|
148
|
+
"Experiment #{experiment_id} is not active for visitor #{visitor_code}"
|
149
|
+
end
|
150
|
+
variation_id
|
134
151
|
end
|
135
152
|
|
136
153
|
##
|
@@ -148,9 +165,7 @@ module Kameleoon
|
|
148
165
|
#
|
149
166
|
def add_data(visitor_code, *args)
|
150
167
|
check_visitor_code(visitor_code)
|
151
|
-
while ObjectSpace.memsize_of(@data) > @data_maximum_size * (2**20)
|
152
|
-
@data.shift
|
153
|
-
end
|
168
|
+
@data.shift while ObjectSpace.memsize_of(@data) > @data_maximum_size * (2**20)
|
154
169
|
args.each do |data_element|
|
155
170
|
if data_element.is_a?(UserAgent)
|
156
171
|
add_user_agent_data(visitor_code, data_element)
|
@@ -196,7 +211,7 @@ module Kameleoon
|
|
196
211
|
def flush(visitor_code = nil)
|
197
212
|
check_visitor_code(visitor_code) unless visitor_code.nil?
|
198
213
|
if !visitor_code.nil?
|
199
|
-
|
214
|
+
_send_tracking_request(visitor_code)
|
200
215
|
else
|
201
216
|
@data.select { |_, values| values.any? { |data| !data.sent } }.each_key { |key| flush(key) }
|
202
217
|
end
|
@@ -252,28 +267,11 @@ module Kameleoon
|
|
252
267
|
# @raise [Kameleoon::Exception::NotTargeted] The visitor is not targeted by the experiment, as the
|
253
268
|
# associated targeting segment conditions were not fulfilled. He should see the reference variation
|
254
269
|
# @raise [Kameleoon::Exception::VisitorCodeNotValid] If the visitor code is empty or longer than 255 chars
|
255
|
-
|
270
|
+
#
|
271
|
+
# DEPRECATED. Please use `is_feature_active` instead.
|
256
272
|
def activate_feature(visitor_code, feature_key)
|
257
|
-
|
258
|
-
|
259
|
-
check_site_code_enable(feature_flag)
|
260
|
-
unless check_targeting(visitor_code, feature_flag.id.to_i, feature_flag)
|
261
|
-
raise Exception::NotTargeted.new(visitor_code),
|
262
|
-
"Visitor '#{visitor_code}' is not targeted for FF with key '#{feature_key}'"
|
263
|
-
end
|
264
|
-
|
265
|
-
if feature_flag.is_scheduled_active(Time.now.to_i)
|
266
|
-
threshold = obtain_hash_double(visitor_code, {}, feature_flag.id)
|
267
|
-
if threshold >= 1 - feature_flag.exposition_rate
|
268
|
-
track_experiment(visitor_code, feature_flag.id, feature_flag.variations.first['id'])
|
269
|
-
true
|
270
|
-
else
|
271
|
-
track_experiment(visitor_code, feature_flag.id, REFERENCE, none_variation: true)
|
272
|
-
false
|
273
|
-
end
|
274
|
-
else
|
275
|
-
false
|
276
|
-
end
|
273
|
+
warn '[DEPRECATION] `activate_feature` is deprecated. Please use `feature_active?` instead.'
|
274
|
+
feature_active?(visitor_code, feature_key)
|
277
275
|
end
|
278
276
|
|
279
277
|
##
|
@@ -356,7 +354,7 @@ module Kameleoon
|
|
356
354
|
# @raise [Kameleoon::Exception::VariationConfigurationNotFound]
|
357
355
|
#
|
358
356
|
def get_feature_all_variables(feature_key, variation_key)
|
359
|
-
feature_flag =
|
357
|
+
feature_flag = find_feature_flag(feature_key)
|
360
358
|
variation = feature_flag.get_variation_key(variation_key)
|
361
359
|
if variation.nil?
|
362
360
|
raise Exception::VariationConfigurationNotFound.new(variation_key),
|
@@ -381,54 +379,47 @@ module Kameleoon
|
|
381
379
|
# DEPRECATED. Please use `get_feature_variable` instead.
|
382
380
|
def obtain_feature_variable(feature_key, variable_key)
|
383
381
|
warn '[DEPRECATION] `obtain_feature_variable` is deprecated. Please use `get_feature_variable` instead.'
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
case custom_json['type']
|
390
|
-
when 'Boolean'
|
391
|
-
custom_json['value']
|
392
|
-
when 'String'
|
393
|
-
custom_json['value']
|
394
|
-
when 'Number'
|
395
|
-
custom_json['value'].to_i
|
396
|
-
when 'JSON'
|
397
|
-
JSON.parse(custom_json['value'])
|
398
|
-
else
|
399
|
-
raise TypeError.new('Unknown type for feature variable')
|
400
|
-
end
|
382
|
+
all_variables = get_feature_all_variables(
|
383
|
+
feature_key,
|
384
|
+
Configuration::VariationType::VARIATION_OFF
|
385
|
+
)
|
386
|
+
all_variables[variable_key]
|
401
387
|
end
|
402
388
|
|
403
389
|
##
|
404
|
-
# The
|
405
|
-
# stored on a remote Kameleoon server. Usually data will be stored on our remote
|
406
|
-
#
|
407
|
-
#
|
390
|
+
# The get_remote_data method allows you to retrieve data (according to a key passed as argument)
|
391
|
+
# stored on a remote Kameleoon server. Usually data will be stored on our remote
|
392
|
+
# servers via the use of our Data API.
|
393
|
+
# This method, along with the availability of our highly scalable servers for this purpose, provides a convenient
|
394
|
+
# way to quickly store massive amounts of data that can be later retrieved for each of your visitors / users.
|
408
395
|
#
|
409
396
|
# @param [String] key Key you want to retrieve data. This field is mandatory.
|
410
|
-
# @param [Int] timeout Timeout for request. Equals default_timeout in a config file.
|
397
|
+
# @param [Int] timeout Timeout for request (in milliseconds). Equals default_timeout in a config file.
|
398
|
+
# This field is optional.
|
411
399
|
#
|
412
400
|
# @return [Hash] Hash object of the json object.
|
413
|
-
|
414
|
-
#
|
415
|
-
def retrieve_data_from_remote_source(key, timeout = @default_timeout)
|
401
|
+
def get_remote_data(key, timeout = @default_timeout)
|
416
402
|
connexion_options = { connect_timeout: (timeout.to_f / 1000.0) }
|
417
|
-
|
403
|
+
url = @url_provider.make_api_data_get_request_url(key)
|
418
404
|
log "Retrieve API Data connexion: #{connexion_options.inspect}"
|
419
|
-
response = get_sync(
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
405
|
+
response = get_sync(url, connexion_options)
|
406
|
+
return nil unless successful_sync?(response)
|
407
|
+
|
408
|
+
JSON.parse(response.body) unless response.nil?
|
409
|
+
end
|
410
|
+
|
411
|
+
##
|
412
|
+
# DEPRECATED. Please use `get_feature_variable` instead.
|
413
|
+
def retrieve_data_from_remote_source(key, timeout = @default_timeout)
|
414
|
+
warn '[DEPRECATION] `retrieve_data_from_remote_source` is deprecated. Please use `get_remote_date` instead.'
|
415
|
+
get_remote_data(key, timeout)
|
425
416
|
end
|
426
417
|
|
427
418
|
##
|
428
419
|
# Returns a list of all experiment ids
|
429
420
|
#
|
430
421
|
# @return [Array] array of all experiment ids
|
431
|
-
def get_experiment_list
|
422
|
+
def get_experiment_list # rubocop:disable Naming/AccessorMethodName
|
432
423
|
@experiments.map { |it| it.id.to_i }
|
433
424
|
end
|
434
425
|
|
@@ -443,7 +434,7 @@ module Kameleoon
|
|
443
434
|
list_ids = []
|
444
435
|
@experiments.each do |experiment|
|
445
436
|
next unless check_targeting(visitor_code, experiment.id.to_i, experiment)
|
446
|
-
next if only_allocated &&
|
437
|
+
next if only_allocated && calculate_variation_for_experiment(visitor_code, experiment).nil?
|
447
438
|
|
448
439
|
list_ids.push(experiment.id.to_i)
|
449
440
|
end
|
@@ -454,8 +445,8 @@ module Kameleoon
|
|
454
445
|
# Returns a list of all feature flag keys
|
455
446
|
#
|
456
447
|
# @return [Array] array of all feature flag keys
|
457
|
-
def get_feature_list
|
458
|
-
@
|
448
|
+
def get_feature_list # rubocop:disable Naming/AccessorMethodName
|
449
|
+
@feature_flags.map(&:feature_key)
|
459
450
|
end
|
460
451
|
|
461
452
|
##
|
@@ -467,40 +458,113 @@ module Kameleoon
|
|
467
458
|
def get_active_feature_list_for_visitor(visitor_code)
|
468
459
|
check_visitor_code(visitor_code)
|
469
460
|
list_keys = []
|
470
|
-
@
|
471
|
-
|
472
|
-
|
461
|
+
@feature_flags.each do |feature_flag|
|
462
|
+
variation, rule, = _calculate_variation_key_for_feature(visitor_code, feature_flag)
|
463
|
+
variation_key = _get_variation_key(variation, rule, feature_flag)
|
464
|
+
if variation_key != Kameleoon::Configuration::VariationType::VARIATION_OFF
|
465
|
+
list_keys.push(feature_flag.feature_key)
|
466
|
+
end
|
473
467
|
end
|
474
468
|
list_keys
|
475
469
|
end
|
476
470
|
|
471
|
+
##
|
472
|
+
# The `on_update_configuration()` method allows you to handle the event when configuration
|
473
|
+
# has updated data. It takes one input parameter: callable **handler**. The handler
|
474
|
+
# that will be called when the configuration is updated using a real-time configuration event.
|
475
|
+
#
|
476
|
+
# @param handler [Callable | NilClass] The handler that will be called when the configuration
|
477
|
+
# is updated using a real-time configuration event.
|
478
|
+
def on_update_configuration(handler)
|
479
|
+
@update_configuration_handler = handler
|
480
|
+
end
|
481
|
+
|
482
|
+
##
|
483
|
+
# The `get_engine_tracking_code` returns the JavasScript code to be inserted in your page
|
484
|
+
# to send automatically the exposure events to the analytics solution you are using.
|
485
|
+
#
|
486
|
+
# @param [String] visitor_code The user's unique identifier. This field is mandatory.
|
487
|
+
#
|
488
|
+
# @return [String] JavasScript code to be inserted in your page to send automatically
|
489
|
+
# the exposure events to the analytics solution you are using.
|
490
|
+
def get_engine_tracking_code(visitor_code)
|
491
|
+
@hybrid_manager.get_engine_tracking_code(visitor_code)
|
492
|
+
end
|
493
|
+
|
477
494
|
private
|
478
495
|
|
479
|
-
API_SSX_URL = 'https://api-ssx.kameleoon.com'
|
480
496
|
REFERENCE = 0
|
481
497
|
DEFAULT_ENVIRONMENT = 'production'
|
482
|
-
|
498
|
+
CACHE_EXPIRATION_TIMEOUT = 5
|
499
|
+
attr :site_code, :client_id, :client_secret, :access_token, :experiments, :feature_flags, :scheduler, :data,
|
483
500
|
:tracking_url, :default_timeout, :interval, :memory_limit, :verbose_mode
|
484
501
|
|
485
502
|
def fetch_configuration
|
486
|
-
|
487
|
-
|
488
|
-
log('Scheduled job to fetch configuration is starting.')
|
489
|
-
fetch_configuration_job
|
490
|
-
end
|
491
|
-
@scheduler.schedule '0s' do
|
492
|
-
log('Start-up, fetching is starting')
|
503
|
+
Rufus::Scheduler.singleton.in '0s' do
|
504
|
+
log('Initial configuration fetch is started.')
|
493
505
|
fetch_configuration_job
|
494
506
|
end
|
495
507
|
end
|
496
508
|
|
497
|
-
def fetch_configuration_job
|
509
|
+
def fetch_configuration_job(time_stamp = nil)
|
498
510
|
EM.synchrony do
|
499
|
-
|
511
|
+
begin
|
512
|
+
ok = obtain_configuration(@environment, time_stamp)
|
513
|
+
if !ok && @settings.real_time_update
|
514
|
+
@settings.real_time_update = false
|
515
|
+
log('Switching to polling mode due to failed fetch')
|
516
|
+
end
|
517
|
+
rescue StandardError => e
|
518
|
+
log("Error occurred during configuration fetching: #{e}")
|
519
|
+
end
|
520
|
+
manage_configuration_update(@settings.real_time_update)
|
500
521
|
EM.stop
|
501
522
|
end
|
502
523
|
end
|
503
524
|
|
525
|
+
def start_configuration_update_job_if_needed
|
526
|
+
return unless @fetch_configuration_update_job.nil?
|
527
|
+
|
528
|
+
@fetch_configuration_update_job = Rufus::Scheduler.singleton.schedule_every @interval do
|
529
|
+
log('Scheduled job to fetch configuration is started.')
|
530
|
+
fetch_configuration_job
|
531
|
+
end
|
532
|
+
end
|
533
|
+
|
534
|
+
def stop_configuration_update_job_if_needed
|
535
|
+
return if @fetch_configuration_update_job.nil?
|
536
|
+
|
537
|
+
@fetch_configuration_update_job.unschedule
|
538
|
+
@fetch_configuration_update_job = nil
|
539
|
+
log('Scheduled job to fetch configuration is stopped.')
|
540
|
+
end
|
541
|
+
|
542
|
+
def start_real_time_configuration_service_if_needed
|
543
|
+
return unless @real_time_configuration_service.nil?
|
544
|
+
|
545
|
+
url = @url_provider.make_real_time_url
|
546
|
+
fetch_func = proc { |real_time_event| fetch_configuration_job(real_time_event.time_stamp) }
|
547
|
+
@real_time_configuration_service =
|
548
|
+
Kameleoon::RealTime::RealTimeConfigurationService.new(url, fetch_func, method(:log))
|
549
|
+
end
|
550
|
+
|
551
|
+
def stop_real_time_configuration_service_if_needed
|
552
|
+
return if @real_time_configuration_service.nil?
|
553
|
+
|
554
|
+
@real_time_configuration_service.close
|
555
|
+
@real_time_configuration_service = nil
|
556
|
+
end
|
557
|
+
|
558
|
+
def manage_configuration_update(is_real_time_update)
|
559
|
+
if is_real_time_update
|
560
|
+
stop_configuration_update_job_if_needed
|
561
|
+
start_real_time_configuration_service_if_needed
|
562
|
+
else
|
563
|
+
stop_real_time_configuration_service_if_needed
|
564
|
+
start_configuration_update_job_if_needed
|
565
|
+
end
|
566
|
+
end
|
567
|
+
|
504
568
|
# def hash_headers
|
505
569
|
# if @access_token.nil?
|
506
570
|
# CredentialsNotFound.new
|
@@ -515,26 +579,35 @@ module Kameleoon
|
|
515
579
|
# { 'field' => field, 'operator' => operator, 'parameters' => parameters }
|
516
580
|
# end
|
517
581
|
|
518
|
-
def obtain_configuration(
|
519
|
-
log
|
520
|
-
|
521
|
-
|
522
|
-
|
523
|
-
return unless request
|
582
|
+
def obtain_configuration(environment = @environment, time_stamp = nil)
|
583
|
+
log 'Fetching configuration from Client-Config service'
|
584
|
+
url = @url_provider.make_configuration_url(environment, time_stamp)
|
585
|
+
request = request_configuration(url)
|
586
|
+
return false unless request
|
524
587
|
|
525
588
|
configuration = JSON.parse(request.response)
|
526
589
|
@experiments = Kameleoon::Configuration::Experiment.create_from_array(configuration['experiments']) ||
|
527
590
|
@experiments
|
528
|
-
@feature_flags = Kameleoon::Configuration::FeatureFlag.create_from_array(
|
529
|
-
@feature_flags
|
530
|
-
feature_flags_v2_new = Kameleoon::Configuration::FeatureFlagV2.create_from_array(
|
591
|
+
@feature_flags = Kameleoon::Configuration::FeatureFlag.create_from_array(
|
531
592
|
configuration['featureFlagConfigurations']
|
532
593
|
)
|
533
|
-
@
|
594
|
+
@settings.update(configuration['configuration'])
|
595
|
+
call_update_handler_if_needed(!time_stamp.nil?)
|
534
596
|
log "Feature flags are fetched: #{request.inspect}"
|
597
|
+
true
|
535
598
|
end
|
536
599
|
|
537
|
-
|
600
|
+
##
|
601
|
+
# Call the handler when configuraiton was updated with new time stamp.
|
602
|
+
#
|
603
|
+
# @param need_call [Bool] Indicates if we need to call handler or not.
|
604
|
+
def call_update_handler_if_needed(need_call)
|
605
|
+
return if !need_call || @update_configuration_handler.nil?
|
606
|
+
|
607
|
+
@update_configuration_handler.call
|
608
|
+
end
|
609
|
+
|
610
|
+
def calculate_variation_for_experiment(visitor_code, experiment)
|
538
611
|
threshold = obtain_hash_double(visitor_code, experiment.respool_time, experiment.id)
|
539
612
|
experiment.deviations.each do |key, value|
|
540
613
|
threshold -= value
|
@@ -543,8 +616,8 @@ module Kameleoon
|
|
543
616
|
nil
|
544
617
|
end
|
545
618
|
|
546
|
-
def request_configuration(
|
547
|
-
request = EM::Synchrony.sync get({
|
619
|
+
def request_configuration(url)
|
620
|
+
request = EM::Synchrony.sync get({}, url)
|
548
621
|
unless successful?(request)
|
549
622
|
log "Failed to fetch #{request.inspect}"
|
550
623
|
return false
|
@@ -552,60 +625,9 @@ module Kameleoon
|
|
552
625
|
request
|
553
626
|
end
|
554
627
|
|
555
|
-
def get_common_ssx_parameters(visitor_code)
|
556
|
-
{
|
557
|
-
nonce: Kameleoon::Utils.generate_random_string(16),
|
558
|
-
siteCode: @site_code,
|
559
|
-
visitorCode: visitor_code
|
560
|
-
}
|
561
|
-
end
|
562
|
-
|
563
|
-
def get_experiment_register_url(visitor_code, experiment_id, variation_id = nil, none_variation = false)
|
564
|
-
url = "/experimentTracking?#{URI.encode_www_form(get_common_ssx_parameters(visitor_code))}"
|
565
|
-
url += "&experimentId=#{experiment_id}"
|
566
|
-
if variation_id.nil?
|
567
|
-
return url
|
568
|
-
end
|
569
|
-
url += "&variationId=#{variation_id}"
|
570
|
-
if none_variation
|
571
|
-
url += '&noneVariation=true'
|
572
|
-
end
|
573
|
-
url
|
574
|
-
end
|
575
|
-
|
576
|
-
def get_data_register_url(visitor_code)
|
577
|
-
"/dataTracking?#{URI.encode_www_form(get_common_ssx_parameters(visitor_code))}"
|
578
|
-
end
|
579
|
-
|
580
|
-
def get_api_data_request_url(key)
|
581
|
-
mapKey = {
|
582
|
-
siteCode: site_code,
|
583
|
-
key: key
|
584
|
-
}
|
585
|
-
"/data?#{URI.encode_www_form(mapKey)}"
|
586
|
-
end
|
587
|
-
|
588
628
|
def find_feature_flag(feature_key)
|
589
|
-
case feature_key
|
590
|
-
when String
|
591
|
-
feature_flag = @feature_flags.select { |ff| ff.identification_key == feature_key }.first
|
592
|
-
when Integer
|
593
|
-
feature_flag = @feature_flags.select { |ff| ff.id.to_i == feature_key }.first
|
594
|
-
print "\nPassing `feature_key` with type of `int` to `activate_feature` or `obtain_feature_variable` "\
|
595
|
-
"is deprecated, it will be removed in next releases. This is necessary to support multi-environment feature\n"
|
596
|
-
else
|
597
|
-
raise TypeError.new('Feature key should be a String or an Integer.'),
|
598
|
-
'Feature key should be a String or an Integer.'
|
599
|
-
end
|
600
|
-
error_message = "Feature #{feature_key} not found"
|
601
|
-
raise Exception::FeatureConfigurationNotFound.new(feature_key), error_message if feature_flag.nil?
|
602
|
-
|
603
|
-
feature_flag
|
604
|
-
end
|
605
|
-
|
606
|
-
def find_feature_flag_v2(feature_key)
|
607
629
|
if feature_key.is_a?(String)
|
608
|
-
feature_flag = @
|
630
|
+
feature_flag = @feature_flags.select { |ff| ff.feature_key == feature_key }.first
|
609
631
|
else
|
610
632
|
raise TypeError.new('Feature key should be a String or an Integer.'),
|
611
633
|
'Feature key should be a String or an Integer.'
|
@@ -616,67 +638,6 @@ module Kameleoon
|
|
616
638
|
feature_flag
|
617
639
|
end
|
618
640
|
|
619
|
-
def track_experiment(visitor_code, experiment_id, variation_id = nil, none_variation: false)
|
620
|
-
data_not_sent = data_not_sent(visitor_code)
|
621
|
-
options = {
|
622
|
-
path: get_experiment_register_url(visitor_code, experiment_id, variation_id, none_variation),
|
623
|
-
body: (data_not_sent.map(&:obtain_full_post_text_line).join("\n") || '').encode('UTF-8'),
|
624
|
-
head: { 'Content-Type': 'text/plain' }
|
625
|
-
}
|
626
|
-
set_user_agent_to_headers(visitor_code, options[:head])
|
627
|
-
trial = 0
|
628
|
-
success = false
|
629
|
-
log "Start post tracking experiment: #{data_not_sent.inspect}"
|
630
|
-
Thread.new do
|
631
|
-
while trial < 10
|
632
|
-
log "Send Experiment Tracking #{options.inspect}"
|
633
|
-
response = post_sync(options, @tracking_url)
|
634
|
-
log "Response #{response.inspect}"
|
635
|
-
if successful_sync?(response)
|
636
|
-
data_not_sent.each { |it| it.sent = true }
|
637
|
-
success = true
|
638
|
-
break
|
639
|
-
end
|
640
|
-
trial += 1
|
641
|
-
end
|
642
|
-
if success
|
643
|
-
log "Post to experiment tracking is done after #{trial + 1} trials"
|
644
|
-
else
|
645
|
-
log "Post to experiment tracking is failed after #{trial} trials"
|
646
|
-
end
|
647
|
-
end
|
648
|
-
end
|
649
|
-
|
650
|
-
def track_data(visitor_code)
|
651
|
-
Thread.new do
|
652
|
-
trials = 0
|
653
|
-
data_not_sent = data_not_sent(visitor_code)
|
654
|
-
log "Start post tracking data: #{data_not_sent.inspect}"
|
655
|
-
while trials < 10 && !data_not_sent.empty?
|
656
|
-
options = {
|
657
|
-
path: get_data_register_url(visitor_code),
|
658
|
-
body: (data_not_sent.map(&:obtain_full_post_text_line).join("\n") || '').encode('UTF-8'),
|
659
|
-
head: { 'Content-Type': 'text/plain' }
|
660
|
-
}
|
661
|
-
set_user_agent_to_headers(visitor_code, options[:head])
|
662
|
-
log "Post tracking data for visitor_code: #{visitor_code} with options: #{options.inspect}"
|
663
|
-
response = post_sync(options, @tracking_url)
|
664
|
-
log "Response #{response.inspect}"
|
665
|
-
if successful_sync?(response)
|
666
|
-
data_not_sent.each { |it| it.sent = true }
|
667
|
-
success = true
|
668
|
-
break
|
669
|
-
end
|
670
|
-
trials += 1
|
671
|
-
end
|
672
|
-
if success
|
673
|
-
log "Post to data tracking is done after #{trials + 1} trials"
|
674
|
-
else
|
675
|
-
log "Post to data tracking is failed after #{trials} trials"
|
676
|
-
end
|
677
|
-
end
|
678
|
-
end
|
679
|
-
|
680
641
|
def check_site_code_enable(campaign)
|
681
642
|
raise Exception::SiteCodeDisabled.new(site_code), site_code unless campaign.site_enabled
|
682
643
|
end
|
@@ -686,7 +647,7 @@ module Kameleoon
|
|
686
647
|
end
|
687
648
|
|
688
649
|
def log(text)
|
689
|
-
print "Kameleoon Log: #{text}\n" if @verbose_mode
|
650
|
+
print "Kameleoon SDK Log: #{text}\n" if @verbose_mode
|
690
651
|
end
|
691
652
|
|
692
653
|
def add_user_agent_data(visitor_code, user_agent)
|
@@ -698,15 +659,16 @@ module Kameleoon
|
|
698
659
|
headers['User-Agent'] = user_agent.value unless user_agent.nil?
|
699
660
|
end
|
700
661
|
|
701
|
-
|
702
|
-
|
703
|
-
|
704
|
-
|
705
|
-
|
706
|
-
|
707
|
-
|
708
|
-
|
709
|
-
|
662
|
+
# Uncomment when using storage
|
663
|
+
# def get_valid_saved_variation(visitor_code, experiment)
|
664
|
+
# variation_id = @variation_storage.get_variation_id(visitor_code, experiment.id.to_i)
|
665
|
+
# unless variation_id.nil?
|
666
|
+
# return @variation_storage.variation_valid?(visitor_code,
|
667
|
+
# experiment.id.to_i,
|
668
|
+
# experiment.respool_time[variation_id])
|
669
|
+
# end
|
670
|
+
# nil
|
671
|
+
# end
|
710
672
|
|
711
673
|
def check_targeting(visitor_code, campaign_id, exp_ff_rule)
|
712
674
|
segment = exp_ff_rule.targeting_segment
|
@@ -716,8 +678,17 @@ module Kameleoon
|
|
716
678
|
def get_condition_data(type, visitor_code, campaign_id)
|
717
679
|
condition_data = nil
|
718
680
|
case type
|
719
|
-
when Kameleoon::Targeting::ConditionType::CUSTOM_DATUM
|
681
|
+
when Kameleoon::Targeting::ConditionType::CUSTOM_DATUM,
|
682
|
+
Kameleoon::Targeting::ConditionType::PAGE_TITLE,
|
683
|
+
Kameleoon::Targeting::ConditionType::PAGE_URL,
|
684
|
+
Kameleoon::Targeting::ConditionType::DEVICE_TYPE,
|
685
|
+
Kameleoon::Targeting::ConditionType::BROWSER,
|
686
|
+
Kameleoon::Targeting::ConditionType::CONVERSIONS
|
720
687
|
condition_data = (@data[visitor_code] || []).flatten
|
688
|
+
when Kameleoon::Targeting::ConditionType::SDK_LANGUAGE
|
689
|
+
condition_data = Kameleoon::Targeting::SdkInfo.new(Kameleoon::SDK_NAME, Kameleoon::SDK_VERSION)
|
690
|
+
when Kameleoon::Targeting::ConditionType::VISITOR_CODE
|
691
|
+
condition_data = visitor_code
|
721
692
|
when Kameleoon::Targeting::ConditionType::TARGET_EXPERIMENT
|
722
693
|
condition_data = @variation_storage.get_hash_saved_variation_id(visitor_code)
|
723
694
|
when Kameleoon::Targeting::ConditionType::EXCLUSIVE_EXPERIMENT
|
@@ -732,11 +703,15 @@ module Kameleoon
|
|
732
703
|
# helper method for getting variation key for feature flag
|
733
704
|
def _get_feature_variation_key(visitor_code, feature_key)
|
734
705
|
check_visitor_code(visitor_code)
|
735
|
-
feature_flag =
|
736
|
-
|
737
|
-
|
738
|
-
|
739
|
-
|
706
|
+
feature_flag = find_feature_flag(feature_key)
|
707
|
+
variation, rule = _calculate_variation_key_for_feature(visitor_code, feature_flag)
|
708
|
+
variation_key = _get_variation_key(variation, rule, feature_flag)
|
709
|
+
unless rule.nil?
|
710
|
+
experiment_id = rule.experiment_id
|
711
|
+
variation_id = variation.variation_id unless variation.nil?
|
712
|
+
save_variation(visitor_code, experiment_id, variation_id)
|
713
|
+
end
|
714
|
+
_send_tracking_request(visitor_code, experiment_id, variation_id)
|
740
715
|
[feature_flag, variation_key]
|
741
716
|
end
|
742
717
|
|
@@ -744,37 +719,79 @@ module Kameleoon
|
|
744
719
|
# helper method for calculate variation key for feature flag
|
745
720
|
def _calculate_variation_key_for_feature(visitor_code, feature_flag)
|
746
721
|
# no rules -> return default_variation_key
|
747
|
-
|
722
|
+
feature_flag.rules.each do |rule|
|
723
|
+
# check if visitor is targeted for rule, else next rule
|
724
|
+
next unless check_targeting(visitor_code, feature_flag.id, rule)
|
725
|
+
|
748
726
|
# uses for rule exposition
|
749
|
-
hash_rule =
|
750
|
-
#
|
751
|
-
|
752
|
-
|
753
|
-
|
754
|
-
|
755
|
-
|
756
|
-
#
|
757
|
-
|
758
|
-
|
759
|
-
|
760
|
-
|
761
|
-
|
762
|
-
# if visitor is targeted for targeted rule then break cycle -> return default
|
763
|
-
elsif rule.type == Kameleoon::Configuration::RuleType::TARGETED_DELIVERY
|
764
|
-
break
|
765
|
-
end
|
727
|
+
hash_rule = obtain_hash_double_rule(visitor_code, rule.id, rule.respool_time)
|
728
|
+
# check main expostion for rule with hashRule
|
729
|
+
if hash_rule <= rule.exposition
|
730
|
+
return [rule.variation_by_exposition[0], rule] if rule.targeted_delivery_type?
|
731
|
+
|
732
|
+
# uses for variation's expositions
|
733
|
+
hash_variation = obtain_hash_double_rule(visitor_code, rule.experiment_id, rule.respool_time)
|
734
|
+
# get variation key with new hashVariation
|
735
|
+
variation = rule.get_variation(hash_variation)
|
736
|
+
return [variation, rule] unless variation.nil?
|
737
|
+
# if visitor is targeted for targeted rule then break cycle -> return default
|
738
|
+
elsif rule.targeted_delivery_type?
|
739
|
+
break
|
766
740
|
end
|
767
741
|
end
|
768
|
-
[
|
742
|
+
[nil, nil]
|
743
|
+
end
|
744
|
+
|
745
|
+
def save_variation(visitor_code, experiment_id, variation_id)
|
746
|
+
return if experiment_id.nil? || variation_id.nil?
|
747
|
+
|
748
|
+
@variation_storage.update_variation(visitor_code, experiment_id, variation_id)
|
749
|
+
@hybrid_manager.add_variation(visitor_code, experiment_id, variation_id)
|
750
|
+
end
|
751
|
+
|
752
|
+
def _get_variation_key(var_by_exp, rule, feature_flag)
|
753
|
+
return var_by_exp.variation_key unless var_by_exp.nil?
|
754
|
+
|
755
|
+
return Kameleoon::Configuration::VariationType::VARIATION_OFF if !rule.nil? && rule.experimentation_type?
|
756
|
+
|
757
|
+
feature_flag.default_variation_key
|
769
758
|
end
|
770
759
|
|
771
760
|
##
|
772
|
-
# helper method for sending tracking requests for new FF
|
773
|
-
def _send_tracking_request(visitor_code,
|
774
|
-
|
775
|
-
|
761
|
+
# helper method for sending tracking requests for new FF
|
762
|
+
def _send_tracking_request(visitor_code, experiment_id = nil, variation_id = nil)
|
763
|
+
data_not_sent = data_not_sent(visitor_code)
|
764
|
+
if experiment_id.nil? || variation_id.nil?
|
765
|
+
data_not_sent.append(Network::ActivityEvent.new) if data_not_sent.empty?
|
776
766
|
else
|
777
|
-
|
767
|
+
data_not_sent.append(Network::ExperimentEvent.new(experiment_id, variation_id))
|
768
|
+
end
|
769
|
+
options = {
|
770
|
+
body: (data_not_sent.map(&:obtain_full_post_text_line).join("\n") || '').encode('UTF-8'),
|
771
|
+
head: { 'Content-Type': 'text/plain' }
|
772
|
+
}
|
773
|
+
set_user_agent_to_headers(visitor_code, options[:head])
|
774
|
+
url = @url_provider.make_tracking_url(visitor_code)
|
775
|
+
trial = 0
|
776
|
+
success = false
|
777
|
+
log "Start post tracking: #{data_not_sent.inspect}"
|
778
|
+
Thread.new do
|
779
|
+
while trial < 10
|
780
|
+
log "Send tracking #{options.inspect}"
|
781
|
+
response = post_sync(options, url)
|
782
|
+
log "Response #{response.inspect}"
|
783
|
+
if successful_sync?(response)
|
784
|
+
data_not_sent.each { |it| it.sent = true }
|
785
|
+
success = true
|
786
|
+
break
|
787
|
+
end
|
788
|
+
trial += 1
|
789
|
+
end
|
790
|
+
if success
|
791
|
+
log "Post to tracking is done after #{trial + 1} trials"
|
792
|
+
else
|
793
|
+
log "Post to tracking is failed after #{trial} trials"
|
794
|
+
end
|
778
795
|
end
|
779
796
|
end
|
780
797
|
|