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.
- checksums.yaml +4 -4
- data/lib/kameleoon/client.rb +541 -404
- data/lib/kameleoon/configuration/experiment.rb +42 -0
- data/lib/kameleoon/configuration/feature_flag.rb +30 -0
- data/lib/kameleoon/configuration/rule.rb +57 -0
- data/lib/kameleoon/configuration/settings.rb +20 -0
- data/lib/kameleoon/configuration/variable.rb +23 -0
- data/lib/kameleoon/configuration/variation.rb +31 -0
- data/lib/kameleoon/configuration/variation_exposition.rb +23 -0
- data/lib/kameleoon/cookie.rb +13 -6
- data/lib/kameleoon/data.rb +60 -40
- data/lib/kameleoon/exceptions.rb +46 -23
- data/lib/kameleoon/factory.rb +21 -18
- data/lib/kameleoon/hybrid/manager.rb +60 -0
- data/lib/kameleoon/real_time/real_time_configuration_service.rb +98 -0
- data/lib/kameleoon/real_time/real_time_event.rb +22 -0
- data/lib/kameleoon/real_time/sse_client.rb +111 -0
- data/lib/kameleoon/real_time/sse_message.rb +23 -0
- data/lib/kameleoon/real_time/sse_request.rb +59 -0
- data/lib/kameleoon/request.rb +14 -13
- data/lib/kameleoon/storage/cache.rb +84 -0
- data/lib/kameleoon/storage/cache_factory.rb +23 -0
- data/lib/kameleoon/storage/variation_storage.rb +42 -0
- data/lib/kameleoon/storage/visitor_variation.rb +20 -0
- data/lib/kameleoon/targeting/condition.rb +17 -5
- data/lib/kameleoon/targeting/condition_factory.rb +9 -2
- data/lib/kameleoon/targeting/conditions/custom_datum.rb +67 -48
- data/lib/kameleoon/targeting/conditions/exclusive_experiment.rb +29 -0
- data/lib/kameleoon/targeting/conditions/target_experiment.rb +44 -0
- data/lib/kameleoon/targeting/models.rb +36 -36
- data/lib/kameleoon/utils.rb +4 -1
- data/lib/kameleoon/version.rb +4 -2
- metadata +35 -3
- data/lib/kameleoon/query_graphql.rb +0 -76
@@ -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
|
data/lib/kameleoon/request.rb
CHANGED
@@ -1,16 +1,18 @@
|
|
1
|
-
require
|
2
|
-
require
|
1
|
+
require 'em-synchrony/em-http'
|
2
|
+
require 'kameleoon/version'
|
3
3
|
require 'net/http'
|
4
4
|
|
5
5
|
module Kameleoon
|
6
6
|
# @api private
|
7
7
|
module Request
|
8
8
|
protected
|
9
|
-
|
9
|
+
|
10
|
+
API_URL = 'https://api.kameleoon.com'.freeze
|
11
|
+
CLIENT_CONFIG_URL = 'https://client-config.kameleoon.com'.freeze
|
10
12
|
|
11
13
|
module Method
|
12
|
-
GET =
|
13
|
-
POST =
|
14
|
+
GET = 'get'.freeze
|
15
|
+
POST = 'post'.freeze
|
14
16
|
end
|
15
17
|
|
16
18
|
def get(request_options, url = API_URL, connexion_options = {})
|
@@ -32,7 +34,7 @@ module Kameleoon
|
|
32
34
|
private
|
33
35
|
|
34
36
|
def request(method, request_options, url, connexion_options)
|
35
|
-
connexion_options[:tls] = {verify_peer: false}
|
37
|
+
connexion_options[:tls] = { verify_peer: false }
|
36
38
|
add_user_agent(request_options)
|
37
39
|
case method
|
38
40
|
when Method::POST then
|
@@ -40,7 +42,7 @@ module Kameleoon
|
|
40
42
|
when Method::GET then
|
41
43
|
return EventMachine::HttpRequest.new(url, connexion_options).get request_options
|
42
44
|
else
|
43
|
-
print
|
45
|
+
print 'Unknown request type'
|
44
46
|
return false
|
45
47
|
end
|
46
48
|
end
|
@@ -68,22 +70,21 @@ module Kameleoon
|
|
68
70
|
end
|
69
71
|
end
|
70
72
|
|
71
|
-
def
|
73
|
+
def successful?(request)
|
72
74
|
!request.nil? && request != false && /20\d/.match(request.response_header.status.to_s)
|
73
75
|
end
|
74
76
|
|
75
|
-
def
|
77
|
+
def successful_sync?(response)
|
76
78
|
!response.nil? && response != false && response.is_a?(Net::HTTPSuccess)
|
77
79
|
end
|
78
80
|
|
79
81
|
def add_user_agent(request_options)
|
82
|
+
sdk_version = "sdk/ruby/#{Kameleoon::VERSION}"
|
80
83
|
if request_options[:head].nil?
|
81
|
-
request_options[:head] = {'Kameleoon-Client' =>
|
84
|
+
request_options[:head] = { 'Kameleoon-Client' => sdk_version }
|
82
85
|
else
|
83
|
-
request_options[:head].store('Kameleoon-Client',
|
86
|
+
request_options[:head].store('Kameleoon-Client', sdk_version)
|
84
87
|
end
|
85
88
|
end
|
86
89
|
end
|
87
90
|
end
|
88
|
-
|
89
|
-
|
@@ -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
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'kameleoon/storage/visitor_variation'
|
4
|
+
|
5
|
+
module Kameleoon
|
6
|
+
module Storage
|
7
|
+
# VariationStorage is a container for saved variations associated with a visitor
|
8
|
+
class VariationStorage
|
9
|
+
def initialize
|
10
|
+
@storage = {}
|
11
|
+
end
|
12
|
+
|
13
|
+
def get_variation_id(visitor_code, experiment_id)
|
14
|
+
variation_valid?(visitor_code, experiment_id, nil)
|
15
|
+
end
|
16
|
+
|
17
|
+
def variation_valid?(visitor_code, experiment_id, respool_time)
|
18
|
+
return nil if @storage[visitor_code].nil? || @storage[visitor_code][experiment_id].nil?
|
19
|
+
|
20
|
+
variation = @storage[visitor_code][experiment_id]
|
21
|
+
return nil unless variation.valid?(respool_time)
|
22
|
+
|
23
|
+
variation.variation_id
|
24
|
+
end
|
25
|
+
|
26
|
+
def update_variation(visitor_code, experiment_id, variation_id)
|
27
|
+
@storage[visitor_code] = {} if @storage[visitor_code].nil?
|
28
|
+
@storage[visitor_code][experiment_id] = Kameleoon::Storage::VisitorVariation.new(variation_id)
|
29
|
+
end
|
30
|
+
|
31
|
+
def get_hash_saved_variation_id(visitor_code)
|
32
|
+
return nil if @storage[visitor_code].nil?
|
33
|
+
|
34
|
+
map_variations = {}
|
35
|
+
@storage[visitor_code].each do |key, value|
|
36
|
+
map_variations[key] = value.variation_id
|
37
|
+
end
|
38
|
+
map_variations
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Kameleoon
|
4
|
+
module Storage
|
5
|
+
# VisitorVariation contains a saved variation id associated with a visitor
|
6
|
+
# and time when it was associated.
|
7
|
+
class VisitorVariation
|
8
|
+
attr_accessor :variation_id
|
9
|
+
|
10
|
+
def initialize(variation_id)
|
11
|
+
@variation_id = variation_id
|
12
|
+
@assignment_date = Time.now.to_i
|
13
|
+
end
|
14
|
+
|
15
|
+
def valid?(respool_time)
|
16
|
+
respool_time.nil? || @assignment_date > respool_time
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -1,16 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Kameleoon
|
2
|
-
|
4
|
+
# @api private
|
3
5
|
module Targeting
|
4
6
|
class Condition
|
5
7
|
attr_accessor :type, :include
|
6
8
|
|
7
|
-
def initialize(
|
8
|
-
|
9
|
+
def initialize(json_condition)
|
10
|
+
if json_condition['targetingType'].nil?
|
11
|
+
raise Exception::NotFound.new('targetingType'), 'targetingType missed'
|
12
|
+
end
|
13
|
+
|
14
|
+
@type = json_condition['targetingType']
|
15
|
+
|
16
|
+
if json_condition['include'].nil? && json_condition['isInclude'].nil?
|
17
|
+
raise Exception::NotFound.new('include / isInclude missed'), 'include / isInclude missed'
|
18
|
+
end
|
19
|
+
|
20
|
+
@include = json_condition['include'] || json_condition['isInclude']
|
9
21
|
end
|
10
22
|
|
11
23
|
def check(conditions)
|
12
|
-
raise
|
24
|
+
raise 'Abstract method `check` call'
|
13
25
|
end
|
14
26
|
end
|
15
27
|
end
|
16
|
-
end
|
28
|
+
end
|
@@ -1,4 +1,6 @@
|
|
1
1
|
require 'kameleoon/targeting/conditions/custom_datum'
|
2
|
+
require 'kameleoon/targeting/conditions/target_experiment'
|
3
|
+
require 'kameleoon/targeting/conditions/exclusive_experiment'
|
2
4
|
|
3
5
|
module Kameleoon
|
4
6
|
#@api private
|
@@ -6,11 +8,16 @@ module Kameleoon
|
|
6
8
|
module ConditionFactory
|
7
9
|
def get_condition(condition_json)
|
8
10
|
condition = nil
|
9
|
-
|
11
|
+
case condition_json['targetingType']
|
12
|
+
when ConditionType::CUSTOM_DATUM.to_s
|
10
13
|
condition = CustomDatum.new(condition_json)
|
14
|
+
when ConditionType::TARGET_EXPERIMENT.to_s
|
15
|
+
condition = TargetExperiment.new(condition_json)
|
16
|
+
when ConditionType::EXCLUSIVE_EXPERIMENT.to_s
|
17
|
+
condition = ExclusiveExperiment.new(condition_json)
|
11
18
|
end
|
12
19
|
condition
|
13
20
|
end
|
14
21
|
end
|
15
22
|
end
|
16
|
-
end
|
23
|
+
end
|