vwo-fme-ruby-sdk 1.0.0 → 1.1.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/vwo/api/get_flag.rb +236 -0
- data/lib/vwo/api/set_attribute.rb +57 -0
- data/lib/vwo/api/track_event.rb +77 -0
- data/lib/vwo/constants/constants.rb +54 -0
- data/lib/vwo/decorators/storage_decorator.rb +86 -0
- data/lib/vwo/{utils/logger_helper.rb → enums/api_enum.rb} +5 -10
- data/lib/vwo/enums/campaign_type_enum.rb +19 -0
- data/lib/vwo/enums/decision_types_enum.rb +18 -0
- data/lib/vwo/enums/event_enum.rb +19 -0
- data/lib/vwo/enums/headers_enum.rb +20 -0
- data/lib/vwo/enums/hooks_enum.rb +17 -0
- data/lib/vwo/enums/http_method_enum.rb +18 -0
- data/lib/vwo/enums/log_level_enum.rb +21 -0
- data/lib/vwo/enums/status_enum.rb +19 -0
- data/lib/vwo/enums/storage_enum.rb +22 -0
- data/lib/vwo/enums/url_enum.rb +21 -0
- data/lib/vwo/models/campaign/campaign_model.rb +192 -0
- data/lib/vwo/models/campaign/feature_model.rb +111 -0
- data/lib/vwo/models/campaign/impact_campaign_model.rb +38 -0
- data/lib/vwo/models/campaign/metric_model.rb +44 -0
- data/lib/vwo/models/campaign/rule_model.rb +56 -0
- data/lib/vwo/models/campaign/variable_model.rb +51 -0
- data/lib/vwo/models/campaign/variation_model.rb +137 -0
- data/lib/vwo/models/gateway_service_model.rb +39 -0
- data/lib/vwo/models/schemas/settings_schema_validation.rb +102 -0
- data/lib/vwo/models/settings/settings_model.rb +85 -0
- data/lib/vwo/models/storage/storage_data_model.rb +44 -0
- data/lib/vwo/models/user/context_model.rb +100 -0
- data/lib/vwo/models/user/context_vwo_model.rb +38 -0
- data/lib/vwo/{utils/feature_flag_response.rb → models/user/get_flag_response.rb} +14 -14
- data/lib/vwo/models/vwo_options_model.rb +107 -0
- data/lib/vwo/packages/decision_maker/decision_maker.rb +60 -0
- data/lib/vwo/packages/logger/core/log_manager.rb +90 -0
- data/lib/vwo/packages/logger/core/transport_manager.rb +87 -0
- data/lib/vwo/packages/logger/log_message_builder.rb +70 -0
- data/lib/vwo/packages/logger/logger.rb +38 -0
- data/lib/vwo/packages/logger/transports/console_transport.rb +49 -0
- data/lib/vwo/packages/network_layer/client/network_client.rb +107 -0
- data/lib/vwo/packages/network_layer/handlers/request_handler.rb +37 -0
- data/lib/vwo/packages/network_layer/manager/network_manager.rb +78 -0
- data/lib/vwo/packages/network_layer/models/global_request_model.rb +105 -0
- data/lib/vwo/packages/network_layer/models/request_model.rb +145 -0
- data/lib/vwo/packages/network_layer/models/response_model.rb +45 -0
- data/lib/vwo/packages/segmentation_evaluator/core/segmentation_manager.rb +76 -0
- data/lib/vwo/packages/segmentation_evaluator/enums/segment_operand_regex_enum.rb +29 -0
- data/lib/vwo/packages/segmentation_evaluator/enums/segment_operand_value_enum.rb +26 -0
- data/lib/vwo/packages/segmentation_evaluator/enums/segment_operator_value_enum.rb +30 -0
- data/lib/vwo/packages/segmentation_evaluator/evaluators/segment_evaluator.rb +210 -0
- data/lib/vwo/packages/segmentation_evaluator/evaluators/segment_operand_evaluator.rb +198 -0
- data/lib/vwo/packages/segmentation_evaluator/utils/segment_util.rb +44 -0
- data/lib/vwo/{constants.rb → packages/storage/connector.rb} +12 -10
- data/lib/vwo/packages/storage/storage.rb +45 -0
- data/lib/vwo/services/campaign_decision_service.rb +153 -0
- data/lib/vwo/services/hooks_service.rb +51 -0
- data/lib/vwo/services/logger_service.rb +83 -0
- data/lib/vwo/services/settings_service.rb +120 -0
- data/lib/vwo/services/storage_service.rb +65 -0
- data/lib/vwo/utils/campaign_util.rb +249 -0
- data/lib/vwo/utils/data_type_util.rb +105 -0
- data/lib/vwo/utils/decision_util.rb +253 -0
- data/lib/vwo/utils/function_util.rb +123 -0
- data/lib/vwo/utils/gateway_service_util.rb +101 -0
- data/lib/vwo/utils/impression_util.rb +49 -0
- data/lib/vwo/utils/log_message_util.rb +42 -0
- data/lib/vwo/utils/meg_util.rb +350 -0
- data/lib/vwo/utils/network_util.rb +235 -0
- data/lib/vwo/utils/rule_evaluation_util.rb +57 -0
- data/lib/vwo/utils/settings_util.rb +38 -0
- data/lib/vwo/utils/url_util.rb +46 -0
- data/lib/vwo/utils/uuid_util.rb +55 -0
- data/lib/vwo/vwo_builder.rb +156 -11
- data/lib/vwo/vwo_client.rb +163 -113
- data/lib/vwo.rb +49 -31
- metadata +187 -9
- data/lib/vwo/utils/request.rb +0 -89
@@ -0,0 +1,350 @@
|
|
1
|
+
# Copyright 2025 Wingify Software Pvt. Ltd.
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
|
15
|
+
require_relative '../constants/constants'
|
16
|
+
require_relative '../decorators/storage_decorator'
|
17
|
+
require_relative '../enums/campaign_type_enum'
|
18
|
+
require_relative '../models/campaign/campaign_model'
|
19
|
+
require_relative '../models/campaign/feature_model'
|
20
|
+
require_relative '../models/campaign/variation_model'
|
21
|
+
require_relative '../models/settings/settings_model'
|
22
|
+
require_relative '../models/user/context_model'
|
23
|
+
require_relative '../packages/decision_maker/decision_maker'
|
24
|
+
require_relative '../services/campaign_decision_service'
|
25
|
+
require_relative '../services/storage_service'
|
26
|
+
require_relative './rule_evaluation_util'
|
27
|
+
require_relative './campaign_util'
|
28
|
+
require_relative './data_type_util'
|
29
|
+
require_relative './decision_util'
|
30
|
+
require_relative './function_util'
|
31
|
+
require_relative '../services/logger_service'
|
32
|
+
require_relative '../enums/log_level_enum'
|
33
|
+
|
34
|
+
# Evaluates groups for a given feature and group ID.
|
35
|
+
# @param settings [SettingsModel] The settings for the VWO instance
|
36
|
+
# @param feature [FeatureModel] The feature to evaluate
|
37
|
+
# @param group_id [String] The group ID to evaluate
|
38
|
+
# @param evaluated_feature_map [Hash] The map of evaluated features
|
39
|
+
# @param context [ContextModel] The context for the evaluation
|
40
|
+
# @param storage_service [StorageService] The storage service for the evaluation
|
41
|
+
def evaluate_groups(settings, feature, group_id, evaluated_feature_map, context, storage_service)
|
42
|
+
feature_to_skip = []
|
43
|
+
campaign_map = {}
|
44
|
+
|
45
|
+
# Get all feature keys and campaignIds from the groupId
|
46
|
+
feature_keys, group_campaign_ids = get_feature_keys_from_group(settings, group_id)
|
47
|
+
|
48
|
+
feature_keys.each do |feature_key|
|
49
|
+
temp_feature = get_feature_from_key(settings, feature_key)
|
50
|
+
|
51
|
+
next if feature_to_skip.include?(feature_key)
|
52
|
+
|
53
|
+
# Evaluate the feature rollout rules
|
54
|
+
is_rollout_rule_passed = is_rollout_rule_for_feature_passed(settings, temp_feature, evaluated_feature_map, feature_to_skip, storage_service, context)
|
55
|
+
if is_rollout_rule_passed
|
56
|
+
settings.get_features.each do |current_feature|
|
57
|
+
if current_feature.key == feature_key
|
58
|
+
current_feature.get_rules_linked_campaign.each do |rule|
|
59
|
+
if group_campaign_ids.include?(rule.id.to_s) || group_campaign_ids.include?("#{rule.id}_#{rule.variations[0].id}")
|
60
|
+
campaign_map[feature_key] ||= []
|
61
|
+
# Check if the campaign is already present in the campaignMap for the feature
|
62
|
+
if campaign_map[feature_key].find_index { |item| item.rule_key == rule.rule_key }.nil?
|
63
|
+
campaign_map[feature_key] << rule
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
eligible_campaigns, eligible_campaigns_with_storage = get_eligible_campaigns(settings, campaign_map, context, storage_service)
|
73
|
+
|
74
|
+
find_winner_campaign_among_eligible_campaigns(settings, feature.key, eligible_campaigns, eligible_campaigns_with_storage, group_id, context, storage_service)
|
75
|
+
end
|
76
|
+
|
77
|
+
# Get the feature keys from the group
|
78
|
+
# @param settings [SettingsModel] The settings for the VWO instance
|
79
|
+
# @param group_id [String] The group ID to get the feature keys from
|
80
|
+
# @return [Array] The feature keys and the group campaign IDs
|
81
|
+
def get_feature_keys_from_group(settings, group_id)
|
82
|
+
group_campaign_ids = CampaignUtil.get_campaigns_by_group_id(settings, group_id)
|
83
|
+
feature_keys = CampaignUtil.get_feature_keys_from_campaign_ids(settings, group_campaign_ids)
|
84
|
+
|
85
|
+
return feature_keys, group_campaign_ids
|
86
|
+
end
|
87
|
+
|
88
|
+
# Check if the rollout rule for the feature is passed
|
89
|
+
# @param settings [SettingsModel] The settings for the VWO instance
|
90
|
+
# @param feature [FeatureModel] The feature to check the rollout rule for
|
91
|
+
# @param evaluated_feature_map [Hash] The map of evaluated features
|
92
|
+
# @param feature_to_skip [Array] The list of features to skip
|
93
|
+
# @param storage_service [StorageService] The storage service for the evaluation
|
94
|
+
# @param context [ContextModel] The context for the evaluation
|
95
|
+
def is_rollout_rule_for_feature_passed(settings, feature, evaluated_feature_map, feature_to_skip, storage_service, context)
|
96
|
+
return true if evaluated_feature_map.key?(feature.key) && evaluated_feature_map[feature.key].key?(:rollout_id)
|
97
|
+
|
98
|
+
rollout_rules = get_specific_rules_based_on_type(feature, CampaignTypeEnum::ROLLOUT)
|
99
|
+
|
100
|
+
if rollout_rules.any?
|
101
|
+
rule_to_test_for_traffic = nil
|
102
|
+
rollout_rules.each do |rule|
|
103
|
+
result = evaluate_rule(settings, feature, rule, context, evaluated_feature_map, nil, storage_service, {})
|
104
|
+
if result[:pre_segmentation_result]
|
105
|
+
rule_to_test_for_traffic = rule
|
106
|
+
break
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
if rule_to_test_for_traffic
|
111
|
+
campaign = CampaignModel.new.model_from_dictionary(rule_to_test_for_traffic)
|
112
|
+
variation = evaluate_traffic_and_get_variation(settings, campaign, context.id)
|
113
|
+
if variation.is_a?(VariationModel) && !variation.nil? && variation.id.is_a?(Integer)
|
114
|
+
evaluated_feature_map[feature.key] = {
|
115
|
+
rollout_id: rule_to_test_for_traffic.id,
|
116
|
+
rollout_key: rule_to_test_for_traffic.key,
|
117
|
+
rollout_variation_id: rule_to_test_for_traffic.variations[0].id
|
118
|
+
}
|
119
|
+
return true
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
# No rollout rule passed
|
124
|
+
feature_to_skip.push(feature.key)
|
125
|
+
return false
|
126
|
+
end
|
127
|
+
|
128
|
+
# No rollout rule, evaluate experiments
|
129
|
+
LoggerService.log(LogLevelEnum::INFO, "MEG_SKIP_ROLLOUT_EVALUATE_EXPERIMENTS", { featureKey: feature.key })
|
130
|
+
return true
|
131
|
+
end
|
132
|
+
|
133
|
+
# Get the eligible campaigns
|
134
|
+
# @param settings [SettingsModel] The settings for the VWO instance
|
135
|
+
# @param campaign_map [Hash] The map of campaigns
|
136
|
+
# @param context [ContextModel] The context for the evaluation
|
137
|
+
# @param storage_service [StorageService] The storage service for the evaluation
|
138
|
+
def get_eligible_campaigns(settings, campaign_map, context, storage_service)
|
139
|
+
eligible_campaigns = []
|
140
|
+
eligible_campaigns_with_storage = []
|
141
|
+
ineligible_campaigns = []
|
142
|
+
|
143
|
+
campaign_map.each do |feature_key, campaigns|
|
144
|
+
campaigns.each do |campaign|
|
145
|
+
stored_data = StorageDecorator.new.get_feature_from_storage(feature_key, context, storage_service)
|
146
|
+
|
147
|
+
if stored_data && stored_data[:experiment_variation_id]
|
148
|
+
if stored_data[:experiment_key] == campaign.key
|
149
|
+
variation = CampaignUtil.get_variation_from_campaign_key(settings, stored_data[:experiment_key], stored_data[:experiment_variation_id])
|
150
|
+
if variation
|
151
|
+
LoggerService.log(LogLevelEnum::INFO, "MEG_CAMPAIGN_FOUND_IN_STORAGE", { campaignKey: stored_data[:experiment_key], userId: context.id })
|
152
|
+
|
153
|
+
unless eligible_campaigns_with_storage.any? { |item| item.key == campaign.key }
|
154
|
+
eligible_campaigns_with_storage.push(campaign)
|
155
|
+
end
|
156
|
+
next
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
# Check if user is eligible for the campaign
|
162
|
+
if CampaignDecisionService.new.get_pre_segmentation_decision(campaign, context) && CampaignDecisionService.new.is_user_part_of_campaign(context.id, campaign)
|
163
|
+
LoggerService.log(LogLevelEnum::INFO, "MEG_CAMPAIGN_ELIGIBLE", { campaignKey: campaign.key, userId: context.id })
|
164
|
+
|
165
|
+
eligible_campaigns.push(campaign)
|
166
|
+
next
|
167
|
+
end
|
168
|
+
|
169
|
+
ineligible_campaigns.push(campaign)
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
return eligible_campaigns, eligible_campaigns_with_storage
|
174
|
+
end
|
175
|
+
|
176
|
+
# Find the winner campaign among the eligible campaigns
|
177
|
+
# @param settings [SettingsModel] The settings for the VWO instance
|
178
|
+
# @param feature_key [String] The key of the feature
|
179
|
+
# @param eligible_campaigns [Array] The list of eligible campaigns
|
180
|
+
# @param eligible_campaigns_with_storage [Array] The list of eligible campaigns with storage
|
181
|
+
# @param group_id [String] The ID of the group
|
182
|
+
def find_winner_campaign_among_eligible_campaigns(settings, feature_key, eligible_campaigns, eligible_campaigns_with_storage, group_id, context, storage_service)
|
183
|
+
winner_campaign = nil
|
184
|
+
campaign_ids = CampaignUtil.get_campaign_ids_from_feature_key(settings, feature_key)
|
185
|
+
meg_algo_number = settings.get_groups[group_id.to_s][:et.to_s] || Constants::RANDOM_ALGO
|
186
|
+
|
187
|
+
# Check eligible_campaigns_with_storage first
|
188
|
+
if eligible_campaigns_with_storage.length == 1
|
189
|
+
winner_campaign = eligible_campaigns_with_storage[0]
|
190
|
+
LoggerService.log(LogLevelEnum::INFO, "MEG_WINNER_CAMPAIGN", { campaignKey: winner_campaign.key, groupId: group_id, userId: context.id, algo: 'using random algorithm' })
|
191
|
+
elsif eligible_campaigns_with_storage.length > 1 && meg_algo_number == Constants::RANDOM_ALGO
|
192
|
+
winner_campaign = normalize_weights_and_find_winning_campaign(eligible_campaigns_with_storage, context, campaign_ids, group_id, storage_service)
|
193
|
+
elsif eligible_campaigns_with_storage.length > 1
|
194
|
+
winner_campaign = get_campaign_using_advanced_algo(settings, eligible_campaigns_with_storage, context, campaign_ids, group_id, storage_service)
|
195
|
+
end
|
196
|
+
|
197
|
+
# Fallback to eligible_campaigns if no winner found in storage
|
198
|
+
if eligible_campaigns_with_storage.empty?
|
199
|
+
if eligible_campaigns.length == 1
|
200
|
+
winner_campaign = eligible_campaigns[0]
|
201
|
+
LoggerService.log(LogLevelEnum::INFO, "MEG_WINNER_CAMPAIGN", { campaignKey: winner_campaign.key, groupId: group_id, userId: context.id, algo: 'using random algorithm' })
|
202
|
+
elsif eligible_campaigns.length > 1 && meg_algo_number == Constants::RANDOM_ALGO
|
203
|
+
winner_campaign = normalize_weights_and_find_winning_campaign(eligible_campaigns, context, campaign_ids, group_id, storage_service)
|
204
|
+
elsif eligible_campaigns.length > 1
|
205
|
+
winner_campaign = get_campaign_using_advanced_algo(settings, eligible_campaigns, context, campaign_ids, group_id, storage_service)
|
206
|
+
else
|
207
|
+
LoggerService.log(LogLevelEnum::INFO, "No winner campaign found for MEG group: #{group_id}", nil)
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
winner_campaign
|
212
|
+
end
|
213
|
+
|
214
|
+
# Helper for random allocation winner selection
|
215
|
+
# @param shortlisted_campaigns [Array] The list of shortlisted campaigns
|
216
|
+
# @param context [ContextModel] The context for the evaluation
|
217
|
+
# @param called_campaign_ids [Array] The list of called campaign IDs
|
218
|
+
# @param group_id [String] The ID of the group
|
219
|
+
# @param storage_service [StorageService] The storage service for the evaluation
|
220
|
+
def normalize_weights_and_find_winning_campaign(shortlisted_campaigns, context, called_campaign_ids, group_id, storage_service)
|
221
|
+
# Convert to VariationModel first and then normalize weights
|
222
|
+
shortlisted_variations = shortlisted_campaigns.map do |campaign|
|
223
|
+
variation = VariationModel.new.model_from_dictionary(campaign)
|
224
|
+
variation.weight = (100.0 / shortlisted_campaigns.length).round(4)
|
225
|
+
variation
|
226
|
+
end
|
227
|
+
|
228
|
+
# Set campaign allocation
|
229
|
+
CampaignUtil.set_campaign_allocation(shortlisted_variations)
|
230
|
+
|
231
|
+
winner_campaign = CampaignDecisionService.new.get_variation(
|
232
|
+
shortlisted_variations,
|
233
|
+
DecisionMaker.new.calculate_bucket_value(CampaignUtil.get_bucketing_seed(context.id, nil, group_id))
|
234
|
+
)
|
235
|
+
|
236
|
+
if winner_campaign
|
237
|
+
campaign_key = winner_campaign.type == CampaignTypeEnum::AB ?
|
238
|
+
winner_campaign.key :
|
239
|
+
"#{winner_campaign.key}_#{winner_campaign.rule_key}"
|
240
|
+
|
241
|
+
LoggerService.log(
|
242
|
+
LogLevelEnum::INFO,
|
243
|
+
"MEG_WINNER_CAMPAIGN",
|
244
|
+
{
|
245
|
+
campaignKey: campaign_key,
|
246
|
+
groupId: group_id,
|
247
|
+
userId: context.id,
|
248
|
+
algo: 'using random algorithm'
|
249
|
+
}
|
250
|
+
)
|
251
|
+
|
252
|
+
StorageDecorator.new.set_data_in_storage(
|
253
|
+
{
|
254
|
+
feature_key: "#{Constants::VWO_META_MEG_KEY}#{group_id}",
|
255
|
+
context: context,
|
256
|
+
experiment_id: winner_campaign.id,
|
257
|
+
experiment_key: winner_campaign.key,
|
258
|
+
experiment_variation_id: winner_campaign.type == CampaignTypeEnum::PERSONALIZE ? winner_campaign.variations[0].id : -1
|
259
|
+
},
|
260
|
+
storage_service
|
261
|
+
)
|
262
|
+
|
263
|
+
return winner_campaign if called_campaign_ids.include?(winner_campaign.id)
|
264
|
+
else
|
265
|
+
LoggerService.log(LogLevelEnum::INFO, "No winner campaign found for MEG group: #{group_id}, using random algorithm", nil)
|
266
|
+
end
|
267
|
+
|
268
|
+
nil
|
269
|
+
end
|
270
|
+
|
271
|
+
# Advanced algorithm for campaign selection
|
272
|
+
# @param settings [SettingsModel] The settings for the VWO instance
|
273
|
+
# @param shortlisted_campaigns [Array] The list of shortlisted campaigns
|
274
|
+
# @param context [ContextModel] The context for the evaluation
|
275
|
+
# @param called_campaign_ids [Array] The list of called campaign IDs
|
276
|
+
# @param group_id [String] The ID of the group
|
277
|
+
# @param storage_service [StorageService] The storage service for the evaluation
|
278
|
+
def get_campaign_using_advanced_algo(settings, shortlisted_campaigns, context, called_campaign_ids, group_id, storage_service)
|
279
|
+
winner_campaign = nil
|
280
|
+
found = false
|
281
|
+
priority_order = settings.get_groups[group_id.to_s][:p.to_s] || []
|
282
|
+
weights = settings.get_groups[group_id.to_s][:wt.to_s] || {}
|
283
|
+
|
284
|
+
# Check priority order first
|
285
|
+
priority_order.each do |priority|
|
286
|
+
shortlisted_campaigns.each do |campaign|
|
287
|
+
if campaign.id.to_s == priority.to_s || "#{campaign.id}_#{campaign.variations[0].id}" == priority
|
288
|
+
winner_campaign = campaign.clone
|
289
|
+
found = true
|
290
|
+
break
|
291
|
+
end
|
292
|
+
end
|
293
|
+
break if found
|
294
|
+
end
|
295
|
+
|
296
|
+
# If no winner found through priority, try weighted distribution
|
297
|
+
if winner_campaign.nil?
|
298
|
+
participating_campaign_list = shortlisted_campaigns.map do |campaign|
|
299
|
+
campaign_id = campaign.id.to_s
|
300
|
+
weight = weights[campaign_id] || weights["#{campaign_id}_#{campaign.variations[0].id}"]
|
301
|
+
next nil unless weight
|
302
|
+
|
303
|
+
cloned_campaign = campaign.clone
|
304
|
+
variation = VariationModel.new.model_from_dictionary(cloned_campaign)
|
305
|
+
variation.weight = weight
|
306
|
+
variation
|
307
|
+
end.compact # Remove nil values
|
308
|
+
|
309
|
+
# Convert to VariationModel and set allocations
|
310
|
+
CampaignUtil.set_campaign_allocation(participating_campaign_list)
|
311
|
+
winner_campaign = CampaignDecisionService.new.get_variation(
|
312
|
+
participating_campaign_list,
|
313
|
+
DecisionMaker.new.calculate_bucket_value(CampaignUtil.get_bucketing_seed(context.id, nil, group_id))
|
314
|
+
)
|
315
|
+
end
|
316
|
+
|
317
|
+
if winner_campaign
|
318
|
+
campaign_key = winner_campaign.type == CampaignTypeEnum::AB ?
|
319
|
+
winner_campaign.key :
|
320
|
+
"#{winner_campaign.key}_#{winner_campaign.rule_key}"
|
321
|
+
|
322
|
+
LoggerService.log(
|
323
|
+
LogLevelEnum::INFO,
|
324
|
+
"MEG_WINNER_CAMPAIGN",
|
325
|
+
{
|
326
|
+
campaignKey: campaign_key,
|
327
|
+
groupId: group_id,
|
328
|
+
userId: context.id,
|
329
|
+
algo: 'using advanced algorithm'
|
330
|
+
}
|
331
|
+
)
|
332
|
+
|
333
|
+
StorageDecorator.new.set_data_in_storage(
|
334
|
+
{
|
335
|
+
feature_key: "#{Constants::VWO_META_MEG_KEY}#{group_id}",
|
336
|
+
context: context,
|
337
|
+
experiment_id: winner_campaign.id,
|
338
|
+
experiment_key: winner_campaign.key,
|
339
|
+
experiment_variation_id: winner_campaign.type == CampaignTypeEnum::PERSONALIZE ? winner_campaign.variations[0].id : -1
|
340
|
+
},
|
341
|
+
storage_service
|
342
|
+
)
|
343
|
+
|
344
|
+
return winner_campaign if called_campaign_ids.include?(winner_campaign.id)
|
345
|
+
else
|
346
|
+
LoggerService.log(LogLevelEnum::INFO, "No winner campaign found for MEG group: #{group_id}, using advanced algorithm", nil)
|
347
|
+
end
|
348
|
+
|
349
|
+
nil
|
350
|
+
end
|
@@ -0,0 +1,235 @@
|
|
1
|
+
# Copyright 2025 Wingify Software Pvt. Ltd.
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
|
15
|
+
require_relative '../enums/log_level_enum'
|
16
|
+
require_relative '../services/logger_service'
|
17
|
+
require_relative '../enums/headers_enum'
|
18
|
+
require_relative '../enums/http_method_enum'
|
19
|
+
require_relative '../enums/url_enum'
|
20
|
+
require_relative '../constants/constants'
|
21
|
+
require_relative '../packages/network_layer/manager/network_manager'
|
22
|
+
require_relative '../packages/network_layer/models/request_model'
|
23
|
+
require_relative '../packages/network_layer/models/response_model'
|
24
|
+
require_relative '../utils/url_util'
|
25
|
+
require_relative '../utils/uuid_util'
|
26
|
+
|
27
|
+
class NetworkUtil
|
28
|
+
class << self
|
29
|
+
# Converts hash map query parameters to URL-encoded query string
|
30
|
+
# @param params [Hash] Hash containing query parameters
|
31
|
+
# @return [String] URL-encoded query string
|
32
|
+
def convert_params_to_string(params)
|
33
|
+
return '' if params.nil? || params.empty?
|
34
|
+
|
35
|
+
'?' + params.map do |key, value|
|
36
|
+
"#{URI.encode_www_form_component(key.to_s)}=#{URI.encode_www_form_component(value.to_s)}"
|
37
|
+
end.join('&')
|
38
|
+
end
|
39
|
+
|
40
|
+
# Returns the base properties for bulk operations
|
41
|
+
def get_base_properties_for_bulk(account_id, user_id)
|
42
|
+
{
|
43
|
+
sId: get_current_unix_timestamp, # Session ID
|
44
|
+
u: UUIDUtil.get_uuid(user_id, account_id) # UUID based on user and account ID
|
45
|
+
}
|
46
|
+
end
|
47
|
+
|
48
|
+
# Returns settings path with sdkKey and accountId
|
49
|
+
def get_settings_path(sdk_key, account_id)
|
50
|
+
{
|
51
|
+
i: sdk_key, # API key
|
52
|
+
r: rand, # Random number for cache busting
|
53
|
+
a: account_id # Account ID
|
54
|
+
}
|
55
|
+
end
|
56
|
+
|
57
|
+
# Returns the event tracking path
|
58
|
+
def get_track_event_path(event, account_id, user_id)
|
59
|
+
{
|
60
|
+
event_type: event, # Type of event
|
61
|
+
account_id: account_id, # Account ID
|
62
|
+
uId: user_id, # User ID
|
63
|
+
u: UUIDUtil.get_uuid(user_id, account_id), # UUID for user
|
64
|
+
sdk: Constants::SDK_NAME, # SDK Name
|
65
|
+
'sdk-v': Constants::SDK_VERSION, # SDK Version
|
66
|
+
random: get_random_number, # Random number for uniqueness
|
67
|
+
sId: get_current_unix_timestamp, # Session ID
|
68
|
+
ed: JSON.generate({ p: 'server' }) # Additional encoded data
|
69
|
+
}
|
70
|
+
end
|
71
|
+
|
72
|
+
# Returns query params for event batching
|
73
|
+
def get_event_batching_query_params(account_id)
|
74
|
+
{
|
75
|
+
a: account_id, # Account ID
|
76
|
+
sd: Constants::SDK_NAME, # SDK Name
|
77
|
+
sv: Constants::SDK_VERSION # SDK Version
|
78
|
+
}
|
79
|
+
end
|
80
|
+
|
81
|
+
# Builds generic properties for different tracking calls
|
82
|
+
def get_events_base_properties(setting, event_name, visitor_user_agent = '', ip_address = '')
|
83
|
+
sdk_key = setting.sdk_key || ''
|
84
|
+
{
|
85
|
+
en: event_name,
|
86
|
+
a: setting.account_id,
|
87
|
+
env: sdk_key,
|
88
|
+
eTime: get_current_unix_timestamp_in_millis,
|
89
|
+
random: get_random_number,
|
90
|
+
p: 'FS',
|
91
|
+
visitor_ua: visitor_user_agent || '',
|
92
|
+
visitor_ip: ip_address || '',
|
93
|
+
url: "#{UrlUtil.get_base_url}#{UrlEnum::EVENTS}"
|
94
|
+
}
|
95
|
+
end
|
96
|
+
|
97
|
+
# Builds base payload for tracking events
|
98
|
+
def _get_event_base_payload(settings, user_id, event_name, visitor_user_agent = '', ip_address = '')
|
99
|
+
uuid = UUIDUtil.get_uuid(user_id.to_s, settings.account_id)
|
100
|
+
sdk_key = settings.sdk_key
|
101
|
+
|
102
|
+
{
|
103
|
+
d: {
|
104
|
+
msgId: "#{uuid}-#{get_current_unix_timestamp_in_millis}",
|
105
|
+
visId: uuid,
|
106
|
+
sessionId: get_current_unix_timestamp,
|
107
|
+
visitor_ua: visitor_user_agent,
|
108
|
+
visitor_ip: ip_address,
|
109
|
+
event: {
|
110
|
+
props: {
|
111
|
+
vwo_sdkName: Constants::SDK_NAME,
|
112
|
+
vwo_sdkVersion: Constants::SDK_VERSION,
|
113
|
+
vwo_envKey: sdk_key
|
114
|
+
},
|
115
|
+
name: event_name,
|
116
|
+
time: get_current_unix_timestamp_in_millis
|
117
|
+
},
|
118
|
+
visitor: {
|
119
|
+
props: {
|
120
|
+
vwo_fs_environment: sdk_key
|
121
|
+
}
|
122
|
+
}
|
123
|
+
}
|
124
|
+
}
|
125
|
+
end
|
126
|
+
|
127
|
+
# Builds track-user payload data
|
128
|
+
def get_track_user_payload_data(settings, user_id, event_name, campaign_id, variation_id, visitor_user_agent = '', ip_address = '')
|
129
|
+
properties = _get_event_base_payload(settings, user_id, event_name, visitor_user_agent, ip_address)
|
130
|
+
properties[:d][:event][:props][:id] = campaign_id
|
131
|
+
properties[:d][:event][:props][:variation] = variation_id
|
132
|
+
properties[:d][:event][:props][:isFirst] = 1
|
133
|
+
|
134
|
+
LoggerService.log(LogLevelEnum::DEBUG, "IMPRESSION_FOR_TRACK_USER", {
|
135
|
+
accountId: settings.account_id,
|
136
|
+
userId: user_id,
|
137
|
+
campaignId: campaign_id
|
138
|
+
})
|
139
|
+
|
140
|
+
properties
|
141
|
+
end
|
142
|
+
|
143
|
+
# Constructs payload for tracking goals with custom event properties
|
144
|
+
def get_track_goal_payload_data(settings, user_id, event_name, event_properties, visitor_user_agent = '', ip_address = '')
|
145
|
+
properties = _get_event_base_payload(settings, user_id, event_name, visitor_user_agent, ip_address)
|
146
|
+
properties[:d][:event][:props][:isCustomEvent] = true
|
147
|
+
|
148
|
+
if SettingsService.instance.is_gateway_service_provided
|
149
|
+
properties[:d][:event][:props][:variation] = 1
|
150
|
+
properties[:d][:event][:props][:id] = 1 # Temporary value for ID
|
151
|
+
end
|
152
|
+
|
153
|
+
if event_properties.is_a?(Hash) && !event_properties.empty?
|
154
|
+
event_properties.each { |key, value| properties[:d][:event][:props][key] = value }
|
155
|
+
end
|
156
|
+
|
157
|
+
LoggerService.log(LogLevelEnum::DEBUG, "IMPRESSION_FOR_TRACK_GOAL", {
|
158
|
+
eventName: event_name,
|
159
|
+
accountId: settings.account_id,
|
160
|
+
userId: user_id
|
161
|
+
})
|
162
|
+
|
163
|
+
properties
|
164
|
+
end
|
165
|
+
|
166
|
+
def get_attribute_payload_data(settings, user_id, event_name, event_properties, visitor_user_agent = '', ip_address = '')
|
167
|
+
properties = _get_event_base_payload(settings, user_id, event_name, visitor_user_agent, ip_address)
|
168
|
+
properties[:d][:event][:props][:isCustomEvent] = true
|
169
|
+
|
170
|
+
if event_properties.is_a?(Hash) && !event_properties.empty?
|
171
|
+
event_properties.each { |key, value| properties[:d][:visitor][:props][key] = value }
|
172
|
+
end
|
173
|
+
|
174
|
+
LoggerService.log(LogLevelEnum::DEBUG, "IMPRESSION_FOR_SYNC_VISITOR_PROP", {
|
175
|
+
eventName: event_name,
|
176
|
+
accountId: settings.account_id,
|
177
|
+
userId: user_id
|
178
|
+
})
|
179
|
+
properties
|
180
|
+
end
|
181
|
+
|
182
|
+
# Sends a POST API request with given properties and payload
|
183
|
+
def send_post_api_request(properties, payload)
|
184
|
+
network_instance = NetworkManager.instance
|
185
|
+
headers = {}
|
186
|
+
headers[HeadersEnum::USER_AGENT] = payload[:d][:visitor_ua] if payload[:d][:visitor_ua]
|
187
|
+
headers[HeadersEnum::IP] = payload[:d][:visitor_ip] if payload[:d][:visitor_ip]
|
188
|
+
|
189
|
+
request = RequestModel.new(
|
190
|
+
UrlUtil.get_base_url,
|
191
|
+
HttpMethodEnum::POST,
|
192
|
+
UrlEnum::EVENTS,
|
193
|
+
properties,
|
194
|
+
payload,
|
195
|
+
headers,
|
196
|
+
SettingsService.instance.protocol,
|
197
|
+
SettingsService.instance.port
|
198
|
+
)
|
199
|
+
|
200
|
+
begin
|
201
|
+
network_instance.post(request)
|
202
|
+
rescue ResponseModel => err
|
203
|
+
LoggerService.log(LogLevelEnum::ERROR, "NETWORK_CALL_FAILED", {
|
204
|
+
method: HttpMethodEnum::POST,
|
205
|
+
err: err.is_a?(Hash) ? err.to_json : err
|
206
|
+
})
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
# Sends a GET API request to the specified endpoint with given properties
|
211
|
+
def send_get_api_request(properties, endpoint)
|
212
|
+
network_instance = NetworkManager.instance
|
213
|
+
|
214
|
+
request = RequestModel.new(
|
215
|
+
UrlUtil.get_base_url,
|
216
|
+
HttpMethodEnum::GET,
|
217
|
+
endpoint,
|
218
|
+
properties,
|
219
|
+
nil,
|
220
|
+
nil,
|
221
|
+
SettingsService.Instance.protocol,
|
222
|
+
SettingsService.Instance.port
|
223
|
+
)
|
224
|
+
|
225
|
+
begin
|
226
|
+
network_instance.get(request)
|
227
|
+
rescue ResponseModel => err
|
228
|
+
LoggerService.log(LogLevelEnum::ERROR, "NETWORK_CALL_FAILED", {
|
229
|
+
method: HttpMethodEnum::GET,
|
230
|
+
err: err.is_a?(Hash) ? err.to_json : err
|
231
|
+
})
|
232
|
+
end
|
233
|
+
end
|
234
|
+
end
|
235
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# Copyright 2025 Wingify Software Pvt. Ltd.
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
|
15
|
+
require_relative '../models/campaign/campaign_model'
|
16
|
+
require_relative '../models/campaign/feature_model'
|
17
|
+
require_relative '../models/settings/settings_model'
|
18
|
+
require_relative '../models/user/context_model'
|
19
|
+
require_relative '../services/storage_service'
|
20
|
+
require_relative './data_type_util'
|
21
|
+
require_relative './decision_util'
|
22
|
+
require_relative './network_util'
|
23
|
+
require_relative './impression_util'
|
24
|
+
|
25
|
+
# Evaluates the rules for a given campaign and feature based on the provided context.
|
26
|
+
#
|
27
|
+
# @param settings [SettingsModel] The settings configuration for evaluation.
|
28
|
+
# @param feature [FeatureModel] The feature being evaluated.
|
29
|
+
# @param campaign [CampaignModel] The campaign associated with the feature.
|
30
|
+
# @param context [ContextModel] The user context for evaluation.
|
31
|
+
# @param evaluated_feature_map [Hash] A hash of evaluated features.
|
32
|
+
# @param meg_group_winner_campaigns [Hash] A hash of MEG group winner campaigns.
|
33
|
+
# @param storage_service [StorageService] The storage service for persistence.
|
34
|
+
# @param decision [Hash] The decision object that will be updated based on the evaluation.
|
35
|
+
# @return [Hash] A hash containing the result of the pre-segmentation and the whitelisted object.
|
36
|
+
def evaluate_rule(settings, feature, campaign, context, evaluated_feature_map, meg_group_winner_campaigns, storage_service, decision)
|
37
|
+
# Perform whitelisting and pre-segmentation checks
|
38
|
+
pre_segmentation_result, whitelisted_object = DecisionUtil.check_whitelisting_and_pre_seg(
|
39
|
+
settings, feature, campaign, context, evaluated_feature_map, meg_group_winner_campaigns, storage_service, decision
|
40
|
+
)
|
41
|
+
|
42
|
+
# If pre-segmentation is successful and a whitelisted object exists, proceed to send an impression
|
43
|
+
if pre_segmentation_result && whitelisted_object.is_a?(Hash) && !whitelisted_object.empty?
|
44
|
+
# Update the decision object with campaign and variation details
|
45
|
+
decision.merge!(
|
46
|
+
experiment_id: campaign.get_id,
|
47
|
+
experiment_key: campaign.get_key,
|
48
|
+
experiment_variation_id: whitelisted_object[:variation_id]
|
49
|
+
)
|
50
|
+
|
51
|
+
# Send an impression for the variation shown
|
52
|
+
create_and_send_impression_for_variation_shown(settings, campaign.get_id, whitelisted_object[:variation_id], context)
|
53
|
+
end
|
54
|
+
|
55
|
+
# Return the results of the evaluation
|
56
|
+
{ pre_segmentation_result: pre_segmentation_result, whitelisted_object: whitelisted_object, updated_decision: decision }
|
57
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# Copyright 2025 Wingify Software Pvt. Ltd.
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
|
15
|
+
require_relative '../models/settings/settings_model'
|
16
|
+
require_relative 'campaign_util'
|
17
|
+
require_relative 'function_util'
|
18
|
+
require_relative 'gateway_service_util'
|
19
|
+
|
20
|
+
# Sets settings and adds campaigns to rules
|
21
|
+
#
|
22
|
+
# @param settings [Hash] The settings configuration
|
23
|
+
# @param vwo_client_instance [VWOClient] The VWOClient instance
|
24
|
+
def set_settings_and_add_campaigns_to_rules(settings, vwo_client_instance)
|
25
|
+
# Create settings model and assign it to vwo_client_instance
|
26
|
+
vwo_client_instance.settings = SettingsModel.new(settings)
|
27
|
+
vwo_client_instance.original_settings = settings
|
28
|
+
|
29
|
+
# Optimize loop by avoiding multiple calls to get_campaigns()
|
30
|
+
campaigns = vwo_client_instance.settings.get_campaigns
|
31
|
+
campaigns.each_with_index do |campaign, index|
|
32
|
+
CampaignUtil.set_variation_allocation(campaign)
|
33
|
+
campaigns[index] = campaign
|
34
|
+
end
|
35
|
+
|
36
|
+
add_linked_campaigns_to_settings(vwo_client_instance.settings)
|
37
|
+
add_is_gateway_service_required_flag(vwo_client_instance.settings)
|
38
|
+
end
|