vwo-sdk 1.3.0 → 1.14.1
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 +5 -5
- data/lib/vwo.rb +1020 -84
- data/lib/vwo/constants.rb +64 -13
- data/lib/vwo/core/bucketer.rb +10 -14
- data/lib/vwo/core/variation_decider.rb +391 -80
- data/lib/vwo/enums.rb +113 -10
- data/lib/vwo/logger.rb +9 -3
- data/lib/vwo/schemas/settings_file.rb +1 -3
- data/lib/vwo/services/batch_events_dispatcher.rb +110 -0
- data/lib/vwo/services/batch_events_queue.rb +175 -0
- data/lib/vwo/services/event_dispatcher.rb +3 -17
- data/lib/vwo/services/hooks_manager.rb +36 -0
- data/lib/vwo/services/operand_evaluator.rb +122 -0
- data/lib/vwo/services/segment_evaluator.rb +88 -0
- data/lib/vwo/services/settings_file_manager.rb +11 -9
- data/lib/vwo/services/settings_file_processor.rb +6 -3
- data/lib/vwo/services/usage_stats.rb +29 -0
- data/lib/vwo/user_storage.rb +1 -3
- data/lib/vwo/utils/campaign.rb +151 -22
- data/lib/vwo/utils/custom_dimensions.rb +72 -0
- data/lib/vwo/utils/feature.rb +56 -0
- data/lib/vwo/utils/function.rb +6 -3
- data/lib/vwo/utils/impression.rb +76 -7
- data/lib/vwo/utils/request.rb +15 -3
- data/lib/vwo/utils/segment.rb +116 -0
- data/lib/vwo/utils/uuid.rb +3 -5
- data/lib/vwo/utils/validations.rb +96 -4
- metadata +30 -8
data/lib/vwo/constants.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
# Copyright 2019 Wingify Software Pvt. Ltd.
|
1
|
+
# Copyright 2019-2021 Wingify Software Pvt. Ltd.
|
2
2
|
#
|
3
3
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
4
|
# you may not use this file except in compliance with the License.
|
@@ -12,8 +12,6 @@
|
|
12
12
|
# See the License for the specific language governing permissions and
|
13
13
|
# limitations under the License.
|
14
14
|
|
15
|
-
# frozen_string_literal: true
|
16
|
-
|
17
15
|
class VWO
|
18
16
|
module CONSTANTS
|
19
17
|
API_VERSION = 1
|
@@ -22,23 +20,35 @@ class VWO
|
|
22
20
|
MAX_TRAFFIC_PERCENT = 100
|
23
21
|
MAX_TRAFFIC_VALUE = 10_000
|
24
22
|
STATUS_RUNNING = 'RUNNING'
|
23
|
+
# rubocop:disable Style/ExpandPathArguments
|
25
24
|
LIBRARY_PATH = File.expand_path('../..', __FILE__)
|
25
|
+
# rubocop:enable Style/ExpandPathArguments
|
26
26
|
HTTP_PROTOCOL = 'http://'
|
27
27
|
HTTPS_PROTOCOL = 'https://'
|
28
28
|
URL_NAMESPACE = '6ba7b811-9dad-11d1-80b4-00c04fd430c8'
|
29
|
-
SDK_VERSION = '1.
|
29
|
+
SDK_VERSION = '1.14.1'
|
30
30
|
SDK_NAME = 'ruby'
|
31
|
+
VWO_DELIMITER = '_vwo_'
|
32
|
+
MAX_EVENTS_PER_REQUEST = 5000
|
33
|
+
MIN_EVENTS_PER_REQUEST = 1
|
34
|
+
DEFAULT_EVENTS_PER_REQUEST = 100
|
35
|
+
DEFAULT_REQUEST_TIME_INTERVAL = 600 # 10 * 60(secs) = 600 secs i.e. 10 minutes
|
36
|
+
MIN_REQUEST_TIME_INTERVAL = 2
|
31
37
|
|
32
38
|
module ENDPOINTS
|
33
39
|
BASE_URL = 'dev.visualwebsiteoptimizer.com'
|
34
|
-
|
40
|
+
SETTINGS_URL = '/server-side/settings'
|
41
|
+
WEBHOOK_SETTINGS_URL = '/server-side/pull'
|
35
42
|
TRACK_USER = '/server-side/track-user'
|
36
43
|
TRACK_GOAL = '/server-side/track-goal'
|
44
|
+
PUSH = '/server-side/push'
|
45
|
+
BATCH_EVENTS = '/server-side/batch-events'
|
37
46
|
end
|
38
47
|
|
39
48
|
module EVENTS
|
40
49
|
TRACK_USER = 'track-user'
|
41
50
|
TRACK_GOAL = 'track-goal'
|
51
|
+
PUSH = 'push'
|
42
52
|
end
|
43
53
|
|
44
54
|
module DATATYPE
|
@@ -48,16 +58,57 @@ class VWO
|
|
48
58
|
BOOLEAN = 'boolean'
|
49
59
|
end
|
50
60
|
|
51
|
-
module
|
52
|
-
CREATE_INSTANCE = 'CREATE_INSTANCE'
|
53
|
-
ACTIVATE = 'ACTIVATE'
|
54
|
-
GET_VARIATION = 'GET_VARIATION'
|
55
|
-
TRACK = 'TRACK'
|
56
|
-
end
|
57
|
-
|
58
|
-
module GOALTYPES
|
61
|
+
module GoalTypes
|
59
62
|
REVENUE = 'REVENUE_TRACKING'
|
60
63
|
CUSTOM = 'CUSTOM_GOAL'
|
61
64
|
end
|
65
|
+
|
66
|
+
module VariableTypes
|
67
|
+
STRING = 'string'
|
68
|
+
INTEGER = 'integer'
|
69
|
+
DOUBLE = 'double'
|
70
|
+
BOOLEAN = 'boolean'
|
71
|
+
end
|
72
|
+
|
73
|
+
module Hooks
|
74
|
+
DECISION_TYPES = {
|
75
|
+
'CAMPAIGN_DECISION' => 'CAMPAIGN_DECISION'
|
76
|
+
}
|
77
|
+
end
|
78
|
+
|
79
|
+
RUBY_VARIABLE_TYPES = {
|
80
|
+
'string' => [String],
|
81
|
+
'integer' => [Integer],
|
82
|
+
'double' => [Float],
|
83
|
+
'boolean' => [TrueClass, FalseClass]
|
84
|
+
}
|
85
|
+
|
86
|
+
GOAL_TYPES = {
|
87
|
+
'REVENUE' => 'REVENUE_TRACKING',
|
88
|
+
'CUSTOM' => 'CUSTOM_GOAL',
|
89
|
+
'ALL' => 'ALL'
|
90
|
+
}
|
91
|
+
|
92
|
+
module ApiMethods
|
93
|
+
ACTIVATE = 'activate'
|
94
|
+
GET_VARIATION_NAME = 'get_variation_name'
|
95
|
+
TRACK = 'track'
|
96
|
+
IS_FEATURE_ENABLED = 'is_feature_enabled'
|
97
|
+
GET_FEATURE_VARIABLE_VALUE = 'get_feature_variable_value'
|
98
|
+
PUSH = 'push'
|
99
|
+
GET_AND_UPDATE_SETTINGS_FILE = 'get_and_update_settings_file'
|
100
|
+
FLUSH_EVENTS = 'flush_events'
|
101
|
+
end
|
102
|
+
|
103
|
+
module PushApi
|
104
|
+
TAG_VALUE_LENGTH = 255
|
105
|
+
TAG_KEY_LENGTH = 255
|
106
|
+
end
|
107
|
+
|
108
|
+
module CampaignTypes
|
109
|
+
VISUAL_AB = 'VISUAL_AB'
|
110
|
+
FEATURE_TEST = 'FEATURE_TEST'
|
111
|
+
FEATURE_ROLLOUT = 'FEATURE_ROLLOUT'
|
112
|
+
end
|
62
113
|
end
|
63
114
|
end
|
data/lib/vwo/core/bucketer.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
# Copyright 2019 Wingify Software Pvt. Ltd.
|
1
|
+
# Copyright 2019-2021 Wingify Software Pvt. Ltd.
|
2
2
|
#
|
3
3
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
4
|
# you may not use this file except in compliance with the License.
|
@@ -12,8 +12,6 @@
|
|
12
12
|
# See the License for the specific language governing permissions and
|
13
13
|
# limitations under the License.
|
14
14
|
|
15
|
-
# frozen_string_literal: true
|
16
|
-
|
17
15
|
require 'murmurhash3'
|
18
16
|
require_relative '../logger'
|
19
17
|
require_relative '../enums'
|
@@ -43,7 +41,7 @@ class VWO
|
|
43
41
|
# @param[String] :user_id The unique ID assigned to a user
|
44
42
|
# @param[Dict] :campaign For getting traffic allotted to the campaign
|
45
43
|
# @return[Boolean] If User is a part of Campaign or not
|
46
|
-
|
44
|
+
|
47
45
|
def user_part_of_campaign?(user_id, campaign)
|
48
46
|
unless valid_value?(user_id)
|
49
47
|
@logger.log(
|
@@ -62,7 +60,6 @@ class VWO
|
|
62
60
|
end
|
63
61
|
|
64
62
|
traffic_allocation = campaign['percentTraffic']
|
65
|
-
|
66
63
|
value_assigned_to_user = get_bucket_value_for_user(user_id)
|
67
64
|
is_user_part = (value_assigned_to_user != 0) && value_assigned_to_user <= traffic_allocation
|
68
65
|
@logger.log(
|
@@ -98,9 +95,9 @@ class VWO
|
|
98
95
|
end
|
99
96
|
|
100
97
|
hash_value = MurmurHash3::V32.str_hash(user_id, SEED_VALUE) & U_MAX_32_BIT
|
101
|
-
normalize = MAX_TRAFFIC_VALUE / campaign['percentTraffic']
|
98
|
+
normalize = MAX_TRAFFIC_VALUE.to_f / campaign['percentTraffic']
|
102
99
|
multiplier = normalize / 100
|
103
|
-
bucket_value =
|
100
|
+
bucket_value = get_bucket_value(
|
104
101
|
hash_value,
|
105
102
|
MAX_TRAFFIC_VALUE,
|
106
103
|
multiplier
|
@@ -118,10 +115,9 @@ class VWO
|
|
118
115
|
hash_value: hash_value
|
119
116
|
)
|
120
117
|
)
|
121
|
-
get_variation(campaign, bucket_value)
|
122
|
-
end
|
123
118
|
|
124
|
-
|
119
|
+
get_variation(campaign['variations'], bucket_value)
|
120
|
+
end
|
125
121
|
|
126
122
|
# Returns the Variation by checking the Start and End
|
127
123
|
# Bucket Allocations of each Variation
|
@@ -130,8 +126,8 @@ class VWO
|
|
130
126
|
# @param[Integer] :bucket_value The bucket Value of the user
|
131
127
|
# @return[Hash|nil] Variation data allotted to the user or None if not
|
132
128
|
#
|
133
|
-
def get_variation(
|
134
|
-
|
129
|
+
def get_variation(variations, bucket_value)
|
130
|
+
variations.find do |variation|
|
135
131
|
(variation['start_variation_allocation']..variation['end_variation_allocation']).cover?(bucket_value)
|
136
132
|
end
|
137
133
|
end
|
@@ -144,7 +140,7 @@ class VWO
|
|
144
140
|
# (between 1 to $this->$MAX_TRAFFIC_PERCENT)
|
145
141
|
def get_bucket_value_for_user(user_id)
|
146
142
|
hash_value = MurmurHash3::V32.str_hash(user_id, SEED_VALUE) & U_MAX_32_BIT
|
147
|
-
bucket_value =
|
143
|
+
bucket_value = get_bucket_value(hash_value, MAX_TRAFFIC_PERCENT)
|
148
144
|
|
149
145
|
@logger.log(
|
150
146
|
LogLevelEnum::DEBUG,
|
@@ -167,7 +163,7 @@ class VWO
|
|
167
163
|
# @param[Integer] :multiplier
|
168
164
|
# @return[Integer] Bucket Value of the User
|
169
165
|
#
|
170
|
-
def
|
166
|
+
def get_bucket_value(hash_value, max_value, multiplier = 1)
|
171
167
|
ratio = hash_value.to_f / MAX_HASH_VALUE
|
172
168
|
multiplied_value = (max_value * ratio + 1) * multiplier
|
173
169
|
multiplied_value.to_i
|
@@ -1,4 +1,4 @@
|
|
1
|
-
# Copyright 2019 Wingify Software Pvt. Ltd.
|
1
|
+
# Copyright 2019-2021 Wingify Software Pvt. Ltd.
|
2
2
|
#
|
3
3
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
4
|
# you may not use this file except in compliance with the License.
|
@@ -12,22 +12,26 @@
|
|
12
12
|
# See the License for the specific language governing permissions and
|
13
13
|
# limitations under the License.
|
14
14
|
|
15
|
-
# frozen_string_literal: true
|
16
|
-
|
17
15
|
require_relative '../logger'
|
18
16
|
require_relative '../enums'
|
19
17
|
require_relative '../utils/campaign'
|
18
|
+
require_relative '../services/segment_evaluator'
|
20
19
|
require_relative '../utils/validations'
|
21
20
|
require_relative 'bucketer'
|
21
|
+
require_relative '../constants'
|
22
|
+
require_relative '../services/hooks_manager'
|
23
|
+
require_relative '../utils/uuid'
|
22
24
|
|
23
25
|
class VWO
|
24
26
|
module Core
|
25
27
|
class VariationDecider
|
26
|
-
attr_reader :user_storage_service
|
28
|
+
attr_reader :user_storage_service, :has_stored_variation, :hooks_manager
|
27
29
|
|
28
30
|
include VWO::Enums
|
29
31
|
include VWO::Utils::Campaign
|
30
32
|
include VWO::Utils::Validations
|
33
|
+
include VWO::CONSTANTS
|
34
|
+
include VWO::Utils::UUID
|
31
35
|
|
32
36
|
FILE = FileNameEnum::VariationDecider
|
33
37
|
|
@@ -35,34 +39,125 @@ class VWO
|
|
35
39
|
# @param[Hash] - Settings file
|
36
40
|
# @param[Class] - Class instance having the capability of
|
37
41
|
# get and save.
|
38
|
-
def initialize(settings_file, user_storage_service = nil)
|
42
|
+
def initialize(settings_file, user_storage_service = nil, options = {})
|
39
43
|
@logger = VWO::Logger.get_instance
|
40
44
|
@user_storage_service = user_storage_service
|
41
|
-
# Check if user_storage_service provided is valid or not
|
42
|
-
@user_storage_service = user_storage_service
|
43
45
|
@bucketer = VWO::Core::Bucketer.new
|
44
46
|
@settings_file = settings_file
|
47
|
+
@segment_evaluator = VWO::Services::SegmentEvaluator.new
|
48
|
+
@hooks_manager = VWO::Services::HooksManager.new(options)
|
45
49
|
end
|
46
50
|
|
47
51
|
# Returns variation for the user for the passed campaign-key
|
52
|
+
# Check if Whitelisting is applicable, evaluate it, if any eligible variation is found,return, otherwise skip it
|
48
53
|
# Check in User Storage, if user found, validate variation and return
|
49
54
|
# Otherwise, proceed with variation assignment logic
|
50
55
|
#
|
51
56
|
#
|
52
57
|
# @param[String] :user_id The unique ID assigned to User
|
53
|
-
# @param[Hash] :campaign Campaign hash
|
58
|
+
# @param[Hash] :campaign Campaign hash itself
|
59
|
+
# @param[String] :campaign_key The unique ID of the campaign passed
|
60
|
+
# @param[String] :goal_identifier The unique campaign's goal identifier
|
54
61
|
# @return[String,String] ({variation_id, variation_name}|Nil): Tuple of
|
55
62
|
# variation_id and variation_name if variation allotted, else nil
|
56
63
|
|
57
|
-
def get_variation(user_id, campaign)
|
58
|
-
|
59
|
-
|
64
|
+
def get_variation(user_id, campaign, api_name, campaign_key, custom_variables = {}, variation_targeting_variables = {}, goal_identifier = '')
|
65
|
+
campaign_key ||= campaign['key']
|
66
|
+
|
67
|
+
return unless campaign
|
68
|
+
|
69
|
+
@has_stored_variation = false
|
70
|
+
decision = {
|
71
|
+
:campaign_id => campaign['id'],
|
72
|
+
:campaign_key => campaign_key,
|
73
|
+
:campaign_type => campaign['type'],
|
74
|
+
# campaign segmentation conditions
|
75
|
+
:custom_variables => custom_variables,
|
76
|
+
# event name
|
77
|
+
:event => Hooks::DECISION_TYPES['CAMPAIGN_DECISION'],
|
78
|
+
# goal tracked in case of track API
|
79
|
+
:goal_identifier => goal_identifier,
|
80
|
+
# campaign whitelisting flag
|
81
|
+
:is_forced_variation_enabled => campaign['isForcedVariationEnabled'] ? campaign['isForcedVariationEnabled'] : false,
|
82
|
+
:sdk_version => SDK_VERSION,
|
83
|
+
# API name which triggered the event
|
84
|
+
:source => api_name,
|
85
|
+
# Passed in API
|
86
|
+
:user_id => user_id,
|
87
|
+
# Campaign Whitelisting conditions
|
88
|
+
:variation_targeting_variables => variation_targeting_variables,
|
89
|
+
:is_user_whitelisted => false,
|
90
|
+
:from_user_storage_service => false,
|
91
|
+
:is_feature_enabled => true,
|
92
|
+
# VWO generated UUID based on passed UserId and Account ID
|
93
|
+
:vwo_user_id => generator_for(user_id, @settings_file['accountId'])
|
94
|
+
}
|
95
|
+
|
96
|
+
if campaign['isForcedVariationEnabled']
|
97
|
+
variation = evaluate_whitelisting(
|
98
|
+
user_id,
|
99
|
+
campaign,
|
100
|
+
api_name,
|
101
|
+
campaign_key,
|
102
|
+
variation_targeting_variables
|
103
|
+
)
|
104
|
+
status = if variation
|
105
|
+
StatusEnum::PASSED
|
106
|
+
else
|
107
|
+
StatusEnum::FAILED
|
108
|
+
end
|
109
|
+
|
110
|
+
@logger.log(
|
111
|
+
LogLevelEnum::INFO,
|
112
|
+
format(
|
113
|
+
LogMessageEnum::InfoMessages::SEGMENTATION_STATUS,
|
114
|
+
file: FILE,
|
115
|
+
campaign_key: campaign_key,
|
116
|
+
user_id: user_id,
|
117
|
+
status: status,
|
118
|
+
custom_variables: variation_targeting_variables,
|
119
|
+
variation_name: status == StatusEnum::PASSED ? "and #{variation['name']} is Assigned" : ' ',
|
120
|
+
segmentation_type: SegmentationTypeEnum::WHITELISTING,
|
121
|
+
api_name: api_name
|
122
|
+
)
|
123
|
+
)
|
124
|
+
|
125
|
+
if variation
|
126
|
+
if campaign['type'] == CampaignTypes::VISUAL_AB || campaign['type'] == CampaignTypes::FEATURE_TEST
|
127
|
+
decision[:variation_name] = variation['name']
|
128
|
+
decision[:variation_id] = variation['id']
|
129
|
+
if campaign['type'] == CampaignTypes::FEATURE_TEST
|
130
|
+
decision[:is_feature_enabled] = variation['isFeatureEnabled']
|
131
|
+
elsif campaign['type'] == CampaignTypes::VISUAL_AB
|
132
|
+
decision[:is_user_whitelisted] = !!variation['name']
|
133
|
+
end
|
134
|
+
end
|
135
|
+
@hooks_manager.execute(decision)
|
136
|
+
end
|
137
|
+
|
138
|
+
return variation if variation && variation['name']
|
139
|
+
else
|
140
|
+
@logger.log(
|
141
|
+
LogLevelEnum::INFO,
|
142
|
+
format(
|
143
|
+
LogMessageEnum::InfoMessages::WHITELISTING_SKIPPED,
|
144
|
+
file: FILE,
|
145
|
+
campaign_key: campaign_key,
|
146
|
+
user_id: user_id,
|
147
|
+
api_name: api_name
|
148
|
+
)
|
149
|
+
)
|
60
150
|
end
|
61
151
|
|
62
152
|
user_campaign_map = get_user_storage(user_id, campaign_key)
|
63
153
|
variation = get_stored_variation(user_id, campaign_key, user_campaign_map) if valid_hash?(user_campaign_map)
|
154
|
+
variation = variation.dup # deep copy
|
64
155
|
|
65
156
|
if variation
|
157
|
+
if valid_string?(user_campaign_map['goal_identifier']) && api_name == ApiMethods::TRACK
|
158
|
+
variation['goal_identifier'] = user_campaign_map['goal_identifier']
|
159
|
+
end
|
160
|
+
@has_stored_variation = true
|
66
161
|
@logger.log(
|
67
162
|
LogLevelEnum::INFO,
|
68
163
|
format(
|
@@ -73,13 +168,117 @@ class VWO
|
|
73
168
|
variation_name: variation['name']
|
74
169
|
)
|
75
170
|
)
|
76
|
-
|
171
|
+
decision[:from_user_storage_service] = !!variation['name']
|
172
|
+
if variation
|
173
|
+
if campaign['type'] == CampaignTypes::VISUAL_AB || campaign['type'] == CampaignTypes::FEATURE_TEST
|
174
|
+
decision[:variation_name] = variation['name']
|
175
|
+
decision[:variation_id] = variation['id']
|
176
|
+
if campaign['type'] == CampaignTypes::FEATURE_TEST
|
177
|
+
decision[:is_feature_enabled] = variation['isFeatureEnabled']
|
178
|
+
end
|
179
|
+
end
|
180
|
+
@hooks_manager.execute(decision)
|
181
|
+
end
|
182
|
+
return variation
|
183
|
+
else
|
184
|
+
@logger.log(
|
185
|
+
LogLevelEnum::DEBUG,
|
186
|
+
format(
|
187
|
+
LogMessageEnum::DebugMessages::NO_STORED_VARIATION,
|
188
|
+
file: FILE,
|
189
|
+
campaign_key: campaign_key,
|
190
|
+
user_id: user_id
|
191
|
+
)
|
192
|
+
)
|
193
|
+
|
194
|
+
if ([ApiMethods::TRACK, ApiMethods::GET_VARIATION_NAME, ApiMethods::GET_FEATURE_VARIABLE_VALUE].include? api_name) &&
|
195
|
+
@user_storage_service && campaign['type'] != CampaignTypes::FEATURE_ROLLOUT
|
196
|
+
@logger.log(
|
197
|
+
LogLevelEnum::DEBUG,
|
198
|
+
format(
|
199
|
+
LogMessageEnum::DebugMessages::CAMPAIGN_NOT_ACTIVATED,
|
200
|
+
file: FILE,
|
201
|
+
campaign_key: campaign_key,
|
202
|
+
user_id: user_id,
|
203
|
+
api_name: api_name
|
204
|
+
)
|
205
|
+
)
|
206
|
+
|
207
|
+
@logger.log(
|
208
|
+
LogLevelEnum::INFO,
|
209
|
+
format(
|
210
|
+
LogMessageEnum::InfoMessages::CAMPAIGN_NOT_ACTIVATED,
|
211
|
+
file: FILE,
|
212
|
+
campaign_key: campaign_key,
|
213
|
+
user_id: user_id,
|
214
|
+
api_name: api_name,
|
215
|
+
reason: api_name == ApiMethods::TRACK ? 'track it' : 'get the decision/value'
|
216
|
+
)
|
217
|
+
)
|
218
|
+
return
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
# Pre-segmentation
|
223
|
+
|
224
|
+
segments = get_segments(campaign)
|
225
|
+
is_valid_segments = valid_value?(segments)
|
226
|
+
|
227
|
+
if is_valid_segments
|
228
|
+
unless custom_variables
|
229
|
+
@logger.log(
|
230
|
+
LogLevelEnum::INFO,
|
231
|
+
format(
|
232
|
+
LogMessageEnum::InfoMessages::NO_CUSTOM_VARIABLES,
|
233
|
+
file: FILE,
|
234
|
+
campaign_key: campaign_key,
|
235
|
+
user_id: user_id,
|
236
|
+
api_name: api_name
|
237
|
+
)
|
238
|
+
)
|
239
|
+
custom_variables = {}
|
240
|
+
end
|
241
|
+
unless @segment_evaluator.evaluate(campaign_key, user_id, segments, custom_variables)
|
242
|
+
@logger.log(
|
243
|
+
LogLevelEnum::INFO,
|
244
|
+
format(
|
245
|
+
LogMessageEnum::InfoMessages::USER_FAILED_SEGMENTATION,
|
246
|
+
file: FileNameEnum::SegmentEvaluator,
|
247
|
+
user_id: user_id,
|
248
|
+
campaign_key: campaign_key,
|
249
|
+
custom_variables: custom_variables
|
250
|
+
)
|
251
|
+
)
|
252
|
+
return
|
253
|
+
end
|
254
|
+
@logger.log(
|
255
|
+
LogLevelEnum::INFO,
|
256
|
+
format(
|
257
|
+
LogMessageEnum::InfoMessages::USER_PASSED_SEGMENTATION,
|
258
|
+
file: FileNameEnum::SegmentEvaluator,
|
259
|
+
user_id: user_id,
|
260
|
+
campaign_key: campaign_key,
|
261
|
+
custom_variables: custom_variables
|
262
|
+
)
|
263
|
+
)
|
264
|
+
else
|
265
|
+
@logger.log(
|
266
|
+
LogLevelEnum::INFO,
|
267
|
+
format(
|
268
|
+
LogMessageEnum::InfoMessages::SKIPPING_SEGMENTATION,
|
269
|
+
file: FILE,
|
270
|
+
campaign_key: campaign_key,
|
271
|
+
user_id: user_id,
|
272
|
+
api_name: api_name,
|
273
|
+
variation: ''
|
274
|
+
)
|
275
|
+
)
|
77
276
|
end
|
78
277
|
|
79
|
-
|
278
|
+
variation = get_variation_allotted(user_id, campaign)
|
80
279
|
|
81
|
-
if
|
82
|
-
save_user_storage(user_id, campaign_key,
|
280
|
+
if variation && variation['name']
|
281
|
+
save_user_storage(user_id, campaign_key, variation['name'], goal_identifier) if variation['name']
|
83
282
|
|
84
283
|
@logger.log(
|
85
284
|
LogLevelEnum::INFO,
|
@@ -88,7 +287,8 @@ class VWO
|
|
88
287
|
file: FILE,
|
89
288
|
campaign_key: campaign_key,
|
90
289
|
user_id: user_id,
|
91
|
-
variation_name:
|
290
|
+
variation_name: variation['name'],
|
291
|
+
campaign_type: campaign['type']
|
92
292
|
)
|
93
293
|
)
|
94
294
|
else
|
@@ -97,39 +297,50 @@ class VWO
|
|
97
297
|
format(LogMessageEnum::InfoMessages::NO_VARIATION_ALLOCATED, file: FILE, campaign_key: campaign_key, user_id: user_id)
|
98
298
|
)
|
99
299
|
end
|
100
|
-
|
300
|
+
|
301
|
+
if variation
|
302
|
+
if campaign['type'] == CampaignTypes::VISUAL_AB || campaign['type'] == CampaignTypes::FEATURE_TEST
|
303
|
+
decision[:variation_name] = variation['name']
|
304
|
+
decision[:variation_id] = variation['id']
|
305
|
+
if campaign['type'] == CampaignTypes::FEATURE_TEST
|
306
|
+
decision[:is_feature_enabled] = variation['isFeatureEnabled']
|
307
|
+
end
|
308
|
+
end
|
309
|
+
@hooks_manager.execute(decision)
|
310
|
+
end
|
311
|
+
variation
|
101
312
|
end
|
102
313
|
|
103
314
|
# Returns the Variation Alloted for required campaign
|
104
315
|
#
|
105
|
-
# @param[String] :user_id The unique
|
316
|
+
# @param[String] :user_id The unique key assigned to User
|
106
317
|
# @param[Hash] :campaign Campaign hash for Unique campaign key
|
107
318
|
#
|
108
319
|
# @return[Hash]
|
109
320
|
|
110
321
|
def get_variation_allotted(user_id, campaign)
|
111
|
-
variation_id, variation_name = nil
|
112
322
|
unless valid_value?(user_id)
|
113
323
|
@logger.log(
|
114
324
|
LogLevelEnum::ERROR,
|
115
325
|
format(LogMessageEnum::ErrorMessages::INVALID_USER_ID, file: FILE, user_id: user_id, method: 'get_variation_alloted')
|
116
326
|
)
|
117
|
-
return
|
327
|
+
return
|
118
328
|
end
|
119
329
|
|
120
330
|
if @bucketer.user_part_of_campaign?(user_id, campaign)
|
121
|
-
|
331
|
+
variation = get_variation_of_campaign_for_user(user_id, campaign)
|
122
332
|
@logger.log(
|
123
333
|
LogLevelEnum::DEBUG,
|
124
334
|
format(
|
125
335
|
LogMessageEnum::DebugMessages::GOT_VARIATION_FOR_USER,
|
126
336
|
file: FILE,
|
127
|
-
variation_name:
|
337
|
+
variation_name: variation['name'],
|
128
338
|
user_id: user_id,
|
129
339
|
campaign_key: campaign['key'],
|
130
340
|
method: 'get_variation_allotted'
|
131
341
|
)
|
132
342
|
)
|
343
|
+
variation
|
133
344
|
else
|
134
345
|
# not part of campaign
|
135
346
|
@logger.log(
|
@@ -142,8 +353,8 @@ class VWO
|
|
142
353
|
method: 'get_variation_allotted'
|
143
354
|
)
|
144
355
|
)
|
356
|
+
nil
|
145
357
|
end
|
146
|
-
[variation_id, variation_name]
|
147
358
|
end
|
148
359
|
|
149
360
|
# Assigns variation to a particular user depending on the campaign PercentTraffic.
|
@@ -151,6 +362,7 @@ class VWO
|
|
151
362
|
# @param[String] :user_id The unique ID assigned to a user
|
152
363
|
# @param[Hash] :campaign The Campaign of which user is to be made a part of
|
153
364
|
# @return[Hash] Variation allotted to User
|
365
|
+
|
154
366
|
def get_variation_of_campaign_for_user(user_id, campaign)
|
155
367
|
unless campaign
|
156
368
|
@logger.log(
|
@@ -161,7 +373,7 @@ class VWO
|
|
161
373
|
method: 'get_variation_of_campaign_for_user'
|
162
374
|
)
|
163
375
|
)
|
164
|
-
return nil
|
376
|
+
return nil
|
165
377
|
end
|
166
378
|
|
167
379
|
variation = @bucketer.bucket_user_to_variation(user_id, campaign)
|
@@ -177,7 +389,7 @@ class VWO
|
|
177
389
|
campaign_key: campaign['key']
|
178
390
|
)
|
179
391
|
)
|
180
|
-
return variation
|
392
|
+
return variation
|
181
393
|
end
|
182
394
|
|
183
395
|
@logger.log(
|
@@ -189,11 +401,155 @@ class VWO
|
|
189
401
|
campaign_key: campaign['key']
|
190
402
|
)
|
191
403
|
)
|
192
|
-
|
404
|
+
nil
|
405
|
+
end
|
406
|
+
|
407
|
+
# If UserStorageService is provided, save the assigned variation
|
408
|
+
#
|
409
|
+
# @param[String] :user_id Unique user identifier
|
410
|
+
# @param[String] :campaign_key Unique campaign identifier
|
411
|
+
# @param[String] :variation_name Variation identifier
|
412
|
+
# @param[String] :goal_identifier The unique campaign's goal identifier
|
413
|
+
# @return[Boolean] true if found otherwise false
|
414
|
+
|
415
|
+
def save_user_storage(user_id, campaign_key, variation_name, goal_identifier)
|
416
|
+
unless @user_storage_service
|
417
|
+
@logger.log(
|
418
|
+
LogLevelEnum::DEBUG,
|
419
|
+
format(LogMessageEnum::DebugMessages::NO_USER_STORAGE_SERVICE_SAVE, file: FILE)
|
420
|
+
)
|
421
|
+
return false
|
422
|
+
end
|
423
|
+
new_campaign_user_mapping = {}
|
424
|
+
new_campaign_user_mapping['campaign_key'] = campaign_key
|
425
|
+
new_campaign_user_mapping['user_id'] = user_id
|
426
|
+
new_campaign_user_mapping['variation_name'] = variation_name
|
427
|
+
if !goal_identifier.empty?
|
428
|
+
new_campaign_user_mapping['goal_identifier'] = goal_identifier
|
429
|
+
end
|
430
|
+
|
431
|
+
@user_storage_service.set(new_campaign_user_mapping)
|
432
|
+
|
433
|
+
@logger.log(
|
434
|
+
LogLevelEnum::INFO,
|
435
|
+
format(LogMessageEnum::InfoMessages::SAVING_DATA_USER_STORAGE_SERVICE, file: FILE, user_id: user_id)
|
436
|
+
)
|
437
|
+
true
|
438
|
+
rescue StandardError
|
439
|
+
@logger.log(
|
440
|
+
LogLevelEnum::ERROR,
|
441
|
+
format(LogMessageEnum::ErrorMessages::SAVE_USER_STORAGE_SERVICE_FAILED, file: FILE, user_id: user_id)
|
442
|
+
)
|
443
|
+
false
|
193
444
|
end
|
194
445
|
|
195
446
|
private
|
196
447
|
|
448
|
+
# Evaluate all the variations in the campaign to find
|
449
|
+
#
|
450
|
+
# @param[String] :user_id The unique key assigned to User
|
451
|
+
# @param[Hash] :campaign Campaign hash for Unique campaign key
|
452
|
+
# @param[String] :api_name The key Passed to identify the calling API
|
453
|
+
# @param[String] :campaign_key Unique campaign key
|
454
|
+
# @param[Hash] :variation_targeting_variables Key/value pair of Whitelisting Custom Attributes
|
455
|
+
#
|
456
|
+
# @return[Hash]
|
457
|
+
|
458
|
+
def evaluate_whitelisting(user_id, campaign, api_name, campaign_key, variation_targeting_variables = {})
|
459
|
+
if variation_targeting_variables.nil?
|
460
|
+
variation_targeting_variables = { '_vwo_user_id' => user_id }
|
461
|
+
else
|
462
|
+
variation_targeting_variables['_vwo_user_id'] = user_id
|
463
|
+
end
|
464
|
+
targeted_variations = []
|
465
|
+
|
466
|
+
campaign['variations'].each do |variation|
|
467
|
+
segments = get_segments(variation)
|
468
|
+
is_valid_segments = valid_value?(segments)
|
469
|
+
if is_valid_segments
|
470
|
+
if @segment_evaluator.evaluate(campaign_key, user_id, segments, variation_targeting_variables)
|
471
|
+
targeted_variations.push(variation)
|
472
|
+
status = StatusEnum::PASSED
|
473
|
+
else
|
474
|
+
status = StatusEnum::FAILED
|
475
|
+
end
|
476
|
+
@logger.log(
|
477
|
+
LogLevelEnum::DEBUG,
|
478
|
+
format(
|
479
|
+
LogMessageEnum::DebugMessages::SEGMENTATION_STATUS,
|
480
|
+
file: FILE,
|
481
|
+
campaign_key: campaign_key,
|
482
|
+
user_id: user_id,
|
483
|
+
status: status,
|
484
|
+
custom_variables: variation_targeting_variables,
|
485
|
+
variation_name: variation['name'],
|
486
|
+
segmentation_type: SegmentationTypeEnum::WHITELISTING,
|
487
|
+
api_name: api_name
|
488
|
+
)
|
489
|
+
)
|
490
|
+
else
|
491
|
+
@logger.log(
|
492
|
+
LogLevelEnum::DEBUG,
|
493
|
+
format(
|
494
|
+
LogMessageEnum::InfoMessages::SKIPPING_SEGMENTATION,
|
495
|
+
file: FILE,
|
496
|
+
campaign_key: campaign_key,
|
497
|
+
user_id: user_id,
|
498
|
+
api_name: api_name,
|
499
|
+
variation: variation['name']
|
500
|
+
)
|
501
|
+
)
|
502
|
+
end
|
503
|
+
end
|
504
|
+
|
505
|
+
if targeted_variations.length > 1
|
506
|
+
targeted_variations_deep_clone = Marshal.load(Marshal.dump(targeted_variations))
|
507
|
+
scale_variation_weights(targeted_variations_deep_clone)
|
508
|
+
current_allocation = 0
|
509
|
+
targeted_variations_deep_clone.each do |variation|
|
510
|
+
step_factor = get_variation_bucketing_range(variation['weight'])
|
511
|
+
if step_factor > 0
|
512
|
+
start_range = current_allocation + 1
|
513
|
+
end_range = current_allocation + step_factor
|
514
|
+
variation['start_variation_allocation'] = start_range
|
515
|
+
variation['end_variation_allocation'] = end_range
|
516
|
+
current_allocation += step_factor
|
517
|
+
else
|
518
|
+
variation['start_variation_allocation'] = -1
|
519
|
+
variation['end_variation_allocation'] = -1
|
520
|
+
end
|
521
|
+
end
|
522
|
+
whitelisted_variation = @bucketer.get_variation(
|
523
|
+
targeted_variations_deep_clone,
|
524
|
+
@bucketer.get_bucket_value_for_user(
|
525
|
+
user_id
|
526
|
+
)
|
527
|
+
)
|
528
|
+
else
|
529
|
+
whitelisted_variation = targeted_variations[0]
|
530
|
+
end
|
531
|
+
whitelisted_variation
|
532
|
+
end
|
533
|
+
|
534
|
+
# It extracts the weights from all the variations inside the campaign
|
535
|
+
# and scales them so that the total sum of eligible variations' weights become 100%
|
536
|
+
#
|
537
|
+
# 1. variations
|
538
|
+
|
539
|
+
def scale_variation_weights(variations)
|
540
|
+
total_weight = variations.reduce(0) { |final_weight, variation| final_weight + variation['weight'].to_f }
|
541
|
+
if total_weight == 0
|
542
|
+
weight = 100 / variations.length
|
543
|
+
variations.each do |variation|
|
544
|
+
variation['weight'] = weight
|
545
|
+
end
|
546
|
+
else
|
547
|
+
variations.each do |variation|
|
548
|
+
variation['weight'] = (variation['weight'] / total_weight) * 100
|
549
|
+
end
|
550
|
+
end
|
551
|
+
end
|
552
|
+
|
197
553
|
# Get the UserStorageData after looking up into get method
|
198
554
|
# Being provided via UserStorageService
|
199
555
|
#
|
@@ -238,70 +594,25 @@ class VWO
|
|
238
594
|
# @return[Object, nil] if found then variation settings object otherwise None
|
239
595
|
|
240
596
|
def get_stored_variation(user_id, campaign_key, user_campaign_map)
|
241
|
-
|
242
|
-
variation_name = user_campaign_map[:variationName]
|
243
|
-
@logger.log(
|
244
|
-
LogLevelEnum::DEBUG,
|
245
|
-
format(
|
246
|
-
LogMessageEnum::DebugMessages::GETTING_STORED_VARIATION,
|
247
|
-
file: FILE,
|
248
|
-
campaign_key: campaign_key,
|
249
|
-
user_id: user_id,
|
250
|
-
variation_name: variation_name
|
251
|
-
)
|
252
|
-
)
|
253
|
-
return get_campaign_variation(
|
254
|
-
@settings_file,
|
255
|
-
campaign_key,
|
256
|
-
variation_name
|
257
|
-
)
|
258
|
-
end
|
597
|
+
return unless user_campaign_map['campaign_key'] == campaign_key
|
259
598
|
|
599
|
+
variation_name = user_campaign_map['variation_name']
|
260
600
|
@logger.log(
|
261
601
|
LogLevelEnum::DEBUG,
|
262
602
|
format(
|
263
|
-
LogMessageEnum::DebugMessages::
|
603
|
+
LogMessageEnum::DebugMessages::GETTING_STORED_VARIATION,
|
264
604
|
file: FILE,
|
265
605
|
campaign_key: campaign_key,
|
266
|
-
user_id: user_id
|
606
|
+
user_id: user_id,
|
607
|
+
variation_name: variation_name
|
267
608
|
)
|
268
609
|
)
|
269
|
-
nil
|
270
|
-
end
|
271
610
|
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
# @param[String] :variation_name Variation identifier
|
277
|
-
# @return[Boolean] true if found otherwise false
|
278
|
-
|
279
|
-
def save_user_storage(user_id, campaign_key, variation_name)
|
280
|
-
unless @user_storage_service
|
281
|
-
@logger.log(
|
282
|
-
LogLevelEnum::DEBUG,
|
283
|
-
format(LogMessageEnum::DebugMessages::NO_USER_STORAGE_SERVICE_SAVE, file: FILE)
|
284
|
-
)
|
285
|
-
return false
|
286
|
-
end
|
287
|
-
new_campaign_user_mapping = {}
|
288
|
-
new_campaign_user_mapping['campaign_key'] = campaign_key
|
289
|
-
new_campaign_user_mapping['user_id'] = user_id
|
290
|
-
new_campaign_user_mapping['variation_name'] = variation_name
|
291
|
-
|
292
|
-
@user_storage_service.set(new_campaign_user_mapping)
|
293
|
-
|
294
|
-
@logger.log(
|
295
|
-
LogLevelEnum::INFO,
|
296
|
-
format(LogMessageEnum::InfoMessages::SAVING_DATA_USER_STORAGE_SERVICE, file: FILE, user_id: user_id)
|
297
|
-
)
|
298
|
-
true
|
299
|
-
rescue StandardError
|
300
|
-
@logger.log(
|
301
|
-
LogLevelEnum::ERROR,
|
302
|
-
format(LogMessageEnum::ErrorMessages::SAVE_USER_STORAGE_SERVICE_FAILED, file: FILE, user_id: user_id)
|
611
|
+
get_campaign_variation(
|
612
|
+
@settings_file,
|
613
|
+
campaign_key,
|
614
|
+
variation_name
|
303
615
|
)
|
304
|
-
false
|
305
616
|
end
|
306
617
|
end
|
307
618
|
end
|