kameleoon-client-ruby 3.12.1 → 3.14.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.
Files changed (31) hide show
  1. checksums.yaml +4 -4
  2. data/lib/kameleoon/configuration/custom_data_info.rb +7 -1
  3. data/lib/kameleoon/configuration/data_file.rb +31 -11
  4. data/lib/kameleoon/configuration/feature_flag.rb +8 -4
  5. data/lib/kameleoon/configuration/rule.rb +5 -6
  6. data/lib/kameleoon/data/cookie.rb +0 -1
  7. data/lib/kameleoon/data/data.rb +1 -0
  8. data/lib/kameleoon/data/device.rb +1 -1
  9. data/lib/kameleoon/data/manager/visitor.rb +16 -4
  10. data/lib/kameleoon/data/visitor_visits.rb +64 -11
  11. data/lib/kameleoon/kameleoon_client.rb +37 -26
  12. data/lib/kameleoon/managers/remote_data/remote_data_manager.rb +3 -3
  13. data/lib/kameleoon/managers/remote_data/remote_visitor_data.rb +20 -14
  14. data/lib/kameleoon/network/content_type.rb +1 -0
  15. data/lib/kameleoon/network/fetched_configuration.rb +14 -0
  16. data/lib/kameleoon/network/net_provider.rb +4 -3
  17. data/lib/kameleoon/network/network_manager.rb +25 -11
  18. data/lib/kameleoon/network/request.rb +1 -1
  19. data/lib/kameleoon/network/response.rb +5 -4
  20. data/lib/kameleoon/network/url_provider.rb +2 -2
  21. data/lib/kameleoon/targeting/condition.rb +0 -1
  22. data/lib/kameleoon/targeting/condition_factory.rb +0 -3
  23. data/lib/kameleoon/targeting/conditions/time_elapsed_since_visit_condition.rb +6 -5
  24. data/lib/kameleoon/targeting/conditions/visit_number_today_condition.rb +16 -4
  25. data/lib/kameleoon/targeting/conditions/visit_number_total_condition.rb +4 -3
  26. data/lib/kameleoon/targeting/conditions/visitor_new_return_condition.rb +5 -4
  27. data/lib/kameleoon/targeting/models.rb +4 -3
  28. data/lib/kameleoon/targeting/targeting_manager.rb +3 -4
  29. data/lib/kameleoon/version.rb +1 -1
  30. metadata +3 -3
  31. data/lib/kameleoon/targeting/conditions/exclusive_feature_flag_condition.rb +0 -27
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 02e0aa66de89608c98d6dd7aee47b01532554c8b1e721061d6fd9cf30c486702
4
- data.tar.gz: 637b3bbee24635ca8e021d86fa9ea94030f58da8d3afe181f67133b227676839
3
+ metadata.gz: b5566ec9ff02ebb63ff3ee337f4c26fc0c229f2de8df0893fe85dc3f83fbc0ce
4
+ data.tar.gz: fcd11bacbaead7a0c357a0fd4828fb6c4b7347ebf1fab728f4e00882b0224dc7
5
5
  SHA512:
6
- metadata.gz: b3e214eba6a1481863758cb794ce08523e759c25fe1200c535ea82e4173c2a51a31e42f7d3ee10cd09adcb4c19cc88f968c337646c11875ecf30f7fb542e5fc7
7
- data.tar.gz: 74d531f7d1d3ea7db51a0e336eef0e7f639dd95e50e3a769f8e5a226c0266cd5e1fdf631c5a4c9fe464131ef45d6c42238b430b4d4d872331cd6940321950778
6
+ metadata.gz: 78418ebd561b9d1fe2639751d0dae68a875a4a76f4782ac079fe2f3f3c6e2b7466c41cb762e978b4895973a97ddf4940b10171d36c2695a565ed2e1b66445f53
7
+ data.tar.gz: b91b577c23c13547d78c09c98a2c39bb788a314b280b50c57bf1cc25891dd74747127aec2bc14b0a5a4908a2cde2d0e2089ec13c6f5200087b7c52bd80b7a68f
@@ -13,16 +13,18 @@ module Kameleoon
13
13
  def initialize(hashes)
14
14
  @local_only = Set[]
15
15
  @visitor_scope = Set[]
16
+ @custom_data_index_by_id = {}
16
17
  unless hashes.nil?
17
18
  for hash in hashes
18
19
  index = hash['index']
19
20
  @local_only.add(index) if hash['localOnly']
20
21
  @visitor_scope.add(index) if hash['scope'] == SCOPE_VISITOR
22
+ id = hash['id']
23
+ @custom_data_index_by_id[id] = index if id && index
21
24
  if hash['isMappingIdentifier']
22
25
  unless @mapping_identifier_index.nil?
23
26
  Logging::KameleoonLogger.warning('More than one mapping identifier is set. Undefined behavior ' \
24
27
  'may occur on cross-device reconciliation.')
25
-
26
28
  end
27
29
  @mapping_identifier_index = index
28
30
  end
@@ -42,6 +44,10 @@ module Kameleoon
42
44
  @visitor_scope.include?(index)
43
45
  end
44
46
 
47
+ def get_custom_data_index_by_id(custom_data_id)
48
+ @custom_data_index_by_id[custom_data_id]
49
+ end
50
+
45
51
  def self.mapping_identifier?(custom_data_info, custom_data)
46
52
  !custom_data_info.nil? && (custom_data.id == custom_data_info.mapping_identifier_index) && \
47
53
  !(custom_data.values.empty? || custom_data.values[0].empty?)
@@ -6,32 +6,41 @@ require 'kameleoon/configuration/feature_flag'
6
6
  require 'kameleoon/configuration/me_group'
7
7
  require 'kameleoon/configuration/settings'
8
8
  require 'kameleoon/logging/kameleoon_logger'
9
+ require 'kameleoon/targeting/models'
9
10
 
10
11
  module Kameleoon
11
12
  module Configuration
12
13
  class DataFile
13
- attr_reader :settings, :feature_flags, :me_groups, :has_any_targeted_delivery_rule, :feature_flag_by_id,
14
- :rule_by_segment_id, :rule_info_by_exp_id, :variation_by_id, :custom_data_info,
14
+ attr_reader :last_modified, :settings, :feature_flags, :me_groups, :has_any_targeted_delivery_rule,
15
+ :feature_flag_by_id, :rule_by_segment_id, :rule_info_by_exp_id, :variation_by_id, :custom_data_info,
15
16
  :experiment_ids_with_js_css_variable, :holdout
16
17
 
17
18
  def to_s
18
19
  'DataFile{' \
19
20
  "environment:#{@environment}," \
21
+ "last_modified:#{@last_modified}," \
20
22
  "feature_flags:#{@feature_flags.size}," \
21
23
  "settings:#{@settings}" \
22
24
  '}'
23
25
  end
24
26
 
25
- def initialize(environment, configuration = nil)
26
- Logging::KameleoonLogger.debug('CALL: DataFile.new(environment: %s)', environment)
27
+ def initialize(environment, configuration = nil, last_modified = nil)
28
+ Logging::KameleoonLogger.debug(
29
+ "CALL: DataFile.new(environment: '%s', configuration, last_modified: '%s')",
30
+ environment, last_modified
31
+ )
27
32
  @environment = environment
33
+ @last_modified = last_modified
28
34
  if configuration.nil?
29
35
  init_default
30
36
  else
31
37
  init(configuration)
32
38
  end
33
39
  collect_indices
34
- Logging::KameleoonLogger.debug('RETURN: DataFile.new(environment: %s)', environment)
40
+ Logging::KameleoonLogger.debug(
41
+ "RETURN: DataFile.new(environment: '%s', configuration, last_modified: '%s')",
42
+ environment, last_modified
43
+ )
35
44
  end
36
45
 
37
46
  def get_feature_flag(feature_key)
@@ -61,18 +70,29 @@ module Kameleoon
61
70
  def init(configuration)
62
71
  Logging::KameleoonLogger.debug('CALL: DataFile.init(configuration: %s)', configuration)
63
72
  @settings = Settings.new(configuration['configuration'])
64
- @feature_flags = {}
65
- configuration['featureFlags'].each do |raw|
66
- ff = FeatureFlag.new(raw)
67
- @feature_flags[ff.feature_key] = ff
68
- end
73
+ segments = parse_segments(configuration)
74
+ @custom_data_info = CustomDataInfo.new(configuration['customData'])
75
+ @feature_flags = parse_feature_flags(configuration, segments, @custom_data_info)
69
76
  @me_groups = make_me_groups(@feature_flags)
70
77
  @has_any_targeted_delivery_rule = any_targeted_delivery_rule?
71
- @custom_data_info = CustomDataInfo.new(configuration['customData'])
72
78
  @holdout = Experiment.from_json(configuration['holdout']) if configuration.include?('holdout')
73
79
  Logging::KameleoonLogger.debug('RETURN: DataFile.init(configuration: %s)', configuration)
74
80
  end
75
81
 
82
+ def parse_segments(configuration)
83
+ configuration['segments'].to_h do |raw_seg|
84
+ seg = Targeting::Segment.new(raw_seg)
85
+ [seg.id, seg]
86
+ end
87
+ end
88
+
89
+ def parse_feature_flags(configuration, segments, cdi)
90
+ configuration['featureFlags'].to_h do |raw_ff|
91
+ ff = FeatureFlag.new(raw_ff, segments, cdi)
92
+ [ff.feature_key, ff]
93
+ end
94
+ end
95
+
76
96
  def any_targeted_delivery_rule?
77
97
  @feature_flags.any? { |_, ff| ff.environment_enabled && ff.rules.any?(&:targeted_delivery_type?) }
78
98
  end
@@ -8,7 +8,8 @@ module Kameleoon
8
8
  module Configuration
9
9
  # Class for manage all feature flags with rules
10
10
  class FeatureFlag
11
- attr_accessor :id, :feature_key, :variations, :default_variation_key, :me_group_name, :environment_enabled, :rules
11
+ attr_reader :id, :feature_key, :variations, :default_variation_key, :me_group_name, :environment_enabled, :rules,
12
+ :bucketing_custom_data_index
12
13
 
13
14
  def self.create_from_array(array)
14
15
  array&.map { |it| FeatureFlag.new(it) }
@@ -21,18 +22,21 @@ module Kameleoon
21
22
  "environment_enabled:#{@environment_enabled}," \
22
23
  "default_variation_key:'#{@default_variation_key}'," \
23
24
  "me_group_name:'#{@me_group_name}'," \
24
- "rules:#{@rules.size}" \
25
+ "rules:#{@rules.size}," \
26
+ "bucketing_custom_data_index:#{@bucketing_custom_data_index}" \
25
27
  '}'
26
28
  end
27
29
 
28
- def initialize(hash)
30
+ def initialize(hash, segments, cdi)
29
31
  @id = hash['id']
30
32
  @feature_key = hash['featureKey']
31
33
  @variations = Variation.create_from_array(hash['variations'])
32
34
  @default_variation_key = hash['defaultVariationKey']
33
35
  @me_group_name = hash['mutuallyExclusiveGroup']
34
36
  @environment_enabled = hash['environmentEnabled']
35
- @rules = Rule.create_from_array(hash['rules'])
37
+ @rules = Rule.create_from_array(hash['rules'], segments)
38
+ bucketing_cd_id = hash['bucketingCustomDataId']
39
+ @bucketing_custom_data_index = bucketing_cd_id ? cdi.get_custom_data_index_by_id(bucketing_cd_id) : nil
36
40
  end
37
41
 
38
42
  def get_variation_by_key(key)
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'experiment'
4
- require 'kameleoon/targeting/models'
5
4
 
6
5
  module Kameleoon
7
6
  # Module which contains all internal data of SDK
@@ -32,22 +31,22 @@ module Kameleoon
32
31
  attr_reader :id, :order, :type, :exposition, :experiment, :respool_time, :segment_id
33
32
  attr_accessor :targeting_segment
34
33
 
35
- def self.create_from_array(array)
36
- array&.map { |it| Rule.new(it) }
34
+ def self.create_from_array(array, segments)
35
+ array&.map { |it| Rule.new(it, segments) }
37
36
  end
38
37
 
39
38
  def to_s
40
39
  "Rule{id:#{@id}}"
41
40
  end
42
41
 
43
- def initialize(hash)
42
+ def initialize(hash, segments)
44
43
  @id = hash['id']
45
44
  @order = hash['order']
46
45
  @type = RuleType.from_literal(hash['type'])
47
46
  @exposition = hash['exposition']
48
47
  @respool_time = hash['respoolTime']
49
- @targeting_segment = Kameleoon::Targeting::Segment.new((hash['segment'])) if hash['segment']
50
- @segment_id = @targeting_segment != nil ? targeting_segment.id : -1
48
+ @segment_id = hash['segmentId'] || -1
49
+ @targeting_segment = segments[@segment_id] if @segment_id != -1
51
50
  @experiment = Experiment.from_json(hash)
52
51
  end
53
52
 
@@ -15,4 +15,3 @@ module Kameleoon
15
15
  end
16
16
  end
17
17
  end
18
-
@@ -18,6 +18,7 @@ module Kameleoon
18
18
  FORCED_FEATURE_VARIATION = 'FORCED_FEATURE_VARIATION'
19
19
  OPERATING_SYSTEM = 'OPERATING_SYSTEM'
20
20
  GEOLOCATION = 'GEOLOCATION'
21
+ VISITOR_VISITS = 'VISITOR_VISITS'
21
22
  end
22
23
 
23
24
  module DataState
@@ -30,7 +30,7 @@ module Kameleoon
30
30
  deviceType: @device_type,
31
31
  nonce: nonce
32
32
  }
33
- Kameleoon::Network::UriHelper.encode_query(params)
33
+ Network::UriHelper.encode_query(params)
34
34
  end
35
35
  end
36
36
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'concurrent'
4
+ require 'time'
4
5
  require 'kameleoon/data/browser'
5
6
  require 'kameleoon/data/cbscores'
6
7
  require 'kameleoon/data/conversion'
@@ -42,6 +43,10 @@ module Kameleoon
42
43
  update_last_activity_time
43
44
  end
44
45
 
46
+ def time_started
47
+ @data.time_started
48
+ end
49
+
45
50
  def last_activity_time
46
51
  @data.last_activity_time
47
52
  end
@@ -263,7 +268,7 @@ module Kameleoon
263
268
  when CBScores
264
269
  @data.set_cbscores(data, overwrite)
265
270
  when VisitorVisits
266
- @data.visitor_visits = data
271
+ @data.set_visitor_visits(data, overwrite)
267
272
  when UniqueIdentifier
268
273
  @is_unique_identifier = data.value
269
274
  else
@@ -286,12 +291,13 @@ module Kameleoon
286
291
  end
287
292
 
288
293
  class VisitorData
289
- attr_reader :mutex, :device, :browser, :geolocation, :operating_system
290
- attr_accessor :last_activity_time, :legal_consent, :user_agent, :cookie, :kcs_heat, :cbscores, :visitor_visits,
294
+ attr_reader :time_started, :mutex, :device, :browser, :geolocation, :operating_system, :visitor_visits
295
+ attr_accessor :last_activity_time, :legal_consent, :user_agent, :cookie, :kcs_heat, :cbscores,
291
296
  :mapping_identifier, :forced_variations, :simulated_variations
292
297
 
293
298
  def initialize
294
299
  Logging::KameleoonLogger.debug('CALL: VisitorData.new')
300
+ @time_started = (Time.now.to_f * 1000).to_i
295
301
  @mutex = Concurrent::ReadWriteLock.new
296
302
  @legal_consent = false
297
303
  Logging::KameleoonLogger.debug('RETURN: VisitorData.new')
@@ -318,6 +324,7 @@ module Kameleoon
318
324
  blk.call(@browser) unless @browser.nil?
319
325
  blk.call(@operating_system) unless @operating_system.nil?
320
326
  blk.call(@geolocation) unless @geolocation.nil?
327
+ blk.call(@visitor_visits) unless @visitor_visits.nil?
321
328
  @mutex.with_read_lock do
322
329
  @custom_data_map&.each { |_, cd| blk.call(cd) }
323
330
  @page_view_visits&.each { |_, pvv| blk.call(pvv.page_view) }
@@ -332,8 +339,9 @@ module Kameleoon
332
339
  @mutex.with_read_lock do
333
340
  count += 1 unless @device.nil?
334
341
  count += 1 unless @browser.nil?
335
- count += 1 unless @geolocation.nil?
336
342
  count += 1 unless @operating_system.nil?
343
+ count += 1 unless @geolocation.nil?
344
+ count += 1 unless @visitor_visits.nil?
337
345
  count += @custom_data_map.size unless @custom_data_map.nil?
338
346
  count += @page_view_visits.size unless @page_view_visits.nil?
339
347
  count += @conversions.size unless @conversions.nil?
@@ -433,6 +441,10 @@ module Kameleoon
433
441
  @cbscores = cbs if overwrite || @cbscores.nil?
434
442
  end
435
443
 
444
+ def set_visitor_visits(visitor_visits, overwrite)
445
+ @visitor_visits = visitor_visits.localize(@time_started) if overwrite || @visitor_visits.nil?
446
+ end
447
+
436
448
  def add_forced_feature_variation(forced_variation)
437
449
  @simulated_variations ||= {}
438
450
  @simulated_variations[forced_variation.feature_key] = forced_variation
@@ -1,24 +1,77 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'kameleoon/data/data'
4
+ require 'kameleoon/network/uri_helper'
5
+
3
6
  module Kameleoon
4
- class VisitorVisits
5
- attr_reader :previous_visit_timestamps
7
+ class VisitorVisits < DuplicationUnsafeData
8
+ attr_reader :visit_number, :prev_visits, :time_started, :time_since_previous_visit
6
9
 
7
- def to_s
8
- "VisitorVisits{previous_visit_timestamps:#{@previous_visit_timestamps}}"
10
+ def initialize(prev_visits, visit_number = 0, time_started: 0, time_since_previous_visit: 0)
11
+ super(DataType::VISITOR_VISITS)
12
+ @visit_number = visit_number >= prev_visits.size ? visit_number : prev_visits.size
13
+ @prev_visits = prev_visits
14
+ @prev_visits.freeze
15
+ @time_started = time_started
16
+ @time_since_previous_visit = time_since_previous_visit
17
+ end
18
+
19
+ def localize(time_started)
20
+ time_since_previous_visit = 0
21
+ @prev_visits.each do |visit|
22
+ time_delta = time_started - visit.time_last_activity
23
+ unless time_delta.negative?
24
+ time_since_previous_visit = time_delta
25
+ break
26
+ end
27
+ end
28
+ VisitorVisits.new(
29
+ @prev_visits,
30
+ @visit_number,
31
+ time_started: time_started,
32
+ time_since_previous_visit: time_since_previous_visit
33
+ )
34
+ end
35
+
36
+ def obtain_full_post_text_line
37
+ params = {
38
+ eventType: 'staticData',
39
+ visitNumber: @visit_number,
40
+ timeSincePreviousVisit: @time_since_previous_visit,
41
+ nonce: nonce
42
+ }
43
+ Network::UriHelper.encode_query(params)
9
44
  end
10
45
 
11
- def initialize(previous_visit_timestamps = [])
12
- @previous_visit_timestamps = previous_visit_timestamps
13
- @previous_visit_timestamps.freeze
46
+ def to_s
47
+ 'VisitorVisits{' \
48
+ "visit_number:#{@visit_number}," \
49
+ "prev_visits:#{@prev_visits.map(&:to_s)}," \
50
+ "time_started:#{@time_started}," \
51
+ "time_since_previous_visit:#{@time_since_previous_visit}" \
52
+ '}'
14
53
  end
15
54
 
16
- def self.get_previous_visit_timestamps(visitor_visits)
17
- visitor_visits.is_a?(VisitorVisits) ? visitor_visits.previous_visit_timestamps : []
55
+ def ==(other)
56
+ other.is_a?(VisitorVisits) && (@visit_number == other.visit_number) && (@prev_visits == other.prev_visits) &&
57
+ (@time_started == other.time_started) && (@time_since_previous_visit == other.time_since_previous_visit)
18
58
  end
19
59
 
20
- def self.visitor_visits?(obj)
21
- obj.nil? || obj.is_a?(VisitorVisits)
60
+ class Visit
61
+ attr_reader :time_started, :time_last_activity
62
+
63
+ def initialize(time_started, time_last_activity = nil)
64
+ @time_started = time_started
65
+ @time_last_activity = time_last_activity || time_started
66
+ end
67
+
68
+ def to_s
69
+ "Visit{time_started:#{@time_started},time_last_activity:#{time_last_activity}}"
70
+ end
71
+
72
+ def ==(other)
73
+ other.is_a?(Visit) && (@time_started == other.time_started) && (@time_last_activity == other.time_last_activity)
74
+ end
22
75
  end
23
76
  end
24
77
  end
@@ -852,14 +852,17 @@ module Kameleoon
852
852
 
853
853
  def obtain_configuration(time_stamp = nil)
854
854
  Logging::KameleoonLogger.info('Fetching configuration from Client-Config service')
855
- response = @network_manager.fetch_configuration(time_stamp)
855
+ last_modified = @data_manager.data_file.last_modified
856
+ response = @network_manager.fetch_configuration(time_stamp, if_modified_since: last_modified)
856
857
  return false unless response
857
858
 
858
- configuration = JSON.parse(response)
859
- data_file = Configuration::DataFile.new(@config.environment, configuration)
860
- apply_new_configuration(data_file)
861
- call_update_handler_if_needed(!time_stamp.nil?)
862
- Logging::KameleoonLogger.info('Feature flags are fetched: %s', response.inspect)
859
+ if response.configuration
860
+ configuration = JSON.parse(response.configuration)
861
+ data_file = Configuration::DataFile.new(@config.environment, configuration, response.last_modified)
862
+ apply_new_configuration(data_file)
863
+ call_update_handler_if_needed(!time_stamp.nil?)
864
+ Logging::KameleoonLogger.info('Feature flags are fetched: %s', response.inspect)
865
+ end
863
866
  true
864
867
  end
865
868
 
@@ -871,7 +874,7 @@ module Kameleoon
871
874
  end
872
875
 
873
876
  ##
874
- # Call the handler when configuraiton was updated with new time stamp.
877
+ # Call the handler when configuration was updated with new time stamp.
875
878
  #
876
879
  # @param need_call [Bool] Indicates if we need to call handler or not.
877
880
  def call_update_handler_if_needed(need_call)
@@ -930,7 +933,7 @@ module Kameleoon
930
933
  forced_variation = visitor&.get_forced_feature_variation(feature_flag.feature_key)
931
934
  if forced_variation
932
935
  eval_exp = EvaluatedExperiment.from_forced_variation(forced_variation)
933
- elsif visitor_not_in_holdout?(visitor, visitor_code, track, save) && \
936
+ elsif visitor_not_in_holdout?(visitor, visitor_code, track, save, feature_flag.bucketing_custom_data_index) && \
934
937
  ff_unrestricted_by_me_group?(visitor, visitor_code, feature_flag)
935
938
  eval_exp = _calculate_variation_key_for_feature(visitor_code, feature_flag)
936
939
  end
@@ -952,7 +955,7 @@ module Kameleoon
952
955
  unrestricted = true
953
956
  me_group = @data_manager.data_file.me_groups[feature_flag.me_group_name]
954
957
  if me_group
955
- code_for_hash = get_code_for_hash(visitor, visitor_code)
958
+ code_for_hash = get_code_for_hash(visitor, visitor_code, feature_flag.bucketing_custom_data_index)
956
959
  me_group_hash = Utils::Hasher.obtain_hash_for_me_group(code_for_hash, feature_flag.me_group_name)
957
960
  Logging::KameleoonLogger.debug(
958
961
  "Calculated ME group hash %s for code: '%s', meGroup: '%s'",
@@ -967,17 +970,17 @@ module Kameleoon
967
970
  unrestricted
968
971
  end
969
972
 
970
- def visitor_not_in_holdout?(visitor, visitor_code, track, save)
973
+ def visitor_not_in_holdout?(visitor, visitor_code, track, save, bucketing_custom_data_index)
971
974
  holdout = @data_manager.data_file.holdout
972
975
  return true if holdout.nil?
973
976
 
974
977
  in_holdout_variation_key = 'in-holdout'
975
978
  Logging::KameleoonLogger.debug(
976
- "CALL: KameleoonClient.visitor_not_in_holdout?(visitor, visitor_code: '%s', track: %s, save: %s)",
977
- visitor_code, track, save
979
+ "CALL: KameleoonClient.visitor_not_in_holdout?(visitor, visitor_code: '%s', track: %s, save: %s, " \
980
+ 'bucketing_custom_data_index: %s)', visitor_code, track, save, bucketing_custom_data_index
978
981
  )
979
982
  is_not_in_holdout = true
980
- code_for_hash = get_code_for_hash(visitor, visitor_code)
983
+ code_for_hash = get_code_for_hash(visitor, visitor_code, bucketing_custom_data_index)
981
984
  variation_hash = Utils::Hasher.obtain(code_for_hash, holdout.id)
982
985
  Logging::KameleoonLogger.debug("Calculated holdout hash %s for code '%s'", variation_hash, code_for_hash)
983
986
  var_by_exp = holdout.get_variation(variation_hash)
@@ -989,9 +992,9 @@ module Kameleoon
989
992
  end
990
993
  end
991
994
  Logging::KameleoonLogger.debug(
992
- "RETURN: KameleoonClient.visitor_not_in_holdout?(visitor, visitor_code: '%s', track: %s, save: %s)" \
993
- ' -> (is_not_in_holdout: %s)',
994
- visitor_code, track, save, is_not_in_holdout
995
+ "RETURN: KameleoonClient.visitor_not_in_holdout?(visitor, visitor_code: '%s', track: %s, save: %s, " \
996
+ 'bucketing_custom_data_index: %s) -> (is_not_in_holdout: %s)',
997
+ visitor_code, track, save, bucketing_custom_data_index, is_not_in_holdout
995
998
  )
996
999
  is_not_in_holdout
997
1000
  end
@@ -1035,16 +1038,24 @@ module Kameleoon
1035
1038
  )
1036
1039
  end
1037
1040
 
1038
- def get_code_for_hash(visitor, visitor_code)
1039
- # use mappingIdentifier instead of visitor_code if it was set up
1040
- visitor&.mapping_identifier || visitor_code
1041
+ def get_code_for_hash(visitor, visitor_code, bucketing_custom_data_index)
1042
+ return visitor_code if visitor.nil?
1043
+
1044
+ # 1. Try to use the bucketing custom data's value if bucketingCustomDataId is defined
1045
+ if bucketing_custom_data_index
1046
+ bucketing_custom_data = visitor.custom_data.get(bucketing_custom_data_index)
1047
+ return bucketing_custom_data.values[0] if bucketing_custom_data && !bucketing_custom_data.values.empty?
1048
+ end
1049
+ # 2. Use mappingIdentifier instead of visitorCode if it was set up
1050
+ visitor.mapping_identifier || visitor_code
1041
1051
  end
1042
1052
 
1043
- def evaluate_cbscores(visitor, visitor_code, rule)
1053
+ def evaluate_cbscores(visitor, visitor_code, rule, bucketing_custom_data_index)
1044
1054
  return nil if visitor&.cbscores.nil?
1045
1055
 
1046
1056
  Logging::KameleoonLogger.debug(
1047
- "CALL: KameleoonClient.evaluate_cbscores(visitor, visitor_code: '%s', rule: %s)", visitor_code, rule
1057
+ "CALL: KameleoonClient.evaluate_cbscores(visitor, visitor_code: '%s', rule: %s, " \
1058
+ 'bucketing_custom_data_index: %s)', visitor_code, rule, bucketing_custom_data_index
1048
1059
  )
1049
1060
  eval_exp = nil
1050
1061
  var_id_group_by_scores = visitor.cbscores.values[rule.experiment.id]
@@ -1059,7 +1070,7 @@ module Kameleoon
1059
1070
  if (var_by_exp_in_cbs&.size || 0).positive?
1060
1071
  size = var_by_exp_in_cbs.size
1061
1072
  if size > 1
1062
- code_for_hash = get_code_for_hash(visitor, visitor_code)
1073
+ code_for_hash = get_code_for_hash(visitor, visitor_code, bucketing_custom_data_index)
1063
1074
  variation_hash = Utils::Hasher.obtain(code_for_hash, rule.experiment.id, rule.respool_time)
1064
1075
  Logging::KameleoonLogger.debug("Calculated CBS hash %s for code '%s'", variation_hash, code_for_hash)
1065
1076
  idx = [(variation_hash * size).to_i, size - 1].min
@@ -1070,8 +1081,8 @@ module Kameleoon
1070
1081
  end
1071
1082
  end
1072
1083
  Logging::KameleoonLogger.debug(
1073
- "RETURN: KameleoonClient.evaluate_cbscores(visitor, visitor_code: '%s', rule: %s) -> (eval_exp: %s)",
1074
- visitor_code, rule, eval_exp
1084
+ "RETURN: KameleoonClient.evaluate_cbscores(visitor, visitor_code: '%s', rule: %s," \
1085
+ 'bucketing_custom_data_index: %s) -> (eval_exp: %s)', visitor_code, rule, bucketing_custom_data_index, eval_exp
1075
1086
  )
1076
1087
  eval_exp
1077
1088
  end
@@ -1084,7 +1095,7 @@ module Kameleoon
1084
1095
  visitor_code, feature_flag
1085
1096
  )
1086
1097
  visitor = @visitor_manager.get_visitor(visitor_code)
1087
- code_for_hash = get_code_for_hash(visitor, visitor_code)
1098
+ code_for_hash = get_code_for_hash(visitor, visitor_code, feature_flag.bucketing_custom_data_index)
1088
1099
  # no rules -> return default_variation_key
1089
1100
  eval_exp = nil
1090
1101
  feature_flag.rules.each do |rule|
@@ -1107,7 +1118,7 @@ module Kameleoon
1107
1118
  Logging::KameleoonLogger.debug("Calculated rule hash %s for code '%s'", hash_rule, code_for_hash)
1108
1119
  # check main expostion for rule with hashRule
1109
1120
  if hash_rule <= rule.exposition
1110
- eval_exp = evaluate_cbscores(visitor, visitor_code, rule)
1121
+ eval_exp = evaluate_cbscores(visitor, visitor_code, rule, feature_flag.bucketing_custom_data_index)
1111
1122
  break unless eval_exp.nil?
1112
1123
 
1113
1124
  if rule.targeted_delivery_type?
@@ -42,7 +42,7 @@ module Kameleoon
42
42
  filter = Types::RemoteVisitorDataFilter.new unless filter.is_a?(Types::RemoteVisitorDataFilter)
43
43
  is_unique_identifier = @visitor_manger.get_visitor(visitor_code)&.is_unique_identifier || false
44
44
  response = @network_manager.get_remote_visitor_data(visitor_code, filter, is_unique_identifier, timeout)
45
- remote_visitor_data = parse_custom_data_array(visitor_code, response)
45
+ remote_visitor_data = parse_custom_data_array(visitor_code, response, filter)
46
46
  remote_visitor_data.mark_data_as_sent(@data_manager.data_file.custom_data_info)
47
47
  data_to_add = remote_visitor_data.collect_data_to_add
48
48
  if add_data && !data_to_add.empty?
@@ -66,8 +66,8 @@ module Kameleoon
66
66
 
67
67
  ##
68
68
  # helper method used by `get_remote_visitor_data`
69
- def parse_custom_data_array(visitor_code, response)
70
- RemoteVisitorData.new(JSON.parse(response))
69
+ def parse_custom_data_array(visitor_code, response, filter)
70
+ RemoteVisitorData.new(JSON.parse(response), filter)
71
71
  rescue StandardError => e
72
72
  Logging::KameleoonLogger.error("Parsing of remote visitor data of '#{visitor_code}' failed: #{e}")
73
73
  raise
@@ -18,21 +18,23 @@ module Kameleoon
18
18
  module RemoteData
19
19
  class RemoteVisitorData
20
20
  attr_reader :custom_data_dict, :page_view_visits, :conversions, :experiments, :personalizations, :device,
21
- :browser, :operating_system, :geolocation, :previous_visitor_visits, :kcs_heat, :cbs, :visitor_code
21
+ :browser, :operating_system, :geolocation, :visitor_visits, :kcs_heat, :cbs, :visitor_code
22
22
 
23
- def initialize(hash)
23
+ def initialize(hash, filter)
24
+ @filter = filter
25
+ @visit_number = 0
24
26
  current_visit = hash['currentVisit']
25
- parse_visit(current_visit) unless current_visit.nil?
27
+ parse_visit(current_visit, false) unless current_visit.nil?
26
28
  previous_visits = hash['previousVisits']
27
29
  previous_visits = [] if previous_visits.nil?
28
30
 
29
31
  if previous_visits.size.positive?
30
- times_started = []
32
+ prev_visits = []
31
33
  previous_visits.each do |visit|
32
- times_started.push(visit['timeStarted'])
33
- parse_visit(visit)
34
+ prev_visits.push(VisitorVisits::Visit.new(visit['timeStarted'] || 0, visit['timeLastEvent']))
35
+ parse_visit(visit, true)
34
36
  end
35
- @previous_visitor_visits = VisitorVisits.new(times_started)
37
+ @visitor_visits = VisitorVisits.new(prev_visits, @visit_number)
36
38
  end
37
39
  @kcs_heat = parse_kcs_heat(hash['kcs'])
38
40
  @cbs = parse_cbscores(hash['cbs'])
@@ -41,7 +43,7 @@ module Kameleoon
41
43
  def collect_data_to_add
42
44
  data_to_add = []
43
45
  data_to_add.concat(@custom_data_dict.values) unless @custom_data_dict.nil?
44
- data_to_add.push(@previous_visitor_visits) unless @previous_visitor_visits.nil?
46
+ data_to_add.push(@visitor_visits) unless @visitor_visits.nil?
45
47
  data_to_add.push(@kcs_heat) unless @kcs_heat.nil?
46
48
  data_to_add.push(@cbs) unless @cbs.nil?
47
49
  data_to_add.concat(@page_view_visits.values) unless @page_view_visits.nil?
@@ -72,7 +74,7 @@ module Kameleoon
72
74
 
73
75
  private
74
76
 
75
- def parse_visit(hash)
77
+ def parse_visit(hash, is_prev_visit)
76
78
  @visitor_code = hash['visitorCode'] if @visitor_code.nil?
77
79
  custom_data_events = hash['customDataEvents']
78
80
  parse_custom_data(custom_data_events) if !custom_data_events.nil? && custom_data_events.size.positive?
@@ -86,7 +88,7 @@ module Kameleoon
86
88
  @geolocation = parse_geolocation(geolocation_events) if @geolocation.nil? && !geolocation_events.nil? && \
87
89
  geolocation_events.size.positive?
88
90
  static_data_events = hash['staticDataEvent']
89
- parse_static_data(static_data_events) unless static_data_events.nil?
91
+ parse_static_data(static_data_events, is_prev_visit) unless static_data_events.nil?
90
92
  personalization_events = hash['personalizationEvents']
91
93
  parse_personalizations(personalization_events) unless personalization_events.nil?
92
94
  end
@@ -144,17 +146,21 @@ module Kameleoon
144
146
  Geolocation.new(data['country'], data['region'], data['city'])
145
147
  end
146
148
 
147
- def parse_static_data(static_data_event)
149
+ def parse_static_data(static_data_event, is_prev_visit)
148
150
  data = static_data_event['data']
149
- if @device.nil?
151
+ if @visit_number.zero?
152
+ @visit_number = data['visitNumber'] || 0
153
+ @visit_number += 1 if is_prev_visit && @visit_number.positive?
154
+ end
155
+ if @filter.device && @device.nil?
150
156
  device_type = data['deviceType']
151
157
  @device = Device.new(device_type) unless device_type.nil?
152
158
  end
153
- if @browser.nil?
159
+ if @filter.browser && @browser.nil?
154
160
  browser_type = BrowserType.from_name(data['browser'])
155
161
  @browser = Browser.new(browser_type, data['browserVersion']) unless browser_type.nil?
156
162
  end
157
- if @operating_system.nil?
163
+ if @filter.operating_system && @operating_system.nil?
158
164
  operating_system_type = OperatingSystemType.from_name(data['os'])
159
165
  @operating_system = OperatingSystem.new(operating_system_type) unless operating_system_type.nil?
160
166
  end
@@ -3,6 +3,7 @@
3
3
  module Kameleoon
4
4
  module Network
5
5
  module ContentType
6
+ WILDCARD = '*/*'
6
7
  TEXT = 'text/plain'
7
8
  JSON = 'application/json'
8
9
  FORM = 'application/x-www-form-urlencoded'
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kameleoon
4
+ module Network
5
+ class FetchedConfiguration
6
+ attr_reader :configuration, :last_modified
7
+
8
+ def initialize(configuration, last_modified)
9
+ @configuration = configuration
10
+ @last_modified = last_modified
11
+ end
12
+ end
13
+ end
14
+ end
@@ -22,7 +22,7 @@ module Kameleoon
22
22
  end
23
23
 
24
24
  def unknown_method_response(method, request)
25
- Response.new("Unknown request method '#{method}'", nil, nil, request)
25
+ Response.new("Unknown request method '#{method}'", nil, nil, nil, request)
26
26
  end
27
27
  end
28
28
 
@@ -48,9 +48,10 @@ module Kameleoon
48
48
  end
49
49
  body = resp.body
50
50
  body = nil if body&.empty?
51
- Response.new(nil, resp.code.to_i, body, request)
51
+ headers = resp.to_hash
52
+ Response.new(nil, resp.code.to_i, body, headers, request)
52
53
  rescue StandardError => e
53
- Response.new(e, nil, nil, request)
54
+ Response.new(e, nil, nil, nil, request)
54
55
  end
55
56
  end
56
57
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'kameleoon/logging/kameleoon_logger'
4
4
  require 'kameleoon/network/content_type'
5
+ require 'kameleoon/network/fetched_configuration'
5
6
  require 'kameleoon/network/method'
6
7
  require 'kameleoon/network/request'
7
8
  require 'kameleoon/network/net_provider'
@@ -19,6 +20,8 @@ module Kameleoon
19
20
  SDK_TYPE_HEADER = 'X-Kameleoon-SDK-Type'
20
21
  SDK_VERSION_HEADER = 'X-Kameleoon-SDK-Version'
21
22
  ACCESS_TOKEN_GRANT_TYPE = 'client_credentials'
23
+ HEADER_IF_MODIFIED_SINCE = 'If-Modified-Since'
24
+ HEADER_LAST_MODIFIED = 'last-modified' # in lower case because the network lib casts response headers to lower
22
25
 
23
26
  attr_reader :environment, :default_timeout, :access_token_source, :url_provider
24
27
 
@@ -30,26 +33,32 @@ module Kameleoon
30
33
  @sync_net_provider = SyncNetProvider.new
31
34
  end
32
35
 
33
- def fetch_configuration(timestamp = nil, timeout = nil)
36
+ def fetch_configuration(timestamp = nil, timeout = nil, if_modified_since: nil)
34
37
  url = @url_provider.make_configuration_url(@environment, timestamp)
35
38
  timeout = ensure_timeout(timeout)
36
- sdk_headers = { SDK_TYPE_HEADER => SDK_NAME, SDK_VERSION_HEADER => SDK_VERSION }
37
- request = Request.new(Method::GET, url, ContentType::JSON, timeout, extra_headers: sdk_headers)
38
- make_call(request, false, FETCH_CONFIGURATION_ATTEMPT_NUMBER - 1)
39
+ headers = { SDK_TYPE_HEADER => SDK_NAME, SDK_VERSION_HEADER => SDK_VERSION }
40
+ headers[HEADER_IF_MODIFIED_SINCE] = if_modified_since if if_modified_since
41
+ request = Request.new(Method::GET, url, ContentType::JSON, timeout, extra_headers: headers)
42
+ response, success = make_call(request, false, FETCH_CONFIGURATION_ATTEMPT_NUMBER - 1)
43
+ return nil unless success
44
+ return FetchedConfiguration.new(nil, nil) if response.code == 304
45
+
46
+ last_modified = response.headers&.[](HEADER_LAST_MODIFIED)&.first
47
+ FetchedConfiguration.new(response.body, last_modified)
39
48
  end
40
49
 
41
50
  def get_remote_data(key, timeout = nil)
42
51
  url = @url_provider.make_api_data_get_request_url(key)
43
52
  timeout = ensure_timeout(timeout)
44
53
  request = Request.new(Method::GET, url, ContentType::JSON, timeout)
45
- make_call(request, true)
54
+ unwrap_response(*make_call(request, true))
46
55
  end
47
56
 
48
57
  def get_remote_visitor_data(visitor_code, filter, is_unique_identifier, timeout = nil)
49
58
  url = @url_provider.make_visitor_data_get_url(visitor_code, filter, is_unique_identifier)
50
59
  timeout = ensure_timeout(timeout)
51
60
  request = Request.new(Method::GET, url, ContentType::JSON, timeout)
52
- make_call(request, true)
61
+ unwrap_response(*make_call(request, true))
53
62
  end
54
63
 
55
64
  def send_tracking_data(lines, timeout = nil)
@@ -57,8 +66,8 @@ module Kameleoon
57
66
 
58
67
  url = @url_provider.make_tracking_url
59
68
  timeout = ensure_timeout(timeout)
60
- request = Request.new(Method::POST, url, ContentType::TEXT, timeout, data: lines)
61
- make_call(request, true, TRACKING_CALL_ATTEMPT_NUMBER - 1, TRACKING_CALL_RETRY_DELAY)
69
+ request = Request.new(Method::POST, url, ContentType::WILDCARD, timeout, data: lines)
70
+ unwrap_response(*make_call(request, true, TRACKING_CALL_ATTEMPT_NUMBER - 1, TRACKING_CALL_RETRY_DELAY))
62
71
  end
63
72
 
64
73
  def fetch_access_jwtoken(client_id, client_secret, timeout = nil)
@@ -71,7 +80,7 @@ module Kameleoon
71
80
  }
72
81
  data = UriHelper.encode_query(data_map).encode('UTF-8')
73
82
  request = Request.new(Method::POST, url, ContentType::FORM, timeout, data: data)
74
- make_call(request, false)
83
+ unwrap_response(*make_call(request, false))
75
84
  end
76
85
 
77
86
  private
@@ -81,6 +90,7 @@ module Kameleoon
81
90
  request, try_access_token_auth, retry_limit, retry_delay)
82
91
  attempt = 0
83
92
  success = false
93
+ response = nil
84
94
  while !success && (attempt <= retry_limit)
85
95
  delay(retry_delay) if attempt.positive? && retry_delay.positive?
86
96
  try_authorize(request) if try_access_token_auth
@@ -110,11 +120,11 @@ module Kameleoon
110
120
  attempt += 1
111
121
  end
112
122
  Logging::KameleoonLogger.debug('Fetched response %s for request %s', response, request)
113
- success ? response.body : false
123
+ [response, success]
114
124
  end
115
125
 
116
126
  def get_log_level(attempt, attempt_count)
117
- return log_level = attempt < attempt_count ? Logging::LogLevel::WARNING : Logging::LogLevel::ERROR
127
+ attempt < attempt_count ? Logging::LogLevel::WARNING : Logging::LogLevel::ERROR
118
128
  end
119
129
 
120
130
  def delay(period)
@@ -129,6 +139,10 @@ module Kameleoon
129
139
  token = @access_token_source.get_token(request.timeout)
130
140
  request.authorize(token)
131
141
  end
142
+
143
+ def unwrap_response(response, success)
144
+ success ? response.body : false
145
+ end
132
146
  end
133
147
  end
134
148
  end
@@ -16,7 +16,7 @@ module Kameleoon
16
16
  body = @data
17
17
  end
18
18
  end
19
- "HttpRequest{Method:'#{@method}',Url:'#{@url}',Headers:#{@extra_headers},Body:'#{body}'}"
19
+ "Request{method:'#{@method}',url:'#{@url}',headers:#{@extra_headers},body:'#{body}'}"
20
20
  end
21
21
 
22
22
  def initialize(method, url, content_type, timeout, extra_headers: nil, data: nil)
@@ -5,21 +5,22 @@ module Kameleoon
5
5
  ##
6
6
  # Response represents HTTP response.
7
7
  class Response
8
- attr_reader :error, :code, :body, :request
8
+ attr_reader :error, :code, :body, :headers, :request
9
9
 
10
10
  def to_s
11
- "HttpResponse{Code:'#{@code}',Reason:'#{@error}',Body:#{@body}}"
11
+ "Response{code:'#{@code}',error:'#{@error}',body:#{@body}}"
12
12
  end
13
13
 
14
- def initialize(error, code, body, request)
14
+ def initialize(error, code, body, headers, request)
15
15
  @error = error
16
16
  @code = code
17
17
  @body = body
18
+ @headers = headers
18
19
  @request = request
19
20
  end
20
21
 
21
22
  def success?
22
- @error.nil? && ((@code / 100 == 2) || (@code == 403))
23
+ @error.nil? && ((@code / 100 == 2) || (@code == 403) || (@code == 304))
23
24
  end
24
25
  end
25
26
  end
@@ -19,7 +19,7 @@ module Kameleoon
19
19
  DEFAULT_ACCESS_TOKEN_DOMAIN = 'api.kameleoon.com'
20
20
  DEFAULT_DATA_API_DOMAIN = 'data.kameleoon.io'
21
21
 
22
- CONFIGURATION_API_URL_FORMAT = 'https://%s/%s'
22
+ CONFIGURATION_API_URL_FORMAT = 'https://%s/v3/%s'
23
23
  DATA_API_URL_FORMAT = 'https://%s%s?%s'
24
24
  RT_CONFIGURATION_URL_FORMAT = 'https://%s:8110/sse?%s'
25
25
  ACCESS_TOKEN_URL_FORMAT = 'https://%s/oauth/token'
@@ -68,6 +68,7 @@ module Kameleoon
68
68
  params[is_unique_identifier ? :mappingValue : :visitorCode] = visitor_code
69
69
  params[:maxNumberPreviousVisits] = filter.previous_visit_amount
70
70
  params[:version] = 0
71
+ params[:staticData] = true
71
72
  params[:kcs] = true if filter.kcs
72
73
  params[:currentVisit] = true if filter.current_visit
73
74
  params[:customData] = true if filter.custom_data
@@ -75,7 +76,6 @@ module Kameleoon
75
76
  params[:geolocation] = true if filter.geolocation
76
77
  params[:experiment] = true if filter.experiments
77
78
  params[:page] = true if filter.page_views
78
- params[:staticData] = true if filter.device || filter.browser || filter.operating_system
79
79
  params[:personalization] = true if filter.personalization
80
80
  params[:cbs] = true if filter.cbs
81
81
  format(DATA_API_URL_FORMAT, @data_api_domain, VISITOR_DATA_PATH, UriHelper.encode_query(params))
@@ -11,7 +11,6 @@ module Kameleoon
11
11
  TARGET_FEATURE_FLAG = 'TARGET_FEATURE_FLAG'
12
12
  TARGET_EXPERIMENT = 'TARGET_EXPERIMENT'
13
13
  TARGET_PERSONALIZATION = 'TARGET_PERSONALIZATION'
14
- EXCLUSIVE_FEATURE_FLAG = 'EXCLUSIVE_FEATURE_FLAG'
15
14
  EXCLUSIVE_EXPERIMENT = 'EXCLUSIVE_EXPERIMENT'
16
15
  PAGE_URL = 'PAGE_URL'
17
16
  PAGE_VIEWS = 'PAGE_VIEWS'
@@ -3,7 +3,6 @@ require_relative 'conditions/target_experiment_condition'
3
3
  require_relative 'conditions/target_feature_flag_condition'
4
4
  require_relative 'conditions/target_personalization_condition'
5
5
  require_relative 'conditions/exclusive_experiment_condition'
6
- require_relative 'conditions/exclusive_feature_flag_condition'
7
6
  require_relative 'conditions/page_title_condition'
8
7
  require_relative 'conditions/page_url_condition'
9
8
  require_relative 'conditions/page_view_number_condition'
@@ -39,8 +38,6 @@ module Kameleoon
39
38
  TargetExperimentCondition.new(condition_json)
40
39
  when ConditionType::TARGET_PERSONALIZATION
41
40
  TargetPersonalizationCondition.new(condition_json)
42
- when ConditionType::EXCLUSIVE_FEATURE_FLAG
43
- ExclusiveFeatureFlagCondition.new(condition_json)
44
41
  when ConditionType::EXCLUSIVE_EXPERIMENT
45
42
  ExclusiveExperimentCondition.new(condition_json)
46
43
  when ConditionType::PAGE_URL
@@ -10,16 +10,17 @@ module Kameleoon
10
10
  def initialize(json_condition)
11
11
  count_in_millis = json_condition['countInMillis']
12
12
  super(json_condition, count_in_millis)
13
- @is_first_visit = @type == Kameleoon::Targeting::ConditionType::FIRST_VISIT
13
+ @is_first_visit = @type == ConditionType::FIRST_VISIT
14
14
  end
15
15
 
16
16
  def check(data)
17
- return false unless data.is_a?(Kameleoon::VisitorVisits) && !@condition_value.nil?
17
+ data ||= VisitorVisits.new([])
18
+ return false unless data.is_a?(VisitorVisits) && !@condition_value.nil?
18
19
 
19
- previous_visits_count = data.previous_visit_timestamps.count
20
- if previous_visits_count >= 1
20
+ prev_visits = data.prev_visits
21
+ if prev_visits.size >= 1
21
22
  now = (Time.now.to_f * 1000).to_i # ... * 1000 for convert seconds to milliseconds
22
- visit_time = data.previous_visit_timestamps[@is_first_visit ? previous_visits_count - 1 : 0]
23
+ visit_time = prev_visits[@is_first_visit ? prev_visits.size - 1 : 0].time_started
23
24
  return check_targeting(now - visit_time)
24
25
  end
25
26
  false
@@ -13,15 +13,27 @@ module Kameleoon
13
13
  end
14
14
 
15
15
  def check(data)
16
- return false unless VisitorVisits.visitor_visits?(data) && !@condition_value.nil?
16
+ return false unless data.is_a?(TargetingData) && !@condition_value.nil?
17
17
 
18
18
  number_of_visits_today = 0
19
19
  start_of_day = (Time.new.to_date.to_time.to_f * 1000).to_i # ... * 1000 to convert seconds to milliseconds
20
- for timestamp in VisitorVisits.get_previous_visit_timestamps(data)
21
- break if timestamp < start_of_day
20
+ data.visitor_visits.prev_visits.each do |visit|
21
+ break if visit.time_started < start_of_day
22
+
22
23
  number_of_visits_today += 1
23
24
  end
24
- check_targeting(number_of_visits_today + 1) # +1 for current visit
25
+ number_of_visits_today += 1 if data.current_visit_time_started >= start_of_day
26
+ check_targeting(number_of_visits_today)
27
+ end
28
+
29
+ class TargetingData
30
+ attr_reader :current_visit_time_started, :visitor_visits
31
+
32
+ def initialize(current_visit_time_started, visitor_visits)
33
+ @current_visit_time_started =
34
+ current_visit_time_started.is_a?(Integer) ? current_visit_time_started : (Time.new.to_f * 1000).to_i
35
+ @visitor_visits = visitor_visits.is_a?(VisitorVisits) ? visitor_visits : VisitorVisits.new([])
36
+ end
25
37
  end
26
38
  end
27
39
  end
@@ -13,10 +13,11 @@ module Kameleoon
13
13
  end
14
14
 
15
15
  def check(data)
16
- return false unless VisitorVisits.visitor_visits?(data) && !@condition_value.nil?
16
+ data ||= VisitorVisits.new([])
17
+ return false unless data.is_a?(VisitorVisits) && !@condition_value.nil?
17
18
 
18
- previous_visit_timestamps = VisitorVisits.get_previous_visit_timestamps(data)
19
- check_targeting(previous_visit_timestamps.size + 1) # +1 for current visit
19
+ prev_visits = data.prev_visits
20
+ check_targeting(prev_visits.size + 1) # +1 for current visit
20
21
  end
21
22
  end
22
23
  end
@@ -17,14 +17,15 @@ module Kameleoon
17
17
  end
18
18
 
19
19
  def check(data)
20
- return false unless VisitorVisits.visitor_visits?(data)
20
+ data ||= VisitorVisits.new([])
21
+ return false unless data.is_a?(VisitorVisits)
21
22
 
22
- previous_visit_timestamps = VisitorVisits.get_previous_visit_timestamps(data)
23
+ prev_visits = data.prev_visits
23
24
  case @visitor_type
24
25
  when VisitorType::NEW
25
- previous_visit_timestamps.empty?
26
+ prev_visits.empty?
26
27
  when VisitorType::RETURNING
27
- !previous_visit_timestamps.empty?
28
+ !prev_visits.empty?
28
29
  else
29
30
  false
30
31
  end
@@ -9,6 +9,7 @@ module Kameleoon
9
9
  class Segment
10
10
  include TreeBuilder
11
11
  attr_accessor :id, :tree
12
+
12
13
  def to_s
13
14
  @tree.to_s
14
15
  end
@@ -84,13 +85,13 @@ module Kameleoon
84
85
  # Computing results
85
86
  if is_left_child_targeted.nil?
86
87
  if is_right_child_targeted == @or_operator
87
- is_targeted = Marshal.load(Marshal.dump(@or_operator)) #Deep copy
88
+ is_targeted = Marshal.load(Marshal.dump(@or_operator)) # Deep copy
88
89
  else
89
90
  is_targeted = nil
90
91
  end
91
92
  else
92
93
  if is_left_child_targeted == @or_operator
93
- is_targeted = Marshal.load(Marshal.dump(@or_operator)) #Deep copy
94
+ is_targeted = Marshal.load(Marshal.dump(@or_operator)) # Deep copy
94
95
  else
95
96
  if is_right_child_targeted == true
96
97
  is_targeted = true
@@ -102,7 +103,7 @@ module Kameleoon
102
103
  end
103
104
  end
104
105
  end
105
- Marshal.load(Marshal.dump(is_targeted)) #Deep copy
106
+ Marshal.load(Marshal.dump(is_targeted)) # Deep copy
106
107
  end
107
108
 
108
109
  def check_condition(datas, condition = @condition)
@@ -3,12 +3,12 @@
3
3
  require 'kameleoon/logging/kameleoon_logger'
4
4
  require 'kameleoon/targeting/condition'
5
5
  require 'kameleoon/targeting/conditions/exclusive_experiment_condition'
6
- require 'kameleoon/targeting/conditions/exclusive_feature_flag_condition'
7
6
  require 'kameleoon/targeting/conditions/sdk_language_condition'
8
7
  require 'kameleoon/targeting/conditions/segment_condition'
9
8
  require 'kameleoon/targeting/conditions/target_experiment_condition'
10
9
  require 'kameleoon/targeting/conditions/target_feature_flag_condition'
11
10
  require 'kameleoon/targeting/conditions/target_personalization_condition'
11
+ require 'kameleoon/targeting/conditions/visit_number_today_condition'
12
12
 
13
13
  module Kameleoon
14
14
  # @api private
@@ -85,16 +85,15 @@ module Kameleoon
85
85
  condition_data = TargetExperimentInfo.new(visitor&.variations)
86
86
  when ConditionType::TARGET_PERSONALIZATION
87
87
  condition_data = TargetPersonalizationInfo.new(visitor&.personalizations)
88
- when ConditionType::EXCLUSIVE_FEATURE_FLAG
89
- condition_data = ExclusiveFeatureFlagInfo.new(campaign_id, visitor&.variations)
90
88
  when ConditionType::EXCLUSIVE_EXPERIMENT
91
89
  condition_data = ExclusiveExperimentInfo.new(campaign_id, visitor&.variations, visitor&.personalizations)
92
90
  when ConditionType::FIRST_VISIT,
93
91
  ConditionType::LAST_VISIT,
94
92
  ConditionType::VISITS,
95
- ConditionType::SAME_DAY_VISITS,
96
93
  ConditionType::NEW_VISITORS
97
94
  condition_data = visitor.visitor_visits unless visitor.nil?
95
+ when ConditionType::SAME_DAY_VISITS
96
+ condition_data = VisitNumberTodayCondition::TargetingData.new(visitor&.time_started, visitor&.visitor_visits)
98
97
  when ConditionType::HEAT_SLICE
99
98
  condition_data = visitor&.kcs_heat
100
99
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Kameleoon
4
- SDK_VERSION = '3.12.1'
4
+ SDK_VERSION = '3.14.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.12.1
4
+ version: 3.14.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kameleoon
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-04-08 00:00:00.000000000 Z
11
+ date: 2025-06-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: em-http-request
@@ -130,6 +130,7 @@ files:
130
130
  - lib/kameleoon/network/activity_event.rb
131
131
  - lib/kameleoon/network/content_type.rb
132
132
  - lib/kameleoon/network/cookie/cookie_manager.rb
133
+ - lib/kameleoon/network/fetched_configuration.rb
133
134
  - lib/kameleoon/network/method.rb
134
135
  - lib/kameleoon/network/net_provider.rb
135
136
  - lib/kameleoon/network/network_manager.rb
@@ -153,7 +154,6 @@ files:
153
154
  - lib/kameleoon/targeting/conditions/custom_datum.rb
154
155
  - lib/kameleoon/targeting/conditions/device_condition.rb
155
156
  - lib/kameleoon/targeting/conditions/exclusive_experiment_condition.rb
156
- - lib/kameleoon/targeting/conditions/exclusive_feature_flag_condition.rb
157
157
  - lib/kameleoon/targeting/conditions/geolocation_condition.rb
158
158
  - lib/kameleoon/targeting/conditions/kcs_heat_range_condition.rb
159
159
  - lib/kameleoon/targeting/conditions/number_condition.rb
@@ -1,27 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'kameleoon/targeting/condition'
4
-
5
- module Kameleoon
6
- # @api private
7
- module Targeting
8
- # ExclusiveFeatureFlag represents an instance of Exclusive FeatureFlag condition in user account
9
- class ExclusiveFeatureFlagCondition < Condition
10
- def check(data)
11
- return false unless data.is_a?(ExclusiveFeatureFlagInfo)
12
-
13
- size = data.variations_storage&.size || 0
14
- size.zero? || (size == 1 && !data.variations_storage.get(data.current_experiment_id).nil?)
15
- end
16
- end
17
-
18
- class ExclusiveFeatureFlagInfo
19
- attr_reader :current_experiment_id, :variations_storage
20
-
21
- def initialize(current_experiment_id, variations_storage)
22
- @current_experiment_id = current_experiment_id
23
- @variations_storage = variations_storage
24
- end
25
- end
26
- end
27
- end