kameleoon-client-ruby 1.1.2 → 2.1.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 (34) hide show
  1. checksums.yaml +4 -4
  2. data/lib/kameleoon/client.rb +541 -404
  3. data/lib/kameleoon/configuration/experiment.rb +42 -0
  4. data/lib/kameleoon/configuration/feature_flag.rb +30 -0
  5. data/lib/kameleoon/configuration/rule.rb +57 -0
  6. data/lib/kameleoon/configuration/settings.rb +20 -0
  7. data/lib/kameleoon/configuration/variable.rb +23 -0
  8. data/lib/kameleoon/configuration/variation.rb +31 -0
  9. data/lib/kameleoon/configuration/variation_exposition.rb +23 -0
  10. data/lib/kameleoon/cookie.rb +13 -6
  11. data/lib/kameleoon/data.rb +60 -40
  12. data/lib/kameleoon/exceptions.rb +46 -23
  13. data/lib/kameleoon/factory.rb +21 -18
  14. data/lib/kameleoon/hybrid/manager.rb +60 -0
  15. data/lib/kameleoon/real_time/real_time_configuration_service.rb +98 -0
  16. data/lib/kameleoon/real_time/real_time_event.rb +22 -0
  17. data/lib/kameleoon/real_time/sse_client.rb +111 -0
  18. data/lib/kameleoon/real_time/sse_message.rb +23 -0
  19. data/lib/kameleoon/real_time/sse_request.rb +59 -0
  20. data/lib/kameleoon/request.rb +14 -13
  21. data/lib/kameleoon/storage/cache.rb +84 -0
  22. data/lib/kameleoon/storage/cache_factory.rb +23 -0
  23. data/lib/kameleoon/storage/variation_storage.rb +42 -0
  24. data/lib/kameleoon/storage/visitor_variation.rb +20 -0
  25. data/lib/kameleoon/targeting/condition.rb +17 -5
  26. data/lib/kameleoon/targeting/condition_factory.rb +9 -2
  27. data/lib/kameleoon/targeting/conditions/custom_datum.rb +67 -48
  28. data/lib/kameleoon/targeting/conditions/exclusive_experiment.rb +29 -0
  29. data/lib/kameleoon/targeting/conditions/target_experiment.rb +44 -0
  30. data/lib/kameleoon/targeting/models.rb +36 -36
  31. data/lib/kameleoon/utils.rb +4 -1
  32. data/lib/kameleoon/version.rb +4 -2
  33. metadata +35 -3
  34. data/lib/kameleoon/query_graphql.rb +0 -76
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'kameleoon/targeting/models'
4
+
5
+ module Kameleoon
6
+ # Module which contains all internal data of SDK
7
+ module Configuration
8
+ # Class for manage all experiments and old feature flags
9
+ class Experiment
10
+ attr_accessor :id, :status, :site_enabled, :deviations, :respool_time, :variations, :targeting_segment
11
+
12
+ def self.create_from_array(array)
13
+ array&.map { |it| Experiment.new(it) }
14
+ end
15
+
16
+ def initialize(experiment_hash)
17
+ @id = experiment_hash['id']
18
+ @status = experiment_hash['status']
19
+ @site_enabled = experiment_hash['siteEnabled']
20
+ unless experiment_hash['deviations'].nil?
21
+ @deviations =
22
+ Hash[*experiment_hash['deviations'].map do |it|
23
+ [it['variationId'] == 'origin' ? '0' : it['variationId'], it['value']]
24
+ end.flatten]
25
+ end
26
+ unless experiment_hash['respoolTime'].nil?
27
+ @respool_time =
28
+ Hash[*experiment_hash['respoolTime'].map do |it|
29
+ [it['variationId'] == 'origin' ? '0' : it['variationId'], it['value']]
30
+ end.flatten]
31
+ end
32
+ unless experiment_hash['variations'].nil?
33
+ @variations =
34
+ experiment_hash['variations'].map do |it|
35
+ { 'id' => it['id'].to_i, 'customJson' => it['customJson'] }
36
+ end
37
+ end
38
+ @targeting_segment = Kameleoon::Targeting::Segment.new((experiment_hash['segment'])) if experiment_hash['segment']
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'rule'
4
+ require_relative 'variation'
5
+
6
+ module Kameleoon
7
+ # Module which contains all internal data of SDK
8
+ module Configuration
9
+ # Class for manage all feature flags with rules
10
+ class FeatureFlag
11
+ attr_accessor :id, :feature_key, :variations, :default_variation_key, :rules
12
+
13
+ def self.create_from_array(array)
14
+ array&.map { |it| FeatureFlag.new(it) }
15
+ end
16
+
17
+ def initialize(hash)
18
+ @id = hash['id']
19
+ @feature_key = hash['featureKey']
20
+ @default_variation_key = hash['defaultVariationKey']
21
+ @variations = Variation.create_from_array(hash['variations'])
22
+ @rules = Rule.create_from_array(hash['rules'])
23
+ end
24
+
25
+ def get_variation_key(key)
26
+ variations.select { |v| v.key == key }.first
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'variation_exposition'
4
+ require 'kameleoon/targeting/models'
5
+
6
+ module Kameleoon
7
+ # Module which contains all internal data of SDK
8
+ module Configuration
9
+ # RuleType has a possible rule types
10
+ module RuleType
11
+ EXPERIMENTATION = 'EXPERIMENTATION'
12
+ TARGETED_DELIVERY = 'TARGETED_DELIVERY'
13
+ end
14
+
15
+ # Rule is a class for new rules of feature flags
16
+ class Rule
17
+ attr_reader :id, :order, :type, :exposition, :experiment_id, :variation_by_exposition, :respool_time
18
+ attr_accessor :targeting_segment
19
+
20
+ def self.create_from_array(array)
21
+ array&.map { |it| Rule.new(it) }
22
+ end
23
+
24
+ def initialize(hash)
25
+ @id = hash['id']
26
+ @order = hash['order']
27
+ @type = hash['type']
28
+ @exposition = hash['exposition']
29
+ @experiment_id = hash['experimentId']
30
+ @respool_time = hash['respoolTime']
31
+ @variation_by_exposition = VariationByExposition.create_from_array(hash['variationByExposition'])
32
+ @targeting_segment = Kameleoon::Targeting::Segment.new((hash['segment'])) if hash['segment']
33
+ end
34
+
35
+ def get_variation(hash_double)
36
+ total = 0.0
37
+ variation_by_exposition.each do |var_by_exp|
38
+ total += var_by_exp.exposition
39
+ return var_by_exp if total >= hash_double
40
+ end
41
+ nil
42
+ end
43
+
44
+ def get_variation_id_by_key(key)
45
+ variation_by_exposition.select { |v| v.variation_key == key }.first&.variation_id
46
+ end
47
+
48
+ def experiment_type?
49
+ @type == RuleType::EXPERIMENTATION
50
+ end
51
+
52
+ def targeted_delivery_type?
53
+ @type == RuleType::TARGETED_DELIVERY
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kameleoon
4
+ # Module which contains all internal data of SDK
5
+ module Configuration
6
+ # KameleoonConfigurationSettings is used for saving setting's parameters, e.g
7
+ # state of real time update for site code and etc
8
+ class Settings
9
+ attr_accessor :real_time_update
10
+
11
+ def initialize
12
+ @real_time_update = false
13
+ end
14
+
15
+ def update(configuration)
16
+ @real_time_update = configuration['realTimeUpdate'] || false
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'experiment'
4
+
5
+ module Kameleoon
6
+ # Module which contains all internal data of SDK
7
+ module Configuration
8
+ # Variable class contains key / type / value of variable
9
+ class Variable
10
+ attr_accessor :key, :type, :value
11
+
12
+ def self.create_from_array(array)
13
+ array&.map { |it| Variable.new(it) }
14
+ end
15
+
16
+ def initialize(hash)
17
+ @key = hash['key']
18
+ @type = hash['type']
19
+ @value = hash['value']
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'variable'
4
+
5
+ module Kameleoon
6
+ # Module which contains all internal data of SDK
7
+ module Configuration
8
+ # Constant values of types of variations
9
+ module VariationType
10
+ VARIATION_OFF = 'off'
11
+ end
12
+
13
+ # Variation of feature flag
14
+ class Variation
15
+ attr_accessor :key, :variables
16
+
17
+ def self.create_from_array(array)
18
+ array&.map { |it| Variation.new(it) }
19
+ end
20
+
21
+ def initialize(hash)
22
+ @key = hash['key']
23
+ @variables = Variable.create_from_array(hash['variables'])
24
+ end
25
+
26
+ def get_variable_by_key(key)
27
+ variables.select { |var| var.key == key }.first
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'experiment'
4
+
5
+ module Kameleoon
6
+ # Module which contains all internal data of SDK
7
+ module Configuration
8
+ # VariationByExposition represents a variation with exposition rate for rule
9
+ class VariationByExposition
10
+ attr_accessor :variation_key, :variation_id, :exposition
11
+
12
+ def self.create_from_array(array)
13
+ array&.map { |it| VariationByExposition.new(it) }
14
+ end
15
+
16
+ def initialize(hash)
17
+ @variation_key = hash['variationKey']
18
+ @variation_id = hash['variationId']
19
+ @exposition = hash['exposition']
20
+ end
21
+ end
22
+ end
23
+ end
@@ -23,15 +23,22 @@ module Kameleoon
23
23
  end
24
24
 
25
25
  def obtain_hash_double(visitor_code, respool_times = {}, container_id = '')
26
+ obtain_hash_double_helper(visitor_code, respool_times, container_id, '')
27
+ end
28
+
29
+ def obtain_hash_double_rule(visitor_code, container_id = '', suffix = '')
30
+ obtain_hash_double_helper(visitor_code, {}, container_id, suffix)
31
+ end
32
+
33
+ def obtain_hash_double_helper(visitor_code, respool_times, container_id, suffix)
26
34
  identifier = visitor_code.to_s
27
35
  identifier += container_id.to_s
28
- if !respool_times.nil? && !respool_times.empty?
29
- identifier += respool_times.sort.to_h.values.join.to_s
30
- end
31
- (Digest::SHA256.hexdigest(identifier.encode('UTF-8')).to_i(16) / (BigDecimal("2") ** BigDecimal("256"))).round(16)
36
+ identifier += suffix.to_s
37
+ identifier += respool_times.sort.to_h.values.join.to_s if !respool_times.nil? && !respool_times.empty?
38
+ (Digest::SHA256.hexdigest(identifier.encode('UTF-8')).to_i(16) / (BigDecimal('2')**BigDecimal('256'))).round(16)
32
39
  end
33
-
34
- def check_visitor_code(visitor_code)
40
+
41
+ def check_visitor_code(visitor_code)
35
42
  if visitor_code.nil?
36
43
  check_default_visitor_code('')
37
44
  elsif
@@ -1,12 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'kameleoon/exceptions'
4
+
1
5
  module Kameleoon
2
6
  NONCE_LENGTH = 16
3
7
 
4
8
  module DataType
5
- CUSTOM = "CUSTOM"
6
- BROWSER = "BROWSER"
7
- CONVERSION = "CONVERSION"
8
- INTEREST = "INTEREST"
9
- PAGE_VIEW = "PAGE_VIEW"
9
+ CUSTOM = 'CUSTOM'
10
+ BROWSER = 'BROWSER'
11
+ CONVERSION = 'CONVERSION'
12
+ DEVICE = 'DEVICE'
13
+ PAGE_VIEW = 'PAGE_VIEW'
10
14
  end
11
15
 
12
16
  module BrowserType
@@ -18,6 +22,12 @@ module Kameleoon
18
22
  OTHER = 5
19
23
  end
20
24
 
25
+ module DeviceType
26
+ PHONE = 'PHONE'
27
+ TABLET = 'TABLET'
28
+ DESKTOP = 'DESKTOP'
29
+ end
30
+
21
31
  class Data
22
32
  attr_accessor :instance, :sent
23
33
 
@@ -42,38 +52,42 @@ module Kameleoon
42
52
  end
43
53
 
44
54
  class CustomData < Data
45
- attr_accessor :id, :value
55
+ attr_reader :id, :values
46
56
 
47
57
  # @param [Integer] id Id of the custom data
48
58
  # @param [String] value Value of the custom data
49
59
  #
50
60
  # @overload
51
61
  # @param [Hash] hash Json value encoded in a hash.
52
- def initialize(*args)
62
+ def initialize(arg0, *args)
53
63
  @instance = DataType::CUSTOM
54
64
  @sent = false
55
- unless args.empty?
56
- if args.length == 1
57
- hash = args.first
58
- if hash["id"].nil?
59
- raise NotFoundError.new("id")
60
- end
61
- @id = hash["id"].to_s
62
- if hash["value"].nil?
63
- raise NotFoundError.new(hash["value"])
64
- end
65
- @value = hash["value"]
66
- elsif args.length == 2
67
- @id = args[0].to_s
68
- @value = args[1]
65
+ if arg0.is_a?(Hash)
66
+ hash = arg0
67
+ id = hash['id']
68
+ raise Kameleoon::Exception::NotFound.new('id') if id.nil?
69
+ @id = id.to_s
70
+ value = hash['value']
71
+ values = hash['values']
72
+ raise Kameleoon::Exception::NotFound.new('values') if values.nil? && value.nil?
73
+ if values.nil?
74
+ @values = [value]
75
+ else
76
+ @values = values.dup
77
+ @values.append(value) unless value.nil?
69
78
  end
79
+ else
80
+ @id = arg0.to_s
81
+ @values = args
70
82
  end
71
83
  end
72
84
 
73
85
  def obtain_full_post_text_line
74
- to_encode = "[[\"" + @value.to_s.gsub("\"", "\\\"") + "\",1]]"
86
+ return '' if @values.empty?
87
+
88
+ str_values = "[[\"#{@values.join('",1],["')}\",1]]"
75
89
  nonce = Kameleoon::Utils.generate_random_string(NONCE_LENGTH)
76
- "eventType=customData&index=" + @id.to_s + "&valueToCount=" + encode(to_encode) + "&overwrite=true&nonce=" + nonce
90
+ "eventType=customData&index=#{@id}&valueToCount=#{encode(str_values)}&overwrite=true&nonce=#{nonce}"
77
91
  end
78
92
  end
79
93
 
@@ -89,7 +103,7 @@ module Kameleoon
89
103
 
90
104
  def obtain_full_post_text_line
91
105
  nonce = Kameleoon::Utils.generate_random_string(NONCE_LENGTH)
92
- "eventType=staticData&browser=" + @browser.to_s + "&nonce=" + nonce
106
+ "eventType=staticData&browserIndex=" + @browser.to_s + "&nonce=" + nonce
93
107
  end
94
108
  end
95
109
 
@@ -98,22 +112,22 @@ module Kameleoon
98
112
 
99
113
  # @param [String] url Url of the page
100
114
  # @param [String] title Title of the page
101
- # @param [Integer] referrer Optional field - Referrer id
102
- def initialize(url, title, referrer = nil)
115
+ # @param [Array] referrers Optional field - Referrer ids
116
+ def initialize(url, title, referrers = nil)
103
117
  @instance = DataType::PAGE_VIEW
104
118
  @sent = false
105
119
  @url = url
106
120
  @title = title
107
- @referrer = referrer
121
+ @referrers = referrers
108
122
  end
109
123
 
110
124
  def obtain_full_post_text_line
111
125
  nonce = Kameleoon::Utils.generate_random_string(NONCE_LENGTH)
112
- referrer_text = ""
113
- unless @referrer.nil?
114
- referrer_text = "&referrers=[" + @referrer.to_s + "]"
126
+ referrer_text = ''
127
+ unless @referrers.nil?
128
+ referrer_text = "&referrersIndices=" + @referrers.to_s
115
129
  end
116
- "eventType=page&href=" + encode(@url) + "&title=" + @title + "&keyPages=[]" + referrer_text + "&nonce=" + nonce
130
+ "eventType=page&href=" + encode(@url) + "&title=" + @title + referrer_text + "&nonce=" + nonce
117
131
  end
118
132
  end
119
133
 
@@ -137,19 +151,25 @@ module Kameleoon
137
151
  end
138
152
  end
139
153
 
140
- class Interest < Data
141
- attr_accessor :index
142
-
143
- # @param [Integer] index Index of the interest
144
- def initialize(index)
145
- @instance = DataType::INTEREST
154
+ # Device uses for sending deviceType parameter for tracking calls
155
+ class Device < Data
156
+ def initialize(device_type)
157
+ @instance = DataType::DEVICE
146
158
  @sent = false
147
- @index = index
159
+ @device_type = device_type
148
160
  end
149
161
 
150
162
  def obtain_full_post_text_line
151
163
  nonce = Kameleoon::Utils.generate_random_string(NONCE_LENGTH)
152
- "eventType=interests&indexes=[" + @index.to_s + "]&fresh=true&nonce=" + nonce
164
+ "eventType=staticData&deviceType=#{@device_type}&nonce=#{nonce}"
165
+ end
166
+ end
167
+
168
+ # UserAgent uses for changing User-Agent header for tracking calls
169
+ class UserAgent
170
+ attr_accessor :value
171
+ def initialize(value)
172
+ @value = value
153
173
  end
154
174
  end
155
- end
175
+ end
@@ -1,59 +1,82 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Kameleoon
2
4
  module Exception
5
+ # Base Error
3
6
  class KameleoonError < ::StandardError
4
7
  def initialize(message = nil)
5
- super("Kameleoon error: " + message)
8
+ super("Kameleoon error: #{message}")
6
9
  end
7
10
  end
11
+
12
+ # Base Not Found Error
8
13
  class NotFound < KameleoonError
9
- def initialize(value = "")
10
- super(value.to_s + " not found.")
14
+ def initialize(value = '')
15
+ super("#{value} not found.")
11
16
  end
12
17
  end
18
+
19
+ # Variation Not Found
13
20
  class VariationConfigurationNotFound < NotFound
14
- def initialize(id = "")
15
- super("Variation " + id.to_s)
21
+ def initialize(id = '')
22
+ super("Variation #{id}")
16
23
  end
17
24
  end
18
- class ExperimentConfigurationNotFound < NotFound
19
- def initialize(id = "")
20
- super("Experiment " + id.to_s)
25
+
26
+ # Experiment Configuration Not Found
27
+ class ExperimentConfigurationNotFound < NotFound
28
+ def initialize(id = '')
29
+ super("Experiment #{id}")
21
30
  end
22
31
  end
32
+
33
+ # Feature Flag Configuration Not Found
23
34
  class FeatureConfigurationNotFound < NotFound
24
- def initialize(id = "")
25
- super("Feature flag " + id.to_s)
35
+ def initialize(id = '')
36
+ super("Feature flag #{id}")
26
37
  end
27
38
  end
39
+
40
+ # Feature Variable Not Found
28
41
  class FeatureVariableNotFound < NotFound
29
- def initialize(key = "")
30
- super("Feature variable " + key.to_s)
42
+ def initialize(key = '')
43
+ super("Feature variable #{key}")
31
44
  end
32
45
  end
46
+
47
+ # Credentials Not Found
33
48
  class CredentialsNotFound < NotFound
34
49
  def initialize
35
- super("Credentials")
50
+ super('Credentials')
36
51
  end
37
52
  end
53
+
54
+ # Not Targeted (when visitor is not targeted for experiment or feature flag)
38
55
  class NotTargeted < KameleoonError
39
- def initialize(visitor_code = "")
40
- super("Visitor " + visitor_code + " is not targeted.")
56
+ def initialize(visitor_code = '')
57
+ super("Visitor #{visitor_code} is not targeted.")
41
58
  end
42
59
  end
43
- class NotActivated < KameleoonError
44
- def initialize(visitor_code = "")
45
- super("Visitor " + visitor_code + " is not activated.")
60
+
61
+ # Not Allocated (when visitor is not allocated for experiment)
62
+ class NotAllocated < KameleoonError
63
+ def initialize(visitor_code = '')
64
+ super("Visitor #{visitor_code} is not targeted.")
46
65
  end
47
66
  end
67
+
68
+ # Visitor Code Not Valod (empty or length > 255)
48
69
  class VisitorCodeNotValid < KameleoonError
49
- def initialize(message = "")
50
- super("Visitor code not valid: " + message)
70
+ def initialize(message = '')
71
+ super("Visitor code not valid: #{message}")
51
72
  end
52
73
  end
74
+
75
+ # SiteCode Disabled
53
76
  class SiteCodeDisabled < KameleoonError
54
- def initialize(message = "")
55
- super("Site with siteCode '" + message + "' is disabled")
77
+ def initialize(message = '')
78
+ super("Site with siteCode '#{message}' is disabled")
56
79
  end
57
80
  end
58
81
  end
59
- end
82
+ end
@@ -1,26 +1,29 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'kameleoon/client'
2
4
 
3
5
  module Kameleoon
6
+ # A Factory class for creating kameleoon clients
4
7
  module ClientFactory
5
8
  CONFIGURATION_UPDATE_INTERVAL = '60m'
6
- CONFIG_PATH = "/etc/kameleoon/client-ruby.yaml"
7
- DEFAULT_TIMEOUT = 2000 #milli-seconds
9
+ CONFIG_PATH = '/etc/kameleoon/client-ruby.yaml'
10
+ DEFAULT_TIMEOUT = 2000 # milli-seconds
11
+
12
+ @clients = {}
13
+
14
+ def self.create(site_code, config_path = CONFIG_PATH, client_id = nil, client_secret = nil)
15
+ if @clients[site_code].nil?
16
+ client = Client.new(site_code, config_path, CONFIGURATION_UPDATE_INTERVAL,
17
+ DEFAULT_TIMEOUT, client_id, client_secret)
18
+ client.send(:log, "Client created with site code: #{site_code}")
19
+ client.send(:fetch_configuration)
20
+ @clients.store(site_code, client)
21
+ end
22
+ @clients[site_code]
23
+ end
8
24
 
9
- ##
10
- # Create a kameleoon client object, each call create a new client.
11
- # The starting point for using the SDK is the initialization step. All interaction with the SDK is done through an object of the Kameleoon::Client class, therefore you need to create this object via Kameleoon::ClientFactory create static method.
12
- #
13
- # @param [String] site_code Site code
14
- # @param [Boolean] blocking - optional, default is false
15
- #
16
- # @return [Kameleoon::Client]
17
- #
18
- def self.create(site_code, blocking = false, config_path = CONFIG_PATH, client_id = nil, client_secret = nil)
19
- client = Client.new(site_code, config_path, blocking, CONFIGURATION_UPDATE_INTERVAL, DEFAULT_TIMEOUT, client_id, client_secret)
20
- client.send(:log, "Warning: you are using the blocking mode") if blocking
21
- client.send(:log, "Client created with site code: " + site_code.to_s)
22
- client.send(:fetch_configuration)
23
- client
25
+ def self.forget(site_code)
26
+ @clients.delete(site_code)
24
27
  end
25
28
  end
26
- end
29
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kameleoon
4
+ module Hybrid
5
+ TC_INIT = 'window.kameleoonQueue=window.kameleoonQueue||[];'
6
+ TC_ASSIGN_VARIATION_FORMAT = "window.kameleoonQueue.push(['Experiments.assignVariation',%d,%d]);"
7
+ TC_TRIGGER_FORMAT = "window.kameleoonQueue.push(['Experiments.trigger',%d,true]);"
8
+ TC_ASSIGN_VARIATION_TRIGGER_FORMAT = TC_ASSIGN_VARIATION_FORMAT + TC_TRIGGER_FORMAT
9
+
10
+ # Will be useful for Ruby 3.0
11
+ # Abstract Manager class (interface)
12
+ class Manager
13
+ def add_variation(_visitor, _experiment_id, _variation_id)
14
+ raise 'Abstract method `add` called'
15
+ end
16
+
17
+ def get_engine_tracking_code(_visitor)
18
+ raise 'Abstract method `get_engine_tracking_code` called'
19
+ end
20
+ end
21
+
22
+ # Implementation of Cache with auto cleaning feature
23
+ class ManagerImpl < Manager
24
+ def initialize(expiration_time, cleaning_interval, cache_factory, log_func)
25
+ super()
26
+ # synchronization is necessary for adding same visitor_code from different threads
27
+ @mutex = Mutex.new
28
+ @expiration_time = expiration_time
29
+ @cache_factory = cache_factory
30
+ @log = log_func
31
+ # it's recommend to use cleaning_interval 3-4 times more than experiation_time for more performance
32
+ # in this case on every cleaning iteration storage will be cleaned 2/3 - 3/4 of volume
33
+ @cache = cache_factory.create(expiration_time, cleaning_interval)
34
+ @log.call('Hybrid Manager was successfully initialized')
35
+ end
36
+
37
+ def add_variation(visitor_code, experiment_id, variation_id)
38
+ @mutex.synchronize do
39
+ visitor_cache = @cache.get(visitor_code)
40
+ visitor_cache = @cache_factory.create(@expiration_time, 0) if visitor_cache.nil?
41
+ visitor_cache.set(experiment_id, variation_id)
42
+ @cache.set(visitor_code, visitor_cache)
43
+ end
44
+ @log.call("Hybrid Manager successfully added variation for visitor_code: #{visitor_code}, " \
45
+ "experiment: #{experiment_id}, variation: #{variation_id}")
46
+ end
47
+
48
+ def get_engine_tracking_code(visitor_code)
49
+ tracking_code = TC_INIT
50
+ visitor_cache = @cache.get(visitor_code)
51
+ return tracking_code if visitor_cache.nil?
52
+
53
+ visitor_cache.active_items.each_pair do |key, value|
54
+ tracking_code += format(TC_ASSIGN_VARIATION_TRIGGER_FORMAT, key, value, key)
55
+ end
56
+ tracking_code
57
+ end
58
+ end
59
+ end
60
+ end