kameleoon-client-ruby 3.6.1 → 3.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4c0ad80a65622c47bed3f869a3a08fadadd4938b972681cdd3018620ff4f2974
4
- data.tar.gz: bfa0d10a433d9bdceea6fcea05e5aa7d744c78955041d65691af488f37c39801
3
+ metadata.gz: a64d8edfee07c22561b166de49f9185d89a6fbdec9b49f6fd65860125bd54eab
4
+ data.tar.gz: ee7ccf78d0d11dec198b930e77e496c39a220b87471d724015c78114b85368f8
5
5
  SHA512:
6
- metadata.gz: af9846da6969cb33d87d96130631772d00bd72d0f60fda9f81f25e48b2e0c08bd5fe075308073d54378d0478a6a11fc16fcb0e05627a2360db445989c95214d4
7
- data.tar.gz: 97e21352e78bfb0c115cfab6b25793da9ac87c1a241c2a04de537f95c4384690aec85150abc92e013a110866e8de81d2152d78e94e2fcde54fa6cd39b35a8f3e
6
+ metadata.gz: ccfba38b35f50d2b1dda0ef2bf31406f74fd92663276f4297ae867bc7b88b8606b345a8a4234f24ef71c71fdc3555cc9852d7324c1a6a668ed8b6cbc98cfeedb
7
+ data.tar.gz: 38543c67b7c8949ccba5c8e0f08eae86d2755a737bc25a5dc82e94dd92011c7678cb4b32647ffae0edb606913bff537455f2f42295fe9f421554726a57675808
@@ -1,15 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'kameleoon/configuration/settings'
4
- require 'kameleoon/configuration/feature_flag'
5
3
  require 'kameleoon/configuration/custom_data_info'
4
+ require 'kameleoon/configuration/experiment'
5
+ require 'kameleoon/configuration/feature_flag'
6
+ require 'kameleoon/configuration/settings'
6
7
  require 'kameleoon/logging/kameleoon_logger'
7
8
 
8
9
  module Kameleoon
9
10
  module Configuration
10
11
  class DataFile
11
12
  attr_reader :settings, :feature_flags, :has_any_targeted_delivery_rule, :feature_flag_by_id, :rule_by_segment_id,
12
- :variation_by_id, :custom_data_info, :experiment_ids_with_js_css_variable
13
+ :rule_info_by_exp_id, :variation_by_id, :custom_data_info, :experiment_ids_with_js_css_variable,
14
+ :holdout
13
15
 
14
16
  def to_s
15
17
  'DataFile{' \
@@ -64,6 +66,7 @@ module Kameleoon
64
66
  end
65
67
  @has_any_targeted_delivery_rule = any_targeted_delivery_rule?
66
68
  @custom_data_info = CustomDataInfo.new(configuration['customData'])
69
+ @holdout = Experiment.from_json(configuration['holdout']) if configuration.include?('holdout')
67
70
  Logging::KameleoonLogger.debug('RETURN: DataFile.init(configuration: %s)', configuration)
68
71
  end
69
72
 
@@ -74,6 +77,7 @@ module Kameleoon
74
77
  def collect_indices
75
78
  @feature_flag_by_id = {}
76
79
  @rule_by_segment_id = {}
80
+ @rule_info_by_exp_id = {}
77
81
  @variation_by_id = {}
78
82
  @experiment_ids_with_js_css_variable = Set.new
79
83
 
@@ -84,10 +88,11 @@ module Kameleoon
84
88
  has_feature_flag_variable_js_css = feature_flag_variable_js_css?(feature_flag)
85
89
  feature_flag.rules.each do |rule|
86
90
  @rule_by_segment_id[rule.segment_id] = rule
87
- rule.variation_by_exposition.each do |variation|
91
+ @rule_info_by_exp_id[rule.experiment.id] = RuleInfo.new(feature_flag, rule)
92
+ rule.experiment.variations_by_exposition.each do |variation|
88
93
  @variation_by_id[variation.variation_id] = variation
89
94
  end
90
- @experiment_ids_with_js_css_variable.add(rule.experiment_id) if has_feature_flag_variable_js_css
95
+ @experiment_ids_with_js_css_variable.add(rule.experiment.id) if has_feature_flag_variable_js_css
91
96
  end
92
97
  end
93
98
  @feature_flag_by_id.freeze
@@ -102,5 +107,14 @@ module Kameleoon
102
107
  end
103
108
  end
104
109
  end
110
+
111
+ class RuleInfo
112
+ attr_reader :feature_flag, :rule
113
+
114
+ def initialize(feature_flag, rule)
115
+ @feature_flag = feature_flag
116
+ @rule = rule
117
+ end
118
+ end
105
119
  end
106
120
  end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'variation_exposition'
4
+ require 'kameleoon/exceptions'
5
+
6
+ module Kameleoon
7
+ module Configuration
8
+ class Experiment
9
+ attr_reader :id, :variations_by_exposition
10
+
11
+ def initialize(id, variations_by_exposition)
12
+ @id = id
13
+ @variations_by_exposition = variations_by_exposition
14
+ end
15
+
16
+ def self.from_json(hash)
17
+ id = hash['experimentId'] || 0
18
+ variations_by_exposition = VariationByExposition.create_from_array(hash['variationByExposition'])
19
+ variations_by_exposition.freeze
20
+ Experiment.new(id, variations_by_exposition)
21
+ end
22
+
23
+ def to_s
24
+ "Experiment{id:#{@id}}"
25
+ end
26
+
27
+ def get_variation(hash_double)
28
+ total = 0.0
29
+ @variations_by_exposition.each do |var_by_exp|
30
+ total += var_by_exp.exposition
31
+ return var_by_exp if total >= hash_double
32
+ end
33
+ nil
34
+ end
35
+
36
+ def get_variation_by_key(variation_key)
37
+ var_by_exp = @variations_by_exposition.find { |v| v.variation_key == variation_key }
38
+ unless var_by_exp
39
+ raise Exception::FeatureVariationNotFound.new(variation_key),
40
+ "#{self} does not contain variation '#{variation_key}'"
41
+ end
42
+ var_by_exp
43
+ end
44
+ end
45
+ end
46
+ end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'variation_exposition'
3
+ require_relative 'experiment'
4
4
  require 'kameleoon/targeting/models'
5
5
 
6
6
  module Kameleoon
@@ -29,7 +29,7 @@ module Kameleoon
29
29
 
30
30
  # Rule is a class for new rules of feature flags
31
31
  class Rule
32
- attr_reader :id, :order, :type, :exposition, :experiment_id, :variation_by_exposition, :respool_time, :segment_id, :first_variation
32
+ attr_reader :id, :order, :type, :exposition, :experiment, :respool_time, :segment_id
33
33
  attr_accessor :targeting_segment
34
34
 
35
35
  def self.create_from_array(array)
@@ -45,26 +45,10 @@ module Kameleoon
45
45
  @order = hash['order']
46
46
  @type = RuleType.from_literal(hash['type'])
47
47
  @exposition = hash['exposition']
48
- @experiment_id = hash['experimentId']
49
48
  @respool_time = hash['respoolTime']
50
- @variation_by_exposition = VariationByExposition.create_from_array(hash['variationByExposition'])
51
- @variation_by_exposition.freeze
52
- @first_variation = @variation_by_exposition.first
53
49
  @targeting_segment = Kameleoon::Targeting::Segment.new((hash['segment'])) if hash['segment']
54
50
  @segment_id = @targeting_segment != nil ? targeting_segment.id : -1
55
- end
56
-
57
- def get_variation(hash_double)
58
- total = 0.0
59
- variation_by_exposition.each do |var_by_exp|
60
- total += var_by_exp.exposition
61
- return var_by_exp if total >= hash_double
62
- end
63
- nil
64
- end
65
-
66
- def get_variation_id_by_key(key)
67
- variation_by_exposition.select { |v| v.variation_key == key }.first&.variation_id
51
+ @experiment = Experiment.from_json(hash)
68
52
  end
69
53
 
70
54
  def experimentation_type?
@@ -14,6 +14,8 @@ module Kameleoon
14
14
  DEVICE = 'DEVICE'
15
15
  PAGE_VIEW = 'PAGE_VIEW'
16
16
  ASSIGNED_VARIATION = 'ASSIGNED_VARIATION'
17
+ FORCED_EXPERIMENT_VARIATION = 'FORCED_EXPERIMENT_VARIATION'
18
+ FORCED_FEATURE_VARIATION = 'FORCED_FEATURE_VARIATION'
17
19
  OPERATING_SYSTEM = 'OPERATING_SYSTEM'
18
20
  GEOLOCATION = 'GEOLOCATION'
19
21
  end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'kameleoon/data/data'
4
+ require 'kameleoon/data/manager/forced_variation'
5
+
6
+ module Kameleoon
7
+ module DataManager
8
+ class ForcedExperimentVariation < ForcedVariation
9
+ attr_reader :force_targeting
10
+
11
+ def initialize(rule, var_by_exp, force_targeting)
12
+ super(DataType::FORCED_EXPERIMENT_VARIATION, rule, var_by_exp)
13
+ @force_targeting = force_targeting
14
+ end
15
+
16
+ def ==(other)
17
+ super(other) && other.is_a?(ForcedExperimentVariation) && (@force_targeting == other.force_targeting)
18
+ end
19
+
20
+ def to_s
21
+ "ForcedExperimentVariation{rule:#{@rule},var_by_exp:#{@var_by_exp},force_targeting:#{@force_targeting}}"
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'kameleoon/data/data'
4
+ require 'kameleoon/data/manager/forced_variation'
5
+
6
+ module Kameleoon
7
+ module DataManager
8
+ class ForcedFeatureVariation < ForcedVariation
9
+ attr_reader :feature_key, :simulated
10
+
11
+ def initialize(feature_key, rule, var_by_exp, simulated)
12
+ super(DataType::FORCED_FEATURE_VARIATION, rule, var_by_exp)
13
+ @feature_key = feature_key
14
+ @simulated = simulated
15
+ end
16
+
17
+ def ==(other)
18
+ super(other) && other.is_a?(ForcedFeatureVariation) && \
19
+ (@feature_key == other.feature_key) && (@simulated == other.simulated)
20
+ end
21
+
22
+ def to_s
23
+ "ForcedFeatureVariation{feature_key:'#{@feature_key}',rule:#{@rule}," \
24
+ "var_by_exp:#{@var_by_exp},simulated:#{@simulated}}"
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kameleoon
4
+ module DataManager
5
+ class ForcedVariation
6
+ attr_reader :instance, :rule, :var_by_exp, :rule_type, :assignment_time
7
+
8
+ def initialize(data_type, rule, var_by_exp)
9
+ @instance = data_type
10
+ @rule = rule
11
+ @var_by_exp = var_by_exp
12
+ end
13
+
14
+ def ==(other)
15
+ (@rule == other.rule) && (@var_by_exp == other.var_by_exp)
16
+ end
17
+ end
18
+ end
19
+ end
@@ -10,9 +10,11 @@ require 'kameleoon/data/page_view'
10
10
  require 'kameleoon/data/unique_identifier'
11
11
  require 'kameleoon/data/user_agent'
12
12
  require 'kameleoon/data/manager/assigned_variation'
13
- require 'kameleoon/data/manager/page_view_visit'
14
- require 'kameleoon/data/manager/data_map_storage'
15
13
  require 'kameleoon/data/manager/data_array_storage'
14
+ require 'kameleoon/data/manager/data_map_storage'
15
+ require 'kameleoon/data/manager/forced_experiment_variation'
16
+ require 'kameleoon/data/manager/forced_feature_variation'
17
+ require 'kameleoon/data/manager/page_view_visit'
16
18
  require 'kameleoon/logging/kameleoon_logger'
17
19
 
18
20
  module Kameleoon
@@ -163,38 +165,86 @@ module Kameleoon
163
165
  variations
164
166
  end
165
167
 
168
+ def get_forced_feature_variation(feature_key)
169
+ Logging::KameleoonLogger.debug("CALL: Visitor.get_forced_feature_variation(feature_key: '%s')", feature_key)
170
+ variation = @data.get_from_map(@data.simulated_variations, feature_key)
171
+ Logging::KameleoonLogger.debug(
172
+ "RETURN: Visitor.get_forced_feature_variation(feature_key: '%s') -> (variation: %s)",
173
+ feature_key, variation
174
+ )
175
+ variation
176
+ end
177
+
178
+ def get_forced_experiment_variation(experiment_id)
179
+ Logging::KameleoonLogger.debug(
180
+ 'CALL: Visitor.get_forced_experiment_variation(experiment_id: %d)', experiment_id
181
+ )
182
+ variation = @data.get_from_map(@data.forced_variations, experiment_id)
183
+ Logging::KameleoonLogger.debug(
184
+ 'RETURN: Visitor.get_forced_experiment_variation(experiment_id: %d) -> (variation: %s)',
185
+ experiment_id, variation
186
+ )
187
+ variation
188
+ end
189
+
190
+ def reset_forced_experiment_variation(experiment_id)
191
+ Logging::KameleoonLogger.debug(
192
+ 'CALL: Visitor.reset_forced_experiment_variation(experiment_id: %d)', experiment_id
193
+ )
194
+ @data.remove_from_map(@data.forced_variations, experiment_id)
195
+ Logging::KameleoonLogger.debug(
196
+ 'RETURN: Visitor.reset_forced_experiment_variation(experiment_id: %d)', experiment_id
197
+ )
198
+ end
199
+
200
+ def update_simulated_variations(variations)
201
+ return if (@data.simulated_variations.nil? || @data.simulated_variations.empty?) && variations.empty?
202
+
203
+ Logging::KameleoonLogger.debug('CALL: Visitor.update_simulated_variations(variations: %s)', variations)
204
+ new_simulated_variations = {}
205
+ variations.each { |sv| new_simulated_variations[sv.feature_key] = sv }
206
+ @data.mutex.with_write_lock do
207
+ @data.simulated_variations = new_simulated_variations
208
+ end
209
+ Logging::KameleoonLogger.debug('RETURN: Visitor.update_simulated_variations(variations: %s)', variations)
210
+ end
211
+
166
212
  def add_data(*args, overwrite: true)
167
213
  Logging::KameleoonLogger.debug('CALL: Visitor.add_data(args: %s, overwrite: %s)', args, overwrite)
168
214
  @data.mutex.with_write_lock do
169
215
  args.each do |data|
170
216
  case data
171
- when Kameleoon::UserAgent
217
+ when UserAgent
172
218
  @data.user_agent = data.value
173
- when Kameleoon::DataManager::AssignedVariation
219
+ when AssignedVariation
174
220
  @data.add_variation(data, overwrite)
175
- when Kameleoon::Device
221
+ when ForcedFeatureVariation
222
+ @data.add_forced_feature_variation(data)
223
+ when ForcedExperimentVariation
224
+ @data.add_forced_experiment_variation(data)
225
+ when Device
176
226
  @data.set_device(data, overwrite)
177
- when Kameleoon::Browser
227
+ when Browser
178
228
  @data.set_browser(data, overwrite)
179
- when Kameleoon::CustomData
229
+ when CustomData
180
230
  @data.add_custom_data(data, overwrite)
181
- when Kameleoon::PageView
231
+ when PageView
182
232
  @data.add_page_view(data)
183
- when Kameleoon::DataManager::PageViewVisit
233
+ when PageViewVisit
184
234
  @data.add_page_view_visit(data)
185
- when Kameleoon::Conversion
235
+ when Conversion
186
236
  @data.add_conversion(data)
187
- when Kameleoon::Cookie
237
+ when Cookie
188
238
  @data.cookie = data
189
- when Kameleoon::OperatingSystem
239
+ when OperatingSystem
190
240
  @data.set_operating_system(data, overwrite)
191
- when Kameleoon::Geolocation
241
+ when Geolocation
192
242
  @data.set_geolocation(data, overwrite)
193
- when Kameleoon::KcsHeat
243
+ when KcsHeat
194
244
  @data.kcs_heat = data
195
- when Kameleoon::VisitorVisits
245
+ when VisitorVisits
196
246
  @data.visitor_visits = data
197
- when Kameleoon::UniqueIdentifier
247
+ when UniqueIdentifier
198
248
  @is_unique_identifier = data.value
199
249
  else
200
250
  Logging::KameleoonLogger.warning("Data has unsupported type '%s'", data.class)
@@ -218,7 +268,7 @@ module Kameleoon
218
268
  class VisitorData
219
269
  attr_reader :mutex, :device, :browser, :geolocation, :operating_system
220
270
  attr_accessor :last_activity_time, :legal_consent, :user_agent, :cookie, :kcs_heat, :visitor_visits,
221
- :mapping_identifier
271
+ :mapping_identifier, :forced_variations, :simulated_variations
222
272
 
223
273
  def initialize
224
274
  Logging::KameleoonLogger.debug('CALL: VisitorData.new')
@@ -227,6 +277,22 @@ module Kameleoon
227
277
  Logging::KameleoonLogger.debug('RETURN: VisitorData.new')
228
278
  end
229
279
 
280
+ def get_from_map(map, key)
281
+ return nil if map.nil?
282
+
283
+ @mutex.with_read_lock do
284
+ return map[key]
285
+ end
286
+ end
287
+
288
+ def remove_from_map(map, key)
289
+ return false if map.nil?
290
+
291
+ @mutex.with_write_lock do
292
+ return map.delete(key).nil?
293
+ end
294
+ end
295
+
230
296
  def enumerate_sendable_data(&blk)
231
297
  blk.call(@device) unless @device.nil?
232
298
  blk.call(@browser) unless @browser.nil?
@@ -278,7 +344,7 @@ module Kameleoon
278
344
  end
279
345
 
280
346
  def add_variation(variation, overwrite)
281
- @variations = {} if @variations.nil?
347
+ @variations ||= {}
282
348
  if overwrite || !@variations.include?(variation.experiment_id)
283
349
  @variations[variation.experiment_id] = variation
284
350
  end
@@ -289,7 +355,7 @@ module Kameleoon
289
355
  end
290
356
 
291
357
  def add_custom_data(custom_data, overwrite)
292
- @custom_data_map = {} if @custom_data_map.nil?
358
+ @custom_data_map ||= {}
293
359
  if overwrite || !@custom_data_map.include?(custom_data.id)
294
360
  @custom_data_map[custom_data.id] = custom_data
295
361
  end
@@ -298,10 +364,10 @@ module Kameleoon
298
364
  def add_page_view(page_view)
299
365
  return if page_view.url.nil? || page_view.url.empty?
300
366
 
301
- @page_view_visits = {} if @page_view_visits.nil?
367
+ @page_view_visits ||= {}
302
368
  visit = @page_view_visits[page_view.url]
303
369
  if visit.nil?
304
- visit = DataManager::PageViewVisit.new(page_view)
370
+ visit = PageViewVisit.new(page_view)
305
371
  else
306
372
  visit.overwrite(page_view)
307
373
  end
@@ -309,7 +375,7 @@ module Kameleoon
309
375
  end
310
376
 
311
377
  def add_page_view_visit(page_view_visit)
312
- @page_view_visits = {} if @page_view_visits.nil?
378
+ @page_view_visits ||= {}
313
379
  visit = @page_view_visits[page_view_visit.page_view.url]
314
380
  if visit.nil?
315
381
  visit = page_view_visit
@@ -320,7 +386,7 @@ module Kameleoon
320
386
  end
321
387
 
322
388
  def add_conversion(conversion)
323
- @conversions = [] if @conversions.nil?
389
+ @conversions ||= []
324
390
  @conversions.push(conversion)
325
391
  end
326
392
 
@@ -331,6 +397,16 @@ module Kameleoon
331
397
  def set_operating_system(operating_system, overwrite)
332
398
  @operating_system = operating_system if overwrite || @operating_system.nil?
333
399
  end
400
+
401
+ def add_forced_feature_variation(forced_variation)
402
+ @simulated_variations ||= {}
403
+ @simulated_variations[forced_variation.feature_key] = forced_variation
404
+ end
405
+
406
+ def add_forced_experiment_variation(forced_variation)
407
+ @forced_variations ||= {}
408
+ @forced_variations[forced_variation.rule.experiment.id] = forced_variation
409
+ end
334
410
  end
335
411
  end
336
412
  end
@@ -30,10 +30,10 @@ module Kameleoon
30
30
  end
31
31
  end
32
32
 
33
- # Feature Variable Not Found
34
- class FeatureVariableNotFound < FeatureError
35
- def initialize(key = '')
36
- super("Feature variable #{key}")
33
+ # Feature Experiment Not Found
34
+ class FeatureExperimentNotFound < FeatureError
35
+ def initialize(id = '')
36
+ super("Experiment #{id}")
37
37
  end
38
38
  end
39
39
 
@@ -44,6 +44,13 @@ module Kameleoon
44
44
  end
45
45
  end
46
46
 
47
+ # Feature Variable Not Found
48
+ class FeatureVariableNotFound < FeatureError
49
+ def initialize(key = '')
50
+ super("Feature variable #{key}")
51
+ end
52
+ end
53
+
47
54
  # Feature Environment Disabled
48
55
  class FeatureEnvironmentDisabled < FeatureError
49
56
  def initialize(feature_key, environment = nil)
@@ -7,6 +7,7 @@ require 'kameleoon/configuration/data_file'
7
7
  require 'kameleoon/data/custom_data'
8
8
  require 'kameleoon/data/user_agent'
9
9
  require 'kameleoon/data/manager/assigned_variation'
10
+ require 'kameleoon/data/manager/forced_experiment_variation'
10
11
  require 'kameleoon/data/manager/visitor_manager'
11
12
  require 'kameleoon/exceptions'
12
13
  require 'kameleoon/hybrid/manager'
@@ -75,7 +76,7 @@ module Kameleoon
75
76
  @remote_data_manager = Managers::RemoteData::RemoteDataManager.new(
76
77
  @data_manager, @network_manager, @visitor_manager
77
78
  )
78
- @cookie_manager = Network::Cookie::CookieManager.new(@data_manager, config.top_level_domain)
79
+ @cookie_manager = Network::Cookie::CookieManager.new(@data_manager, @visitor_manager, config.top_level_domain)
79
80
  @readiness = ClientReadiness.new
80
81
  @targeting_manager = Targeting::TargetingManager.new(@data_manager, @visitor_manager)
81
82
 
@@ -320,9 +321,9 @@ module Kameleoon
320
321
  )
321
322
  Utils::VisitorCode.validate(visitor_code)
322
323
  feature_flag = @data_manager.data_file.get_feature_flag(feature_key)
323
- variation_key, var_by_exp, rule = get_variation_info(visitor_code, feature_flag, track)
324
+ variation_key, eval_exp = get_variation_info(visitor_code, feature_flag, track)
324
325
  variation = feature_flag.get_variation_by_key(variation_key)
325
- external_variation = make_external_variation(variation, var_by_exp&.variation_id, rule&.experiment_id)
326
+ external_variation = create_external_variation(variation, eval_exp)
326
327
  @tracking_manager.add_visitor_code(visitor_code) if track
327
328
  Logging::KameleoonLogger.info(
328
329
  "RETURN: KameleoonClient.get_variation(visitor_code: '%s', feature_key: '%s', track: %s)" \
@@ -357,12 +358,11 @@ module Kameleoon
357
358
  @data_manager.data_file.feature_flags.each_value do |feature_flag|
358
359
  next unless feature_flag.environment_enabled
359
360
 
360
- variation_key, var_by_exp, rule = get_variation_info(visitor_code, feature_flag, track)
361
+ variation_key, eval_exp = get_variation_info(visitor_code, feature_flag, track)
361
362
  next if only_active && (variation_key == Configuration::VariationType::VARIATION_OFF)
362
363
 
363
364
  variation = feature_flag.get_variation_by_key(variation_key)
364
- variations[feature_flag.feature_key] =
365
- make_external_variation(variation, var_by_exp&.variation_id, rule&.experiment_id)
365
+ variations[feature_flag.feature_key] = create_external_variation(variation, eval_exp)
366
366
  end
367
367
  variations.freeze
368
368
  @tracking_manager.add_visitor_code(visitor_code) if track
@@ -611,17 +611,20 @@ module Kameleoon
611
611
  #
612
612
  # DEPRECATED. Please use `get_active_features` instead.
613
613
  def get_active_feature_list_for_visitor(visitor_code)
614
- Logging::KameleoonLogger.info('[DEPRECATION] `get_active_feature_list_for_visitor` is deprecated.' \
615
- ' Please use `get_active_features` instead.')
616
- Logging::KameleoonLogger.info("CALL: KameleoonClient.get_active_feature_list_for_visitor(visitor_code: '%s')",
617
- visitor_code)
614
+ Logging::KameleoonLogger.info(
615
+ '[DEPRECATION] `get_active_feature_list_for_visitor` is deprecated. Please use `get_active_features` instead.'
616
+ )
617
+ Logging::KameleoonLogger.info(
618
+ "CALL: KameleoonClient.get_active_feature_list_for_visitor(visitor_code: '%s')", visitor_code
619
+ )
618
620
  Utils::VisitorCode.validate(visitor_code)
621
+ visitor = @visitor_manager.get_visitor(visitor_code)
619
622
  list_keys = []
620
623
  @data_manager.data_file.feature_flags.each do |feature_key, feature_flag|
621
624
  next unless feature_flag.environment_enabled
622
625
 
623
- variation, rule, = _calculate_variation_key_for_feature(visitor_code, feature_flag)
624
- variation_key = _get_variation_key(variation, rule, feature_flag)
626
+ eval_exp = evaluate(visitor, visitor_code, feature_flag, false, false)
627
+ variation_key = calculate_variation_key(eval_exp, feature_flag.default_variation_key)
625
628
  list_keys.push(feature_key) if variation_key != Kameleoon::Configuration::VariationType::VARIATION_OFF
626
629
  end
627
630
  Logging::KameleoonLogger.info(
@@ -649,19 +652,19 @@ module Kameleoon
649
652
  )
650
653
  Logging::KameleoonLogger.info("CALL: KameleoonClient.get_active_features(visitor_code: '%s')", visitor_code)
651
654
  Utils::VisitorCode.validate(visitor_code)
655
+ visitor = @visitor_manager.get_visitor(visitor_code)
652
656
  map_active_features = {}
653
657
 
654
658
  @data_manager.data_file.feature_flags.each_value do |feature_flag|
655
659
  next unless feature_flag.environment_enabled
656
660
 
657
- var_by_exp, rule = _calculate_variation_key_for_feature(visitor_code, feature_flag)
658
- variation_key = _get_variation_key(var_by_exp, rule, feature_flag)
661
+ eval_exp = evaluate(visitor, visitor_code, feature_flag, false, false)
662
+ variation_key = calculate_variation_key(eval_exp, feature_flag.default_variation_key)
659
663
 
660
664
  next if variation_key == Configuration::VariationType::VARIATION_OFF
661
665
 
662
666
  variation = feature_flag.get_variation_by_key(variation_key)
663
- map_active_features[feature_flag.feature_key] =
664
- make_external_variation(variation, var_by_exp&.variation_id, rule&.experiment_id)
667
+ map_active_features[feature_flag.feature_key] = create_external_variation(variation, eval_exp)
665
668
  end
666
669
 
667
670
  map_active_features.freeze
@@ -701,6 +704,52 @@ module Kameleoon
701
704
  engine_tracking_code
702
705
  end
703
706
 
707
+ ##
708
+ # Sets or resets a forced variation for a visitor in a specific experiment,
709
+ # so the experiment will be evaluated to the variation for the visitor.
710
+ #
711
+ # In order to reset the forced variation set the `variation_key` parameter to `nil`.
712
+ # If the forced variation you want to reset does not exist, the method will have no effect.
713
+ #
714
+ # @param [String] visitor_code The unique visitor code identifying the visitor.
715
+ # @param [Integer] experiment_id The identifier of the experiment you want to set/reset the forced variation for.
716
+ # @param [String | NilClass] variation_key The identifier of the variation you want the experiment to be evaluated
717
+ # to. Set to `nil` to reset the forced variation.
718
+ # @param [Bool] force_targeting If `true`, the visitor will be targeted to the experiment regardless its
719
+ # conditions. Otherwise, the normal targeting logic will be preserved. Optional (defaults to `true`).
720
+ #
721
+ # @raise [Kameleoon::Exception::VisitorCodeInvalid] The provided **visitor code** is invalid.
722
+ # @raise [Kameleoon::Exception::FeatureExperimentNotFound] The provided **experiment id** does not exist in
723
+ # the feature flag.
724
+ # @raise [Kameleoon::Exception::FeatureVariationNotFound] The provided **variation key** does not belong to
725
+ # the experiment.
726
+ def set_forced_variation(visitor_code, experiment_id, variation_key, force_targeting: true)
727
+ Logging::KameleoonLogger.info(
728
+ "CALL: KameleoonClient.set_forced_variation(visitor_code: '%s', experiment_id: %d, variation_key: %s, " \
729
+ 'force_targeting: %s)',
730
+ visitor_code, experiment_id, variation_key.nil? ? 'nil' : "'#{variation_key}'", force_targeting
731
+ )
732
+ Utils::VisitorCode.validate(visitor_code)
733
+ if variation_key.nil?
734
+ visitor = @visitor_manager.get_visitor(visitor_code)
735
+ visitor&.reset_forced_experiment_variation(experiment_id)
736
+ else
737
+ rule_info = @data_manager.data_file.rule_info_by_exp_id[experiment_id]
738
+ if rule_info.nil?
739
+ raise Exception::FeatureExperimentNotFound.new(experiment_id), "Experiment #{experiment_id} is not found"
740
+ end
741
+
742
+ var_by_exp = rule_info.rule.experiment.get_variation_by_key(variation_key)
743
+ forced_variation = DataManager::ForcedExperimentVariation.new(rule_info.rule, var_by_exp, force_targeting)
744
+ @visitor_manager.add_data(visitor_code, forced_variation)
745
+ end
746
+ Logging::KameleoonLogger.info(
747
+ "RETURN: KameleoonClient.set_forced_variation(visitor_code: '%s', experiment_id: %d, variation_key: %s, " \
748
+ 'force_targeting: %s)',
749
+ visitor_code, experiment_id, variation_key.nil? ? 'nil' : "'#{variation_key}'", force_targeting
750
+ )
751
+ end
752
+
704
753
  private
705
754
 
706
755
  HYBRID_EXPIRATION_TIME = 5
@@ -853,15 +902,65 @@ module Kameleoon
853
902
  "CALL: KameleoonClient.get_variation_info(visitor_code: '%s', feature_flag: %s, track: %s)",
854
903
  visitor_code, feature_flag, track
855
904
  )
856
- var_by_exp, rule = _calculate_variation_key_for_feature(visitor_code, feature_flag)
857
- variation_key = _get_variation_key(var_by_exp, rule, feature_flag)
858
- save_variation(visitor_code, rule, var_by_exp, track: track)
905
+ visitor = @visitor_manager.get_visitor(visitor_code)
906
+ eval_exp = evaluate(visitor, visitor_code, feature_flag, track, true)
907
+ variation_key = calculate_variation_key(eval_exp, feature_flag.default_variation_key)
859
908
  Logging::KameleoonLogger.debug(
860
909
  "RETURN: KameleoonClient.get_variation_info(visitor_code: '%s', feature_flag: %s, track: %s)" \
861
- ' -> (variation_key: %s, variation_by_exposition: %s, rule: %s)',
862
- visitor_code, feature_flag, track, variation_key, var_by_exp, rule
910
+ ' -> (variation_key: %s, eval_exp: %s)',
911
+ visitor_code, feature_flag, track, variation_key, eval_exp
912
+ )
913
+ [variation_key, eval_exp]
914
+ end
915
+
916
+ def evaluate(visitor, visitor_code, feature_flag, track, save)
917
+ Logging::KameleoonLogger.debug(
918
+ "CALL: KameleoonClient.evaluate(visitor, visitor_code: '%s', feature_flag: %s, track: %s, save: %s)",
919
+ visitor_code, feature_flag, track, save
920
+ )
921
+ eval_exp = nil
922
+ forced_variation = visitor&.get_forced_feature_variation(feature_flag.feature_key)
923
+ if forced_variation
924
+ eval_exp = EvaluatedExperiment.from_forced_variation(forced_variation)
925
+ elsif visitor_not_in_holdout?(visitor, visitor_code, track, save)
926
+ eval_exp = _calculate_variation_key_for_feature(visitor_code, feature_flag)
927
+ end
928
+ save_variation(visitor_code, eval_exp, track: track) if save && !forced_variation&.simulated
929
+ Logging::KameleoonLogger.debug(
930
+ "RETURN: KameleoonClient.evaluate(visitor, visitor_code: '%s', feature_flag: %s, track: %s, save: %s)" \
931
+ ' -> (eval_exp: %s)',
932
+ visitor_code, feature_flag, track, save, eval_exp
933
+ )
934
+ eval_exp
935
+ end
936
+
937
+ def visitor_not_in_holdout?(visitor, visitor_code, track, save)
938
+ in_holdout_variation_key = 'in-holdout'
939
+ Logging::KameleoonLogger.debug(
940
+ "CALL: KameleoonClient.visitor_not_in_holdout?(visitor, visitor_code: '%s', track: %s, save: %s)",
941
+ visitor_code, track, save
942
+ )
943
+ holdout = @data_manager.data_file.holdout
944
+ is_not_in_holdout = true
945
+ unless holdout.nil?
946
+ code_for_hash = get_code_for_hash(visitor, visitor_code)
947
+ variation_hash = Utils::Hasher.obtain(code_for_hash, holdout.id)
948
+ Logging::KameleoonLogger.debug("Calculated holdout hash %s for code '%s'", variation_hash, code_for_hash)
949
+ var_by_exp = holdout.get_variation(variation_hash)
950
+ unless var_by_exp.nil?
951
+ is_not_in_holdout = var_by_exp.variation_key != in_holdout_variation_key
952
+ if save
953
+ eval_exp = EvaluatedExperiment.new(var_by_exp, holdout, Configuration::RuleType::EXPERIMENTATION)
954
+ save_variation(visitor_code, eval_exp, track: track)
955
+ end
956
+ end
957
+ end
958
+ Logging::KameleoonLogger.debug(
959
+ "RETURN: KameleoonClient.visitor_not_in_holdout?(visitor, visitor_code: '%s', track: %s, save: %s)" \
960
+ ' -> (is_not_in_holdout: %s)',
961
+ visitor_code, track, save, is_not_in_holdout
863
962
  )
864
- [variation_key, var_by_exp, rule]
963
+ is_not_in_holdout
865
964
  end
866
965
 
867
966
  ##
@@ -872,9 +971,9 @@ module Kameleoon
872
971
  visitor_code, feature_key
873
972
  )
874
973
  feature_flag = @data_manager.data_file.get_feature_flag(feature_key)
875
- variation, rule = _calculate_variation_key_for_feature(visitor_code, feature_flag)
876
- variation_key = _get_variation_key(variation, rule, feature_flag)
877
- save_variation(visitor_code, rule, variation)
974
+ visitor = @visitor_manager.get_visitor(visitor_code)
975
+ eval_exp = evaluate(visitor, visitor_code, feature_flag, true, true)
976
+ variation_key = calculate_variation_key(eval_exp, feature_flag.default_variation_key)
878
977
  @tracking_manager.add_visitor_code(visitor_code)
879
978
  Logging::KameleoonLogger.debug(
880
979
  "RETURN: KameleoonClient._get_feature_variation_key(visitor_code: '%s', feature_key: '%s')" \
@@ -884,25 +983,30 @@ module Kameleoon
884
983
  [feature_flag, variation_key]
885
984
  end
886
985
 
887
- def save_variation(visitor_code, rule, var_by_exp, track: true)
888
- experiment_id = rule&.experiment_id
889
- variation_id = var_by_exp&.variation_id
890
- return if experiment_id.nil? || variation_id.nil?
986
+ def save_variation(visitor_code, eval_exp, track: true)
987
+ return if eval_exp.nil? || eval_exp.experiment.id.zero? || eval_exp.var_by_exp.variation_id.nil?
891
988
 
892
989
  Logging::KameleoonLogger.debug(
893
- "CALL: KameleoonClient.save_variation(visitor_code: '%s', rule: %s, var_by_exp: %s, track: %s)",
894
- visitor_code, rule, var_by_exp, track
990
+ "CALL: KameleoonClient.save_variation(visitor_code: '%s', eval_exp: %s, track: %s)",
991
+ visitor_code, eval_exp, track
895
992
  )
896
993
  visitor = @visitor_manager.get_or_create_visitor(visitor_code)
897
- as_variation = Kameleoon::DataManager::AssignedVariation.new(experiment_id, variation_id, rule.type)
994
+ as_variation = Kameleoon::DataManager::AssignedVariation.new(
995
+ eval_exp.experiment.id, eval_exp.var_by_exp.variation_id, eval_exp.rule_type
996
+ )
898
997
  as_variation.mark_as_sent unless track
899
998
  visitor.assign_variation(as_variation)
900
999
  Logging::KameleoonLogger.debug(
901
- "RETURN: KameleoonClient.save_variation(visitor_code: '%s', rule: %s, var_by_exp: %s, track: %s)",
902
- visitor_code, rule, var_by_exp, track
1000
+ "RETURN: KameleoonClient.save_variation(visitor_code: '%s', eval_exp: %s, track: %s)",
1001
+ visitor_code, eval_exp, track
903
1002
  )
904
1003
  end
905
1004
 
1005
+ def get_code_for_hash(visitor, visitor_code)
1006
+ # use mappingIdentifier instead of visitor_code if it was set up
1007
+ visitor&.mapping_identifier || visitor_code
1008
+ end
1009
+
906
1010
  ##
907
1011
  # helper method for calculate variation key for feature flag
908
1012
  def _calculate_variation_key_for_feature(visitor_code, feature_flag)
@@ -910,41 +1014,42 @@ module Kameleoon
910
1014
  "CALL: KameleoonClient._calculate_variation_key_for_feature(visitor_code: '%s', feature_flag: %s)",
911
1015
  visitor_code, feature_flag
912
1016
  )
1017
+ visitor = @visitor_manager.get_visitor(visitor_code)
1018
+ code_for_hash = get_code_for_hash(visitor, visitor_code)
913
1019
  # no rules -> return default_variation_key
1020
+ selected = nil
914
1021
  feature_flag.rules.each do |rule|
1022
+ forced_variation = visitor&.get_forced_experiment_variation(rule.experiment.id)
1023
+ if forced_variation&.force_targeting
1024
+ # Forcing experiment variation in force-targeting mode
1025
+ selected = EvaluatedExperiment.from_var_by_exp_rule(forced_variation.var_by_exp, rule)
1026
+ break
1027
+ end
915
1028
  # check if visitor is targeted for rule, else next rule
916
- next unless check_targeting(visitor_code, rule.experiment_id, rule)
1029
+ next unless check_targeting(visitor_code, rule.experiment.id, rule)
917
1030
 
918
- vis = @visitor_manager.get_visitor(visitor_code)
919
- # use mappingIdentifier instead of visitorCode if it was set up
920
- code_for_hash = vis&.mapping_identifier || visitor_code
1031
+ unless forced_variation.nil?
1032
+ # Forcing experiment variation in targeting-only mode
1033
+ selected = EvaluatedExperiment.from_var_by_exp_rule(forced_variation.var_by_exp, rule)
1034
+ break
1035
+ end
921
1036
  # uses for rule exposition
922
- hash_rule = Utils::HashDouble.obtain_rule(code_for_hash, rule.id, rule.respool_time)
923
- Logging::KameleoonLogger.debug("Calculated hash_rule: %s for visitor_code: '%s'", hash_rule, code_for_hash)
1037
+ hash_rule = Utils::Hasher.obtain(code_for_hash, rule.id, rule.respool_time)
1038
+ Logging::KameleoonLogger.debug("Calculated rule hash %s for code '%s'", hash_rule, code_for_hash)
924
1039
  # check main expostion for rule with hashRule
925
1040
  if hash_rule <= rule.exposition
926
1041
  if rule.targeted_delivery_type?
927
- Logging::KameleoonLogger.debug(
928
- "RETURN: KameleoonClient._calculate_variation_key_for_feature(visitor_code: '%s', feature_flag: %s) " \
929
- '-> (variation: %s, rule: %s)',
930
- visitor_code, feature_flag, rule.first_variation, rule
931
- )
932
- return [rule.first_variation, rule]
1042
+ selected = EvaluatedExperiment.from_var_by_exp_rule(rule.experiment.variations_by_exposition.first, rule)
1043
+ break
933
1044
  end
934
-
935
1045
  # uses for variation's expositions
936
- hash_variation = Utils::HashDouble.obtain_rule(code_for_hash, rule.experiment_id, rule.respool_time)
937
- Logging::KameleoonLogger.debug(
938
- "Calculated hash_variation: %s for visitor_code: '%s'", hash_variation, code_for_hash
939
- )
1046
+ hash_variation = Utils::Hasher.obtain(code_for_hash, rule.experiment.id, rule.respool_time)
1047
+ Logging::KameleoonLogger.debug("Calculated variation hash %s for code '%s'", hash_variation, code_for_hash)
940
1048
  # get variation key with new hashVariation
941
- variation = rule.get_variation(hash_variation)
1049
+ variation = rule.experiment.get_variation(hash_variation)
942
1050
  unless variation.nil?
943
- Logging::KameleoonLogger.debug(
944
- "RETURN: KameleoonClient._calculate_variation_key_for_feature(visitor_code: '%s', " \
945
- 'feature_flag: %s) -> (variation: %s, rule: %s)', visitor_code, feature_flag, variation, rule
946
- )
947
- return [variation, rule]
1051
+ selected = EvaluatedExperiment.from_var_by_exp_rule(variation, rule)
1052
+ break
948
1053
  end
949
1054
  # if visitor is targeted for targeted rule then break cycle -> return default
950
1055
  elsif rule.targeted_delivery_type?
@@ -953,39 +1058,44 @@ module Kameleoon
953
1058
  end
954
1059
  Logging::KameleoonLogger.debug(
955
1060
  "RETURN: KameleoonClient._calculate_variation_key_for_feature(visitor_code: '%s', feature_flag: %s) " \
956
- '-> (variation: nil, rule: nil)', visitor_code, feature_flag
1061
+ '-> (eval_exp: %s)', visitor_code, feature_flag, selected
957
1062
  )
958
- [nil, nil]
1063
+ selected
959
1064
  end
960
1065
 
961
- def _get_variation_key(var_by_exp, rule, feature_flag)
962
- return var_by_exp.variation_key unless var_by_exp.nil?
963
- return Kameleoon::Configuration::VariationType::VARIATION_OFF if !rule.nil? && rule.experimentation_type?
964
-
965
- feature_flag.default_variation_key
1066
+ def calculate_variation_key(eval_exp, default_value)
1067
+ Logging::KameleoonLogger.debug(
1068
+ "CALL: KameleoonClient.calculate_variation_key(eval_exp: %s, default_value: '%s') ", eval_exp, default_value
1069
+ )
1070
+ variation_key = eval_exp ? eval_exp.var_by_exp.variation_key : default_value
1071
+ Logging::KameleoonLogger.debug(
1072
+ "RETURN: KameleoonClient.calculate_variation_key(eval_exp: %s, default_value: '%s') -> (variation_key: '%s')",
1073
+ eval_exp, default_value, variation_key
1074
+ )
1075
+ variation_key
966
1076
  end
967
1077
 
968
- def make_external_variation(internal_variation, variation_id, experiment_id)
1078
+ def create_external_variation(variation, eval_exp)
969
1079
  Logging::KameleoonLogger.debug(
970
- 'CALL: KameleoonClient.make_external_variation(internal_variation: %s, variation_id: %s, experiment_id: %s)',
971
- internal_variation, variation_id, experiment_id
1080
+ 'CALL: KameleoonClient.create_external_variation(variation: %s, eval_exp: %s)', variation, eval_exp
972
1081
  )
973
- variables = {}
974
- internal_variation&.variables&.each do |variable|
975
- variables[variable.key] = Types::Variable.new(
1082
+ ext_variables = {}
1083
+ variation&.variables&.each do |variable|
1084
+ ext_variables[variable.key] = Types::Variable.new(
976
1085
  variable.key,
977
1086
  variable.type,
978
1087
  _parse_feature_variable(variable)
979
1088
  )
980
1089
  end
981
- variables.freeze
982
- variation = Types::Variation.new(internal_variation&.key, variation_id, experiment_id, variables)
1090
+ ext_variables.freeze
1091
+ ext_variation = Types::Variation.new(
1092
+ variation&.key, eval_exp&.var_by_exp&.variation_id, eval_exp&.experiment&.id, ext_variables
1093
+ )
983
1094
  Logging::KameleoonLogger.debug(
984
- 'RETURN: KameleoonClient.make_external_variation(internal_variation: %s, variation_id: %s, experiment_id: %s)' \
985
- ' -> (variation: %s)',
986
- internal_variation, variation_id, experiment_id, variation
1095
+ 'RETURN: KameleoonClient.create_external_variation(variation: %s, eval_exp: %s) -> (ext_variation: %s)',
1096
+ variation, eval_exp, ext_variation
987
1097
  )
988
- variation
1098
+ ext_variation
989
1099
  end
990
1100
 
991
1101
  ##
@@ -1008,5 +1118,31 @@ module Kameleoon
1008
1118
  )
1009
1119
  @visitor_manager.add_data(visitor_code, UniqueIdentifier.new(is_unique_identifier))
1010
1120
  end
1121
+
1122
+ class EvaluatedExperiment
1123
+ attr_reader :var_by_exp, :experiment, :rule_type
1124
+
1125
+ def initialize(var_by_exp, experiment, rule_type)
1126
+ @var_by_exp = var_by_exp
1127
+ @experiment = experiment
1128
+ @rule_type = rule_type
1129
+ end
1130
+
1131
+ def self.from_var_by_exp_rule(var_by_exp, rule)
1132
+ EvaluatedExperiment.new(var_by_exp, rule.experiment, rule.type)
1133
+ end
1134
+
1135
+ def self.from_forced_variation(forced_variation)
1136
+ if forced_variation.var_by_exp.nil? || forced_variation.rule.nil?
1137
+ nil
1138
+ else
1139
+ EvaluatedExperiment.from_var_by_exp_rule(forced_variation.var_by_exp, forced_variation.rule)
1140
+ end
1141
+ end
1142
+
1143
+ def self.from_forced_experiment_variation(forced_variation)
1144
+ EvaluatedExperiment.from_var_by_exp_rule(forced_variation.var_by_exp, forced_variation.rule)
1145
+ end
1146
+ end
1011
1147
  end
1012
1148
  end
@@ -1,21 +1,29 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'kameleoon/utils'
3
+ require 'kameleoon/data/manager/forced_feature_variation'
4
4
  require 'kameleoon/logging/kameleoon_logger'
5
+ require 'kameleoon/utils'
5
6
 
6
7
  module Kameleoon
7
8
  module Network
8
9
  module Cookie
9
- COOKIE_KEY_JS = '_js_'
10
10
  VISITOR_CODE_COOKIE = 'kameleoonVisitorCode'
11
+ KAMELEOON_SIMULATION_FF_DATA = 'kameleoonSimulationFFData'
12
+ EXPERIMENT_ID_KEY = 'expId'
13
+ VARIATION_ID_KEY = 'varId'
11
14
  COOKIE_TTL_SECONDS = 380 * 86_400 # 380 days in seconds
12
15
 
13
16
  class CookieManager
14
- def initialize(data_manager, top_level_domain)
15
- Logging::KameleoonLogger.debug("CALL: CookieManager.new(top_level_domain: '%s')", top_level_domain)
17
+ def initialize(data_manager, visitor_manager, top_level_domain)
18
+ Logging::KameleoonLogger.debug(
19
+ "CALL: CookieManager.new(data_manager, visitor_manager, top_level_domain: '%s')", top_level_domain
20
+ )
16
21
  @data_manager = data_manager
22
+ @visitor_manager = visitor_manager
17
23
  @top_level_domain = top_level_domain
18
- Logging::KameleoonLogger.debug("RETURN: CookieManager.new(top_level_domain: '%s')", top_level_domain)
24
+ Logging::KameleoonLogger.debug(
25
+ "RETURN: CookieManager.new(data_manager, visitor_manager, top_level_domain: '%s')", top_level_domain
26
+ )
19
27
  end
20
28
 
21
29
  def get_or_add(cookies, default_visitor_code = nil)
@@ -25,35 +33,8 @@ module Kameleoon
25
33
  "CALL: CookieManager.get_or_add(cookies: %s, default_visitor_code: '%s')",
26
34
  cookies, default_visitor_code
27
35
  )
28
-
29
- visitor_code = get_visitor_code_from_cookies(cookies)
30
- unless visitor_code.nil?
31
- Utils::VisitorCode.validate(visitor_code)
32
- Logging::KameleoonLogger.debug("Read visitor code '%s' from cookies %s", visitor_code, cookies)
33
- # Remove adding cookies when we will be sure that it doesn't break anything
34
- add(visitor_code, cookies) unless @data_manager.visitor_code_managed?
35
- Logging::KameleoonLogger.debug(
36
- "RETURN: CookieManager.get_or_add(cookies: %s, default_visitor_code: '%s') -> (visitor_code: '%s')",
37
- cookies, default_visitor_code, visitor_code
38
- )
39
- return visitor_code
40
- end
41
-
42
- if default_visitor_code.nil?
43
- visitor_code = Utils::VisitorCode.generate
44
- Logging::KameleoonLogger.debug("Generated new visitor code '%s'", visitor_code)
45
- add(visitor_code, cookies) unless @data_manager.visitor_code_managed?
46
- Logging::KameleoonLogger.debug(
47
- "RETURN: CookieManager.get_or_add(cookies: %s, default_visitor_code: '%s') -> (visitor_code: '%s')",
48
- cookies, default_visitor_code, visitor_code
49
- )
50
- return visitor_code
51
- end
52
-
53
- visitor_code = default_visitor_code
54
- Utils::VisitorCode.validate(visitor_code)
55
- Logging::KameleoonLogger.debug("Used default visitor code '{%s}'", default_visitor_code)
56
- add(visitor_code, cookies)
36
+ visitor_code = get_or_add_visitor_code(cookies, default_visitor_code)
37
+ process_simulated_variations(cookies, visitor_code)
57
38
  Logging::KameleoonLogger.debug(
58
39
  "RETURN: CookieManager.get_or_add(cookies: %s, default_visitor_code: '%s') -> (visitor_code: '%s')",
59
40
  cookies, default_visitor_code, visitor_code
@@ -78,9 +59,34 @@ module Kameleoon
78
59
 
79
60
  private
80
61
 
62
+ def get_or_add_visitor_code(cookies, default_visitor_code)
63
+ visitor_code = get_value_from_cookies(cookies, VISITOR_CODE_COOKIE)
64
+ unless visitor_code.nil?
65
+ Utils::VisitorCode.validate(visitor_code)
66
+ Logging::KameleoonLogger.debug("Read visitor code '%s' from cookies %s", visitor_code, cookies)
67
+ # Remove adding cookies when we will be sure that it doesn't break anything
68
+ add(visitor_code, cookies) unless @data_manager.visitor_code_managed?
69
+ return visitor_code
70
+ end
71
+
72
+ if default_visitor_code.nil?
73
+ visitor_code = Utils::VisitorCode.generate
74
+ Logging::KameleoonLogger.debug("Generated new visitor code '%s'", visitor_code)
75
+ add(visitor_code, cookies) unless @data_manager.visitor_code_managed?
76
+ return visitor_code
77
+ end
78
+
79
+ visitor_code = default_visitor_code
80
+ Utils::VisitorCode.validate(visitor_code)
81
+ Logging::KameleoonLogger.debug("Used default visitor code '{%s}'", default_visitor_code)
82
+ add(visitor_code, cookies)
83
+ visitor_code
84
+ end
85
+
81
86
  def add(visitor_code, cookies)
82
- Logging::KameleoonLogger.debug("CALL: CookieManager.add(visitor_code: '%s', cookies: %s)",
83
- visitor_code, cookies)
87
+ Logging::KameleoonLogger.debug(
88
+ "CALL: CookieManager.add(visitor_code: '%s', cookies: %s)", visitor_code, cookies
89
+ )
84
90
  cookie = {
85
91
  value: visitor_code,
86
92
  expires: Time.now + COOKIE_TTL_SECONDS,
@@ -88,8 +94,9 @@ module Kameleoon
88
94
  domain: @top_level_domain
89
95
  }
90
96
  cookies[VISITOR_CODE_COOKIE] = cookie
91
- Logging::KameleoonLogger.debug("RETURN: CookieManager.add(visitor_code: '%s', cookies: %s)",
92
- visitor_code, cookies)
97
+ Logging::KameleoonLogger.debug(
98
+ "RETURN: CookieManager.add(visitor_code: '%s', cookies: %s)", visitor_code, cookies
99
+ )
93
100
  cookies
94
101
  end
95
102
 
@@ -100,22 +107,90 @@ module Kameleoon
100
107
  cookies
101
108
  end
102
109
 
103
- def get_visitor_code_from_cookies(cookies)
104
- Logging::KameleoonLogger.debug('CALL: CookieManager.get_visitor_code_from_cookies(cookies: %s)', cookies)
105
- cookie = cookies[VISITOR_CODE_COOKIE]
110
+ def process_simulated_variations(cookies, visitor_code)
111
+ raw = get_value_from_cookies(cookies, KAMELEOON_SIMULATION_FF_DATA)
112
+ return if raw.nil?
113
+
114
+ variations = parse_simulated_variations(raw)
115
+ visitor = @visitor_manager.get_or_create_visitor(visitor_code)
116
+ visitor.update_simulated_variations(variations)
117
+ rescue StandardError => e
118
+ Logging::KameleoonLogger.error('Failed to process simulated variations cookie: %s', e)
119
+ end
120
+
121
+ def parse_simulated_variations(raw)
122
+ data_file = @data_manager.data_file
123
+ jobj = JSON.parse(raw)
124
+ return nil unless jobj.is_a?(Hash)
125
+
126
+ variations = []
127
+ jobj.each do |feature_key, value|
128
+ unless feature_key.is_a?(String) && value.is_a?(Hash)
129
+ log_malformed_simulated_variations_cookie(raw)
130
+ next
131
+ end
132
+
133
+ experiment_id = value[EXPERIMENT_ID_KEY]
134
+ if !experiment_id.is_a?(Integer) || experiment_id.negative?
135
+ log_malformed_simulated_variations_cookie(raw)
136
+ next
137
+ end
138
+
139
+ unless experiment_id.zero?
140
+ variation_id = value[VARIATION_ID_KEY]
141
+ if !variation_id.is_a?(Integer) || variation_id.negative?
142
+ log_malformed_simulated_variations_cookie(raw)
143
+ next
144
+ end
145
+ end
146
+ simulated_variation = simulated_variation_from_data_file(
147
+ data_file, feature_key, experiment_id, variation_id
148
+ )
149
+ variations.push(simulated_variation) unless simulated_variation.nil?
150
+ end
151
+ variations
152
+ end
153
+
154
+ def log_malformed_simulated_variations_cookie(raw)
155
+ Logging::KameleoonLogger.error('Malformed simulated variations cookie: %s', raw)
156
+ end
157
+
158
+ def simulated_variation_from_data_file(data_file, feature_key, experiment_id, variation_id)
159
+ feature_flag = data_file.feature_flags[feature_key]
160
+ unless feature_flag
161
+ Logging::KameleoonLogger.error("Simulated feature flag '%s' is not found", feature_key)
162
+ return nil
163
+ end
164
+ return DataManager::ForcedFeatureVariation.new(feature_key, nil, nil, true) if experiment_id.zero?
165
+
166
+ rule = feature_flag.rules.find { |r| r.experiment.id == experiment_id }
167
+ unless rule
168
+ Logging::KameleoonLogger.error('Simulated experiment %d is not found', experiment_id)
169
+ return nil
170
+ end
171
+ var_by_exp = rule.experiment.variations_by_exposition.find { |v| v.variation_id == variation_id }
172
+ unless var_by_exp
173
+ Logging::KameleoonLogger.error('Simulated variation %d is not found', variation_id)
174
+ return nil
175
+ end
176
+ DataManager::ForcedFeatureVariation.new(feature_key, rule, var_by_exp, true)
177
+ end
178
+
179
+ def get_value_from_cookies(cookies, key)
180
+ Logging::KameleoonLogger.debug('CALL: CookieManager.get_value_from_cookies(cookies: %s)', cookies)
181
+ cookie = cookies[key]
106
182
  case cookie
107
183
  when String
108
- visitor_code = cookie
184
+ value = cookie
109
185
  when Hash
110
- visitor_code = cookie[:value]
186
+ value = cookie[:value]
111
187
  end
112
- visitor_code = visitor_code[COOKIE_KEY_JS.size..] if visitor_code&.start_with?(COOKIE_KEY_JS)
113
- visitor_code = nil if visitor_code&.empty?
188
+ value = nil if value&.empty?
114
189
  Logging::KameleoonLogger.debug(
115
- "RETURN: CookieManager.get_visitor_code_from_cookies(cookies: %s) -> (visitor_code: '%s')",
116
- cookies, visitor_code
190
+ "RETURN: CookieManager.get_value_from_cookies(cookies: %s) -> (value: '%s')",
191
+ cookies, value
117
192
  )
118
- visitor_code
193
+ value
119
194
  end
120
195
  end
121
196
  end
@@ -25,11 +25,10 @@ module Kameleoon
25
25
  end
26
26
 
27
27
  private def check_rule(data, rule)
28
- return false if !rule.is_a?(Kameleoon::Configuration::Rule) || rule.experiment_id.nil?
29
-
28
+ return false unless rule.is_a?(Kameleoon::Configuration::Rule)
30
29
  return false if !@condition_rule_id.nil? && (@condition_rule_id != rule.id)
31
30
 
32
- variation = data.variations_storage.get(rule.experiment_id)
31
+ variation = data.variations_storage.get(rule.experiment.id)
33
32
  return false if variation.nil?
34
33
 
35
34
  return true if @condition_variation_key.nil?
@@ -14,13 +14,13 @@ module Kameleoon
14
14
 
15
15
  def check_targeting(visitor_code, campaign_id, exp_ff_rule)
16
16
  Logging::KameleoonLogger.debug(
17
- "CALL: TargetingManager.check_targeting(visitorCode: '%s', campaignId: %s, exp_ff_rule: %s)",
17
+ "CALL: TargetingManager.check_targeting(visitor_code: '%s', campaign_id: %s, exp_ff_rule: %s)",
18
18
  visitor_code, campaign_id, exp_ff_rule
19
19
  )
20
20
  segment = exp_ff_rule.targeting_segment
21
21
  if segment.nil?
22
22
  Logging::KameleoonLogger.debug(
23
- "RETURN: TargetingManager.check_targeting(visitorCode: '%s', campaignId: %s, exp_ff_rule: %s) -> " \
23
+ "RETURN: TargetingManager.check_targeting(visitor_code: '%s', campaign_id: %s, exp_ff_rule: %s) -> " \
24
24
  '(targeting: true)', visitor_code, campaign_id, exp_ff_rule
25
25
  )
26
26
  return true
@@ -29,7 +29,7 @@ module Kameleoon
29
29
  visitor = @visitor_manager.get_visitor(visitor_code)
30
30
  targeting = segment.check_tree(->(type) { get_condition_data(type, visitor, visitor_code, campaign_id) })
31
31
  Logging::KameleoonLogger.debug(
32
- "RETURN: TargetingManager.check_targeting(visitorCode: '%s', campaignId: %s, exp_ff_rule: %s) -> " \
32
+ "RETURN: TargetingManager.check_targeting(visitor_code: '%s', campaign_id: %s, exp_ff_rule: %s) -> " \
33
33
  '(targeting: %s)', visitor_code, campaign_id, exp_ff_rule, targeting
34
34
  )
35
35
  targeting
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'kameleoon/configuration/variation'
4
+
3
5
  module Kameleoon
4
6
  # Module which contains all internal data of SDK
5
7
  module Types
@@ -19,7 +21,7 @@ module Kameleoon
19
21
  end
20
22
 
21
23
  def active?
22
- variation_key != Configuration::VariationType::VARIATION_OFF
24
+ @key != Configuration::VariationType::VARIATION_OFF
23
25
  end
24
26
  end
25
27
  end
@@ -1,7 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'fiber'
4
3
  require 'bigdecimal'
4
+ require 'digest'
5
+ require 'fiber'
5
6
  require 'kameleoon/utils'
6
7
  require 'kameleoon/exceptions'
7
8
  require 'kameleoon/logging/kameleoon_logger'
@@ -32,23 +33,15 @@ module Kameleoon
32
33
  end
33
34
  end
34
35
 
35
- module HashDouble
36
- def self.obtain(visitor_code, respool_times = {}, container_id = '')
37
- obtain_helper(visitor_code, respool_times, container_id, '')
36
+ module Hasher
37
+ def self.obtain(visitor_code, container_id, suffix = nil)
38
+ calculate("#{visitor_code}#{container_id}#{suffix}")
38
39
  end
39
40
 
40
- def self.obtain_rule(visitor_code, container_id = '', suffix = '')
41
- obtain_helper(visitor_code, {}, container_id, suffix)
42
- end
43
-
44
- private
45
-
46
- def self.obtain_helper(visitor_code, respool_times, container_id, suffix)
47
- identifier = visitor_code.to_s
48
- identifier += container_id.to_s
49
- identifier += suffix.to_s
50
- identifier += respool_times.sort.to_h.values.join.to_s if !respool_times.nil? && !respool_times.empty?
51
- (Digest::SHA256.hexdigest(identifier.encode('UTF-8')).to_i(16) / (BigDecimal('2')**BigDecimal('256'))).round(16)
41
+ def self.calculate(string_to_hash)
42
+ parsed_value = Digest::SHA256.hexdigest(string_to_hash.encode('UTF-8')).to_i(16)
43
+ max_value = BigDecimal('2')**BigDecimal('256')
44
+ (parsed_value / max_value).round(16)
52
45
  end
53
46
  end
54
47
 
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Kameleoon
4
- SDK_VERSION = '3.6.1'
4
+ SDK_VERSION = '3.8.0'
5
5
  SDK_NAME = 'RUBY'
6
6
  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: 3.6.1
4
+ version: 3.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kameleoon
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-11-20 00:00:00.000000000 Z
11
+ date: 2025-02-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: em-http-request
@@ -78,6 +78,7 @@ files:
78
78
  - lib/kameleoon/client_readiness.rb
79
79
  - lib/kameleoon/configuration/custom_data_info.rb
80
80
  - lib/kameleoon/configuration/data_file.rb
81
+ - lib/kameleoon/configuration/experiment.rb
81
82
  - lib/kameleoon/configuration/feature_flag.rb
82
83
  - lib/kameleoon/configuration/rule.rb
83
84
  - lib/kameleoon/configuration/settings.rb
@@ -95,6 +96,9 @@ files:
95
96
  - lib/kameleoon/data/manager/assigned_variation.rb
96
97
  - lib/kameleoon/data/manager/data_array_storage.rb
97
98
  - lib/kameleoon/data/manager/data_map_storage.rb
99
+ - lib/kameleoon/data/manager/forced_experiment_variation.rb
100
+ - lib/kameleoon/data/manager/forced_feature_variation.rb
101
+ - lib/kameleoon/data/manager/forced_variation.rb
98
102
  - lib/kameleoon/data/manager/page_view_visit.rb
99
103
  - lib/kameleoon/data/manager/visitor.rb
100
104
  - lib/kameleoon/data/manager/visitor_manager.rb