kameleoon-client-ruby 3.19.0 → 3.21.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/client_readiness.rb +31 -21
- 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 +1 -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 +23 -4
- data/lib/kameleoon/managers/remote_data/remote_visitor_data.rb +20 -5
- data/lib/kameleoon/managers/tracking/tracking_builder.rb +1 -1
- data/lib/kameleoon/network/access_token_source.rb +2 -2
- data/lib/kameleoon/network/activity_event.rb +1 -1
- 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/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/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ea79d87e992d2f5bf9829c8082427c18251101be90f50c769b46242a1f4fc272
|
|
4
|
+
data.tar.gz: 305d07e5adee2b86cf58acc2b01f8a47f4161daeb06efbc8239e7a585ec355cd
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c3319b271cf4053f01e9fab40b0133cd609a800f55fd3de84f19b76209d2fcbe5438672aa24a42dc39c1dd4dade97641956c2cff1f5c5c0cee8ece0d80636ca8
|
|
7
|
+
data.tar.gz: 10dfdd35cf97d0adb20ed4a866c4b6739be6ec11458c267a550bf02f6e5ae2542b282b2c30ac33ae94797e3e4a8ce8dd6187406ef1d85603c2d02d0b9f09d44f
|
|
@@ -3,37 +3,47 @@
|
|
|
3
3
|
require 'concurrent'
|
|
4
4
|
|
|
5
5
|
module Kameleoon
|
|
6
|
+
# ClientReadiness is a cross-thread latch signalling that the SDK has
|
|
7
|
+
# successfully loaded its configuration.
|
|
8
|
+
#
|
|
9
|
+
# It is created on the thread that builds the client but released (`set`) from
|
|
10
|
+
# the background thread(s) performing configuration fetches, so it relies on
|
|
11
|
+
# Concurrent::Event (which allows cross-thread release) rather than a
|
|
12
|
+
# read/write lock (whose write lock must be released by the acquiring thread).
|
|
13
|
+
#
|
|
14
|
+
# A failed fetch does NOT release the latch: the client keeps retrying in the
|
|
15
|
+
# background and the latch is released as soon as any fetch succeeds. Callers
|
|
16
|
+
# should bound their `wait` with a timeout so they are never blocked forever
|
|
17
|
+
# when the configuration can never be loaded.
|
|
6
18
|
class ClientReadiness
|
|
7
|
-
attr_reader :is_initializing, :success
|
|
8
|
-
|
|
9
19
|
def initialize
|
|
10
|
-
@is_initializing = false
|
|
11
20
|
@success = false
|
|
12
|
-
@
|
|
13
|
-
reset
|
|
21
|
+
@event = Concurrent::Event.new
|
|
14
22
|
end
|
|
15
23
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
@is_initializing = true
|
|
20
|
-
@condition.acquire_write_lock
|
|
21
|
-
end
|
|
24
|
+
# @return [Boolean] whether the SDK has become ready (configuration loaded).
|
|
25
|
+
def success
|
|
26
|
+
@success
|
|
22
27
|
end
|
|
23
28
|
|
|
29
|
+
# Record a fetch outcome. A successful fetch marks the client ready and
|
|
30
|
+
# unblocks all waiting threads; a failed fetch is ignored so that a later
|
|
31
|
+
# successful retry can still release the latch. Idempotent and safe to call
|
|
32
|
+
# from any thread.
|
|
24
33
|
def set(success)
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
end
|
|
34
|
+
return unless success
|
|
35
|
+
|
|
36
|
+
@success = true
|
|
37
|
+
@event.set
|
|
30
38
|
end
|
|
31
39
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
40
|
+
# Block until the SDK becomes ready or the timeout elapses.
|
|
41
|
+
#
|
|
42
|
+
# @param timeout [Numeric, nil] maximum number of seconds to wait;
|
|
43
|
+
# nil waits indefinitely.
|
|
44
|
+
# @return [Boolean] whether the SDK is ready.
|
|
45
|
+
def wait(timeout = nil)
|
|
46
|
+
@event.wait(timeout)
|
|
37
47
|
@success
|
|
38
48
|
end
|
|
39
49
|
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'json'
|
|
4
|
+
require 'set'
|
|
4
5
|
require 'kameleoon/data/custom_data'
|
|
5
6
|
require 'kameleoon/network/uri_helper'
|
|
6
7
|
require 'kameleoon/utils'
|
|
@@ -9,11 +10,11 @@ require_relative 'data'
|
|
|
9
10
|
module Kameleoon
|
|
10
11
|
# Conversion class uses for tracking conversion
|
|
11
12
|
class Conversion < DuplicationSafeData
|
|
12
|
-
attr_reader :goal_id, :revenue, :negative, :metadata
|
|
13
|
+
attr_reader :goal_id, :revenue, :negative, :metadata, :assignment_time
|
|
13
14
|
|
|
14
15
|
def to_s
|
|
15
16
|
"Conversion{goal_id:#{@goal_id},revenue:#{@revenue},negative:#{@negative}," \
|
|
16
|
-
"metadata:#{Utils::Strval.obj_to_s(@metadata)}}"
|
|
17
|
+
"metadata:#{Utils::Strval.obj_to_s(@metadata)},assignment_time:#{@assignment_time}}"
|
|
17
18
|
end
|
|
18
19
|
|
|
19
20
|
# @param [Integer] goal_id Id of the goal associated to the conversion
|
|
@@ -26,9 +27,17 @@ module Kameleoon
|
|
|
26
27
|
@revenue = revenue || 0.0
|
|
27
28
|
@negative = negative || false
|
|
28
29
|
@metadata = metadata
|
|
30
|
+
@assignment_time = Time.now.to_f
|
|
29
31
|
end
|
|
30
32
|
|
|
31
|
-
|
|
33
|
+
# @api private
|
|
34
|
+
def self.build_internal(goal_id, revenue = 0.0, negative = false, metadata: nil, assignment_time: nil)
|
|
35
|
+
conversion = new(goal_id, revenue, negative, metadata: metadata)
|
|
36
|
+
conversion.instance_variable_set(:@assignment_time, assignment_time || Time.now.to_f)
|
|
37
|
+
conversion
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def query
|
|
32
41
|
params = {
|
|
33
42
|
eventType: 'conversion',
|
|
34
43
|
goalId: @goal_id,
|
data/lib/kameleoon/data/data.rb
CHANGED
|
@@ -25,10 +25,10 @@ module Kameleoon
|
|
|
25
25
|
@experiment_id = experiment_id
|
|
26
26
|
@variation_id = variation_id
|
|
27
27
|
@rule_type = rule_type
|
|
28
|
-
@assignment_time = assignment_time || Time.
|
|
28
|
+
@assignment_time = assignment_time || Time.now.to_f
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
-
def
|
|
31
|
+
def query
|
|
32
32
|
params = {
|
|
33
33
|
eventType: EVENT_TYPE,
|
|
34
34
|
id: @experiment_id,
|
|
@@ -12,7 +12,7 @@ module Kameleoon
|
|
|
12
12
|
def initialize(page_view, count = 1, timestamp = nil)
|
|
13
13
|
@page_view = page_view
|
|
14
14
|
@count = count
|
|
15
|
-
@last_timestamp = timestamp.nil? ? Time.new.
|
|
15
|
+
@last_timestamp = timestamp.nil? ? Time.new.to_f : timestamp
|
|
16
16
|
end
|
|
17
17
|
|
|
18
18
|
# Not thread-save method, should be called in synchronized code
|
|
@@ -20,7 +20,7 @@ module Kameleoon
|
|
|
20
20
|
def overwrite(page_view)
|
|
21
21
|
@page_view = page_view
|
|
22
22
|
@count += 1
|
|
23
|
-
@last_timestamp = Time.new.
|
|
23
|
+
@last_timestamp = Time.new.to_f
|
|
24
24
|
end
|
|
25
25
|
|
|
26
26
|
# Not thread-save method, should be called in synchronized code
|
|
@@ -55,7 +55,7 @@ module Kameleoon
|
|
|
55
55
|
end
|
|
56
56
|
|
|
57
57
|
def update_last_activity_time
|
|
58
|
-
@data.last_activity_time = Time.new.
|
|
58
|
+
@data.last_activity_time = Time.new.to_f
|
|
59
59
|
end
|
|
60
60
|
|
|
61
61
|
def enumerate_sendable_data(&blk)
|
|
@@ -322,7 +322,7 @@ module Kameleoon
|
|
|
322
322
|
|
|
323
323
|
def initialize
|
|
324
324
|
Logging::KameleoonLogger.debug('CALL: VisitorData.new')
|
|
325
|
-
@time_started =
|
|
325
|
+
@time_started = Time.now.to_f
|
|
326
326
|
@mutex = Concurrent::ReadWriteLock.new
|
|
327
327
|
@legal_consent = LegalConsent::UNKNOWN
|
|
328
328
|
Logging::KameleoonLogger.debug('RETURN: VisitorData.new')
|
|
@@ -154,7 +154,7 @@ module Kameleoon
|
|
|
154
154
|
|
|
155
155
|
def purge
|
|
156
156
|
Logging::KameleoonLogger.debug('CALL: VisitorManager.purge')
|
|
157
|
-
expired_time = Time.new.
|
|
157
|
+
expired_time = Time.new.to_f - @expiration_period
|
|
158
158
|
@visitors.each_pair do |vc, v|
|
|
159
159
|
next if v.last_activity_time >= expired_time
|
|
160
160
|
|
|
@@ -62,7 +62,7 @@ module Kameleoon
|
|
|
62
62
|
@os_type = os_type
|
|
63
63
|
end
|
|
64
64
|
|
|
65
|
-
def
|
|
65
|
+
def query
|
|
66
66
|
params = {
|
|
67
67
|
eventType: 'staticData',
|
|
68
68
|
os: OperatingSystemType.name_from_type(@os_type),
|
|
@@ -73,4 +73,3 @@ module Kameleoon
|
|
|
73
73
|
end
|
|
74
74
|
end
|
|
75
75
|
end
|
|
76
|
-
|
|
@@ -2,15 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
module Kameleoon
|
|
4
4
|
class Personalization
|
|
5
|
-
attr_reader :id, :variation_id
|
|
5
|
+
attr_reader :id, :variation_id, :assignment_time
|
|
6
6
|
|
|
7
|
-
def initialize(id, variation_id)
|
|
7
|
+
def initialize(id, variation_id, assignment_time: nil)
|
|
8
8
|
@id = id
|
|
9
9
|
@variation_id = variation_id
|
|
10
|
+
@assignment_time = assignment_time || Time.now.to_f
|
|
10
11
|
end
|
|
11
12
|
|
|
12
13
|
def to_s
|
|
13
|
-
"Personalization{id:#{@id},variation_id:#{@variation_id}}"
|
|
14
|
+
"Personalization{id:#{@id},variation_id:#{@variation_id},assignment_time:#{@assignment_time}}"
|
|
14
15
|
end
|
|
15
16
|
end
|
|
16
17
|
end
|
|
@@ -92,9 +92,24 @@ module Kameleoon
|
|
|
92
92
|
site_code, config)
|
|
93
93
|
end
|
|
94
94
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
95
|
+
##
|
|
96
|
+
# Block until the SDK has loaded its configuration and is ready to use.
|
|
97
|
+
#
|
|
98
|
+
# If the initial fetch fails the client keeps retrying in the background, so
|
|
99
|
+
# this method returns as soon as any fetch succeeds. The wait is always
|
|
100
|
+
# bounded by a timeout, so it never blocks indefinitely: when the timeout
|
|
101
|
+
# elapses before the configuration is loaded, the method returns false.
|
|
102
|
+
#
|
|
103
|
+
# @param [Integer, nil] timeout_millisecond Maximum time to wait, in
|
|
104
|
+
# milliseconds. When nil or negative, config.default_timeout_millisecond
|
|
105
|
+
# is applied.
|
|
106
|
+
# @return [Boolean] true if the SDK is ready, false if the timeout elapsed first.
|
|
107
|
+
def wait_init(timeout_millisecond = nil)
|
|
108
|
+
Logging::KameleoonLogger.info('CALL: KameleoonClient.wait_init(timeout_millisecond: %s)', timeout_millisecond)
|
|
109
|
+
if timeout_millisecond.nil? || timeout_millisecond.negative?
|
|
110
|
+
timeout_millisecond = @config.default_timeout_millisecond
|
|
111
|
+
end
|
|
112
|
+
result = @readiness.wait(timeout_millisecond / 1000.0)
|
|
98
113
|
Logging::KameleoonLogger.info('RETURN: KameleoonClient.wait_init -> (result: %s)', result)
|
|
99
114
|
result
|
|
100
115
|
end
|
|
@@ -822,7 +837,8 @@ module Kameleoon
|
|
|
822
837
|
Logging::KameleoonLogger.error('Initial configuration fetch failed: %s', e)
|
|
823
838
|
end
|
|
824
839
|
@readiness.set(ok)
|
|
825
|
-
|
|
840
|
+
real_time_update = ok && @data_manager.data_file.settings.real_time_update
|
|
841
|
+
manage_configuration_update(real_time_update)
|
|
826
842
|
end
|
|
827
843
|
end
|
|
828
844
|
|
|
@@ -834,6 +850,9 @@ module Kameleoon
|
|
|
834
850
|
rescue StandardError => e
|
|
835
851
|
Logging::KameleoonLogger.error('Error occurred during configuration fetching: %s', e)
|
|
836
852
|
end
|
|
853
|
+
# Release any threads still waiting in `wait_init` once a (re)fetch
|
|
854
|
+
# finally succeeds after an unsuccessful initial fetch.
|
|
855
|
+
@readiness.set(ok)
|
|
837
856
|
real_time_update = @data_manager.data_file.settings.real_time_update
|
|
838
857
|
if !ok && real_time_update
|
|
839
858
|
@data_manager.data_file.settings.real_time_update = false
|
|
@@ -32,7 +32,9 @@ module Kameleoon
|
|
|
32
32
|
prev_visits = []
|
|
33
33
|
previous_visits.each_index do |i|
|
|
34
34
|
visit = previous_visits[i]
|
|
35
|
-
prev_visits.push(
|
|
35
|
+
prev_visits.push(
|
|
36
|
+
VisitorVisits::Visit.new(ms_to_s(visit['timeStarted'] || 0), ms_to_s(visit['timeLastEvent']))
|
|
37
|
+
)
|
|
36
38
|
parse_visit(visit, i + 1)
|
|
37
39
|
end
|
|
38
40
|
@visitor_visits = VisitorVisits.new(prev_visits, @visit_number)
|
|
@@ -113,7 +115,7 @@ module Kameleoon
|
|
|
113
115
|
page_view_visit = @page_view_visits[href]
|
|
114
116
|
if page_view_visit.nil?
|
|
115
117
|
page_view = PageView.new(href, page_event['data']['title'])
|
|
116
|
-
@page_view_visits[href] = DataManager::PageViewVisit.new(page_view, 1, page_event['time'])
|
|
118
|
+
@page_view_visits[href] = DataManager::PageViewVisit.new(page_view, 1, ms_to_s(page_event['time']))
|
|
117
119
|
else
|
|
118
120
|
page_view_visit.increase_page_visits
|
|
119
121
|
end
|
|
@@ -128,7 +130,7 @@ module Kameleoon
|
|
|
128
130
|
|
|
129
131
|
@experiments[id] = DataManager::AssignedVariation.new(
|
|
130
132
|
id, experiment_event['data']['variationId'],
|
|
131
|
-
Configuration::RuleType::UNKNOWN, assignment_time: experiment_event['time']
|
|
133
|
+
Configuration::RuleType::UNKNOWN, assignment_time: ms_to_s(experiment_event['time'])
|
|
132
134
|
)
|
|
133
135
|
end
|
|
134
136
|
end
|
|
@@ -137,7 +139,10 @@ module Kameleoon
|
|
|
137
139
|
@conversions = [] if @conversions.nil?
|
|
138
140
|
conversion_events.each do |conversion_event|
|
|
139
141
|
data = conversion_event['data']
|
|
140
|
-
conversion = Conversion.
|
|
142
|
+
conversion = Conversion.send(
|
|
143
|
+
:build_internal, data['goalId'], data['revenue'], data['negative'],
|
|
144
|
+
assignment_time: ms_to_s(conversion_event['time'])
|
|
145
|
+
)
|
|
141
146
|
@conversions.push(conversion)
|
|
142
147
|
end
|
|
143
148
|
end
|
|
@@ -173,10 +178,20 @@ module Kameleoon
|
|
|
173
178
|
id = personalization_event['data']['id']
|
|
174
179
|
next if @personalizations.include?(id)
|
|
175
180
|
|
|
176
|
-
@personalizations[id] = Personalization.new(
|
|
181
|
+
@personalizations[id] = Personalization.new(
|
|
182
|
+
id,
|
|
183
|
+
personalization_event['data']['variationId'],
|
|
184
|
+
assignment_time: ms_to_s(personalization_event['time'])
|
|
185
|
+
)
|
|
177
186
|
end
|
|
178
187
|
end
|
|
179
188
|
|
|
189
|
+
def ms_to_s(timestamp)
|
|
190
|
+
return nil if timestamp.nil?
|
|
191
|
+
|
|
192
|
+
timestamp / 1000.0
|
|
193
|
+
end
|
|
194
|
+
|
|
180
195
|
def conversions_single_objects
|
|
181
196
|
objects = []
|
|
182
197
|
objects += @conversions unless @conversions.nil?
|
|
@@ -39,7 +39,7 @@ module Kameleoon
|
|
|
39
39
|
|
|
40
40
|
def get_token(timeout = nil)
|
|
41
41
|
Logging::KameleoonLogger.debug('CALL: AccessTokenSource.getToken(timeout: %s)', timeout)
|
|
42
|
-
now = Time.new.
|
|
42
|
+
now = Time.new.to_f
|
|
43
43
|
token = @cached_token
|
|
44
44
|
return call_fetch_token(timeout) if token.nil? || token.expired?(now)
|
|
45
45
|
|
|
@@ -105,7 +105,7 @@ module Kameleoon
|
|
|
105
105
|
end
|
|
106
106
|
|
|
107
107
|
def handle_fetched_token(token, expires_in)
|
|
108
|
-
now = Time.new.
|
|
108
|
+
now = Time.new.to_f
|
|
109
109
|
exp_time = now + expires_in - TOKEN_EXPIRATION_GAP
|
|
110
110
|
if expires_in > TOKEN_OBSOLESCENCE_GAP
|
|
111
111
|
obs_time = now + expires_in - TOKEN_OBSOLESCENCE_GAP
|
|
@@ -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
|
|
@@ -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,
|
data/lib/kameleoon/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: kameleoon-client-ruby
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.
|
|
4
|
+
version: 3.21.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Kameleoon
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-07-02 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: em-http-request
|
|
@@ -178,6 +178,7 @@ files:
|
|
|
178
178
|
- lib/kameleoon/targeting/conditions/visit_number_total_condition.rb
|
|
179
179
|
- lib/kameleoon/targeting/conditions/visitor_code_condition.rb
|
|
180
180
|
- lib/kameleoon/targeting/conditions/visitor_new_return_condition.rb
|
|
181
|
+
- lib/kameleoon/targeting/conditions/visitor_scope_condition.rb
|
|
181
182
|
- lib/kameleoon/targeting/models.rb
|
|
182
183
|
- lib/kameleoon/targeting/targeting_manager.rb
|
|
183
184
|
- lib/kameleoon/targeting/tree_builder.rb
|