kameleoon-client-ruby 1.1.2 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
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