kameleoon-client-ruby 2.0.0 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -14,25 +14,29 @@ module Kameleoon
14
14
 
15
15
  # Rule is a class for new rules of feature flags
16
16
  class Rule
17
- attr_accessor :type, :exposition, :experiment_id, :variation_by_exposition, :targeting_segment
17
+ attr_reader :id, :order, :type, :exposition, :experiment_id, :variation_by_exposition, :respool_time
18
+ attr_accessor :targeting_segment
18
19
 
19
20
  def self.create_from_array(array)
20
21
  array&.map { |it| Rule.new(it) }
21
22
  end
22
23
 
23
24
  def initialize(hash)
25
+ @id = hash['id']
26
+ @order = hash['order']
24
27
  @type = hash['type']
25
28
  @exposition = hash['exposition']
26
29
  @experiment_id = hash['experimentId']
30
+ @respool_time = hash['respoolTime']
27
31
  @variation_by_exposition = VariationByExposition.create_from_array(hash['variationByExposition'])
28
32
  @targeting_segment = Kameleoon::Targeting::Segment.new((hash['segment'])) if hash['segment']
29
33
  end
30
34
 
31
- def get_variation_key(hash_double)
35
+ def get_variation(hash_double)
32
36
  total = 0.0
33
- variation_by_exposition.each do |element|
34
- total += element.exposition
35
- return element.variation_key if total >= hash_double
37
+ variation_by_exposition.each do |var_by_exp|
38
+ total += var_by_exp.exposition
39
+ return var_by_exp if total >= hash_double
36
40
  end
37
41
  nil
38
42
  end
@@ -40,6 +44,14 @@ module Kameleoon
40
44
  def get_variation_id_by_key(key)
41
45
  variation_by_exposition.select { |v| v.variation_key == key }.first&.variation_id
42
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
43
55
  end
44
56
  end
45
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
@@ -26,7 +26,7 @@ module Kameleoon
26
26
  obtain_hash_double_helper(visitor_code, respool_times, container_id, '')
27
27
  end
28
28
 
29
- def obtain_hash_double_v2(visitor_code, container_id = '', suffix = '')
29
+ def obtain_hash_double_rule(visitor_code, container_id = '', suffix = '')
30
30
  obtain_hash_double_helper(visitor_code, {}, container_id, suffix)
31
31
  end
32
32
 
@@ -37,8 +37,8 @@ module Kameleoon
37
37
  identifier += respool_times.sort.to_h.values.join.to_s if !respool_times.nil? && !respool_times.empty?
38
38
  (Digest::SHA256.hexdigest(identifier.encode('UTF-8')).to_i(16) / (BigDecimal('2')**BigDecimal('256'))).round(16)
39
39
  end
40
-
41
- def check_visitor_code(visitor_code)
40
+
41
+ def check_visitor_code(visitor_code)
42
42
  if visitor_code.nil?
43
43
  check_default_visitor_code('')
44
44
  elsif
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'kameleoon/exceptions'
4
+
3
5
  module Kameleoon
4
6
  NONCE_LENGTH = 16
5
7
 
@@ -50,38 +52,42 @@ module Kameleoon
50
52
  end
51
53
 
52
54
  class CustomData < Data
53
- attr_accessor :id, :value
55
+ attr_reader :id, :values
54
56
 
55
57
  # @param [Integer] id Id of the custom data
56
58
  # @param [String] value Value of the custom data
57
59
  #
58
60
  # @overload
59
61
  # @param [Hash] hash Json value encoded in a hash.
60
- def initialize(*args)
62
+ def initialize(arg0, *args)
61
63
  @instance = DataType::CUSTOM
62
64
  @sent = false
63
- unless args.empty?
64
- if args.length == 1
65
- hash = args.first
66
- if hash["id"].nil?
67
- raise NotFoundError.new("id")
68
- end
69
- @id = hash["id"].to_s
70
- if hash["value"].nil?
71
- raise NotFoundError.new(hash["value"])
72
- end
73
- @value = hash["value"]
74
- elsif args.length == 2
75
- @id = args[0].to_s
76
- @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?
77
78
  end
79
+ else
80
+ @id = arg0.to_s
81
+ @values = args
78
82
  end
79
83
  end
80
84
 
81
85
  def obtain_full_post_text_line
82
- to_encode = "[[\"" + @value.to_s.gsub("\"", "\\\"") + "\",1]]"
86
+ return '' if @values.empty?
87
+
88
+ str_values = "[[\"#{@values.join('",1],["')}\",1]]"
83
89
  nonce = Kameleoon::Utils.generate_random_string(NONCE_LENGTH)
84
- "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}"
85
91
  end
86
92
  end
87
93
 
@@ -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
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Kameleoon Real Time Configuration Service
4
+
5
+ require 'json'
6
+ require 'kameleoon/real_time/real_time_event'
7
+ require 'kameleoon/real_time/sse_request'
8
+ require 'kameleoon/real_time/sse_client'
9
+
10
+ module Kameleoon
11
+ module RealTime
12
+ CONFIGURATION_UPDATE_EVENT = 'configuration-update-event'
13
+
14
+ ##
15
+ # RealTimeConfigurationService is used for fetching updates of configuration
16
+ # (experiments and feature flags) in real time.
17
+ class RealTimeConfigurationService
18
+ ##
19
+ # Parametrized initializer.
20
+ #
21
+ # @param url [String]
22
+ # @param update_handler [Callable[Kameleoon::RealTime::RealTimeEvent] | NilClass] Handler which
23
+ # is synchronously called for gotten RealTimeEvent objects.
24
+ # @param log_func [Callable[String] | NilClass] Callable object which synchronously called to log.
25
+ def initialize(url, update_handler, log_func, sse_request_source = nil)
26
+ @url = url
27
+ @update_handler = update_handler
28
+ @need_close = false
29
+ @headers = {
30
+ 'Accept': 'text/event-stream',
31
+ 'Cache-Control': 'no-cache',
32
+ 'Connection': 'Keep-Alive'
33
+ }
34
+ @log_func = log_func
35
+ @sse_request_source = sse_request_source
36
+ @sse_thread = nil
37
+ @sse_client = nil
38
+ create_sse_client
39
+ end
40
+
41
+ ##
42
+ # Closes the connection to the server.
43
+ def close
44
+ return if @need_close
45
+
46
+ @log_func&.call('Real-time configuration service is shutting down')
47
+ @need_close = true
48
+ return if @sse_thread.nil?
49
+
50
+ @sse_thread.kill
51
+ @sse_thread = nil
52
+ @sse_client.call_close_handler
53
+ end
54
+
55
+ private
56
+
57
+ def create_sse_client
58
+ @sse_thread = Thread.new { run_sse_client } unless @need_close
59
+ end
60
+
61
+ def init_sse_client
62
+ message_handler = proc do |message|
63
+ @log_func&.call("Got SSE event: #{message.event}")
64
+ if message.event == CONFIGURATION_UPDATE_EVENT
65
+ event_dict = JSON.parse(message.data)
66
+ @update_handler&.call(RealTimeEvent.new(event_dict))
67
+ end
68
+ end
69
+ sse_request = make_sse_request
70
+ @sse_client = SseClient.new(sse_request, message_handler)
71
+ @log_func&.call('Created SSE client')
72
+ end
73
+
74
+ def make_sse_request
75
+ open_handler = proc { @log_func&.call('SSE connection open') }
76
+ close_handler = proc { @log_func&.call('SSE connection closed') }
77
+ unexpected_status_code_handler =
78
+ proc { |resp| @log_func&.call("Unexpected status code of SSE response: #{resp.code}") }
79
+ if @sse_request_source.nil?
80
+ return SseRequest.new(@url, @headers, open_handler, close_handler, unexpected_status_code_handler)
81
+ end
82
+
83
+ @sse_request_source.call(@url, @headers, open_handler, close_handler, unexpected_status_code_handler)
84
+ end
85
+
86
+ def run_sse_client
87
+ until @need_close
88
+ init_sse_client
89
+ begin
90
+ @sse_client.start
91
+ rescue StandardError => e
92
+ @log_func&.call("Error occurred within SSE client: #{e}")
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Kameleoon Real Time Event
4
+
5
+ module Kameleoon
6
+ module RealTime
7
+ ##
8
+ # RealTimeEvent contains information about timestamp when configuration was updated.
9
+ # Timestamp parameter is used to fetch the latest configuration.
10
+ class RealTimeEvent
11
+ attr_reader :time_stamp
12
+
13
+ ##
14
+ # Parametrized initializer. Gets the new time stamp from a JSON message.
15
+ #
16
+ # @param event_dict [Hash] Hash of SSE message data.
17
+ def initialize(event_dict)
18
+ @time_stamp = event_dict['ts']
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'kameleoon/real_time/sse_message'
4
+
5
+ module Kameleoon
6
+ module RealTime
7
+ ##
8
+ # SseClient is used to interpret SSE event stream.
9
+ class SseClient
10
+ ##
11
+ # Parametrized initializer.
12
+ #
13
+ # @param sse_request [Kameleoon::RealTime::SseRequest] Used to access SSE event stream.
14
+ # @param message_handler [Callable[Kameleoon::RealTime::SseMessage] | NilClass] Callable object which
15
+ # is synchronously called for received SSE messages.
16
+ def initialize(sse_request, message_handler)
17
+ @sse_request = sse_request
18
+ @message_handler = message_handler
19
+
20
+ @cr_prev = nil
21
+ @buffer = nil
22
+ @data_buffer = nil
23
+ @event = nil
24
+ @id = nil
25
+ @reconnection_time = nil
26
+
27
+ @sse_request.resp_char_handler = method(:process_char)
28
+ end
29
+
30
+ ##
31
+ # Starts SSE connection and stay in the loop until close.
32
+ def start
33
+ @cr_prev = false
34
+ @data_buffer = []
35
+ @event = nil
36
+ @id = nil
37
+ @buffer = []
38
+ @sse_request.start
39
+ end
40
+
41
+ ##
42
+ # Calls @sse_request@close_handler if it is not nil.
43
+ def call_close_handler
44
+ @sse_request.call_close_handler
45
+ end
46
+
47
+ private
48
+
49
+ def process_char(ch)
50
+ if @cr_prev && (ch == "\n")
51
+ @cr_prev = false
52
+ return
53
+ end
54
+ @cr_prev = ch == "\r"
55
+ if @cr_prev || (ch == "\n")
56
+ line = @buffer.join
57
+ @buffer.clear
58
+ if line.empty?
59
+ dispatch_event
60
+ else
61
+ handle_line(line)
62
+ end
63
+ return
64
+ end
65
+ @buffer << ch
66
+ end
67
+
68
+ def handle_line(line)
69
+ field, value = parse_line(line)
70
+ return if field.nil?
71
+
72
+ case field
73
+ when 'event'
74
+ @event = value
75
+ when 'data'
76
+ @data_buffer << value
77
+ when 'id'
78
+ @id = value
79
+ when 'retry'
80
+ @reconnection_time = value.to_i # This does not affect anything
81
+ end
82
+ end
83
+
84
+ def parse_line(line)
85
+ colon_index = line.index(':')
86
+ return line, nil if colon_index.nil?
87
+ return nil, nil if colon_index.zero?
88
+
89
+ field = line[0, colon_index]
90
+ return field, '' if colon_index + 1 == line.length
91
+
92
+ value = if line[colon_index + 1] == ' '
93
+ line[colon_index + 2..]
94
+ else
95
+ line[colon_index + 1..]
96
+ end
97
+ [field, value]
98
+ end
99
+
100
+ def dispatch_event
101
+ return unless !@event.nil? || !@data_buffer.empty? # Ignoring empty events
102
+
103
+ data = @data_buffer.join("\n")
104
+ message = SseMessage.new(@event, @id, data)
105
+ @event = nil
106
+ @data_buffer.clear
107
+ @message_handler&.call(message)
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kameleoon
4
+ module RealTime
5
+ ##
6
+ # SseMessage is a container for a received SSE message.
7
+ class SseMessage
8
+ attr_reader :event, :id, :data
9
+
10
+ ##
11
+ # Parametrized initializer.
12
+ #
13
+ # @param event [String]
14
+ # @param id [String]
15
+ # @param data [String]
16
+ def initialize(event, id, data)
17
+ @event = event
18
+ @id = id
19
+ @data = data
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'restclient'
4
+
5
+ module Kameleoon
6
+ module RealTime
7
+ ##
8
+ # SseRequest is used keep SSE connection and read from its stream.
9
+ class SseRequest
10
+ attr_writer :resp_char_handler
11
+
12
+ ##
13
+ # Parametrized initializer.
14
+ #
15
+ # @param url [String]
16
+ # @param headers [Hash]
17
+ # @param open_handler [Callable | NilClass] Handler which is synchronously called when SSE connection is open.
18
+ # @param close_handler [Callable | NilClass] Handler which is synchronously called when SSE connection is closed.
19
+ # @param unexpected_status_code_handler [Callable[Net::HTTPResponse] | NilClass] Handler whick
20
+ # is synchronously called for responses with not 200 status code.
21
+ def initialize(url, headers, open_handler, close_handler, unexpected_status_code_handler)
22
+ @url = url
23
+ @headers = headers
24
+ @open_handler = open_handler
25
+ @close_handler = close_handler
26
+ @unexpected_status_code_handler = unexpected_status_code_handler
27
+
28
+ @resp_char_handler = nil
29
+ end
30
+
31
+ ##
32
+ # Starts SSE connection and stay in the loop until close.
33
+ def start
34
+ RestClient::Request.execute(method: :get, url: @url, headers: @headers, read_timeout: nil,
35
+ block_response: method(:handle_resp))
36
+ end
37
+
38
+ ##
39
+ # Calls @close_handler if it is not nil.
40
+ def call_close_handler
41
+ @close_handler&.call
42
+ end
43
+
44
+ private
45
+
46
+ def handle_resp(resp)
47
+ @open_handler&.call
48
+ if resp.code == '200'
49
+ resp.read_body do |chunk|
50
+ chunk.each_char { |ch| @resp_char_handler&.call(ch) }
51
+ end
52
+ else
53
+ @unexpected_status_code_handler&.call(resp)
54
+ end
55
+ call_close_handler
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'concurrent'
4
+
5
+ module Kameleoon
6
+ module Storage
7
+ # Will be useful for Ruby 3.0
8
+ # Abstract Cache class (interface)
9
+ class Cache
10
+ def get(_key)
11
+ raise 'Abstract method `read` called'
12
+ end
13
+
14
+ def set(_key, _value)
15
+ raise 'Abstract method `write` called'
16
+ end
17
+
18
+ def active_items
19
+ raise 'Abstract method `active_items` called'
20
+ end
21
+ end
22
+
23
+ # Implementation of Cache with auto cleaning feature
24
+ class CacheImpl < Cache
25
+ def initialize(expiration_time, cleaning_interval)
26
+ super()
27
+ @mutex = Mutex.new
28
+ @expiration_time = expiration_time
29
+ @cleaning_interval = cleaning_interval
30
+ @cache = {}
31
+ end
32
+
33
+ def set(key, value)
34
+ @mutex.synchronize do
35
+ start_cleaner_timer if @cleaning_interval.positive? && @cleaner_timer.nil?
36
+ @cache[key] = { value: value, expired: Time.now + @expiration_time }
37
+ end
38
+ end
39
+
40
+ def get(key)
41
+ entry = @cache[key]
42
+ return entry[:value] unless entry.nil? || expired?(entry)
43
+
44
+ @mutex.synchronize { @cache.delete(key) }
45
+ nil
46
+ end
47
+
48
+ def active_items
49
+ active_items = {}
50
+ remove_expired_entries
51
+ @mutex.synchronize do
52
+ @cache.each_pair { |key, entry| active_items[key] = entry[:value] }
53
+ end
54
+ active_items
55
+ end
56
+
57
+ private
58
+
59
+ def start_cleaner_timer
60
+ @cleaner_timer = Concurrent::TimerTask.new(execution_interval: @cleaning_interval) do
61
+ remove_expired_entries
62
+ end
63
+ @cleaner_timer.execute
64
+ end
65
+
66
+ def stop_cleaner_timer
67
+ @cleaner_timer&.shutdown
68
+ @cleaner_timer = nil
69
+ end
70
+
71
+ def remove_expired_entries
72
+ time = Time.now
73
+ @mutex.synchronize do
74
+ @cache.delete_if { |_, entry| expired?(entry, time) }
75
+ stop_cleaner_timer if @cache.empty?
76
+ end
77
+ end
78
+
79
+ def expired?(entry, time = Time.now)
80
+ entry[:expired] <= time
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'concurrent'
4
+ require_relative 'cache'
5
+
6
+ module Kameleoon
7
+ module Storage
8
+ # Will be useful for Ruby 3.0
9
+ # Abstract CacheFactory class (interface)
10
+ class CacheFactory
11
+ def create(_experiration_time, _cleaning_time)
12
+ raise 'Abstract method `create` called'
13
+ end
14
+ end
15
+
16
+ # Implementation of CacheFactory with auto cleaning feature
17
+ class CacheFactoryImpl < CacheFactory
18
+ def create(expiration_time, cleaning_interval)
19
+ CacheImpl.new(expiration_time, cleaning_interval)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -8,21 +8,21 @@ module Kameleoon
8
8
 
9
9
  def initialize(json_condition)
10
10
  if json_condition['targetingType'].nil?
11
- raise Exception::NotFoundError.new('targetingType'), 'targetingType missed'
11
+ raise Exception::NotFound.new('targetingType'), 'targetingType missed'
12
12
  end
13
13
 
14
14
  @type = json_condition['targetingType']
15
15
 
16
16
  if json_condition['include'].nil? && json_condition['isInclude'].nil?
17
- raise Exception::NotFoundError.new('include / isInclude missed'), 'include / isInclude missed'
17
+ raise Exception::NotFound.new('include / isInclude missed'), 'include / isInclude missed'
18
18
  end
19
19
 
20
20
  @include = json_condition['include'] || json_condition['isInclude']
21
21
  end
22
22
 
23
23
  def check(conditions)
24
- raise "Todo: Implement check method in condition"
24
+ raise 'Abstract method `check` call'
25
25
  end
26
26
  end
27
27
  end
28
- end
28
+ end