kameleoon-client-ruby 3.18.0 → 3.20.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.
- checksums.yaml +4 -4
- data/lib/kameleoon/configuration/data_file.rb +9 -4
- data/lib/kameleoon/configuration/variable.rb +12 -0
- data/lib/kameleoon/configuration/variation.rb +1 -1
- data/lib/kameleoon/data/browser.rb +1 -1
- data/lib/kameleoon/data/conversion.rb +12 -3
- data/lib/kameleoon/data/custom_data.rb +1 -1
- data/lib/kameleoon/data/data.rb +1 -1
- data/lib/kameleoon/data/device.rb +1 -1
- data/lib/kameleoon/data/geolocation.rb +1 -1
- data/lib/kameleoon/data/manager/assigned_variation.rb +2 -2
- data/lib/kameleoon/data/manager/page_view_visit.rb +2 -2
- data/lib/kameleoon/data/manager/visitor.rb +2 -2
- data/lib/kameleoon/data/manager/visitor_manager.rb +11 -1
- data/lib/kameleoon/data/mapping_identifier.rb +1 -1
- data/lib/kameleoon/data/operating_system.rb +1 -2
- data/lib/kameleoon/data/page_view.rb +1 -1
- data/lib/kameleoon/data/personalization.rb +4 -3
- data/lib/kameleoon/data/targeted_segment.rb +1 -1
- data/lib/kameleoon/data/visitor_visits.rb +1 -1
- data/lib/kameleoon/kameleoon_client.rb +18 -71
- data/lib/kameleoon/managers/data/data_manager.rb +16 -2
- data/lib/kameleoon/managers/remote_data/remote_visitor_data.rb +20 -5
- data/lib/kameleoon/managers/tracking/tracking_builder.rb +2 -2
- data/lib/kameleoon/network/access_token_source.rb +11 -3
- data/lib/kameleoon/network/activity_event.rb +1 -1
- data/lib/kameleoon/network/net_provider.rb +4 -1
- data/lib/kameleoon/network/network_manager.rb +2 -3
- data/lib/kameleoon/network/request.rb +6 -3
- data/lib/kameleoon/network/url_provider.rb +21 -3
- data/lib/kameleoon/targeting/conditions/conversion_condition.rb +28 -13
- data/lib/kameleoon/targeting/conditions/exclusive_experiment_condition.rb +38 -19
- data/lib/kameleoon/targeting/conditions/target_experiment_condition.rb +12 -10
- data/lib/kameleoon/targeting/conditions/target_feature_flag_condition.rb +26 -22
- data/lib/kameleoon/targeting/conditions/target_personalization_condition.rb +12 -10
- data/lib/kameleoon/targeting/conditions/time_elapsed_since_visit_condition.rb +1 -2
- data/lib/kameleoon/targeting/conditions/unknown_condition.rb +1 -0
- data/lib/kameleoon/targeting/conditions/visit_number_today_condition.rb +2 -2
- data/lib/kameleoon/targeting/conditions/visitor_scope_condition.rb +56 -0
- data/lib/kameleoon/targeting/targeting_manager.rb +12 -5
- data/lib/kameleoon/types/data_file.rb +14 -3
- data/lib/kameleoon/types/feature_flag.rb +22 -0
- data/lib/kameleoon/types/rule.rb +21 -0
- data/lib/kameleoon/types/variable.rb +8 -0
- data/lib/kameleoon/types/variation.rb +12 -0
- data/lib/kameleoon/version.rb +1 -1
- metadata +3 -2
|
@@ -5,6 +5,7 @@ require 'net/http'
|
|
|
5
5
|
require 'time'
|
|
6
6
|
require 'kameleoon/utils'
|
|
7
7
|
require 'kameleoon/logging/kameleoon_logger'
|
|
8
|
+
require 'base64'
|
|
8
9
|
|
|
9
10
|
module Kameleoon
|
|
10
11
|
module Network
|
|
@@ -13,6 +14,7 @@ module Kameleoon
|
|
|
13
14
|
TOKEN_OBSOLESCENCE_GAP = 1800 # in seconds
|
|
14
15
|
JWT_ACCESS_TOKEN_FIELD = 'access_token'
|
|
15
16
|
JWT_EXPIRES_IN_FIELD = 'expires_in'
|
|
17
|
+
BASIC_AUTHORIZATION_PREFIX = 'Basic '
|
|
16
18
|
|
|
17
19
|
def initialize(network_manager, client_id, client_secret)
|
|
18
20
|
Logging::KameleoonLogger.debug(lambda {
|
|
@@ -23,15 +25,21 @@ module Kameleoon
|
|
|
23
25
|
@client_id = client_id
|
|
24
26
|
@client_secret = client_secret
|
|
25
27
|
@fetching = false
|
|
28
|
+
@basic_auth_token = AccessTokenSource.construct_basic_token(client_id, client_secret)
|
|
26
29
|
Logging::KameleoonLogger.debug(lambda {
|
|
27
30
|
format("RETURN: AccessTokenSource.new(network_manager, client_id: '%s', client_secret: '%s')",
|
|
28
31
|
Utils::Strval.secret(client_id), Utils::Strval.secret(client_secret))
|
|
29
32
|
})
|
|
30
33
|
end
|
|
31
34
|
|
|
35
|
+
def self.construct_basic_token(client_id, client_secret)
|
|
36
|
+
basic_token_content = "#{client_id}:#{client_secret}"
|
|
37
|
+
"#{BASIC_AUTHORIZATION_PREFIX}#{Base64.strict_encode64(basic_token_content)}"
|
|
38
|
+
end
|
|
39
|
+
|
|
32
40
|
def get_token(timeout = nil)
|
|
33
41
|
Logging::KameleoonLogger.debug('CALL: AccessTokenSource.getToken(timeout: %s)', timeout)
|
|
34
|
-
now = Time.new.
|
|
42
|
+
now = Time.new.to_f
|
|
35
43
|
token = @cached_token
|
|
36
44
|
return call_fetch_token(timeout) if token.nil? || token.expired?(now)
|
|
37
45
|
|
|
@@ -70,7 +78,7 @@ module Kameleoon
|
|
|
70
78
|
|
|
71
79
|
def fetch_token(timeout = nil)
|
|
72
80
|
Logging::KameleoonLogger.debug('CALL: AccessTokenSource.fetch_token(timeout: %s)', timeout)
|
|
73
|
-
response_content = @network_manager.fetch_access_jwtoken(@
|
|
81
|
+
response_content = @network_manager.fetch_access_jwtoken(@basic_auth_token, timeout)
|
|
74
82
|
unless response_content
|
|
75
83
|
Logging::KameleoonLogger.error('Failed to fetch access JWT')
|
|
76
84
|
return nil
|
|
@@ -97,7 +105,7 @@ module Kameleoon
|
|
|
97
105
|
end
|
|
98
106
|
|
|
99
107
|
def handle_fetched_token(token, expires_in)
|
|
100
|
-
now = Time.new.
|
|
108
|
+
now = Time.new.to_f
|
|
101
109
|
exp_time = now + expires_in - TOKEN_EXPIRATION_GAP
|
|
102
110
|
if expires_in > TOKEN_OBSOLESCENCE_GAP
|
|
103
111
|
obs_time = now + expires_in - TOKEN_OBSOLESCENCE_GAP
|
|
@@ -17,7 +17,10 @@ module Kameleoon
|
|
|
17
17
|
def collect_headers(request)
|
|
18
18
|
headers = { 'Content-Type' => request.content_type }
|
|
19
19
|
headers.merge!(request.extra_headers) unless request.extra_headers.nil?
|
|
20
|
-
|
|
20
|
+
unless request.access_token.nil? || request.access_token.empty?
|
|
21
|
+
token_prefix = request.access_token.start_with?('Basic') ? '' : 'Bearer '
|
|
22
|
+
headers['Authorization'] = "#{token_prefix}#{request.access_token}"
|
|
23
|
+
end
|
|
21
24
|
headers
|
|
22
25
|
end
|
|
23
26
|
|
|
@@ -70,16 +70,15 @@ module Kameleoon
|
|
|
70
70
|
unwrap_response(*make_call(request, true, TRACKING_CALL_ATTEMPT_NUMBER - 1, TRACKING_CALL_RETRY_DELAY))
|
|
71
71
|
end
|
|
72
72
|
|
|
73
|
-
def fetch_access_jwtoken(
|
|
73
|
+
def fetch_access_jwtoken(basic_auth_token, timeout = nil)
|
|
74
74
|
url = @url_provider.make_access_token_url
|
|
75
75
|
timeout = ensure_timeout(timeout)
|
|
76
76
|
data_map = {
|
|
77
77
|
grant_type: ACCESS_TOKEN_GRANT_TYPE,
|
|
78
|
-
client_id: client_id,
|
|
79
|
-
client_secret: client_secret
|
|
80
78
|
}
|
|
81
79
|
data = UriHelper.encode_query(data_map).encode('UTF-8')
|
|
82
80
|
request = Request.new(Method::POST, url, ContentType::FORM, timeout, data: data)
|
|
81
|
+
request.authorize(basic_auth_token)
|
|
83
82
|
unwrap_response(*make_call(request, false))
|
|
84
83
|
end
|
|
85
84
|
|
|
@@ -11,12 +11,15 @@ module Kameleoon
|
|
|
11
11
|
body = 'null'
|
|
12
12
|
unless @data.nil?
|
|
13
13
|
if @data.is_a?(String)
|
|
14
|
-
body = @data.start_with?('grant_type=client_credentials') ? '****' : @data
|
|
15
|
-
else
|
|
16
14
|
body = @data
|
|
17
15
|
end
|
|
18
16
|
end
|
|
19
|
-
|
|
17
|
+
|
|
18
|
+
headers = ''
|
|
19
|
+
headers += 'Authorization: ***' unless access_token.nil?
|
|
20
|
+
headers += @extra_headers.to_s unless @extra_headers.nil?
|
|
21
|
+
|
|
22
|
+
"Request{method:'#{@method}',url:'#{@url}',headers:#{headers},body:'#{body}'}"
|
|
20
23
|
end
|
|
21
24
|
|
|
22
25
|
def initialize(method, url, content_type, timeout, extra_headers: nil, data: nil)
|
|
@@ -22,7 +22,7 @@ module Kameleoon
|
|
|
22
22
|
CONFIGURATION_API_URL_FORMAT = 'https://%s/v3/%s'
|
|
23
23
|
DATA_API_URL_FORMAT = 'https://%s%s?%s'
|
|
24
24
|
RT_CONFIGURATION_URL_FORMAT = 'https://%s:8110/sse?%s'
|
|
25
|
-
ACCESS_TOKEN_URL_FORMAT = 'https://%s/oauth/token'
|
|
25
|
+
ACCESS_TOKEN_URL_FORMAT = 'https://%s/oauth/token?%s'
|
|
26
26
|
|
|
27
27
|
TEST_DATA_API_DOMAIN = 'data.kameleoon.net'
|
|
28
28
|
TEST_AUTOMATION_API_DOMAIN = 'api.kameleoon.net'
|
|
@@ -39,6 +39,15 @@ module Kameleoon
|
|
|
39
39
|
@configuration_domain = DEFAULT_CONFIGURATION_DOMAIN
|
|
40
40
|
@access_token_domain = DEFAULT_ACCESS_TOKEN_DOMAIN
|
|
41
41
|
@is_custom_domain = false
|
|
42
|
+
|
|
43
|
+
url_params = {
|
|
44
|
+
sdkName: SDK_NAME,
|
|
45
|
+
sdkVersion: SDK_VERSION,
|
|
46
|
+
siteCode: @site_code
|
|
47
|
+
}
|
|
48
|
+
@access_token_url_params = UriHelper.encode_query(url_params)
|
|
49
|
+
url_params[:bodyUa] = true
|
|
50
|
+
@tracking_url_params = UriHelper.encode_query(url_params)
|
|
42
51
|
update_domains(network_domain)
|
|
43
52
|
end
|
|
44
53
|
|
|
@@ -60,7 +69,7 @@ module Kameleoon
|
|
|
60
69
|
siteCode: @site_code,
|
|
61
70
|
bodyUa: true
|
|
62
71
|
}
|
|
63
|
-
format(DATA_API_URL_FORMAT, @data_api_domain, TRACKING_PATH,
|
|
72
|
+
format(DATA_API_URL_FORMAT, @data_api_domain, TRACKING_PATH, @tracking_url_params)
|
|
64
73
|
end
|
|
65
74
|
|
|
66
75
|
def make_visitor_data_get_url(visitor_code, filter, is_unique_identifier = false)
|
|
@@ -104,11 +113,20 @@ module Kameleoon
|
|
|
104
113
|
end
|
|
105
114
|
|
|
106
115
|
def make_access_token_url
|
|
107
|
-
format(ACCESS_TOKEN_URL_FORMAT, @access_token_domain)
|
|
116
|
+
format(ACCESS_TOKEN_URL_FORMAT, @access_token_domain, @access_token_url_params)
|
|
108
117
|
end
|
|
109
118
|
|
|
110
119
|
private
|
|
111
120
|
|
|
121
|
+
def make_post_query_base(site_code)
|
|
122
|
+
data_map = {
|
|
123
|
+
"sdkName": Kameleoon::SDK_NAME,
|
|
124
|
+
"sdkVersion": Kameleoon::SDK_VERSION,
|
|
125
|
+
"siteCode": site_code
|
|
126
|
+
}
|
|
127
|
+
data = UriHelper.encode_query(data_map).encode('UTF-8')
|
|
128
|
+
end
|
|
129
|
+
|
|
112
130
|
def update_domains(network_domain)
|
|
113
131
|
return if network_domain.nil? || network_domain.empty?
|
|
114
132
|
|
|
@@ -1,28 +1,43 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'kameleoon/data/conversion'
|
|
4
|
-
require 'kameleoon/
|
|
4
|
+
require 'kameleoon/targeting/conditions/visitor_scope_condition'
|
|
5
5
|
|
|
6
6
|
module Kameleoon
|
|
7
|
-
# @api private
|
|
8
7
|
module Targeting
|
|
9
|
-
|
|
10
|
-
class ConversionCondition < Condition
|
|
8
|
+
class ConversionCondition < VisitorScopeCondition
|
|
11
9
|
def initialize(json_condition)
|
|
12
|
-
super(json_condition)
|
|
10
|
+
super(json_condition, VisitScope::VISITOR)
|
|
13
11
|
@goal_id = json_condition['goalId']
|
|
14
12
|
end
|
|
15
13
|
|
|
16
|
-
def check(
|
|
17
|
-
return false unless
|
|
18
|
-
return true if @goal_id.nil?
|
|
14
|
+
def check(data)
|
|
15
|
+
return false unless data.is_a?(TargetingData)
|
|
19
16
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
17
|
+
threshold = assignment_threshold(data.visitor_visits)
|
|
18
|
+
targeted_conversion?(data.conversions, threshold)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def targeted_conversion?(conversions, threshold)
|
|
24
|
+
return false unless conversions.respond_to?(:enumerate)
|
|
25
|
+
|
|
26
|
+
targeted = false
|
|
27
|
+
conversions.enumerate do |conversion|
|
|
28
|
+
targeted = (@goal_id.nil? || @goal_id == conversion.goal_id) && conversion.assignment_time >= threshold
|
|
29
|
+
break if targeted
|
|
30
|
+
end
|
|
31
|
+
targeted
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
class TargetingData
|
|
35
|
+
attr_reader :conversions, :visitor_visits
|
|
36
|
+
|
|
37
|
+
def initialize(conversions, visitor_visits)
|
|
38
|
+
@conversions = conversions
|
|
39
|
+
@visitor_visits = visitor_visits
|
|
24
40
|
end
|
|
25
|
-
is_targeted
|
|
26
41
|
end
|
|
27
42
|
end
|
|
28
43
|
end
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'kameleoon/logging/kameleoon_logger'
|
|
4
|
-
require 'kameleoon/targeting/
|
|
4
|
+
require 'kameleoon/targeting/conditions/visitor_scope_condition'
|
|
5
5
|
|
|
6
6
|
module Kameleoon
|
|
7
7
|
# @api private
|
|
8
8
|
module Targeting
|
|
9
9
|
# ExclusiveExperiment represents an instance of Exclusive Experiment condition in user account
|
|
10
|
-
class ExclusiveExperimentCondition <
|
|
10
|
+
class ExclusiveExperimentCondition < VisitorScopeCondition
|
|
11
11
|
module CampaignType
|
|
12
12
|
EXPERIMENT = 'EXPERIMENT'
|
|
13
13
|
PERSONALIZATION = 'PERSONALIZATION'
|
|
@@ -15,21 +15,22 @@ module Kameleoon
|
|
|
15
15
|
end
|
|
16
16
|
|
|
17
17
|
def initialize(json_condition)
|
|
18
|
-
super(json_condition)
|
|
18
|
+
super(json_condition, VisitScope::VISITOR)
|
|
19
19
|
|
|
20
20
|
@campaign_type = json_condition['campaignType']
|
|
21
21
|
end
|
|
22
22
|
|
|
23
23
|
def check(data)
|
|
24
|
-
return false unless data.is_a?(
|
|
24
|
+
return false unless data.is_a?(TargetingData)
|
|
25
25
|
|
|
26
|
+
threshold = assignment_threshold(data.visitor_visits)
|
|
26
27
|
case @campaign_type
|
|
27
28
|
when CampaignType::EXPERIMENT
|
|
28
|
-
return check_experiment(data)
|
|
29
|
+
return check_experiment(data, threshold)
|
|
29
30
|
when CampaignType::PERSONALIZATION
|
|
30
|
-
return check_personalization(data)
|
|
31
|
+
return check_personalization(data, threshold)
|
|
31
32
|
when CampaignType::ANY
|
|
32
|
-
return check_personalization(data) && check_experiment(data)
|
|
33
|
+
return check_personalization(data, threshold) && check_experiment(data, threshold)
|
|
33
34
|
end
|
|
34
35
|
Logging::KameleoonLogger.error("Unexpected campaign type for '#{type}' condition: '#{@campaign_type}'")
|
|
35
36
|
false
|
|
@@ -37,23 +38,41 @@ module Kameleoon
|
|
|
37
38
|
|
|
38
39
|
private
|
|
39
40
|
|
|
40
|
-
def check_experiment(data)
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
def check_experiment(data, threshold)
|
|
42
|
+
return true if data.variations.nil?
|
|
43
|
+
|
|
44
|
+
result = true
|
|
45
|
+
data.variations.enumerate do |variation|
|
|
46
|
+
if variation.experiment_id != data.current_experiment_id && variation.assignment_time >= threshold
|
|
47
|
+
result = false
|
|
48
|
+
break
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
result
|
|
43
52
|
end
|
|
44
53
|
|
|
45
|
-
def check_personalization(data)
|
|
46
|
-
|
|
54
|
+
def check_personalization(data, threshold)
|
|
55
|
+
return true if data.personalizations.nil?
|
|
56
|
+
|
|
57
|
+
result = true
|
|
58
|
+
data.personalizations.enumerate do |personalization|
|
|
59
|
+
if personalization.assignment_time >= threshold
|
|
60
|
+
result = false
|
|
61
|
+
break
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
result
|
|
47
65
|
end
|
|
48
|
-
end
|
|
49
66
|
|
|
50
|
-
|
|
51
|
-
|
|
67
|
+
class TargetingData
|
|
68
|
+
attr_reader :current_experiment_id, :variations, :personalizations, :visitor_visits
|
|
52
69
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
70
|
+
def initialize(current_experiment_id, variations, personalizations, visitor_visits = nil)
|
|
71
|
+
@current_experiment_id = current_experiment_id
|
|
72
|
+
@variations = variations
|
|
73
|
+
@personalizations = personalizations
|
|
74
|
+
@visitor_visits = visitor_visits
|
|
75
|
+
end
|
|
57
76
|
end
|
|
58
77
|
end
|
|
59
78
|
end
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'kameleoon/logging/kameleoon_logger'
|
|
4
|
-
require 'kameleoon/targeting/
|
|
4
|
+
require 'kameleoon/targeting/conditions/visitor_scope_condition'
|
|
5
5
|
|
|
6
6
|
module Kameleoon
|
|
7
7
|
# @api private
|
|
8
8
|
module Targeting
|
|
9
9
|
# TargetExperiment represents an instance of Experiment condition in user account
|
|
10
|
-
class TargetExperimentCondition <
|
|
10
|
+
class TargetExperimentCondition < VisitorScopeCondition
|
|
11
11
|
include Kameleoon::Exception
|
|
12
12
|
|
|
13
13
|
def initialize(json_condition)
|
|
14
|
-
super(json_condition)
|
|
14
|
+
super(json_condition, VisitScope::CURRENT_VISIT)
|
|
15
15
|
|
|
16
16
|
@variation_id = json_condition['variationId'] || -1
|
|
17
17
|
@experiment_id = json_condition['experimentId'] || -1
|
|
@@ -19,9 +19,10 @@ module Kameleoon
|
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
def check(data)
|
|
22
|
-
return false unless data.is_a?(
|
|
22
|
+
return false unless data.is_a?(TargetingData)
|
|
23
23
|
|
|
24
|
-
variation = data.
|
|
24
|
+
variation = data.variations&.get(@experiment_id)
|
|
25
|
+
variation = nil if !variation.nil? && variation.assignment_time < assignment_threshold(data.visitor_visits)
|
|
25
26
|
case @variation_match_type
|
|
26
27
|
when Operator::ANY
|
|
27
28
|
return !variation.nil?
|
|
@@ -33,13 +34,14 @@ module Kameleoon
|
|
|
33
34
|
)
|
|
34
35
|
false
|
|
35
36
|
end
|
|
36
|
-
end
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
class TargetingData
|
|
39
|
+
attr_reader :variations, :visitor_visits
|
|
40
40
|
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
def initialize(variations, visitor_visits = nil)
|
|
42
|
+
@variations = variations
|
|
43
|
+
@visitor_visits = visitor_visits
|
|
44
|
+
end
|
|
43
45
|
end
|
|
44
46
|
end
|
|
45
47
|
end
|
|
@@ -1,59 +1,63 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'kameleoon/targeting/
|
|
3
|
+
require 'kameleoon/targeting/conditions/visitor_scope_condition'
|
|
4
4
|
require 'kameleoon/exceptions'
|
|
5
5
|
|
|
6
6
|
module Kameleoon
|
|
7
7
|
# @api private
|
|
8
8
|
module Targeting
|
|
9
|
-
|
|
10
|
-
class TargetFeatureFlagCondition < Condition
|
|
9
|
+
class TargetFeatureFlagCondition < VisitorScopeCondition
|
|
11
10
|
include Kameleoon::Exception
|
|
12
11
|
|
|
13
12
|
def initialize(json_condition)
|
|
14
|
-
super(json_condition)
|
|
15
|
-
|
|
13
|
+
super(json_condition, VisitScope::CURRENT_VISIT)
|
|
16
14
|
@feature_flag_id = json_condition['featureFlagId']
|
|
17
15
|
@condition_variation_key = json_condition['variationKey']
|
|
18
16
|
@condition_rule_id = json_condition['ruleId']
|
|
19
17
|
end
|
|
20
18
|
|
|
21
19
|
def check(data)
|
|
22
|
-
return false unless
|
|
20
|
+
return false unless valid_data?(data)
|
|
23
21
|
|
|
24
22
|
get_rules(data).any? { |rule| check_rule(data, rule) }
|
|
25
23
|
end
|
|
26
24
|
|
|
27
25
|
private
|
|
28
26
|
|
|
27
|
+
def valid_data?(data)
|
|
28
|
+
data.is_a?(TargetingData) && !data.data_file.nil? && data.variations&.size.to_i.positive?
|
|
29
|
+
end
|
|
30
|
+
|
|
29
31
|
def check_rule(data, rule)
|
|
30
32
|
return false unless rule.is_a?(Configuration::Rule)
|
|
31
|
-
return false if
|
|
32
|
-
|
|
33
|
-
variation = data.variations_storage.get(rule.experiment.id)
|
|
34
|
-
return false if variation.nil?
|
|
33
|
+
return false if @condition_rule_id && @condition_rule_id != rule.id
|
|
35
34
|
|
|
36
|
-
|
|
35
|
+
variation = data.variations.get(rule.experiment.id)
|
|
36
|
+
return false if variation.nil? || variation.assignment_time < assignment_threshold(data.visitor_visits)
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
check_variation_key(data.data_file, variation)
|
|
39
|
+
end
|
|
39
40
|
|
|
40
|
-
|
|
41
|
+
def check_variation_key(data_file, variation)
|
|
42
|
+
return true if @condition_variation_key.nil?
|
|
41
43
|
|
|
42
|
-
variation.
|
|
44
|
+
resolved = data_file.variation_by_id[variation.variation_id]
|
|
45
|
+
resolved.is_a?(Configuration::VariationByExposition) &&
|
|
46
|
+
resolved.variation_key == @condition_variation_key
|
|
43
47
|
end
|
|
44
48
|
|
|
45
49
|
def get_rules(data)
|
|
46
|
-
|
|
47
|
-
feature_flag&.rules || []
|
|
50
|
+
data.data_file.feature_flag_by_id[@feature_flag_id]&.rules || []
|
|
48
51
|
end
|
|
49
|
-
end
|
|
50
52
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
+
class TargetingData
|
|
54
|
+
attr_reader :data_file, :variations, :visitor_visits
|
|
53
55
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
56
|
+
def initialize(data_file, variations, visitor_visits = nil)
|
|
57
|
+
@data_file = data_file
|
|
58
|
+
@variations = variations
|
|
59
|
+
@visitor_visits = visitor_visits
|
|
60
|
+
end
|
|
57
61
|
end
|
|
58
62
|
end
|
|
59
63
|
end
|
|
@@ -1,32 +1,34 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'kameleoon/targeting/
|
|
3
|
+
require 'kameleoon/targeting/conditions/visitor_scope_condition'
|
|
4
4
|
|
|
5
5
|
module Kameleoon
|
|
6
6
|
# @api private
|
|
7
7
|
module Targeting
|
|
8
8
|
# TargetPersonalization represents an instance of Personalization condition in user account
|
|
9
|
-
class TargetPersonalizationCondition <
|
|
9
|
+
class TargetPersonalizationCondition < VisitorScopeCondition
|
|
10
10
|
include Kameleoon::Exception
|
|
11
11
|
|
|
12
12
|
def initialize(json_condition)
|
|
13
|
-
super(json_condition)
|
|
13
|
+
super(json_condition, VisitScope::CURRENT_VISIT)
|
|
14
14
|
|
|
15
15
|
@personalization_id = json_condition['personalizationId'] || -1
|
|
16
16
|
end
|
|
17
17
|
|
|
18
18
|
def check(data)
|
|
19
|
-
return false unless data.is_a?(
|
|
19
|
+
return false unless data.is_a?(TargetingData)
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
personalization = data.personalizations&.get(@personalization_id)
|
|
22
|
+
!personalization.nil? && personalization.assignment_time >= assignment_threshold(data.visitor_visits)
|
|
22
23
|
end
|
|
23
|
-
end
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
class TargetingData
|
|
26
|
+
attr_reader :personalizations, :visitor_visits
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
def initialize(personalizations, visitor_visits = nil)
|
|
29
|
+
@personalizations = personalizations
|
|
30
|
+
@visitor_visits = visitor_visits
|
|
31
|
+
end
|
|
30
32
|
end
|
|
31
33
|
end
|
|
32
34
|
end
|
|
@@ -19,9 +19,8 @@ module Kameleoon
|
|
|
19
19
|
|
|
20
20
|
prev_visits = data.prev_visits
|
|
21
21
|
if prev_visits.size >= 1
|
|
22
|
-
now = (Time.now.to_f * 1000).to_i # ... * 1000 for convert seconds to milliseconds
|
|
23
22
|
visit_time = prev_visits[@is_first_visit ? prev_visits.size - 1 : 0].time_started
|
|
24
|
-
return check_targeting(now - visit_time)
|
|
23
|
+
return check_targeting(Time.now.to_f - visit_time)
|
|
25
24
|
end
|
|
26
25
|
false
|
|
27
26
|
end
|
|
@@ -8,6 +8,7 @@ module Kameleoon
|
|
|
8
8
|
# UnknownCondition represents not defined condition, always returns that visitor is targeted (true)
|
|
9
9
|
class UnknownCondition < Condition
|
|
10
10
|
def check(_data)
|
|
11
|
+
Logging::KameleoonLogger.warning('Condition of unknown type \'%s\' evaluated as true', type)
|
|
11
12
|
true
|
|
12
13
|
end
|
|
13
14
|
end
|
|
@@ -16,7 +16,7 @@ module Kameleoon
|
|
|
16
16
|
return false unless data.is_a?(TargetingData) && !@condition_value.nil?
|
|
17
17
|
|
|
18
18
|
number_of_visits_today = 0
|
|
19
|
-
start_of_day =
|
|
19
|
+
start_of_day = Time.new.to_date.to_time.to_f
|
|
20
20
|
data.visitor_visits.prev_visits.each do |visit|
|
|
21
21
|
break if visit.time_started < start_of_day
|
|
22
22
|
|
|
@@ -31,7 +31,7 @@ module Kameleoon
|
|
|
31
31
|
|
|
32
32
|
def initialize(current_visit_time_started, visitor_visits)
|
|
33
33
|
@current_visit_time_started =
|
|
34
|
-
current_visit_time_started.is_a?(Integer) ? current_visit_time_started :
|
|
34
|
+
current_visit_time_started.is_a?(Integer) ? current_visit_time_started : Time.new.to_f
|
|
35
35
|
@visitor_visits = visitor_visits.is_a?(VisitorVisits) ? visitor_visits : VisitorVisits.new([])
|
|
36
36
|
end
|
|
37
37
|
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'kameleoon/data/visitor_visits'
|
|
4
|
+
require 'kameleoon/targeting/condition'
|
|
5
|
+
|
|
6
|
+
module Kameleoon
|
|
7
|
+
# @api private
|
|
8
|
+
module Targeting
|
|
9
|
+
module VisitScope
|
|
10
|
+
CURRENT_VISIT = 'CURRENT_VISIT'
|
|
11
|
+
VISITOR = 'VISITOR'
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
class VisitorScopeCondition < Condition
|
|
15
|
+
MIN_VISITOR_VISIT_COUNT = 2
|
|
16
|
+
MAX_VISITOR_VISIT_COUNT = 25
|
|
17
|
+
|
|
18
|
+
def initialize(json_condition, default_visit_scope)
|
|
19
|
+
super(json_condition)
|
|
20
|
+
@visit_scope = parse_visit_scope(json_condition['visitScope'], default_visit_scope)
|
|
21
|
+
@visit_count = parse_visit_count(json_condition['visitCount'])
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def assignment_threshold(visitor_visits)
|
|
27
|
+
return 0.0 unless visitor_visits.is_a?(Kameleoon::VisitorVisits)
|
|
28
|
+
|
|
29
|
+
prev_visits = visitor_visits.prev_visits
|
|
30
|
+
if @visit_scope == VisitScope::CURRENT_VISIT || @visit_count < MIN_VISITOR_VISIT_COUNT || prev_visits.empty?
|
|
31
|
+
return visitor_visits.time_started
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
visit_index = [[@visit_count - MIN_VISITOR_VISIT_COUNT, 0].max, prev_visits.size - 1].min
|
|
35
|
+
prev_visits[visit_index].time_started
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def parse_visit_scope(value, default_visit_scope)
|
|
39
|
+
case value&.upcase
|
|
40
|
+
when VisitScope::CURRENT_VISIT
|
|
41
|
+
VisitScope::CURRENT_VISIT
|
|
42
|
+
when VisitScope::VISITOR
|
|
43
|
+
VisitScope::VISITOR
|
|
44
|
+
else
|
|
45
|
+
default_visit_scope
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def parse_visit_count(value)
|
|
50
|
+
return value if value.is_a?(Integer) && value.positive?
|
|
51
|
+
|
|
52
|
+
MAX_VISITOR_VISIT_COUNT
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require 'kameleoon/logging/kameleoon_logger'
|
|
4
4
|
require 'kameleoon/targeting/condition'
|
|
5
|
+
require 'kameleoon/targeting/conditions/conversion_condition'
|
|
5
6
|
require 'kameleoon/targeting/conditions/exclusive_experiment_condition'
|
|
6
7
|
require 'kameleoon/targeting/conditions/sdk_language_condition'
|
|
7
8
|
require 'kameleoon/targeting/conditions/segment_condition'
|
|
@@ -68,20 +69,26 @@ module Kameleoon
|
|
|
68
69
|
}
|
|
69
70
|
)
|
|
70
71
|
when ConditionType::CONVERSIONS
|
|
71
|
-
condition_data = visitor
|
|
72
|
+
condition_data = ConversionCondition::TargetingData.new(visitor&.conversions, visitor&.visitor_visits)
|
|
72
73
|
when ConditionType::SDK_LANGUAGE
|
|
73
74
|
condition_data = SdkInfo.new(Kameleoon::SDK_NAME, Kameleoon::SDK_VERSION)
|
|
74
75
|
when ConditionType::VISITOR_CODE
|
|
75
76
|
condition_data = visitor_code
|
|
76
77
|
when ConditionType::TARGET_FEATURE_FLAG
|
|
77
|
-
condition_data =
|
|
78
|
+
condition_data = TargetFeatureFlagCondition::TargetingData.new(
|
|
79
|
+
@data_manager.data_file, visitor&.variations, visitor&.visitor_visits
|
|
80
|
+
)
|
|
78
81
|
when ConditionType::TARGET_EXPERIMENT
|
|
79
|
-
condition_data =
|
|
82
|
+
condition_data = TargetExperimentCondition::TargetingData.new(visitor&.variations, visitor&.visitor_visits)
|
|
80
83
|
when ConditionType::TARGET_PERSONALIZATION
|
|
81
|
-
condition_data =
|
|
84
|
+
condition_data = TargetPersonalizationCondition::TargetingData.new(
|
|
85
|
+
visitor&.personalizations, visitor&.visitor_visits
|
|
86
|
+
)
|
|
82
87
|
when ConditionType::EXCLUSIVE_EXPERIMENT
|
|
83
88
|
unless campaign_id.nil?
|
|
84
|
-
condition_data =
|
|
89
|
+
condition_data = ExclusiveExperimentCondition::TargetingData.new(
|
|
90
|
+
campaign_id, visitor&.variations, visitor&.personalizations, visitor&.visitor_visits
|
|
91
|
+
)
|
|
85
92
|
end
|
|
86
93
|
when ConditionType::FIRST_VISIT,
|
|
87
94
|
ConditionType::LAST_VISIT,
|