kameleoon-client-ruby 3.18.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 (47) hide show
  1. checksums.yaml +4 -4
  2. data/lib/kameleoon/configuration/data_file.rb +9 -4
  3. data/lib/kameleoon/configuration/variable.rb +12 -0
  4. data/lib/kameleoon/configuration/variation.rb +1 -1
  5. data/lib/kameleoon/data/browser.rb +1 -1
  6. data/lib/kameleoon/data/conversion.rb +12 -3
  7. data/lib/kameleoon/data/custom_data.rb +1 -1
  8. data/lib/kameleoon/data/data.rb +1 -1
  9. data/lib/kameleoon/data/device.rb +1 -1
  10. data/lib/kameleoon/data/geolocation.rb +1 -1
  11. data/lib/kameleoon/data/manager/assigned_variation.rb +2 -2
  12. data/lib/kameleoon/data/manager/page_view_visit.rb +2 -2
  13. data/lib/kameleoon/data/manager/visitor.rb +2 -2
  14. data/lib/kameleoon/data/manager/visitor_manager.rb +11 -1
  15. data/lib/kameleoon/data/mapping_identifier.rb +1 -1
  16. data/lib/kameleoon/data/operating_system.rb +1 -2
  17. data/lib/kameleoon/data/page_view.rb +1 -1
  18. data/lib/kameleoon/data/personalization.rb +4 -3
  19. data/lib/kameleoon/data/targeted_segment.rb +1 -1
  20. data/lib/kameleoon/data/visitor_visits.rb +1 -1
  21. data/lib/kameleoon/kameleoon_client.rb +18 -71
  22. data/lib/kameleoon/managers/data/data_manager.rb +16 -2
  23. data/lib/kameleoon/managers/remote_data/remote_visitor_data.rb +20 -5
  24. data/lib/kameleoon/managers/tracking/tracking_builder.rb +2 -2
  25. data/lib/kameleoon/network/access_token_source.rb +11 -3
  26. data/lib/kameleoon/network/activity_event.rb +1 -1
  27. data/lib/kameleoon/network/net_provider.rb +4 -1
  28. data/lib/kameleoon/network/network_manager.rb +2 -3
  29. data/lib/kameleoon/network/request.rb +6 -3
  30. data/lib/kameleoon/network/url_provider.rb +21 -3
  31. data/lib/kameleoon/targeting/conditions/conversion_condition.rb +28 -13
  32. data/lib/kameleoon/targeting/conditions/exclusive_experiment_condition.rb +38 -19
  33. data/lib/kameleoon/targeting/conditions/target_experiment_condition.rb +12 -10
  34. data/lib/kameleoon/targeting/conditions/target_feature_flag_condition.rb +26 -22
  35. data/lib/kameleoon/targeting/conditions/target_personalization_condition.rb +12 -10
  36. data/lib/kameleoon/targeting/conditions/time_elapsed_since_visit_condition.rb +1 -2
  37. data/lib/kameleoon/targeting/conditions/unknown_condition.rb +1 -0
  38. data/lib/kameleoon/targeting/conditions/visit_number_today_condition.rb +2 -2
  39. data/lib/kameleoon/targeting/conditions/visitor_scope_condition.rb +56 -0
  40. data/lib/kameleoon/targeting/targeting_manager.rb +12 -5
  41. data/lib/kameleoon/types/data_file.rb +14 -3
  42. data/lib/kameleoon/types/feature_flag.rb +22 -0
  43. data/lib/kameleoon/types/rule.rb +21 -0
  44. data/lib/kameleoon/types/variable.rb +8 -0
  45. data/lib/kameleoon/types/variation.rb +12 -0
  46. data/lib/kameleoon/version.rb +1 -1
  47. metadata +3 -2
@@ -5,6 +5,7 @@ require 'net/http'
5
5
  require 'time'
6
6
  require 'kameleoon/utils'
7
7
  require 'kameleoon/logging/kameleoon_logger'
8
+ require 'base64'
8
9
 
9
10
  module Kameleoon
10
11
  module Network
@@ -13,6 +14,7 @@ module Kameleoon
13
14
  TOKEN_OBSOLESCENCE_GAP = 1800 # in seconds
14
15
  JWT_ACCESS_TOKEN_FIELD = 'access_token'
15
16
  JWT_EXPIRES_IN_FIELD = 'expires_in'
17
+ BASIC_AUTHORIZATION_PREFIX = 'Basic '
16
18
 
17
19
  def initialize(network_manager, client_id, client_secret)
18
20
  Logging::KameleoonLogger.debug(lambda {
@@ -23,15 +25,21 @@ module Kameleoon
23
25
  @client_id = client_id
24
26
  @client_secret = client_secret
25
27
  @fetching = false
28
+ @basic_auth_token = AccessTokenSource.construct_basic_token(client_id, client_secret)
26
29
  Logging::KameleoonLogger.debug(lambda {
27
30
  format("RETURN: AccessTokenSource.new(network_manager, client_id: '%s', client_secret: '%s')",
28
31
  Utils::Strval.secret(client_id), Utils::Strval.secret(client_secret))
29
32
  })
30
33
  end
31
34
 
35
+ def self.construct_basic_token(client_id, client_secret)
36
+ basic_token_content = "#{client_id}:#{client_secret}"
37
+ "#{BASIC_AUTHORIZATION_PREFIX}#{Base64.strict_encode64(basic_token_content)}"
38
+ end
39
+
32
40
  def get_token(timeout = nil)
33
41
  Logging::KameleoonLogger.debug('CALL: AccessTokenSource.getToken(timeout: %s)', timeout)
34
- now = Time.new.to_i
42
+ now = Time.new.to_f
35
43
  token = @cached_token
36
44
  return call_fetch_token(timeout) if token.nil? || token.expired?(now)
37
45
 
@@ -70,7 +78,7 @@ module Kameleoon
70
78
 
71
79
  def fetch_token(timeout = nil)
72
80
  Logging::KameleoonLogger.debug('CALL: AccessTokenSource.fetch_token(timeout: %s)', timeout)
73
- response_content = @network_manager.fetch_access_jwtoken(@client_id, @client_secret, timeout)
81
+ response_content = @network_manager.fetch_access_jwtoken(@basic_auth_token, timeout)
74
82
  unless response_content
75
83
  Logging::KameleoonLogger.error('Failed to fetch access JWT')
76
84
  return nil
@@ -97,7 +105,7 @@ module Kameleoon
97
105
  end
98
106
 
99
107
  def handle_fetched_token(token, expires_in)
100
- now = Time.new.to_i
108
+ now = Time.new.to_f
101
109
  exp_time = now + expires_in - TOKEN_EXPIRATION_GAP
102
110
  if expires_in > TOKEN_OBSOLESCENCE_GAP
103
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)
@@ -17,7 +17,10 @@ module Kameleoon
17
17
  def collect_headers(request)
18
18
  headers = { 'Content-Type' => request.content_type }
19
19
  headers.merge!(request.extra_headers) unless request.extra_headers.nil?
20
- headers['Authorization'] = "Bearer #{request.access_token}" unless request.access_token.nil?
20
+ unless request.access_token.nil? || request.access_token.empty?
21
+ token_prefix = request.access_token.start_with?('Basic') ? '' : 'Bearer '
22
+ headers['Authorization'] = "#{token_prefix}#{request.access_token}"
23
+ end
21
24
  headers
22
25
  end
23
26
 
@@ -70,16 +70,15 @@ module Kameleoon
70
70
  unwrap_response(*make_call(request, true, TRACKING_CALL_ATTEMPT_NUMBER - 1, TRACKING_CALL_RETRY_DELAY))
71
71
  end
72
72
 
73
- def fetch_access_jwtoken(client_id, client_secret, timeout = nil)
73
+ def fetch_access_jwtoken(basic_auth_token, timeout = nil)
74
74
  url = @url_provider.make_access_token_url
75
75
  timeout = ensure_timeout(timeout)
76
76
  data_map = {
77
77
  grant_type: ACCESS_TOKEN_GRANT_TYPE,
78
- client_id: client_id,
79
- client_secret: client_secret
80
78
  }
81
79
  data = UriHelper.encode_query(data_map).encode('UTF-8')
82
80
  request = Request.new(Method::POST, url, ContentType::FORM, timeout, data: data)
81
+ request.authorize(basic_auth_token)
83
82
  unwrap_response(*make_call(request, false))
84
83
  end
85
84
 
@@ -11,12 +11,15 @@ module Kameleoon
11
11
  body = 'null'
12
12
  unless @data.nil?
13
13
  if @data.is_a?(String)
14
- body = @data.start_with?('grant_type=client_credentials') ? '****' : @data
15
- else
16
14
  body = @data
17
15
  end
18
16
  end
19
- "Request{method:'#{@method}',url:'#{@url}',headers:#{@extra_headers},body:'#{body}'}"
17
+
18
+ headers = ''
19
+ headers += 'Authorization: ***' unless access_token.nil?
20
+ headers += @extra_headers.to_s unless @extra_headers.nil?
21
+
22
+ "Request{method:'#{@method}',url:'#{@url}',headers:#{headers},body:'#{body}'}"
20
23
  end
21
24
 
22
25
  def initialize(method, url, content_type, timeout, extra_headers: nil, data: nil)
@@ -22,7 +22,7 @@ module Kameleoon
22
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
- ACCESS_TOKEN_URL_FORMAT = 'https://%s/oauth/token'
25
+ ACCESS_TOKEN_URL_FORMAT = 'https://%s/oauth/token?%s'
26
26
 
27
27
  TEST_DATA_API_DOMAIN = 'data.kameleoon.net'
28
28
  TEST_AUTOMATION_API_DOMAIN = 'api.kameleoon.net'
@@ -39,6 +39,15 @@ module Kameleoon
39
39
  @configuration_domain = DEFAULT_CONFIGURATION_DOMAIN
40
40
  @access_token_domain = DEFAULT_ACCESS_TOKEN_DOMAIN
41
41
  @is_custom_domain = false
42
+
43
+ url_params = {
44
+ sdkName: SDK_NAME,
45
+ sdkVersion: SDK_VERSION,
46
+ siteCode: @site_code
47
+ }
48
+ @access_token_url_params = UriHelper.encode_query(url_params)
49
+ url_params[:bodyUa] = true
50
+ @tracking_url_params = UriHelper.encode_query(url_params)
42
51
  update_domains(network_domain)
43
52
  end
44
53
 
@@ -60,7 +69,7 @@ module Kameleoon
60
69
  siteCode: @site_code,
61
70
  bodyUa: true
62
71
  }
63
- format(DATA_API_URL_FORMAT, @data_api_domain, TRACKING_PATH, UriHelper.encode_query(params))
72
+ format(DATA_API_URL_FORMAT, @data_api_domain, TRACKING_PATH, @tracking_url_params)
64
73
  end
65
74
 
66
75
  def make_visitor_data_get_url(visitor_code, filter, is_unique_identifier = false)
@@ -104,11 +113,20 @@ module Kameleoon
104
113
  end
105
114
 
106
115
  def make_access_token_url
107
- format(ACCESS_TOKEN_URL_FORMAT, @access_token_domain)
116
+ format(ACCESS_TOKEN_URL_FORMAT, @access_token_domain, @access_token_url_params)
108
117
  end
109
118
 
110
119
  private
111
120
 
121
+ def make_post_query_base(site_code)
122
+ data_map = {
123
+ "sdkName": Kameleoon::SDK_NAME,
124
+ "sdkVersion": Kameleoon::SDK_VERSION,
125
+ "siteCode": site_code
126
+ }
127
+ data = UriHelper.encode_query(data_map).encode('UTF-8')
128
+ end
129
+
112
130
  def update_domains(network_domain)
113
131
  return if network_domain.nil? || network_domain.empty?
114
132
 
@@ -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
@@ -8,6 +8,7 @@ module Kameleoon
8
8
  # UnknownCondition represents not defined condition, always returns that visitor is targeted (true)
9
9
  class UnknownCondition < Condition
10
10
  def check(_data)
11
+ Logging::KameleoonLogger.warning('Condition of unknown type \'%s\' evaluated as true', type)
11
12
  true
12
13
  end
13
14
  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,