kameleoon-client-ruby 3.6.0 → 3.7.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: 92e73ac3d81e29ddfc093e8da1a47f42b9970ea6e8717e0752be511b759db17a
4
- data.tar.gz: 710361f25b6338e35bb668bb30f116c931efddd21fdb272fa5367add4d82db22
3
+ metadata.gz: 9e0001c490d29beefab2f33c416affe19f6ccbc05d83e0aaad70be85070986e3
4
+ data.tar.gz: 6959eb4d8f421b9e14e62f3c490d5e1c6f038a25d3e5cb785c1832d7908c291d
5
5
  SHA512:
6
- metadata.gz: 24db6ec9d63c160690506060d703661f77c5b60160fc585117b91460e74185a095f0b52156b268ba18d445797a584c9bd92e824c2bbecf8533d08e09cf89dab6
7
- data.tar.gz: ab2f4e530b33235f7105158414ff77ce52f5b824632e4cae53ed06425f32be6f22719dcae86d7d5660cbd09ea4c090672ee5a237828a6db0e0772e80d65032c5
6
+ metadata.gz: dfe9e75f3563287b6f187a38066a623145ba7ed916d12b9c7ec03dfe7173600ddd16d6a49ca304c4c46ec28794d5372bf29534f36ebaea18fa7eb8c2977dd981
7
+ data.tar.gz: faf2abe7ed35a211fc210a3e72f90a2bf79a8affde8a35c96af2363fabb50091b1069aaea0074458bff1d56156d80e81cd8cb427e3c772b99dc81d318093f7a9
@@ -9,7 +9,7 @@ module Kameleoon
9
9
  module Configuration
10
10
  class DataFile
11
11
  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
12
+ :rule_info_by_exp_id, :variation_by_id, :custom_data_info, :experiment_ids_with_js_css_variable
13
13
 
14
14
  def to_s
15
15
  'DataFile{' \
@@ -74,6 +74,7 @@ module Kameleoon
74
74
  def collect_indices
75
75
  @feature_flag_by_id = {}
76
76
  @rule_by_segment_id = {}
77
+ @rule_info_by_exp_id = {}
77
78
  @variation_by_id = {}
78
79
  @experiment_ids_with_js_css_variable = Set.new
79
80
 
@@ -84,6 +85,7 @@ module Kameleoon
84
85
  has_feature_flag_variable_js_css = feature_flag_variable_js_css?(feature_flag)
85
86
  feature_flag.rules.each do |rule|
86
87
  @rule_by_segment_id[rule.segment_id] = rule
88
+ @rule_info_by_exp_id[rule.experiment_id || 0] = RuleInfo.new(feature_flag, rule)
87
89
  rule.variation_by_exposition.each do |variation|
88
90
  @variation_by_id[variation.variation_id] = variation
89
91
  end
@@ -102,5 +104,14 @@ module Kameleoon
102
104
  end
103
105
  end
104
106
  end
107
+
108
+ class RuleInfo
109
+ attr_reader :feature_flag, :rule
110
+
111
+ def initialize(feature_flag, rule)
112
+ @feature_flag = feature_flag
113
+ @rule = rule
114
+ end
115
+ end
105
116
  end
106
117
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'variation_exposition'
4
+ require 'kameleoon/exceptions'
4
5
  require 'kameleoon/targeting/models'
5
6
 
6
7
  module Kameleoon
@@ -29,7 +30,8 @@ module Kameleoon
29
30
 
30
31
  # Rule is a class for new rules of feature flags
31
32
  class Rule
32
- attr_reader :id, :order, :type, :exposition, :experiment_id, :variation_by_exposition, :respool_time, :segment_id, :first_variation
33
+ attr_reader :id, :order, :type, :exposition, :experiment_id, :variation_by_exposition, :respool_time, :segment_id,
34
+ :first_variation
33
35
  attr_accessor :targeting_segment
34
36
 
35
37
  def self.create_from_array(array)
@@ -63,6 +65,15 @@ module Kameleoon
63
65
  nil
64
66
  end
65
67
 
68
+ def get_variation_by_key(variation_key)
69
+ var_by_exp = variation_by_exposition.find { |v| v.variation_key == variation_key }
70
+ unless var_by_exp
71
+ raise Exception::FeatureVariationNotFound.new(variation_key),
72
+ "#{self} does not contain variation '#{variation_key}'"
73
+ end
74
+ var_by_exp
75
+ end
76
+
66
77
  def get_variation_id_by_key(key)
67
78
  variation_by_exposition.select { |v| v.variation_key == key }.first&.variation_id
68
79
  end
@@ -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 || 0] = 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
 
@@ -611,16 +612,25 @@ module Kameleoon
611
612
  #
612
613
  # DEPRECATED. Please use `get_active_features` instead.
613
614
  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)
615
+ Logging::KameleoonLogger.info(
616
+ '[DEPRECATION] `get_active_feature_list_for_visitor` is deprecated. Please use `get_active_features` instead.'
617
+ )
618
+ Logging::KameleoonLogger.info(
619
+ "CALL: KameleoonClient.get_active_feature_list_for_visitor(visitor_code: '%s')", visitor_code
620
+ )
618
621
  Utils::VisitorCode.validate(visitor_code)
622
+ visitor = @visitor_manager.get_visitor(visitor_code)
619
623
  list_keys = []
620
624
  @data_manager.data_file.feature_flags.each do |feature_key, feature_flag|
621
625
  next unless feature_flag.environment_enabled
622
626
 
623
- variation, rule, = _calculate_variation_key_for_feature(visitor_code, feature_flag)
627
+ forced_variation = visitor&.get_forced_feature_variation(feature_key)
628
+ if forced_variation
629
+ variation = forced_variation.var_by_exp
630
+ rule = forced_variation.rule
631
+ else
632
+ variation, rule = _calculate_variation_key_for_feature(visitor_code, feature_flag)
633
+ end
624
634
  variation_key = _get_variation_key(variation, rule, feature_flag)
625
635
  list_keys.push(feature_key) if variation_key != Kameleoon::Configuration::VariationType::VARIATION_OFF
626
636
  end
@@ -649,12 +659,19 @@ module Kameleoon
649
659
  )
650
660
  Logging::KameleoonLogger.info("CALL: KameleoonClient.get_active_features(visitor_code: '%s')", visitor_code)
651
661
  Utils::VisitorCode.validate(visitor_code)
662
+ visitor = @visitor_manager.get_visitor(visitor_code)
652
663
  map_active_features = {}
653
664
 
654
665
  @data_manager.data_file.feature_flags.each_value do |feature_flag|
655
666
  next unless feature_flag.environment_enabled
656
667
 
657
- var_by_exp, rule = _calculate_variation_key_for_feature(visitor_code, feature_flag)
668
+ forced_variation = visitor&.get_forced_feature_variation(feature_flag.feature_key)
669
+ if forced_variation
670
+ var_by_exp = forced_variation.var_by_exp
671
+ rule = forced_variation.rule
672
+ else
673
+ var_by_exp, rule = _calculate_variation_key_for_feature(visitor_code, feature_flag)
674
+ end
658
675
  variation_key = _get_variation_key(var_by_exp, rule, feature_flag)
659
676
 
660
677
  next if variation_key == Configuration::VariationType::VARIATION_OFF
@@ -701,6 +718,52 @@ module Kameleoon
701
718
  engine_tracking_code
702
719
  end
703
720
 
721
+ ##
722
+ # Sets or resets a forced variation for a visitor in a specific experiment,
723
+ # so the experiment will be evaluated to the variation for the visitor.
724
+ #
725
+ # In order to reset the forced variation set the `variation_key` parameter to `nil`.
726
+ # If the forced variation you want to reset does not exist, the method will have no effect.
727
+ #
728
+ # @param [String] visitor_code The unique visitor code identifying the visitor.
729
+ # @param [Integer] experiment_id The identifier of the experiment you want to set/reset the forced variation for.
730
+ # @param [String | NilClass] variation_key The identifier of the variation you want the experiment to be evaluated
731
+ # to. Set to `nil` to reset the forced variation.
732
+ # @param [Bool] force_targeting If `true`, the visitor will be targeted to the experiment regardless its
733
+ # conditions. Otherwise, the normal targeting logic will be preserved. Optional (defaults to `true`).
734
+ #
735
+ # @raise [Kameleoon::Exception::VisitorCodeInvalid] The provided **visitor code** is invalid.
736
+ # @raise [Kameleoon::Exception::FeatureExperimentNotFound] The provided **experiment id** does not exist in
737
+ # the feature flag.
738
+ # @raise [Kameleoon::Exception::FeatureVariationNotFound] The provided **variation key** does not belong to
739
+ # the experiment.
740
+ def set_forced_variation(visitor_code, experiment_id, variation_key, force_targeting: true)
741
+ Logging::KameleoonLogger.info(
742
+ "CALL: KameleoonClient.set_forced_variation(visitor_code: '%s', experiment_id: %d, variation_key: %s, " \
743
+ 'force_targeting: %s)',
744
+ visitor_code, experiment_id, variation_key.nil? ? 'nil' : "'#{variation_key}'", force_targeting
745
+ )
746
+ Utils::VisitorCode.validate(visitor_code)
747
+ if variation_key.nil?
748
+ visitor = @visitor_manager.get_visitor(visitor_code)
749
+ visitor&.reset_forced_experiment_variation(experiment_id)
750
+ else
751
+ rule_info = @data_manager.data_file.rule_info_by_exp_id[experiment_id]
752
+ if rule_info.nil?
753
+ raise Exception::FeatureExperimentNotFound.new(experiment_id), "Experiment #{experiment_id} is not found"
754
+ end
755
+
756
+ var_by_exp = rule_info.rule.get_variation_by_key(variation_key)
757
+ forced_variation = DataManager::ForcedExperimentVariation.new(rule_info.rule, var_by_exp, force_targeting)
758
+ @visitor_manager.add_data(visitor_code, forced_variation)
759
+ end
760
+ Logging::KameleoonLogger.info(
761
+ "RETURN: KameleoonClient.set_forced_variation(visitor_code: '%s', experiment_id: %d, variation_key: %s, " \
762
+ 'force_targeting: %s)',
763
+ visitor_code, experiment_id, variation_key.nil? ? 'nil' : "'#{variation_key}'", force_targeting
764
+ )
765
+ end
766
+
704
767
  private
705
768
 
706
769
  HYBRID_EXPIRATION_TIME = 5
@@ -853,9 +916,18 @@ module Kameleoon
853
916
  "CALL: KameleoonClient.get_variation_info(visitor_code: '%s', feature_flag: %s, track: %s)",
854
917
  visitor_code, feature_flag, track
855
918
  )
856
- var_by_exp, rule = _calculate_variation_key_for_feature(visitor_code, feature_flag)
919
+ visitor = @visitor_manager.get_visitor(visitor_code)
920
+ forced_variation = visitor&.get_forced_feature_variation(feature_flag.feature_key)
921
+ if forced_variation
922
+ var_by_exp = forced_variation.var_by_exp
923
+ rule = forced_variation.rule
924
+ else
925
+ var_by_exp, rule = _calculate_variation_key_for_feature(visitor_code, feature_flag)
926
+ end
927
+ unless forced_variation&.simulated
928
+ save_variation(visitor_code, rule, var_by_exp, track: track)
929
+ end
857
930
  variation_key = _get_variation_key(var_by_exp, rule, feature_flag)
858
- save_variation(visitor_code, rule, var_by_exp, track: track)
859
931
  Logging::KameleoonLogger.debug(
860
932
  "RETURN: KameleoonClient.get_variation_info(visitor_code: '%s', feature_flag: %s, track: %s)" \
861
933
  ' -> (variation_key: %s, variation_by_exposition: %s, rule: %s)',
@@ -872,9 +944,16 @@ module Kameleoon
872
944
  visitor_code, feature_key
873
945
  )
874
946
  feature_flag = @data_manager.data_file.get_feature_flag(feature_key)
875
- variation, rule = _calculate_variation_key_for_feature(visitor_code, feature_flag)
947
+ visitor = @visitor_manager.get_visitor(visitor_code)
948
+ forced_variation = visitor&.get_forced_feature_variation(feature_flag.feature_key)
949
+ if forced_variation
950
+ variation = forced_variation.var_by_exp
951
+ rule = forced_variation.rule
952
+ else
953
+ variation, rule = _calculate_variation_key_for_feature(visitor_code, feature_flag)
954
+ end
955
+ save_variation(visitor_code, rule, variation) unless forced_variation&.simulated
876
956
  variation_key = _get_variation_key(variation, rule, feature_flag)
877
- save_variation(visitor_code, rule, variation)
878
957
  @tracking_manager.add_visitor_code(visitor_code)
879
958
  Logging::KameleoonLogger.debug(
880
959
  "RETURN: KameleoonClient._get_feature_variation_key(visitor_code: '%s', feature_key: '%s')" \
@@ -910,28 +989,39 @@ module Kameleoon
910
989
  "CALL: KameleoonClient._calculate_variation_key_for_feature(visitor_code: '%s', feature_flag: %s)",
911
990
  visitor_code, feature_flag
912
991
  )
992
+ visitor = @visitor_manager.get_visitor(visitor_code)
913
993
  # no rules -> return default_variation_key
994
+ selected_variation = nil
995
+ selected_rule = nil
914
996
  feature_flag.rules.each do |rule|
997
+ forced_variation = visitor&.get_forced_experiment_variation(rule.experiment_id || 0)
998
+ if forced_variation&.force_targeting
999
+ # Forcing experiment variation in force-targeting mode
1000
+ selected_variation = forced_variation.var_by_exp
1001
+ selected_rule = rule
1002
+ break
1003
+ end
915
1004
  # check if visitor is targeted for rule, else next rule
916
1005
  next unless check_targeting(visitor_code, rule.experiment_id, rule)
917
1006
 
918
- vis = @visitor_manager.get_visitor(visitor_code)
1007
+ unless forced_variation.nil?
1008
+ # Forcing experiment variation in targeting-only mode
1009
+ selected_variation = forced_variation.var_by_exp
1010
+ selected_rule = rule
1011
+ break
1012
+ end
919
1013
  # use mappingIdentifier instead of visitorCode if it was set up
920
- code_for_hash = vis&.mapping_identifier || visitor_code
1014
+ code_for_hash = visitor&.mapping_identifier || visitor_code
921
1015
  # uses for rule exposition
922
1016
  hash_rule = Utils::HashDouble.obtain_rule(code_for_hash, rule.id, rule.respool_time)
923
1017
  Logging::KameleoonLogger.debug("Calculated hash_rule: %s for visitor_code: '%s'", hash_rule, code_for_hash)
924
1018
  # check main expostion for rule with hashRule
925
1019
  if hash_rule <= rule.exposition
926
1020
  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]
1021
+ selected_variation = rule.first_variation
1022
+ selected_rule = rule
1023
+ break
933
1024
  end
934
-
935
1025
  # uses for variation's expositions
936
1026
  hash_variation = Utils::HashDouble.obtain_rule(code_for_hash, rule.experiment_id, rule.respool_time)
937
1027
  Logging::KameleoonLogger.debug(
@@ -940,11 +1030,9 @@ module Kameleoon
940
1030
  # get variation key with new hashVariation
941
1031
  variation = rule.get_variation(hash_variation)
942
1032
  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]
1033
+ selected_variation = variation
1034
+ selected_rule = rule
1035
+ break
948
1036
  end
949
1037
  # if visitor is targeted for targeted rule then break cycle -> return default
950
1038
  elsif rule.targeted_delivery_type?
@@ -953,9 +1041,9 @@ module Kameleoon
953
1041
  end
954
1042
  Logging::KameleoonLogger.debug(
955
1043
  "RETURN: KameleoonClient._calculate_variation_key_for_feature(visitor_code: '%s', feature_flag: %s) " \
956
- '-> (variation: nil, rule: nil)', visitor_code, feature_flag
1044
+ '-> (variation: %s, rule: %s)', visitor_code, feature_flag, selected_variation, selected_rule
957
1045
  )
958
- [nil, nil]
1046
+ [selected_variation, selected_rule]
959
1047
  end
960
1048
 
961
1049
  def _get_variation_key(var_by_exp, rule, feature_flag)
@@ -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.variation_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
@@ -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
@@ -75,6 +75,7 @@ module Kameleoon
75
75
  HTTPS = 'https://'
76
76
  REGEX_DOMAIN = /^(\.?(([a-zA-Z\d][a-zA-Z\d-]*[a-zA-Z\d])|[a-zA-Z\d]))
77
77
  (\.(([a-zA-Z\d][a-zA-Z\d-]*[a-zA-Z\d])|[a-zA-Z\d])){1,126}$/x.freeze
78
+ LOCALHOST = 'localhost'
78
79
 
79
80
  def self.validate_top_level_domain(top_level_domain)
80
81
  return nil if top_level_domain.nil? || top_level_domain.empty?
@@ -91,9 +92,13 @@ module Kameleoon
91
92
  break
92
93
  end
93
94
 
94
- unless REGEX_DOMAIN.match?(top_level_domain)
95
- Logging::KameleoonLogger.error("The top-level domain '%s' is invalid.", top_level_domain)
96
- return nil
95
+ if !REGEX_DOMAIN.match?(top_level_domain) && top_level_domain != LOCALHOST
96
+ Logging::KameleoonLogger.error(
97
+ "The top-level domain '%s' is invalid. The value has been set as provided, but it does not meet " \
98
+ 'the required format for proper SDK functionality. Please check the domain for correctness.',
99
+ top_level_domain
100
+ )
101
+ return top_level_domain
97
102
  end
98
103
 
99
104
  top_level_domain
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Kameleoon
4
- SDK_VERSION = '3.6.0'
4
+ SDK_VERSION = '3.7.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.0
4
+ version: 3.7.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-14 00:00:00.000000000 Z
11
+ date: 2024-12-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: em-http-request
@@ -95,6 +95,9 @@ files:
95
95
  - lib/kameleoon/data/manager/assigned_variation.rb
96
96
  - lib/kameleoon/data/manager/data_array_storage.rb
97
97
  - lib/kameleoon/data/manager/data_map_storage.rb
98
+ - lib/kameleoon/data/manager/forced_experiment_variation.rb
99
+ - lib/kameleoon/data/manager/forced_feature_variation.rb
100
+ - lib/kameleoon/data/manager/forced_variation.rb
98
101
  - lib/kameleoon/data/manager/page_view_visit.rb
99
102
  - lib/kameleoon/data/manager/visitor.rb
100
103
  - lib/kameleoon/data/manager/visitor_manager.rb