kameleoon-client-ruby 3.3.0 → 3.4.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 (67) hide show
  1. checksums.yaml +4 -4
  2. data/lib/kameleoon/configuration/custom_data_info.rb +16 -8
  3. data/lib/kameleoon/configuration/data_file.rb +37 -15
  4. data/lib/kameleoon/configuration/feature_flag.rb +10 -0
  5. data/lib/kameleoon/configuration/rule.rb +4 -0
  6. data/lib/kameleoon/configuration/settings.rb +13 -8
  7. data/lib/kameleoon/configuration/variation_exposition.rb +4 -0
  8. data/lib/kameleoon/data/browser.rb +4 -0
  9. data/lib/kameleoon/data/conversion.rb +4 -0
  10. data/lib/kameleoon/data/cookie.rb +4 -0
  11. data/lib/kameleoon/data/custom_data.rb +11 -3
  12. data/lib/kameleoon/data/data.rb +30 -4
  13. data/lib/kameleoon/data/device.rb +4 -0
  14. data/lib/kameleoon/data/geolocation.rb +5 -0
  15. data/lib/kameleoon/data/kcs_heat.rb +4 -0
  16. data/lib/kameleoon/data/manager/assigned_variation.rb +5 -0
  17. data/lib/kameleoon/data/manager/data_array_storage.rb +7 -0
  18. data/lib/kameleoon/data/manager/data_map_storage.rb +7 -0
  19. data/lib/kameleoon/data/manager/page_view_visit.rb +4 -0
  20. data/lib/kameleoon/data/manager/visitor.rb +197 -73
  21. data/lib/kameleoon/data/manager/visitor_manager.rb +54 -17
  22. data/lib/kameleoon/data/mapping_identifier.rb +33 -0
  23. data/lib/kameleoon/data/operating_system.rb +4 -0
  24. data/lib/kameleoon/data/page_view.rb +6 -1
  25. data/lib/kameleoon/data/unique_identifier.rb +11 -0
  26. data/lib/kameleoon/data/user_agent.rb +4 -0
  27. data/lib/kameleoon/data/visitor_visits.rb +4 -0
  28. data/lib/kameleoon/hybrid/manager.rb +13 -4
  29. data/lib/kameleoon/kameleoon_client.rb +303 -148
  30. data/lib/kameleoon/kameleoon_client_config.rb +64 -17
  31. data/lib/kameleoon/kameleoon_client_factory.rb +15 -2
  32. data/lib/kameleoon/logging/default_logger.rb +20 -0
  33. data/lib/kameleoon/logging/kameleoon_logger.rb +77 -0
  34. data/lib/kameleoon/logging/logger.rb +12 -0
  35. data/lib/kameleoon/managers/data/data_manager.rb +36 -0
  36. data/lib/kameleoon/managers/remote_data/remote_data_manager.rb +32 -15
  37. data/lib/kameleoon/managers/tracking/tracking_builder.rb +149 -0
  38. data/lib/kameleoon/managers/tracking/tracking_manager.rb +97 -0
  39. data/lib/kameleoon/managers/tracking/visitor_tracking_registry.rb +94 -0
  40. data/lib/kameleoon/managers/warehouse/warehouse_manager.rb +22 -5
  41. data/lib/kameleoon/network/access_token_source.rb +46 -14
  42. data/lib/kameleoon/network/cookie/cookie_manager.rb +45 -7
  43. data/lib/kameleoon/network/net_provider.rb +2 -3
  44. data/lib/kameleoon/network/network_manager.rb +16 -21
  45. data/lib/kameleoon/network/request.rb +14 -3
  46. data/lib/kameleoon/network/response.rb +4 -0
  47. data/lib/kameleoon/network/url_provider.rb +4 -4
  48. data/lib/kameleoon/real_time/real_time_configuration_service.rb +10 -11
  49. data/lib/kameleoon/sdk_version.rb +31 -0
  50. data/lib/kameleoon/targeting/condition.rb +4 -2
  51. data/lib/kameleoon/targeting/conditions/browser_condition.rb +3 -3
  52. data/lib/kameleoon/targeting/conditions/cookie_condition.rb +10 -10
  53. data/lib/kameleoon/targeting/conditions/geolocation_condition.rb +0 -1
  54. data/lib/kameleoon/targeting/conditions/number_condition.rb +4 -4
  55. data/lib/kameleoon/targeting/conditions/operating_system_condition.rb +1 -2
  56. data/lib/kameleoon/targeting/conditions/sdk_language_condition.rb +2 -1
  57. data/lib/kameleoon/targeting/conditions/segment_condition.rb +3 -3
  58. data/lib/kameleoon/targeting/conditions/string_value_condition.rb +2 -1
  59. data/lib/kameleoon/targeting/models.rb +0 -14
  60. data/lib/kameleoon/targeting/targeting_manager.rb +35 -7
  61. data/lib/kameleoon/targeting/tree_builder.rb +10 -5
  62. data/lib/kameleoon/types/remote_visitor_data_filter.rb +13 -0
  63. data/lib/kameleoon/types/variable.rb +4 -0
  64. data/lib/kameleoon/types/variation.rb +4 -0
  65. data/lib/kameleoon/utils.rb +18 -0
  66. data/lib/kameleoon/version.rb +1 -27
  67. metadata +12 -2
@@ -1,29 +1,56 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'kameleoon/logging/kameleoon_logger'
4
+
3
5
  module Kameleoon
4
6
  # KameleoonClient configuration which can be used instead of an external configuration file
5
7
  class KameleoonClientConfig
6
8
  DEFAULT_REFRESH_INTERVAL_MINUTES = 60
7
9
  DEFAULT_SESSION_DURATION_MINUTES = 30
8
10
  DEFAULT_TIMEOUT_MILLISECONDS = 10_000
11
+ DEFAULT_TRACKING_INTERVAL_MILLISECONDS = 1000
12
+ MIN_TRACKING_INTERVAL_MILLISECONDS = 300
13
+ MAX_TRACKING_INTERVAL_MILLISECONDS = 1000
9
14
 
10
15
  attr_reader :client_id, :client_secret, :refresh_interval_second, :session_duration_second,
11
- :default_timeout_millisecond, :environment, :top_level_domain, :verbose_mode
16
+ :default_timeout_millisecond, :tracking_interval_second, :environment, :top_level_domain,
17
+ :verbose_mode
18
+
19
+ def to_s
20
+ 'KameleoonClientConfig{' \
21
+ "client_id:'#{Utils::Strval.secret(@client_id)}'," \
22
+ "client_secret:'#{Utils::Strval.secret(@client_secret)}'," \
23
+ "refresh_interval_second:#{@refresh_interval_second}," \
24
+ "session_duration_second:#{@session_duration_second}," \
25
+ "environment:'#{@environment}'," \
26
+ "default_timeout_millisecond:#{@default_timeout_millisecond}," \
27
+ "top_level_domain:'#{@top_level_domain}'," \
28
+ "verbose_mode:#{verbose_mode}" \
29
+ '}'
30
+ end
12
31
 
32
+ # verbose_mode is DEPRECATED. Please use `KameleoonLogger.log_level` instead.
13
33
  def initialize(
14
34
  client_id,
15
35
  client_secret,
16
36
  refresh_interval_minute: DEFAULT_REFRESH_INTERVAL_MINUTES,
17
37
  session_duration_minute: DEFAULT_SESSION_DURATION_MINUTES,
18
38
  default_timeout_millisecond: DEFAULT_TIMEOUT_MILLISECONDS,
39
+ tracking_interval_millisecond: DEFAULT_TRACKING_INTERVAL_MILLISECONDS,
19
40
  environment: nil,
20
41
  top_level_domain: nil,
21
- verbose_mode: false
42
+ verbose_mode: nil
22
43
  )
23
44
  raise Exception::ConfigCredentialsInvalid, 'Client ID is not specified' if client_id&.empty? != false
24
45
  raise Exception::ConfigCredentialsInvalid, 'Client secret is not specified' if client_secret&.empty? != false
25
46
 
26
- @verbose_mode = verbose_mode || false
47
+ unless verbose_mode.nil?
48
+ Logging::KameleoonLogger.warning(
49
+ '[DEPRECATION] `verbose_mode` is deprecated. Please use `KameleoonLogger.log_level` instead.'
50
+ )
51
+ end
52
+
53
+ @verbose_mode = verbose_mode
27
54
 
28
55
  @client_id = client_id
29
56
  @client_secret = client_secret
@@ -31,8 +58,10 @@ module Kameleoon
31
58
  if refresh_interval_minute.nil?
32
59
  refresh_interval_minute = DEFAULT_REFRESH_INTERVAL_MINUTES
33
60
  elsif refresh_interval_minute <= 0
34
- log('Configuration refresh interval must have positive value. ' \
35
- "Default refresh interval (#{DEFAULT_REFRESH_INTERVAL_MINUTES} minutes) is applied.")
61
+ Logging::KameleoonLogger.warning(lambda {
62
+ 'Configuration refresh interval must have positive value. ' \
63
+ "Default refresh interval (#{DEFAULT_REFRESH_INTERVAL_MINUTES} minutes) was applied."
64
+ })
36
65
  refresh_interval_minute = DEFAULT_REFRESH_INTERVAL_MINUTES
37
66
  end
38
67
  @refresh_interval_second = refresh_interval_minute * 60
@@ -40,8 +69,10 @@ module Kameleoon
40
69
  if session_duration_minute.nil?
41
70
  session_duration_minute = DEFAULT_SESSION_DURATION_MINUTES
42
71
  elsif session_duration_minute <= 0
43
- log('Session duration must have positive value. ' \
44
- "Default session duration (#{DEFAULT_SESSION_DURATION_MINUTES} minutes) is applied.")
72
+ Logging::KameleoonLogger.warning(lambda {
73
+ 'Session duration must have positive value. ' \
74
+ "Default session duration (#{DEFAULT_SESSION_DURATION_MINUTES} minutes) was applied."
75
+ })
45
76
  session_duration_minute = DEFAULT_SESSION_DURATION_MINUTES
46
77
  end
47
78
  @session_duration_second = session_duration_minute * 60
@@ -49,17 +80,38 @@ module Kameleoon
49
80
  if default_timeout_millisecond.nil?
50
81
  @default_timeout_millisecond = DEFAULT_TIMEOUT_MILLISECONDS
51
82
  elsif default_timeout_millisecond <= 0
52
- log('Default timeout must have positive value. ' \
53
- "Default timeout (#{DEFAULT_TIMEOUT_MILLISECONDS} ms) is applied.")
83
+ Logging::KameleoonLogger.warning(lambda {
84
+ 'Default timeout must have positive value. ' \
85
+ "Default timeout (#{DEFAULT_TIMEOUT_MILLISECONDS} ms) was applied."
86
+ })
54
87
  @default_timeout_millisecond = DEFAULT_TIMEOUT_MILLISECONDS
55
88
  else
56
89
  @default_timeout_millisecond = default_timeout_millisecond
57
90
  end
58
91
 
92
+ if tracking_interval_millisecond.nil?
93
+ tracking_interval_millisecond = DEFAULT_TRACKING_INTERVAL_MILLISECONDS
94
+ elsif tracking_interval_millisecond < MIN_TRACKING_INTERVAL_MILLISECONDS
95
+ Logging::KameleoonLogger.warning(lambda {
96
+ 'Tracking interval must not be shorter than ' \
97
+ "#{MIN_TRACKING_INTERVAL_MILLISECONDS} ms. Minimum possible interval was applied."
98
+ })
99
+ tracking_interval_millisecond = MIN_TRACKING_INTERVAL_MILLISECONDS
100
+ elsif tracking_interval_millisecond > MAX_TRACKING_INTERVAL_MILLISECONDS
101
+ Logging::KameleoonLogger.warning(lambda {
102
+ 'Tracking interval must not be longer than ' \
103
+ "#{MAX_TRACKING_INTERVAL_MILLISECONDS} ms. Maximum possible interval was applied."
104
+ })
105
+ tracking_interval_millisecond = MAX_TRACKING_INTERVAL_MILLISECONDS
106
+ end
107
+ @tracking_interval_second = tracking_interval_millisecond / 1000.0
108
+
59
109
  @environment = environment
60
110
 
61
111
  if top_level_domain.nil?
62
- log('Setting top level domain is strictly recommended, otherwise you may have problems when using subdomains.')
112
+ Logging::KameleoonLogger.warning(
113
+ 'Setting top level domain is strictly recommended, otherwise you may have problems when using subdomains.'
114
+ )
63
115
  end
64
116
  @top_level_domain = top_level_domain
65
117
  end
@@ -67,7 +119,7 @@ module Kameleoon
67
119
  def self.read_from_yaml(path)
68
120
  yaml = YAML.load_file(path) if File.exist?(path)
69
121
  if yaml.nil?
70
- warn "Kameleoon SDK: Configuration file with path #{path} does not exist"
122
+ Logging::KameleoonLogger.warning(-> { "Configuration file with path '#{path}' does not exist" })
71
123
  yaml = {}
72
124
  end
73
125
  KameleoonClientConfig.new(
@@ -76,16 +128,11 @@ module Kameleoon
76
128
  refresh_interval_minute: yaml['refresh_interval_minute'],
77
129
  session_duration_minute: yaml['session_duration_minute'],
78
130
  default_timeout_millisecond: yaml['default_timeout_millisecond'],
131
+ tracking_interval_millisecond: yaml['tracking_interval_millisecond'],
79
132
  environment: yaml['environment'],
80
133
  top_level_domain: yaml['top_level_domain'],
81
134
  verbose_mode: yaml['verbose_mode']
82
135
  )
83
136
  end
84
-
85
- private
86
-
87
- def log(text)
88
- print "Kameleoon SDK Log: #{text}\n" if @verbose_mode
89
- end
90
137
  end
91
138
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'concurrent'
4
+ require 'kameleoon/logging/kameleoon_logger'
4
5
  require 'kameleoon/kameleoon_client'
5
6
  require 'kameleoon/kameleoon_client_config'
6
7
 
@@ -12,25 +13,37 @@ module Kameleoon
12
13
  @clients = Concurrent::Map.new
13
14
 
14
15
  def self.create(site_code, config: nil, config_path: CONFIG_PATH)
16
+ Logging::KameleoonLogger.info(
17
+ "CALL: KameleoonClientFactory.create(site_code: '%s', config: %s, config_path: '%s')",
18
+ site_code, config, config_path
19
+ )
15
20
  unless config.is_a?(KameleoonClientConfig)
16
21
  config_path = CONFIG_PATH unless config_path.is_a?(String)
17
22
  config = KameleoonClientConfig.read_from_yaml(config_path)
18
23
  end
19
24
  key = get_client_key(site_code, config.environment)
20
- @clients.compute_if_absent(key) do
25
+ client = @clients.compute_if_absent(key) do
21
26
  client = KameleoonClient.new(site_code, config)
22
- client.send(:log, "Client created with site code: #{site_code}")
23
27
  client.send(:fetch_configuration_initially)
24
28
  client
25
29
  end
30
+ Logging::KameleoonLogger.info(
31
+ "RETURN: KameleoonClientFactory.create(site_code: '%s', config: %s, config_path: '%s') -> (client)",
32
+ site_code, config, config_path
33
+ )
34
+ client
26
35
  end
27
36
 
28
37
  def self.forget(site_code, environment = nil)
38
+ Logging::KameleoonLogger.info("CALL: KameleoonClientFactory.forget(site_code: '%s', environment: '%s')",
39
+ site_code, environment)
29
40
  key = get_client_key(site_code, environment)
30
41
  @clients.compute_if_present(key) do |client|
31
42
  client.send(:dispose)
32
43
  nil
33
44
  end
45
+ Logging::KameleoonLogger.info("RETURN: KameleoonClientFactory.forget(site_code: '%s', environment: '%s')",
46
+ site_code, environment)
34
47
  end
35
48
 
36
49
  private_class_method
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'kameleoon/logging/logger'
4
+
5
+ module Kameleoon
6
+ module Logging
7
+ # A default implementation of a logger that prints log messages to the console.
8
+ # This logger implements the Logger interface.
9
+ class DefaultLogger < Logger
10
+
11
+ # Logs a message at the specified log level.
12
+ #
13
+ # @param level [LogLevel] the log level
14
+ # @param message [String] the log message
15
+ def log(level, message)
16
+ puts message
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'kameleoon/logging/default_logger'
4
+
5
+ module Kameleoon
6
+ module Logging
7
+ module LogLevel
8
+ NONE = 0
9
+ ERROR = 1
10
+ WARNING = 2
11
+ INFO = 3
12
+ DEBUG = 4
13
+
14
+ def self.name_from_level(log_level)
15
+ case log_level
16
+ when LogLevel::NONE
17
+ 'NONE'
18
+ when LogLevel::ERROR
19
+ 'ERROR'
20
+ when LogLevel::WARNING
21
+ 'WARNING'
22
+ when LogLevel::INFO
23
+ 'INFO'
24
+ when LogLevel::DEBUG
25
+ 'DEBUG'
26
+ end
27
+ end
28
+ end
29
+
30
+ module KameleoonLogger
31
+ extend self
32
+
33
+ @logger = DefaultLogger.new
34
+ @log_level = LogLevel::WARNING
35
+
36
+ attr_accessor :logger, :log_level
37
+
38
+ def log(level, data, *args)
39
+ return unless check_level(level)
40
+
41
+ if data.class.method_defined?(:call)
42
+ message = data.call
43
+ else
44
+ message = args.empty? ? data : data % args
45
+ end
46
+
47
+ write_message(level, message)
48
+ end
49
+
50
+ def info(data, *args)
51
+ log(LogLevel::INFO, data, *args)
52
+ end
53
+
54
+ def error(data, *args)
55
+ log(LogLevel::ERROR, data, *args)
56
+ end
57
+
58
+ def warning(data, *args)
59
+ log(LogLevel::WARNING, data, *args)
60
+ end
61
+
62
+ def debug(data, *args)
63
+ log(LogLevel::DEBUG, data, *args)
64
+ end
65
+
66
+ private
67
+
68
+ def check_level(level)
69
+ level <= @log_level && level != LogLevel::NONE
70
+ end
71
+
72
+ def write_message(level, message)
73
+ @logger.log(level, "Kameleoon [#{LogLevel.name_from_level(level)}]: #{message}")
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kameleoon
4
+ module Logging
5
+ class Logger
6
+
7
+ def log(level, message)
8
+ raise NotImplementedError, 'Subclasses must implement log(level, message)'
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kameleoon
4
+ module Managers
5
+ module Data
6
+ class DataManager
7
+ def initialize(data_file)
8
+ self.data_file = data_file
9
+ end
10
+
11
+ def data_file
12
+ @container.data_file
13
+ end
14
+
15
+ def data_file=(value)
16
+ @container = Container.new(value)
17
+ end
18
+
19
+ def consent_required?
20
+ @container.is_consent_required
21
+ end
22
+
23
+ class Container
24
+ attr_reader :data_file, :is_consent_required
25
+
26
+ def initialize(data_file)
27
+ @data_file = data_file
28
+ # Regarding GDPR policy we should set visitorCode if legal consent isn't required or we have at
29
+ # least one Targeted Delivery rule in datafile
30
+ @is_consent_required = data_file.settings.is_consent_required && !data_file.has_any_targeted_delivery_rule
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -1,54 +1,71 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'kameleoon/logging/kameleoon_logger'
3
4
  require 'kameleoon/managers/remote_data/remote_visitor_data'
4
5
  require 'kameleoon/types/remote_visitor_data_filter'
6
+ require 'kameleoon/utils'
5
7
 
6
8
  module Kameleoon
7
9
  module Managers
8
10
  module RemoteData
9
11
  class RemoteDataManager
10
- attr_reader :network_manager, :visitor_manger, :log_func
12
+ attr_reader :network_manager, :visitor_manger
11
13
 
12
- def initialize(network_manager, visitor_manger, log_func = nil)
14
+ def initialize(data_manager, network_manager, visitor_manger)
15
+ Logging::KameleoonLogger.debug('CALL: RemoteDataManager.new(data_manager, networkManager, visitorManager)')
16
+ @data_manager = data_manager
13
17
  @network_manager = network_manager
14
18
  @visitor_manger = visitor_manger
15
- @log_func = log_func
19
+ Logging::KameleoonLogger.debug('RETURN: RemoteDataManager.new(data_manager, networkManager, visitorManager)')
16
20
  end
17
21
 
18
22
  def get_data(key, timeout)
23
+ Logging::KameleoonLogger.debug("CALL: RemoteDataManager.get_data(key: '%s', timeout: %s)", key, timeout)
19
24
  response = @network_manager.get_remote_data(key, timeout)
20
- JSON.parse(response) if response
25
+ data = JSON.parse(response) if response
26
+ Logging::KameleoonLogger.debug(
27
+ "RETURN: RemoteDataManager.get_data(key: '%s', timeout: %s) -> (remote_data: %s)",
28
+ key, timeout, data
29
+ )
30
+ data
21
31
  rescue StandardError => e
22
- log("Parsing of visitor data of '#{key}' failed: #{e}")
32
+ Logging::KameleoonLogger.error("Parsing of remote data of '#{key}' failed: #{e}")
23
33
  raise
24
34
  end
25
35
 
26
- def get_visitor_data(visitor_code, add_data, filter = nil, is_unique_identifier = false, timeout = nil)
36
+ def get_visitor_data(visitor_code, add_data, filter = nil, timeout = nil)
37
+ Logging::KameleoonLogger.debug(
38
+ "CALL: RemoteDataManager.get_visitor_data(visitor_code: '%s', add_data: %s, filter: %s, timeout: %s)",
39
+ visitor_code, add_data, filter, timeout
40
+ )
27
41
  Utils::VisitorCode.validate(visitor_code)
28
- filter = Kameleoon::Types::RemoteVisitorDataFilter.new unless filter.is_a?(Kameleoon::Types::RemoteVisitorDataFilter)
42
+ filter = Types::RemoteVisitorDataFilter.new unless filter.is_a?(Types::RemoteVisitorDataFilter)
43
+ is_unique_identifier = @visitor_manger.get_visitor(visitor_code)&.is_unique_identifier || false
29
44
  response = @network_manager.get_remote_visitor_data(visitor_code, filter, is_unique_identifier, timeout)
30
45
  (data_to_add, data_to_return) = parse_custom_data_array(visitor_code, response)
31
46
  if add_data && !data_to_add.empty?
47
+ # Cannot use `VisitorManager.add_data` because it could use remote visitor data for mapping.
32
48
  visitor = @visitor_manger.get_or_create_visitor(visitor_code)
33
- visitor.add_data(@log_func, *data_to_add, overwrite: false)
49
+ visitor.add_data(*data_to_add, overwrite: false)
34
50
  end
51
+ Logging::KameleoonLogger.debug(
52
+ "RETURN: RemoteDataManager.get_visitor_data(visitor_code: '%s', add_data: %s, filter: %s," \
53
+ ' timeout: %s) -> (visitor_data: %s)',
54
+ visitor_code, add_data, filter, timeout, data_to_return
55
+ )
35
56
  data_to_return
36
57
  end
37
58
 
38
59
  ##
39
60
  # helper method used by `get_remote_visitor_data`
40
61
  def parse_custom_data_array(visitor_code, response)
41
- remote_visitor_data = Kameleoon::Managers::RemoteData::RemoteVisitorData.new(JSON.parse(response))
42
- remote_visitor_data.mark_data_as_sent(@visitor_manger.custom_data_info)
62
+ remote_visitor_data = RemoteVisitorData.new(JSON.parse(response))
63
+ remote_visitor_data.mark_data_as_sent(@data_manager.data_file.custom_data_info)
43
64
  [remote_visitor_data.collect_data_to_add, remote_visitor_data.collect_data_to_return]
44
65
  rescue StandardError => e
45
- log("Parsing of visitor data of '#{visitor_code}' failed: #{e}")
66
+ Logging::KameleoonLogger.error("Parsing of remote visitor data of '#{visitor_code}' failed: #{e}")
46
67
  raise
47
68
  end
48
-
49
- def log(text)
50
- @log_func&.call(text)
51
- end
52
69
  end
53
70
  end
54
71
  end
@@ -0,0 +1,149 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'kameleoon/data/custom_data'
4
+ require 'kameleoon/logging/kameleoon_logger'
5
+ require 'kameleoon/network/activity_event'
6
+ require 'kameleoon/network/uri_helper'
7
+
8
+ module Kameleoon
9
+ module Managers
10
+ module Tracking
11
+ class TrackingBuilder
12
+ attr_reader :visitor_codes_to_send, :visitor_codes_to_keep, :tracking_lines, :unsent_visitor_data
13
+
14
+ def initialize(visitor_codes, data_file, visitor_manager, request_size_limit)
15
+ @visitor_codes = visitor_codes
16
+ @data_file = data_file
17
+ @visitor_manager = visitor_manager
18
+ @request_size_limit = request_size_limit
19
+ @built = false
20
+ @total_size = 0
21
+ # result
22
+ @visitor_codes_to_send = []
23
+ @visitor_codes_to_keep = []
24
+ @tracking_lines = []
25
+ @unsent_visitor_data = []
26
+ end
27
+
28
+ # Not thread-safe
29
+ def build
30
+ return if @built
31
+
32
+ @visitor_codes.each do |visitor_code|
33
+ if @total_size <= @request_size_limit
34
+ visitor = @visitor_manager.get_visitor(visitor_code)
35
+ is_consent_given = consent_given?(visitor)
36
+ data = collect_tracking_data(visitor_code, visitor, is_consent_given)
37
+ if !data.empty?
38
+ log_visitor_track_sending(visitor_code, is_consent_given, data)
39
+ @visitor_codes_to_send.push(visitor_code)
40
+ @unsent_visitor_data.concat(data)
41
+ else
42
+ log_visitor_track_no_data(visitor_code, is_consent_given)
43
+ end
44
+ else
45
+ @visitor_codes_to_keep.push(visitor_code)
46
+ end
47
+ end
48
+ @built = true
49
+ end
50
+
51
+ private
52
+
53
+ def log_visitor_track_sending(visitor_code, is_consent_given, data)
54
+ Logging::KameleoonLogger.debug(
55
+ "Sending tracking request for unsent data %s of visitor '%s' with given (or not required) consent %s",
56
+ data, visitor_code, is_consent_given
57
+ )
58
+ end
59
+
60
+ def log_visitor_track_no_data(visitor_code, is_consent_given)
61
+ Logging::KameleoonLogger.debug(
62
+ "No data to send for visitor '%s' with given (or not required) consent %s",
63
+ visitor_code, is_consent_given
64
+ )
65
+ end
66
+
67
+ def consent_given?(visitor)
68
+ !@data_file.settings.is_consent_required || visitor&.legal_consent
69
+ end
70
+
71
+ def collect_tracking_data(visitor_code, visitor, is_consent_given)
72
+ use_mapping_value, visitor = create_self_visitor_link_if_required(visitor_code, visitor)
73
+ Logging::KameleoonLogger.info(
74
+ "'%s' was used as a %s for visitor data tracking.",
75
+ visitor_code, (use_mapping_value ? 'mapping value' : 'visitor code')
76
+ )
77
+ unsent_data = get_unsent_visitor_data(visitor, is_consent_given)
78
+ collect_tracking_lines(visitor_code, visitor, unsent_data, use_mapping_value)
79
+ unsent_data
80
+ end
81
+
82
+ def create_self_visitor_link_if_required(visitor_code, visitor)
83
+ is_mapped = !visitor&.mapping_identifier.nil?
84
+ is_unique_identifier = visitor&.is_unique_identifier
85
+ # need to find if anonymous visitor is behind unique (anonym doesn't exist if MappingIdentifier == null)
86
+ if is_unique_identifier && !is_mapped
87
+ # We haven't anonymous behind, in this case we should create "fake" anonymous with id == visitorCode
88
+ # and link it with with mapping value == visitorCode (like we do as we have real anonymous visitor)
89
+ visitor = @visitor_manager.add_data(
90
+ visitor_code, CustomData.new(@data_file.custom_data_info.mapping_identifier_index, visitor_code)
91
+ )
92
+ end
93
+ use_mapping_value = is_unique_identifier && (visitor_code != visitor&.mapping_identifier)
94
+ [use_mapping_value, visitor]
95
+ end
96
+
97
+ def get_unsent_visitor_data(visitor, is_consent_given)
98
+ unsent_data = []
99
+ unless visitor.nil?
100
+ if is_consent_given
101
+ visitor.enumerate_sendable_data do |data|
102
+ unsent_data.push(data) if data.unsent
103
+ next true
104
+ end
105
+ else
106
+ visitor.conversions.enumerate do |c|
107
+ unsent_data.push(c) if c.unsent
108
+ next true
109
+ end
110
+ if @data_file.has_any_targeted_delivery_rule
111
+ visitor.variations.enumerate do |av|
112
+ unsent_data.push(av) if av.unsent && (av.rule_type == Configuration::RuleType::TARGETED_DELIVERY)
113
+ next true
114
+ end
115
+ end
116
+ end
117
+ end
118
+ unsent_data.push(Network::ActivityEvent.new) if unsent_data.empty? && is_consent_given
119
+ unsent_data
120
+ end
121
+
122
+ def collect_tracking_lines(visitor_code, visitor, unsent_data, use_mapping_value)
123
+ visitor_code_param = Network::UriHelper.encode_query(
124
+ { (use_mapping_value ? :mappingValue : :visitorCode) => visitor_code }
125
+ )
126
+ user_agent = visitor&.user_agent
127
+ unsent_data.each do |data|
128
+ line = data.obtain_full_post_text_line
129
+ next if line.empty?
130
+
131
+ line = add_line_params(line, visitor_code_param, user_agent)
132
+ @tracking_lines.push(line)
133
+ @total_size += line.size
134
+ user_agent = nil
135
+ end
136
+ end
137
+
138
+ def add_line_params(tracking_line, visitor_code_param, user_agent)
139
+ tracking_line += "&#{visitor_code_param}"
140
+ unless user_agent.nil?
141
+ user_agent_param = Network::UriHelper.encode_query({ userAgent: user_agent })
142
+ tracking_line += "&#{user_agent_param}"
143
+ end
144
+ tracking_line
145
+ end
146
+ end
147
+ end
148
+ end
149
+ end