kameleoon-client-ruby 3.19.0 → 3.20.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 (32) hide show
  1. checksums.yaml +4 -4
  2. data/lib/kameleoon/data/browser.rb +1 -1
  3. data/lib/kameleoon/data/conversion.rb +12 -3
  4. data/lib/kameleoon/data/custom_data.rb +1 -1
  5. data/lib/kameleoon/data/data.rb +1 -1
  6. data/lib/kameleoon/data/device.rb +1 -1
  7. data/lib/kameleoon/data/geolocation.rb +1 -1
  8. data/lib/kameleoon/data/manager/assigned_variation.rb +2 -2
  9. data/lib/kameleoon/data/manager/page_view_visit.rb +2 -2
  10. data/lib/kameleoon/data/manager/visitor.rb +2 -2
  11. data/lib/kameleoon/data/manager/visitor_manager.rb +1 -1
  12. data/lib/kameleoon/data/mapping_identifier.rb +1 -1
  13. data/lib/kameleoon/data/operating_system.rb +1 -2
  14. data/lib/kameleoon/data/page_view.rb +1 -1
  15. data/lib/kameleoon/data/personalization.rb +4 -3
  16. data/lib/kameleoon/data/targeted_segment.rb +1 -1
  17. data/lib/kameleoon/data/visitor_visits.rb +1 -1
  18. data/lib/kameleoon/managers/remote_data/remote_visitor_data.rb +20 -5
  19. data/lib/kameleoon/managers/tracking/tracking_builder.rb +1 -1
  20. data/lib/kameleoon/network/access_token_source.rb +2 -2
  21. data/lib/kameleoon/network/activity_event.rb +1 -1
  22. data/lib/kameleoon/targeting/conditions/conversion_condition.rb +28 -13
  23. data/lib/kameleoon/targeting/conditions/exclusive_experiment_condition.rb +38 -19
  24. data/lib/kameleoon/targeting/conditions/target_experiment_condition.rb +12 -10
  25. data/lib/kameleoon/targeting/conditions/target_feature_flag_condition.rb +26 -22
  26. data/lib/kameleoon/targeting/conditions/target_personalization_condition.rb +12 -10
  27. data/lib/kameleoon/targeting/conditions/time_elapsed_since_visit_condition.rb +1 -2
  28. data/lib/kameleoon/targeting/conditions/visit_number_today_condition.rb +2 -2
  29. data/lib/kameleoon/targeting/conditions/visitor_scope_condition.rb +56 -0
  30. data/lib/kameleoon/targeting/targeting_manager.rb +12 -5
  31. data/lib/kameleoon/version.rb +1 -1
  32. metadata +3 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dde5f6ebb9e6779b9bf8d4edbdded4e11b82aa642ec046bd2493306e0b2a7ad8
4
- data.tar.gz: 5a15f627b2832ec10f001ef7ee061fa31c6dbd618b02ec55ff9d1d66ee17d5a8
3
+ metadata.gz: c6e939e54b9b0f43a46d27b3ce7d1d442916d036a7db9ca5159645ffc869415b
4
+ data.tar.gz: 98b6e2c2b04c183ba5052379066e3eff95c9ffdc2b28963a0bd106dcf8adb0db
5
5
  SHA512:
6
- metadata.gz: 91d3f34ab6855ed62d571fa5d40b85a04504926f2d38650c9b62f052bd9f0b79b0ce95ece6636fa37ecc5062020f2361bc5e0e8af80383b0d2b377dffd5919e7
7
- data.tar.gz: c2e5efd80b73d5f8ce9c48a6f069aadda0fc7d1a6bc390a2d98af4a8a69b36928bac0188c77406bd793d8db6c4701df24838c67efc89b98d4d0e6b7e425b9c68
6
+ metadata.gz: c4eb181ded7a566731df5e6af6bcb271fcf852c27503b5e0c44be51eb93b729decb88c378732f9a9584d5d1ef452e71a5ba651a39d20a06f7e689a3b016bc65a
7
+ data.tar.gz: 997194371128c0c25cd394ab7ed22e05a0b57684915c96268874ab9e426aa3e0228f396488e77da61c2b122798eeccfee2ad1e8785900ddff24452faac953025
@@ -49,7 +49,7 @@ module Kameleoon
49
49
  @version = version
50
50
  end
51
51
 
52
- def obtain_full_post_text_line
52
+ def query
53
53
  params = {
54
54
  eventType: 'staticData',
55
55
  browserIndex: @type,
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'json'
4
+ require 'set'
4
5
  require 'kameleoon/data/custom_data'
5
6
  require 'kameleoon/network/uri_helper'
6
7
  require 'kameleoon/utils'
@@ -9,11 +10,11 @@ require_relative 'data'
9
10
  module Kameleoon
10
11
  # Conversion class uses for tracking conversion
11
12
  class Conversion < DuplicationSafeData
12
- attr_reader :goal_id, :revenue, :negative, :metadata
13
+ attr_reader :goal_id, :revenue, :negative, :metadata, :assignment_time
13
14
 
14
15
  def to_s
15
16
  "Conversion{goal_id:#{@goal_id},revenue:#{@revenue},negative:#{@negative}," \
16
- "metadata:#{Utils::Strval.obj_to_s(@metadata)}}"
17
+ "metadata:#{Utils::Strval.obj_to_s(@metadata)},assignment_time:#{@assignment_time}}"
17
18
  end
18
19
 
19
20
  # @param [Integer] goal_id Id of the goal associated to the conversion
@@ -26,9 +27,17 @@ module Kameleoon
26
27
  @revenue = revenue || 0.0
27
28
  @negative = negative || false
28
29
  @metadata = metadata
30
+ @assignment_time = Time.now.to_f
29
31
  end
30
32
 
31
- def obtain_full_post_text_line
33
+ # @api private
34
+ def self.build_internal(goal_id, revenue = 0.0, negative = false, metadata: nil, assignment_time: nil)
35
+ conversion = new(goal_id, revenue, negative, metadata: metadata)
36
+ conversion.instance_variable_set(:@assignment_time, assignment_time || Time.now.to_f)
37
+ conversion
38
+ end
39
+
40
+ def query
32
41
  params = {
33
42
  eventType: 'conversion',
34
43
  goalId: @goal_id,
@@ -85,7 +85,7 @@ module Kameleoon
85
85
  @index
86
86
  end
87
87
 
88
- def obtain_full_post_text_line
88
+ def query
89
89
  str_values = JSON.generate(Hash[@values.collect { |k| [k, 1] }])
90
90
  params = {
91
91
  eventType: 'customData',
@@ -38,7 +38,7 @@ module Kameleoon
38
38
  @state = DataState::UNSENT
39
39
  end
40
40
 
41
- def obtain_full_post_text_line
41
+ def query
42
42
  raise KameleoonError.new('ToDo: implement this method.'), 'ToDo: implement this method.'
43
43
  end
44
44
 
@@ -24,7 +24,7 @@ module Kameleoon
24
24
  @device_type = device_type
25
25
  end
26
26
 
27
- def obtain_full_post_text_line
27
+ def query
28
28
  params = {
29
29
  eventType: 'staticData',
30
30
  deviceType: @device_type,
@@ -26,7 +26,7 @@ module Kameleoon
26
26
  @longitude = longitude
27
27
  end
28
28
 
29
- def obtain_full_post_text_line
29
+ def query
30
30
  params = {
31
31
  eventType: 'geolocation',
32
32
  nonce: nonce
@@ -25,10 +25,10 @@ module Kameleoon
25
25
  @experiment_id = experiment_id
26
26
  @variation_id = variation_id
27
27
  @rule_type = rule_type
28
- @assignment_time = assignment_time || Time.new.to_i
28
+ @assignment_time = assignment_time || Time.now.to_f
29
29
  end
30
30
 
31
- def obtain_full_post_text_line
31
+ def query
32
32
  params = {
33
33
  eventType: EVENT_TYPE,
34
34
  id: @experiment_id,
@@ -12,7 +12,7 @@ module Kameleoon
12
12
  def initialize(page_view, count = 1, timestamp = nil)
13
13
  @page_view = page_view
14
14
  @count = count
15
- @last_timestamp = timestamp.nil? ? Time.new.to_i : timestamp
15
+ @last_timestamp = timestamp.nil? ? Time.new.to_f : timestamp
16
16
  end
17
17
 
18
18
  # Not thread-save method, should be called in synchronized code
@@ -20,7 +20,7 @@ module Kameleoon
20
20
  def overwrite(page_view)
21
21
  @page_view = page_view
22
22
  @count += 1
23
- @last_timestamp = Time.new.to_i
23
+ @last_timestamp = Time.new.to_f
24
24
  end
25
25
 
26
26
  # Not thread-save method, should be called in synchronized code
@@ -55,7 +55,7 @@ module Kameleoon
55
55
  end
56
56
 
57
57
  def update_last_activity_time
58
- @data.last_activity_time = Time.new.to_i
58
+ @data.last_activity_time = Time.new.to_f
59
59
  end
60
60
 
61
61
  def enumerate_sendable_data(&blk)
@@ -322,7 +322,7 @@ module Kameleoon
322
322
 
323
323
  def initialize
324
324
  Logging::KameleoonLogger.debug('CALL: VisitorData.new')
325
- @time_started = (Time.now.to_f * 1000).to_i
325
+ @time_started = Time.now.to_f
326
326
  @mutex = Concurrent::ReadWriteLock.new
327
327
  @legal_consent = LegalConsent::UNKNOWN
328
328
  Logging::KameleoonLogger.debug('RETURN: VisitorData.new')
@@ -154,7 +154,7 @@ module Kameleoon
154
154
 
155
155
  def purge
156
156
  Logging::KameleoonLogger.debug('CALL: VisitorManager.purge')
157
- expired_time = Time.new.to_i - @expiration_period
157
+ expired_time = Time.new.to_f - @expiration_period
158
158
  @visitors.each_pair do |vc, v|
159
159
  next if v.last_activity_time >= expired_time
160
160
 
@@ -28,7 +28,7 @@ module Kameleoon
28
28
  false
29
29
  end
30
30
 
31
- def obtain_full_post_text_line
31
+ def query
32
32
  mip = Kameleoon::Network::UriHelper.encode_query({ mappingIdentifier: true })
33
33
  "#{super}&#{mip}"
34
34
  end
@@ -62,7 +62,7 @@ module Kameleoon
62
62
  @os_type = os_type
63
63
  end
64
64
 
65
- def obtain_full_post_text_line
65
+ def query
66
66
  params = {
67
67
  eventType: 'staticData',
68
68
  os: OperatingSystemType.name_from_type(@os_type),
@@ -73,4 +73,3 @@ module Kameleoon
73
73
  end
74
74
  end
75
75
  end
76
-
@@ -25,7 +25,7 @@ module Kameleoon
25
25
  @referrers = referrers.instance_of?(Integer) ? [referrers] : referrers
26
26
  end
27
27
 
28
- def obtain_full_post_text_line
28
+ def query
29
29
  params = {
30
30
  eventType: 'page',
31
31
  href: @url,
@@ -2,15 +2,16 @@
2
2
 
3
3
  module Kameleoon
4
4
  class Personalization
5
- attr_reader :id, :variation_id
5
+ attr_reader :id, :variation_id, :assignment_time
6
6
 
7
- def initialize(id, variation_id)
7
+ def initialize(id, variation_id, assignment_time: nil)
8
8
  @id = id
9
9
  @variation_id = variation_id
10
+ @assignment_time = assignment_time || Time.now.to_f
10
11
  end
11
12
 
12
13
  def to_s
13
- "Personalization{id:#{@id},variation_id:#{@variation_id}}"
14
+ "Personalization{id:#{@id},variation_id:#{@variation_id},assignment_time:#{@assignment_time}}"
14
15
  end
15
16
  end
16
17
  end
@@ -14,7 +14,7 @@ module Kameleoon
14
14
  @id = id
15
15
  end
16
16
 
17
- def obtain_full_post_text_line
17
+ def query
18
18
  params = {
19
19
  eventType: EVENT_TYPE,
20
20
  id: @id,
@@ -33,7 +33,7 @@ module Kameleoon
33
33
  )
34
34
  end
35
35
 
36
- def obtain_full_post_text_line
36
+ def query
37
37
  params = {
38
38
  eventType: 'staticData',
39
39
  visitNumber: @visit_number,
@@ -32,7 +32,9 @@ module Kameleoon
32
32
  prev_visits = []
33
33
  previous_visits.each_index do |i|
34
34
  visit = previous_visits[i]
35
- prev_visits.push(VisitorVisits::Visit.new(visit['timeStarted'] || 0, visit['timeLastEvent']))
35
+ prev_visits.push(
36
+ VisitorVisits::Visit.new(ms_to_s(visit['timeStarted'] || 0), ms_to_s(visit['timeLastEvent']))
37
+ )
36
38
  parse_visit(visit, i + 1)
37
39
  end
38
40
  @visitor_visits = VisitorVisits.new(prev_visits, @visit_number)
@@ -113,7 +115,7 @@ module Kameleoon
113
115
  page_view_visit = @page_view_visits[href]
114
116
  if page_view_visit.nil?
115
117
  page_view = PageView.new(href, page_event['data']['title'])
116
- @page_view_visits[href] = DataManager::PageViewVisit.new(page_view, 1, page_event['time'])
118
+ @page_view_visits[href] = DataManager::PageViewVisit.new(page_view, 1, ms_to_s(page_event['time']))
117
119
  else
118
120
  page_view_visit.increase_page_visits
119
121
  end
@@ -128,7 +130,7 @@ module Kameleoon
128
130
 
129
131
  @experiments[id] = DataManager::AssignedVariation.new(
130
132
  id, experiment_event['data']['variationId'],
131
- Configuration::RuleType::UNKNOWN, assignment_time: experiment_event['time']
133
+ Configuration::RuleType::UNKNOWN, assignment_time: ms_to_s(experiment_event['time'])
132
134
  )
133
135
  end
134
136
  end
@@ -137,7 +139,10 @@ module Kameleoon
137
139
  @conversions = [] if @conversions.nil?
138
140
  conversion_events.each do |conversion_event|
139
141
  data = conversion_event['data']
140
- conversion = Conversion.new(data['goalId'], data['revenue'], data['negative'])
142
+ conversion = Conversion.send(
143
+ :build_internal, data['goalId'], data['revenue'], data['negative'],
144
+ assignment_time: ms_to_s(conversion_event['time'])
145
+ )
141
146
  @conversions.push(conversion)
142
147
  end
143
148
  end
@@ -173,10 +178,20 @@ module Kameleoon
173
178
  id = personalization_event['data']['id']
174
179
  next if @personalizations.include?(id)
175
180
 
176
- @personalizations[id] = Personalization.new(id, personalization_event['data']['variationId'])
181
+ @personalizations[id] = Personalization.new(
182
+ id,
183
+ personalization_event['data']['variationId'],
184
+ assignment_time: ms_to_s(personalization_event['time'])
185
+ )
177
186
  end
178
187
  end
179
188
 
189
+ def ms_to_s(timestamp)
190
+ return nil if timestamp.nil?
191
+
192
+ timestamp / 1000.0
193
+ end
194
+
180
195
  def conversions_single_objects
181
196
  objects = []
182
197
  objects += @conversions unless @conversions.nil?
@@ -127,7 +127,7 @@ module Kameleoon
127
127
  )
128
128
  user_agent = visitor&.user_agent
129
129
  unsent_data.each do |data|
130
- line = data.obtain_full_post_text_line
130
+ line = data.query
131
131
  next if line.empty?
132
132
 
133
133
  line = add_line_params(line, visitor_code_param, user_agent)
@@ -39,7 +39,7 @@ module Kameleoon
39
39
 
40
40
  def get_token(timeout = nil)
41
41
  Logging::KameleoonLogger.debug('CALL: AccessTokenSource.getToken(timeout: %s)', timeout)
42
- now = Time.new.to_i
42
+ now = Time.new.to_f
43
43
  token = @cached_token
44
44
  return call_fetch_token(timeout) if token.nil? || token.expired?(now)
45
45
 
@@ -105,7 +105,7 @@ module Kameleoon
105
105
  end
106
106
 
107
107
  def handle_fetched_token(token, expires_in)
108
- now = Time.new.to_i
108
+ now = Time.new.to_f
109
109
  exp_time = now + expires_in - TOKEN_EXPIRATION_GAP
110
110
  if expires_in > TOKEN_OBSOLESCENCE_GAP
111
111
  obs_time = now + expires_in - TOKEN_OBSOLESCENCE_GAP
@@ -18,7 +18,7 @@ module Kameleoon
18
18
  @sent = false
19
19
  end
20
20
 
21
- def obtain_full_post_text_line
21
+ def query
22
22
  params = {
23
23
  eventType: EVENT_TYPE,
24
24
  nonce: Kameleoon::Utils.generate_random_string(Kameleoon::NONCE_LENGTH)
@@ -1,28 +1,43 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'kameleoon/data/conversion'
4
- require 'kameleoon/data/manager/data_array_storage'
4
+ require 'kameleoon/targeting/conditions/visitor_scope_condition'
5
5
 
6
6
  module Kameleoon
7
- # @api private
8
7
  module Targeting
9
- # ConversionCondition is a condition for checking targeting of conversions of visitor
10
- class ConversionCondition < Condition
8
+ class ConversionCondition < VisitorScopeCondition
11
9
  def initialize(json_condition)
12
- super(json_condition)
10
+ super(json_condition, VisitScope::VISITOR)
13
11
  @goal_id = json_condition['goalId']
14
12
  end
15
13
 
16
- def check(conversion_storage)
17
- return false unless conversion_storage.is_a?(Kameleoon::DataManager::DataArrayStorage)
18
- return true if @goal_id.nil?
14
+ def check(data)
15
+ return false unless data.is_a?(TargetingData)
19
16
 
20
- is_targeted = false
21
- conversion_storage.enumerate do |conversion|
22
- is_targeted = conversion.is_a?(Kameleoon::Conversion) && (@goal_id == conversion.goal_id)
23
- break if is_targeted
17
+ threshold = assignment_threshold(data.visitor_visits)
18
+ targeted_conversion?(data.conversions, threshold)
19
+ end
20
+
21
+ private
22
+
23
+ def targeted_conversion?(conversions, threshold)
24
+ return false unless conversions.respond_to?(:enumerate)
25
+
26
+ targeted = false
27
+ conversions.enumerate do |conversion|
28
+ targeted = (@goal_id.nil? || @goal_id == conversion.goal_id) && conversion.assignment_time >= threshold
29
+ break if targeted
30
+ end
31
+ targeted
32
+ end
33
+
34
+ class TargetingData
35
+ attr_reader :conversions, :visitor_visits
36
+
37
+ def initialize(conversions, visitor_visits)
38
+ @conversions = conversions
39
+ @visitor_visits = visitor_visits
24
40
  end
25
- is_targeted
26
41
  end
27
42
  end
28
43
  end
@@ -1,13 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'kameleoon/logging/kameleoon_logger'
4
- require 'kameleoon/targeting/condition'
4
+ require 'kameleoon/targeting/conditions/visitor_scope_condition'
5
5
 
6
6
  module Kameleoon
7
7
  # @api private
8
8
  module Targeting
9
9
  # ExclusiveExperiment represents an instance of Exclusive Experiment condition in user account
10
- class ExclusiveExperimentCondition < Condition
10
+ class ExclusiveExperimentCondition < VisitorScopeCondition
11
11
  module CampaignType
12
12
  EXPERIMENT = 'EXPERIMENT'
13
13
  PERSONALIZATION = 'PERSONALIZATION'
@@ -15,21 +15,22 @@ module Kameleoon
15
15
  end
16
16
 
17
17
  def initialize(json_condition)
18
- super(json_condition)
18
+ super(json_condition, VisitScope::VISITOR)
19
19
 
20
20
  @campaign_type = json_condition['campaignType']
21
21
  end
22
22
 
23
23
  def check(data)
24
- return false unless data.is_a?(ExclusiveExperimentInfo)
24
+ return false unless data.is_a?(TargetingData)
25
25
 
26
+ threshold = assignment_threshold(data.visitor_visits)
26
27
  case @campaign_type
27
28
  when CampaignType::EXPERIMENT
28
- return check_experiment(data)
29
+ return check_experiment(data, threshold)
29
30
  when CampaignType::PERSONALIZATION
30
- return check_personalization(data)
31
+ return check_personalization(data, threshold)
31
32
  when CampaignType::ANY
32
- return check_personalization(data) && check_experiment(data)
33
+ return check_personalization(data, threshold) && check_experiment(data, threshold)
33
34
  end
34
35
  Logging::KameleoonLogger.error("Unexpected campaign type for '#{type}' condition: '#{@campaign_type}'")
35
36
  false
@@ -37,23 +38,41 @@ module Kameleoon
37
38
 
38
39
  private
39
40
 
40
- def check_experiment(data)
41
- size = data.variations_storage&.size || 0
42
- size.zero? || (size == 1 && !data.variations_storage.get(data.current_experiment_id).nil?)
41
+ def check_experiment(data, threshold)
42
+ return true if data.variations.nil?
43
+
44
+ result = true
45
+ data.variations.enumerate do |variation|
46
+ if variation.experiment_id != data.current_experiment_id && variation.assignment_time >= threshold
47
+ result = false
48
+ break
49
+ end
50
+ end
51
+ result
43
52
  end
44
53
 
45
- def check_personalization(data)
46
- (data.personalizations_storage&.size || 0).zero?
54
+ def check_personalization(data, threshold)
55
+ return true if data.personalizations.nil?
56
+
57
+ result = true
58
+ data.personalizations.enumerate do |personalization|
59
+ if personalization.assignment_time >= threshold
60
+ result = false
61
+ break
62
+ end
63
+ end
64
+ result
47
65
  end
48
- end
49
66
 
50
- class ExclusiveExperimentInfo
51
- attr_reader :current_experiment_id, :variations_storage, :personalizations_storage
67
+ class TargetingData
68
+ attr_reader :current_experiment_id, :variations, :personalizations, :visitor_visits
52
69
 
53
- def initialize(current_experiment_id, variations_storage, personalizations_storage)
54
- @current_experiment_id = current_experiment_id
55
- @variations_storage = variations_storage
56
- @personalizations_storage = personalizations_storage
70
+ def initialize(current_experiment_id, variations, personalizations, visitor_visits = nil)
71
+ @current_experiment_id = current_experiment_id
72
+ @variations = variations
73
+ @personalizations = personalizations
74
+ @visitor_visits = visitor_visits
75
+ end
57
76
  end
58
77
  end
59
78
  end
@@ -1,17 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'kameleoon/logging/kameleoon_logger'
4
- require 'kameleoon/targeting/condition'
4
+ require 'kameleoon/targeting/conditions/visitor_scope_condition'
5
5
 
6
6
  module Kameleoon
7
7
  # @api private
8
8
  module Targeting
9
9
  # TargetExperiment represents an instance of Experiment condition in user account
10
- class TargetExperimentCondition < Condition
10
+ class TargetExperimentCondition < VisitorScopeCondition
11
11
  include Kameleoon::Exception
12
12
 
13
13
  def initialize(json_condition)
14
- super(json_condition)
14
+ super(json_condition, VisitScope::CURRENT_VISIT)
15
15
 
16
16
  @variation_id = json_condition['variationId'] || -1
17
17
  @experiment_id = json_condition['experimentId'] || -1
@@ -19,9 +19,10 @@ module Kameleoon
19
19
  end
20
20
 
21
21
  def check(data)
22
- return false unless data.is_a?(TargetExperimentInfo)
22
+ return false unless data.is_a?(TargetingData)
23
23
 
24
- variation = data.variations_storage&.get(@experiment_id)
24
+ variation = data.variations&.get(@experiment_id)
25
+ variation = nil if !variation.nil? && variation.assignment_time < assignment_threshold(data.visitor_visits)
25
26
  case @variation_match_type
26
27
  when Operator::ANY
27
28
  return !variation.nil?
@@ -33,13 +34,14 @@ module Kameleoon
33
34
  )
34
35
  false
35
36
  end
36
- end
37
37
 
38
- class TargetExperimentInfo
39
- attr_reader :variations_storage
38
+ class TargetingData
39
+ attr_reader :variations, :visitor_visits
40
40
 
41
- def initialize(variations_storage)
42
- @variations_storage = variations_storage
41
+ def initialize(variations, visitor_visits = nil)
42
+ @variations = variations
43
+ @visitor_visits = visitor_visits
44
+ end
43
45
  end
44
46
  end
45
47
  end
@@ -1,59 +1,63 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'kameleoon/targeting/condition'
3
+ require 'kameleoon/targeting/conditions/visitor_scope_condition'
4
4
  require 'kameleoon/exceptions'
5
5
 
6
6
  module Kameleoon
7
7
  # @api private
8
8
  module Targeting
9
- # TargetFeatureFlag represents an instance of FeatureFlag condition in user account
10
- class TargetFeatureFlagCondition < Condition
9
+ class TargetFeatureFlagCondition < VisitorScopeCondition
11
10
  include Kameleoon::Exception
12
11
 
13
12
  def initialize(json_condition)
14
- super(json_condition)
15
-
13
+ super(json_condition, VisitScope::CURRENT_VISIT)
16
14
  @feature_flag_id = json_condition['featureFlagId']
17
15
  @condition_variation_key = json_condition['variationKey']
18
16
  @condition_rule_id = json_condition['ruleId']
19
17
  end
20
18
 
21
19
  def check(data)
22
- return false unless data.is_a?(TargetFeatureFlagInfo) && data.variations_storage.size.positive?
20
+ return false unless valid_data?(data)
23
21
 
24
22
  get_rules(data).any? { |rule| check_rule(data, rule) }
25
23
  end
26
24
 
27
25
  private
28
26
 
27
+ def valid_data?(data)
28
+ data.is_a?(TargetingData) && !data.data_file.nil? && data.variations&.size.to_i.positive?
29
+ end
30
+
29
31
  def check_rule(data, rule)
30
32
  return false unless rule.is_a?(Configuration::Rule)
31
- return false if !@condition_rule_id.nil? && (@condition_rule_id != rule.id)
32
-
33
- variation = data.variations_storage.get(rule.experiment.id)
34
- return false if variation.nil?
33
+ return false if @condition_rule_id && @condition_rule_id != rule.id
35
34
 
36
- return true if @condition_variation_key.nil?
35
+ variation = data.variations.get(rule.experiment.id)
36
+ return false if variation.nil? || variation.assignment_time < assignment_threshold(data.visitor_visits)
37
37
 
38
- variation = data.data_file.variation_by_id[variation.variation_id]
38
+ check_variation_key(data.data_file, variation)
39
+ end
39
40
 
40
- return false unless variation.is_a?(Configuration::VariationByExposition)
41
+ def check_variation_key(data_file, variation)
42
+ return true if @condition_variation_key.nil?
41
43
 
42
- variation.variation_key == @condition_variation_key
44
+ resolved = data_file.variation_by_id[variation.variation_id]
45
+ resolved.is_a?(Configuration::VariationByExposition) &&
46
+ resolved.variation_key == @condition_variation_key
43
47
  end
44
48
 
45
49
  def get_rules(data)
46
- feature_flag = data.data_file.feature_flag_by_id[@feature_flag_id]
47
- feature_flag&.rules || []
50
+ data.data_file.feature_flag_by_id[@feature_flag_id]&.rules || []
48
51
  end
49
- end
50
52
 
51
- class TargetFeatureFlagInfo
52
- attr_reader :data_file, :variations_storage
53
+ class TargetingData
54
+ attr_reader :data_file, :variations, :visitor_visits
53
55
 
54
- def initialize(data_file, variations_storage)
55
- @data_file = data_file
56
- @variations_storage = variations_storage
56
+ def initialize(data_file, variations, visitor_visits = nil)
57
+ @data_file = data_file
58
+ @variations = variations
59
+ @visitor_visits = visitor_visits
60
+ end
57
61
  end
58
62
  end
59
63
  end
@@ -1,32 +1,34 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'kameleoon/targeting/condition'
3
+ require 'kameleoon/targeting/conditions/visitor_scope_condition'
4
4
 
5
5
  module Kameleoon
6
6
  # @api private
7
7
  module Targeting
8
8
  # TargetPersonalization represents an instance of Personalization condition in user account
9
- class TargetPersonalizationCondition < Condition
9
+ class TargetPersonalizationCondition < VisitorScopeCondition
10
10
  include Kameleoon::Exception
11
11
 
12
12
  def initialize(json_condition)
13
- super(json_condition)
13
+ super(json_condition, VisitScope::CURRENT_VISIT)
14
14
 
15
15
  @personalization_id = json_condition['personalizationId'] || -1
16
16
  end
17
17
 
18
18
  def check(data)
19
- return false unless data.is_a?(TargetPersonalizationInfo)
19
+ return false unless data.is_a?(TargetingData)
20
20
 
21
- !data.personalizations_storage&.get(@personalization_id).nil?
21
+ personalization = data.personalizations&.get(@personalization_id)
22
+ !personalization.nil? && personalization.assignment_time >= assignment_threshold(data.visitor_visits)
22
23
  end
23
- end
24
24
 
25
- class TargetPersonalizationInfo
26
- attr_reader :personalizations_storage
25
+ class TargetingData
26
+ attr_reader :personalizations, :visitor_visits
27
27
 
28
- def initialize(personalizations_storage)
29
- @personalizations_storage = personalizations_storage
28
+ def initialize(personalizations, visitor_visits = nil)
29
+ @personalizations = personalizations
30
+ @visitor_visits = visitor_visits
31
+ end
30
32
  end
31
33
  end
32
34
  end
@@ -19,9 +19,8 @@ module Kameleoon
19
19
 
20
20
  prev_visits = data.prev_visits
21
21
  if prev_visits.size >= 1
22
- now = (Time.now.to_f * 1000).to_i # ... * 1000 for convert seconds to milliseconds
23
22
  visit_time = prev_visits[@is_first_visit ? prev_visits.size - 1 : 0].time_started
24
- return check_targeting(now - visit_time)
23
+ return check_targeting(Time.now.to_f - visit_time)
25
24
  end
26
25
  false
27
26
  end
@@ -16,7 +16,7 @@ module Kameleoon
16
16
  return false unless data.is_a?(TargetingData) && !@condition_value.nil?
17
17
 
18
18
  number_of_visits_today = 0
19
- start_of_day = (Time.new.to_date.to_time.to_f * 1000).to_i # ... * 1000 to convert seconds to milliseconds
19
+ start_of_day = Time.new.to_date.to_time.to_f
20
20
  data.visitor_visits.prev_visits.each do |visit|
21
21
  break if visit.time_started < start_of_day
22
22
 
@@ -31,7 +31,7 @@ module Kameleoon
31
31
 
32
32
  def initialize(current_visit_time_started, visitor_visits)
33
33
  @current_visit_time_started =
34
- current_visit_time_started.is_a?(Integer) ? current_visit_time_started : (Time.new.to_f * 1000).to_i
34
+ current_visit_time_started.is_a?(Integer) ? current_visit_time_started : Time.new.to_f
35
35
  @visitor_visits = visitor_visits.is_a?(VisitorVisits) ? visitor_visits : VisitorVisits.new([])
36
36
  end
37
37
  end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'kameleoon/data/visitor_visits'
4
+ require 'kameleoon/targeting/condition'
5
+
6
+ module Kameleoon
7
+ # @api private
8
+ module Targeting
9
+ module VisitScope
10
+ CURRENT_VISIT = 'CURRENT_VISIT'
11
+ VISITOR = 'VISITOR'
12
+ end
13
+
14
+ class VisitorScopeCondition < Condition
15
+ MIN_VISITOR_VISIT_COUNT = 2
16
+ MAX_VISITOR_VISIT_COUNT = 25
17
+
18
+ def initialize(json_condition, default_visit_scope)
19
+ super(json_condition)
20
+ @visit_scope = parse_visit_scope(json_condition['visitScope'], default_visit_scope)
21
+ @visit_count = parse_visit_count(json_condition['visitCount'])
22
+ end
23
+
24
+ private
25
+
26
+ def assignment_threshold(visitor_visits)
27
+ return 0.0 unless visitor_visits.is_a?(Kameleoon::VisitorVisits)
28
+
29
+ prev_visits = visitor_visits.prev_visits
30
+ if @visit_scope == VisitScope::CURRENT_VISIT || @visit_count < MIN_VISITOR_VISIT_COUNT || prev_visits.empty?
31
+ return visitor_visits.time_started
32
+ end
33
+
34
+ visit_index = [[@visit_count - MIN_VISITOR_VISIT_COUNT, 0].max, prev_visits.size - 1].min
35
+ prev_visits[visit_index].time_started
36
+ end
37
+
38
+ def parse_visit_scope(value, default_visit_scope)
39
+ case value&.upcase
40
+ when VisitScope::CURRENT_VISIT
41
+ VisitScope::CURRENT_VISIT
42
+ when VisitScope::VISITOR
43
+ VisitScope::VISITOR
44
+ else
45
+ default_visit_scope
46
+ end
47
+ end
48
+
49
+ def parse_visit_count(value)
50
+ return value if value.is_a?(Integer) && value.positive?
51
+
52
+ MAX_VISITOR_VISIT_COUNT
53
+ end
54
+ end
55
+ end
56
+ end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'kameleoon/logging/kameleoon_logger'
4
4
  require 'kameleoon/targeting/condition'
5
+ require 'kameleoon/targeting/conditions/conversion_condition'
5
6
  require 'kameleoon/targeting/conditions/exclusive_experiment_condition'
6
7
  require 'kameleoon/targeting/conditions/sdk_language_condition'
7
8
  require 'kameleoon/targeting/conditions/segment_condition'
@@ -68,20 +69,26 @@ module Kameleoon
68
69
  }
69
70
  )
70
71
  when ConditionType::CONVERSIONS
71
- condition_data = visitor.conversions unless visitor.nil?
72
+ condition_data = ConversionCondition::TargetingData.new(visitor&.conversions, visitor&.visitor_visits)
72
73
  when ConditionType::SDK_LANGUAGE
73
74
  condition_data = SdkInfo.new(Kameleoon::SDK_NAME, Kameleoon::SDK_VERSION)
74
75
  when ConditionType::VISITOR_CODE
75
76
  condition_data = visitor_code
76
77
  when ConditionType::TARGET_FEATURE_FLAG
77
- condition_data = TargetFeatureFlagInfo.new(@data_manager.data_file, visitor.variations) unless visitor.nil?
78
+ condition_data = TargetFeatureFlagCondition::TargetingData.new(
79
+ @data_manager.data_file, visitor&.variations, visitor&.visitor_visits
80
+ )
78
81
  when ConditionType::TARGET_EXPERIMENT
79
- condition_data = TargetExperimentInfo.new(visitor&.variations)
82
+ condition_data = TargetExperimentCondition::TargetingData.new(visitor&.variations, visitor&.visitor_visits)
80
83
  when ConditionType::TARGET_PERSONALIZATION
81
- condition_data = TargetPersonalizationInfo.new(visitor&.personalizations)
84
+ condition_data = TargetPersonalizationCondition::TargetingData.new(
85
+ visitor&.personalizations, visitor&.visitor_visits
86
+ )
82
87
  when ConditionType::EXCLUSIVE_EXPERIMENT
83
88
  unless campaign_id.nil?
84
- condition_data = ExclusiveExperimentInfo.new(campaign_id, visitor&.variations, visitor&.personalizations)
89
+ condition_data = ExclusiveExperimentCondition::TargetingData.new(
90
+ campaign_id, visitor&.variations, visitor&.personalizations, visitor&.visitor_visits
91
+ )
85
92
  end
86
93
  when ConditionType::FIRST_VISIT,
87
94
  ConditionType::LAST_VISIT,
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Kameleoon
4
- SDK_VERSION = '3.19.0'
4
+ SDK_VERSION = '3.20.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.19.0
4
+ version: 3.20.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kameleoon
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-04-22 00:00:00.000000000 Z
11
+ date: 2026-05-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: em-http-request
@@ -178,6 +178,7 @@ files:
178
178
  - lib/kameleoon/targeting/conditions/visit_number_total_condition.rb
179
179
  - lib/kameleoon/targeting/conditions/visitor_code_condition.rb
180
180
  - lib/kameleoon/targeting/conditions/visitor_new_return_condition.rb
181
+ - lib/kameleoon/targeting/conditions/visitor_scope_condition.rb
181
182
  - lib/kameleoon/targeting/models.rb
182
183
  - lib/kameleoon/targeting/targeting_manager.rb
183
184
  - lib/kameleoon/targeting/tree_builder.rb