wingify-fme-ruby-sdk 1.50.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 +7 -0
- data/lib/resources/debug_messages.json +21 -0
- data/lib/resources/error_messages.json +63 -0
- data/lib/resources/info_messages.json +43 -0
- data/lib/resources/warn_messages.json +1 -0
- data/lib/vwo.rb +40 -0
- data/lib/wingify/api/get_flag.rb +244 -0
- data/lib/wingify/api/set_attribute.rb +57 -0
- data/lib/wingify/api/track_event.rb +80 -0
- data/lib/wingify/constants/constants.rb +106 -0
- data/lib/wingify/decorators/storage_decorator.rb +82 -0
- data/lib/wingify/enums/api_enum.rb +23 -0
- data/lib/wingify/enums/campaign_type_enum.rb +19 -0
- data/lib/wingify/enums/debug_category_enum.rb +20 -0
- data/lib/wingify/enums/decision_types_enum.rb +18 -0
- data/lib/wingify/enums/event_enum.rb +22 -0
- data/lib/wingify/enums/headers_enum.rb +20 -0
- data/lib/wingify/enums/hooks_enum.rb +17 -0
- data/lib/wingify/enums/http_method_enum.rb +18 -0
- data/lib/wingify/enums/log_level_enum.rb +21 -0
- data/lib/wingify/enums/log_level_to_number.rb +27 -0
- data/lib/wingify/enums/status_enum.rb +19 -0
- data/lib/wingify/enums/storage_enum.rb +22 -0
- data/lib/wingify/enums/url_enum.rb +22 -0
- data/lib/wingify/models/campaign/campaign_model.rb +192 -0
- data/lib/wingify/models/campaign/feature_model.rb +111 -0
- data/lib/wingify/models/campaign/impact_campaign_model.rb +38 -0
- data/lib/wingify/models/campaign/metric_model.rb +44 -0
- data/lib/wingify/models/campaign/rule_model.rb +56 -0
- data/lib/wingify/models/campaign/variable_model.rb +51 -0
- data/lib/wingify/models/campaign/variation_model.rb +137 -0
- data/lib/wingify/models/gateway_service_model.rb +39 -0
- data/lib/wingify/models/schemas/settings_schema_validation.rb +104 -0
- data/lib/wingify/models/settings/settings_model.rb +101 -0
- data/lib/wingify/models/storage/storage_data_model.rb +44 -0
- data/lib/wingify/models/user/context_model.rb +154 -0
- data/lib/wingify/models/user/context_vwo_model.rb +38 -0
- data/lib/wingify/models/user/get_flag_response.rb +50 -0
- data/lib/wingify/models/vwo_options_model.rb +129 -0
- data/lib/wingify/packages/decision_maker/decision_maker.rb +60 -0
- data/lib/wingify/packages/logger/core/log_manager.rb +92 -0
- data/lib/wingify/packages/logger/core/transport_manager.rb +76 -0
- data/lib/wingify/packages/logger/log_message_builder.rb +70 -0
- data/lib/wingify/packages/logger/logger.rb +38 -0
- data/lib/wingify/packages/logger/transports/console_transport.rb +49 -0
- data/lib/wingify/packages/network_layer/client/network_client.rb +276 -0
- data/lib/wingify/packages/network_layer/handlers/request_handler.rb +37 -0
- data/lib/wingify/packages/network_layer/manager/network_manager.rb +145 -0
- data/lib/wingify/packages/network_layer/models/global_request_model.rb +105 -0
- data/lib/wingify/packages/network_layer/models/request_model.rb +179 -0
- data/lib/wingify/packages/network_layer/models/response_model.rb +62 -0
- data/lib/wingify/packages/segmentation_evaluator/core/segmentation_manager.rb +77 -0
- data/lib/wingify/packages/segmentation_evaluator/enums/segment_operand_regex_enum.rb +29 -0
- data/lib/wingify/packages/segmentation_evaluator/enums/segment_operand_value_enum.rb +26 -0
- data/lib/wingify/packages/segmentation_evaluator/enums/segment_operator_value_enum.rb +33 -0
- data/lib/wingify/packages/segmentation_evaluator/evaluators/segment_evaluator.rb +218 -0
- data/lib/wingify/packages/segmentation_evaluator/evaluators/segment_operand_evaluator.rb +415 -0
- data/lib/wingify/packages/segmentation_evaluator/utils/segment_util.rb +44 -0
- data/lib/wingify/packages/storage/connector.rb +26 -0
- data/lib/wingify/packages/storage/storage.rb +47 -0
- data/lib/wingify/services/batch_event_queue.rb +179 -0
- data/lib/wingify/services/campaign_decision_service.rb +161 -0
- data/lib/wingify/services/hooks_service.rb +51 -0
- data/lib/wingify/services/logger_service.rb +114 -0
- data/lib/wingify/services/settings_service.rb +178 -0
- data/lib/wingify/services/storage_service.rb +66 -0
- data/lib/wingify/utils/batch_event_dispatcher_util.rb +178 -0
- data/lib/wingify/utils/brand_context.rb +33 -0
- data/lib/wingify/utils/brand_util.rb +53 -0
- data/lib/wingify/utils/campaign_util.rb +284 -0
- data/lib/wingify/utils/data_type_util.rb +105 -0
- data/lib/wingify/utils/debugger_service_util.rb +40 -0
- data/lib/wingify/utils/decision_util.rb +259 -0
- data/lib/wingify/utils/event_util.rb +55 -0
- data/lib/wingify/utils/function_util.rb +141 -0
- data/lib/wingify/utils/gateway_service_util.rb +101 -0
- data/lib/wingify/utils/impression_util.rb +66 -0
- data/lib/wingify/utils/log_message_util.rb +42 -0
- data/lib/wingify/utils/meg_util.rb +357 -0
- data/lib/wingify/utils/network_util.rb +503 -0
- data/lib/wingify/utils/rule_evaluation_util.rb +57 -0
- data/lib/wingify/utils/settings_util.rb +38 -0
- data/lib/wingify/utils/usage_stats_util.rb +119 -0
- data/lib/wingify/utils/uuid_util.rb +96 -0
- data/lib/wingify/wingify_builder.rb +261 -0
- data/lib/wingify/wingify_client.rb +227 -0
- data/lib/wingify.rb +117 -0
- metadata +327 -0
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
# Copyright 2024-2026 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 '../enums/campaign_type_enum'
|
|
17
|
+
require_relative '../models/campaign/campaign_model'
|
|
18
|
+
require_relative '../models/campaign/feature_model'
|
|
19
|
+
require_relative '../models/campaign/variation_model'
|
|
20
|
+
require_relative '../models/settings/settings_model'
|
|
21
|
+
require_relative '../services/logger_service'
|
|
22
|
+
require_relative '../enums/log_level_enum'
|
|
23
|
+
require_relative '../models/campaign/rule_model'
|
|
24
|
+
|
|
25
|
+
module CampaignUtil
|
|
26
|
+
# Sets the variation allocation for a campaign
|
|
27
|
+
# @param campaign [CampaignModel] The campaign to set the variation allocation for
|
|
28
|
+
def self.set_variation_allocation(campaign)
|
|
29
|
+
if [CampaignTypeEnum::ROLLOUT, CampaignTypeEnum::PERSONALIZE].include?(campaign.get_type)
|
|
30
|
+
handle_rollout_campaign(campaign)
|
|
31
|
+
else
|
|
32
|
+
current_allocation = 0
|
|
33
|
+
campaign.get_variations.each do |variation|
|
|
34
|
+
step_factor = assign_range_values(variation, current_allocation)
|
|
35
|
+
current_allocation += step_factor
|
|
36
|
+
|
|
37
|
+
LoggerService.log(LogLevelEnum::INFO, "VARIATION_RANGE_ALLOCATION", {
|
|
38
|
+
variationKey: variation.get_key,
|
|
39
|
+
campaignKey: campaign.get_key,
|
|
40
|
+
variationWeight: variation.get_weight,
|
|
41
|
+
startRange: variation.get_start_range_variation,
|
|
42
|
+
endRange: variation.get_end_range_variation
|
|
43
|
+
})
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Assigns start and end range values to a variation
|
|
49
|
+
# @param variation [VariationModel] The variation to assign the start and end range values to
|
|
50
|
+
# @param current_allocation [Integer] The current allocation
|
|
51
|
+
# @return [Integer] The step factor
|
|
52
|
+
def self.assign_range_values(variation, current_allocation)
|
|
53
|
+
step_factor = get_variation_bucket_range(variation.get_weight)
|
|
54
|
+
|
|
55
|
+
if step_factor > 0
|
|
56
|
+
variation.set_start_range(current_allocation + 1)
|
|
57
|
+
variation.set_end_range(current_allocation + step_factor)
|
|
58
|
+
else
|
|
59
|
+
variation.set_start_range(-1)
|
|
60
|
+
variation.set_end_range(-1)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
step_factor
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Scales variation weights to sum up to 100%
|
|
67
|
+
# @param variations [Array<VariationModel>] The variations to scale the weights of
|
|
68
|
+
# @return [Array<VariationModel>] The scaled variations
|
|
69
|
+
def self.scale_variation_weights(variations)
|
|
70
|
+
total_weight = variations.sum(&:weight)
|
|
71
|
+
|
|
72
|
+
if total_weight.zero?
|
|
73
|
+
equal_weight = 100.0 / variations.length
|
|
74
|
+
variations.each { |variation| variation.weight = equal_weight }
|
|
75
|
+
else
|
|
76
|
+
variations.each { |variation| variation.weight = (variation.weight / total_weight) * 100 }
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Generates a bucketing seed based on user ID and campaign
|
|
81
|
+
# @param user_id [String] The ID of the user
|
|
82
|
+
# @param campaign [CampaignModel] The campaign to generate the bucketing seed for
|
|
83
|
+
# @param group_id [String] The ID of the group
|
|
84
|
+
# @return [String] The bucketing seed
|
|
85
|
+
def self.get_bucketing_seed(user_id, campaign, group_id = nil)
|
|
86
|
+
return "#{group_id}_#{user_id}" if group_id
|
|
87
|
+
|
|
88
|
+
is_rollout_or_personalize = [CampaignTypeEnum::ROLLOUT, CampaignTypeEnum::PERSONALIZE].include?(campaign.get_type)
|
|
89
|
+
salt = is_rollout_or_personalize ? campaign.get_variations.first.get_salt : campaign.get_salt
|
|
90
|
+
|
|
91
|
+
salt ? "#{salt}_#{user_id}" : "#{campaign.get_id}_#{user_id}"
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Retrieves variation from campaign key
|
|
95
|
+
# @param settings [SettingsModel] The settings for the VWO instance
|
|
96
|
+
# @param campaign_key [String] The key of the campaign
|
|
97
|
+
# @param variation_id [Integer] The ID of the variation
|
|
98
|
+
# @return [VariationModel] The variation
|
|
99
|
+
def self.get_variation_from_campaign_key(settings, campaign_key, variation_id)
|
|
100
|
+
campaign = settings.get_campaigns.find { |c| c.get_key == campaign_key }
|
|
101
|
+
return nil unless campaign
|
|
102
|
+
|
|
103
|
+
variation = campaign.get_variations.find { |v| v.get_id == variation_id }
|
|
104
|
+
variation ? VariationModel.new.model_from_dictionary(variation) : nil
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Sets campaign allocation ranges
|
|
108
|
+
# @param campaigns [Array<CampaignModel>] The campaigns to set the allocation ranges for
|
|
109
|
+
def self.set_campaign_allocation(campaigns)
|
|
110
|
+
current_allocation = 0
|
|
111
|
+
|
|
112
|
+
campaigns.each do |campaign|
|
|
113
|
+
step_factor = assign_range_values_meg(campaign, current_allocation)
|
|
114
|
+
current_allocation += step_factor
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Retrieves campaign group details if part of a group
|
|
119
|
+
# @param settings [SettingsModel] The settings for the VWO instance
|
|
120
|
+
# @param campaign_id [Integer] The ID of the campaign
|
|
121
|
+
# @param variation_id [Integer] The ID of the variation
|
|
122
|
+
# @return [Hash] The group details
|
|
123
|
+
def self.get_group_details_if_campaign_part_of_it(settings, campaign_id, variation_id = nil)
|
|
124
|
+
campaign_to_check = variation_id ? "#{campaign_id}_#{variation_id}" : campaign_id.to_s
|
|
125
|
+
|
|
126
|
+
if settings.get_campaign_groups.key?(campaign_to_check)
|
|
127
|
+
group_id = settings.get_campaign_groups[campaign_to_check]
|
|
128
|
+
{ group_id: group_id, group_name: settings.get_groups[group_id.to_s][:name.to_s]}
|
|
129
|
+
else
|
|
130
|
+
{}
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Finds groups associated with a feature
|
|
135
|
+
# @param settings [SettingsModel] The settings for the VWO instance
|
|
136
|
+
# @param feature_key [String] The key of the feature
|
|
137
|
+
# @return [Array] The groups associated with the feature
|
|
138
|
+
def self.find_groups_feature_part_of(settings, feature_key)
|
|
139
|
+
rule_array = []
|
|
140
|
+
settings.get_features.each do |feature|
|
|
141
|
+
if feature.get_key == feature_key
|
|
142
|
+
rule_array.concat(feature.get_rules)
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
groups = []
|
|
147
|
+
rule_array.each do |rule|
|
|
148
|
+
group = get_group_details_if_campaign_part_of_it(settings, rule.get_campaign_id, rule.get_type == CampaignTypeEnum::PERSONALIZE ? rule.get_variation_id : nil)
|
|
149
|
+
groups << group unless group.empty? || groups.any? { |g| g[:group_id] == group[:group_id] }
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
groups
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Retrieves campaigns by group ID
|
|
156
|
+
# @param settings [SettingsModel] The settings for the VWO instance
|
|
157
|
+
# @param group_id [String] The ID of the group
|
|
158
|
+
# @return [Array] The campaigns associated with the group
|
|
159
|
+
def self.get_campaigns_by_group_id(settings, group_id)
|
|
160
|
+
settings.get_groups[group_id.to_s]&.fetch(:campaigns.to_s, []) || []
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Retrieves feature keys from campaign IDs
|
|
164
|
+
# @param settings [SettingsModel] The settings for the VWO instance
|
|
165
|
+
# @param campaign_ids [Array] The IDs of the campaigns
|
|
166
|
+
# @return [Array] The feature keys associated with the campaigns
|
|
167
|
+
def self.get_feature_keys_from_campaign_ids(settings, campaign_ids)
|
|
168
|
+
feature_keys = []
|
|
169
|
+
|
|
170
|
+
campaign_ids.each do |campaign|
|
|
171
|
+
campaign_id, variation_id = campaign.split('_').map(&:to_i)
|
|
172
|
+
|
|
173
|
+
settings.get_features.each do |feature|
|
|
174
|
+
next if feature_keys.include?(feature.get_key)
|
|
175
|
+
|
|
176
|
+
feature.get_rules.each do |rule|
|
|
177
|
+
if rule.get_campaign_id == campaign_id
|
|
178
|
+
feature_keys << feature.get_key if variation_id.nil? || rule.get_variation_id == variation_id
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
feature_keys
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Retrieves campaign IDs from a feature key
|
|
188
|
+
# @param settings [SettingsModel] The settings for the VWO instance
|
|
189
|
+
# @param feature_key [String] The key of the feature
|
|
190
|
+
# @return [Array] The campaign IDs associated with the feature
|
|
191
|
+
def self.get_campaign_ids_from_feature_key(settings, feature_key)
|
|
192
|
+
settings.get_features.each do |feature|
|
|
193
|
+
return feature.get_rules.map(&:get_campaign_id) if feature.get_key == feature_key
|
|
194
|
+
end
|
|
195
|
+
[]
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# Assigns range values to a MEG campaign
|
|
199
|
+
# @param data [VariationModel] The variation to assign the start and end range values to
|
|
200
|
+
# @param current_allocation [Integer] The current allocation
|
|
201
|
+
# @return [Integer] The step factor
|
|
202
|
+
def self.assign_range_values_meg(data, current_allocation)
|
|
203
|
+
step_factor = get_variation_bucket_range(data.weight)
|
|
204
|
+
|
|
205
|
+
if step_factor > 0
|
|
206
|
+
data.start_range_variation = current_allocation + 1
|
|
207
|
+
data.end_range_variation = current_allocation + step_factor
|
|
208
|
+
else
|
|
209
|
+
data.start_range_variation = -1
|
|
210
|
+
data.end_range_variation = -1
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
step_factor
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Retrieves the rule type for a given campaign ID from a feature
|
|
217
|
+
def self.get_rule_type_using_campaign_id_from_feature(feature, campaign_id)
|
|
218
|
+
rule = feature.get_rules.find { |r| r.get_campaign_id == campaign_id }
|
|
219
|
+
rule ? rule.get_type : ''
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# Calculates bucket range for a variation
|
|
223
|
+
# @param variation_weight [Float] The weight of the variation
|
|
224
|
+
# @return [Integer] The bucket range
|
|
225
|
+
def self.get_variation_bucket_range(variation_weight)
|
|
226
|
+
return 0 unless variation_weight && variation_weight.positive?
|
|
227
|
+
|
|
228
|
+
start_range = (variation_weight * 100).ceil
|
|
229
|
+
[start_range, Constants::MAX_TRAFFIC_VALUE].min
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# Handles rollout campaign logic
|
|
233
|
+
# @param campaign [CampaignModel] The campaign to handle the rollout logic for
|
|
234
|
+
def self.handle_rollout_campaign(campaign)
|
|
235
|
+
campaign.get_variations.each do |variation|
|
|
236
|
+
end_range = variation.get_weight * 100
|
|
237
|
+
variation.set_start_range(1)
|
|
238
|
+
variation.set_end_range(end_range)
|
|
239
|
+
|
|
240
|
+
LoggerService.log(LogLevelEnum::INFO, "VARIATION_RANGE_ALLOCATION", {
|
|
241
|
+
variationKey: variation.get_key,
|
|
242
|
+
campaignKey: campaign.get_key,
|
|
243
|
+
variationWeight: variation.get_weight,
|
|
244
|
+
startRange: 1,
|
|
245
|
+
endRange: end_range
|
|
246
|
+
})
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
# Retrieves the campaign key from the campaign ID
|
|
251
|
+
# @param settings [SettingsModel] The settings for the VWO instance
|
|
252
|
+
# @param campaign_id [Integer] The ID of the campaign
|
|
253
|
+
# @return [String] The campaign key
|
|
254
|
+
def self.get_campaign_key_from_campaign_id(settings, campaign_id)
|
|
255
|
+
settings.get_campaigns.each do |campaign|
|
|
256
|
+
return campaign.get_key if campaign.get_id == campaign_id
|
|
257
|
+
end
|
|
258
|
+
nil
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# Retrieves the variation name from the campaign ID and variation ID
|
|
262
|
+
# @param settings [SettingsModel] The settings for the VWO instance
|
|
263
|
+
# @param campaign_id [Integer] The ID of the campaign
|
|
264
|
+
# @param variation_id [Integer] The ID of the variation
|
|
265
|
+
# @return [String] The variation name
|
|
266
|
+
def self.get_variation_name_from_campaign_id_and_variation_id(settings, campaign_id, variation_id)
|
|
267
|
+
campaign = settings.get_campaigns.find { |c| c.get_id == campaign_id }
|
|
268
|
+
return nil unless campaign
|
|
269
|
+
|
|
270
|
+
variation = campaign.get_variations.find { |v| v.get_id == variation_id }
|
|
271
|
+
variation ? variation.get_key : nil
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
# Retrieves the campaign type from the campaign ID
|
|
275
|
+
# @param settings [SettingsModel] The settings for the VWO instance
|
|
276
|
+
# @param campaign_id [Integer] The ID of the campaign
|
|
277
|
+
# @return [String] The campaign type
|
|
278
|
+
def self.get_campaign_type_from_campaign_id(settings, campaign_id)
|
|
279
|
+
campaign = settings.get_campaigns.find { |c| c.get_id == campaign_id }
|
|
280
|
+
return nil unless campaign
|
|
281
|
+
|
|
282
|
+
campaign.get_type
|
|
283
|
+
end
|
|
284
|
+
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# Copyright 2024-2026 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
|
+
module DataTypeUtil
|
|
16
|
+
# Checks if the value is a Hash (object in JS)
|
|
17
|
+
def self.is_object(val)
|
|
18
|
+
val.is_a?(Hash)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Checks if the value is an Array
|
|
22
|
+
def self.is_array(val)
|
|
23
|
+
val.is_a?(Array)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Checks if the value is nil
|
|
27
|
+
def self.is_null(val)
|
|
28
|
+
val.nil?
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Checks if the value is undefined (not applicable in Ruby, so return false)
|
|
32
|
+
def self.is_undefined(val)
|
|
33
|
+
false
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Checks if the value is defined (not nil)
|
|
37
|
+
def self.is_defined(val)
|
|
38
|
+
!val.nil?
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Checks if the value is a Number (including NaN)
|
|
42
|
+
def self.is_number(val)
|
|
43
|
+
val.is_a?(Numeric)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Checks if the value is a String
|
|
47
|
+
def self.is_string(val)
|
|
48
|
+
val.is_a?(String)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Checks if the value is a Boolean
|
|
52
|
+
def self.is_boolean(val)
|
|
53
|
+
val.is_a?(TrueClass) || val.is_a?(FalseClass)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Checks if the value is NaN (only applicable for Float in Ruby)
|
|
57
|
+
def self.is_nan(val)
|
|
58
|
+
val.is_a?(Float) && val.nan?
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Checks if the value is a Date
|
|
62
|
+
def self.is_date(val)
|
|
63
|
+
val.is_a?(Date) || val.is_a?(Time) || val.is_a?(DateTime)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Checks if the value is a Function (Proc or Lambda in Ruby)
|
|
67
|
+
def self.is_function(val)
|
|
68
|
+
val.is_a?(Proc) || val.is_a?(Method)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Checks if the value is a Regular Expression
|
|
72
|
+
def self.is_regex(val)
|
|
73
|
+
val.is_a?(Regexp)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Determines the type of the given value
|
|
77
|
+
def self.get_type(val)
|
|
78
|
+
case
|
|
79
|
+
when is_object(val)
|
|
80
|
+
"Object"
|
|
81
|
+
when is_array(val)
|
|
82
|
+
"Array"
|
|
83
|
+
when is_null(val)
|
|
84
|
+
"Null"
|
|
85
|
+
when is_undefined(val)
|
|
86
|
+
"Undefined"
|
|
87
|
+
when is_nan(val)
|
|
88
|
+
"NaN"
|
|
89
|
+
when is_number(val)
|
|
90
|
+
"Number"
|
|
91
|
+
when is_string(val)
|
|
92
|
+
"String"
|
|
93
|
+
when is_boolean(val)
|
|
94
|
+
"Boolean"
|
|
95
|
+
when is_date(val)
|
|
96
|
+
"Date"
|
|
97
|
+
when is_regex(val)
|
|
98
|
+
"Regex"
|
|
99
|
+
when is_function(val)
|
|
100
|
+
"Function"
|
|
101
|
+
else
|
|
102
|
+
"Unknown Type"
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Copyright 2024-2026 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 '../services/logger_service'
|
|
16
|
+
require_relative '../enums/log_level_enum'
|
|
17
|
+
require_relative '../utils/network_util'
|
|
18
|
+
require_relative '../services/batch_event_queue'
|
|
19
|
+
require_relative '../enums/event_enum'
|
|
20
|
+
|
|
21
|
+
class DebuggerServiceUtil
|
|
22
|
+
class << self
|
|
23
|
+
def send_debugger_event(event_props)
|
|
24
|
+
# get base properties for the event
|
|
25
|
+
properties = NetworkUtil.get_events_base_properties(EventEnum::DEBUGGER_EVENT)
|
|
26
|
+
|
|
27
|
+
# get debugger event payload
|
|
28
|
+
payload = NetworkUtil.get_debugger_event_payload(event_props)
|
|
29
|
+
|
|
30
|
+
# send event
|
|
31
|
+
if BatchEventsQueue.instance
|
|
32
|
+
# add the payload to the batch events queue
|
|
33
|
+
BatchEventsQueue.instance.enqueue(payload)
|
|
34
|
+
else
|
|
35
|
+
# Send the prepared payload via POST API request
|
|
36
|
+
NetworkUtil.send_event(properties, payload)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
# Copyright 2024-2026 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/campaign_type_enum'
|
|
16
|
+
require_relative '../enums/status_enum'
|
|
17
|
+
require_relative '../models/campaign/campaign_model'
|
|
18
|
+
require_relative '../models/campaign/feature_model'
|
|
19
|
+
require_relative '../models/campaign/variation_model'
|
|
20
|
+
require_relative '../models/settings/settings_model'
|
|
21
|
+
require_relative '../models/user/context_model'
|
|
22
|
+
require_relative '../packages/decision_maker/decision_maker'
|
|
23
|
+
require_relative '../packages/segmentation_evaluator/core/segmentation_manager'
|
|
24
|
+
require_relative '../services/campaign_decision_service'
|
|
25
|
+
require_relative '../services/storage_service'
|
|
26
|
+
require_relative '../utils/data_type_util'
|
|
27
|
+
require_relative '../constants/constants'
|
|
28
|
+
require_relative '../utils/campaign_util'
|
|
29
|
+
require_relative '../utils/function_util'
|
|
30
|
+
require_relative '../utils/meg_util'
|
|
31
|
+
require_relative '../utils/uuid_util'
|
|
32
|
+
require_relative '../decorators/storage_decorator'
|
|
33
|
+
require_relative '../services/logger_service'
|
|
34
|
+
require_relative '../enums/log_level_enum'
|
|
35
|
+
|
|
36
|
+
class DecisionUtil
|
|
37
|
+
# Check if the campaign satisfies whitelisting and pre-segmentation
|
|
38
|
+
# @param settings [SettingsModel] The settings for the VWO instance
|
|
39
|
+
# @param feature [FeatureModel] The feature to evaluate
|
|
40
|
+
# @param campaign [CampaignModel] The campaign to evaluate
|
|
41
|
+
# @param context [ContextModel] The context for the evaluation
|
|
42
|
+
# @param evaluated_feature_map [Hash] The map of evaluated features
|
|
43
|
+
# @param meg_group_winner_campaigns [Hash] The map of MEG group winner campaigns
|
|
44
|
+
def self.check_whitelisting_and_pre_seg(settings, feature, campaign, context, evaluated_feature_map, meg_group_winner_campaigns, storage_service, decision)
|
|
45
|
+
vwo_user_id = UUIDUtil.get_uuid(context.get_id, settings.get_account_id)
|
|
46
|
+
campaign_id = campaign.get_id
|
|
47
|
+
|
|
48
|
+
if campaign.get_type == CampaignTypeEnum::AB
|
|
49
|
+
# Set _vwoUserId for variation targeting variables
|
|
50
|
+
variation_targeting_vars = context.get_variation_targeting_variables || {}
|
|
51
|
+
variation_targeting_vars["_vwoUserId"] = campaign.get_is_user_list_enabled ? vwo_user_id : context.get_id
|
|
52
|
+
context.set_variation_targeting_variables(variation_targeting_vars)
|
|
53
|
+
decision[:variation_targeting_variables] = variation_targeting_vars
|
|
54
|
+
|
|
55
|
+
# Check for whitelisting
|
|
56
|
+
if campaign.get_is_forced_variation_enabled
|
|
57
|
+
whitelisted_variation = check_campaign_whitelisting(campaign, context)
|
|
58
|
+
return [true, whitelisted_variation] if whitelisted_variation && !whitelisted_variation.empty?
|
|
59
|
+
else
|
|
60
|
+
LoggerService.log(LogLevelEnum::INFO, "WHITELISTING_SKIP", {
|
|
61
|
+
campaignKey: campaign.get_rule_key,
|
|
62
|
+
userId: context.get_id
|
|
63
|
+
})
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# User list segment check for campaign pre-segmentation
|
|
68
|
+
custom_vars = context.get_custom_variables || {}
|
|
69
|
+
custom_vars["_vwoUserId"] = campaign.get_is_user_list_enabled ? vwo_user_id : context.get_id
|
|
70
|
+
context.set_custom_variables(custom_vars)
|
|
71
|
+
decision[:custom_variables] = custom_vars
|
|
72
|
+
|
|
73
|
+
# Check if rule belongs to Mutually Exclusive Group (MEG)
|
|
74
|
+
group_details = CampaignUtil.get_group_details_if_campaign_part_of_it(settings, campaign_id, campaign.get_type == CampaignTypeEnum::PERSONALIZE ? campaign.get_variations[0].get_id : nil)
|
|
75
|
+
group_id = group_details[:group_id]
|
|
76
|
+
|
|
77
|
+
# Check if the group has already been evaluated
|
|
78
|
+
group_winner_campaign_id = meg_group_winner_campaigns[group_id] if meg_group_winner_campaigns && meg_group_winner_campaigns.key?(group_id)
|
|
79
|
+
return evaluate_meg_campaign(group_winner_campaign_id, campaign, context, meg_group_winner_campaigns, group_id) if group_winner_campaign_id
|
|
80
|
+
|
|
81
|
+
# Check in storage if the group was already evaluated
|
|
82
|
+
stored_data = StorageDecorator.new.get_feature_from_storage("#{Constants::VWO_META_MEG_KEY}#{group_id}", context, storage_service)
|
|
83
|
+
if stored_data && stored_data[:experiment_key] && stored_data[:experiment_id]
|
|
84
|
+
LoggerService.log(LogLevelEnum::INFO, "MEG_CAMPAIGN_FOUND_IN_STORAGE", {
|
|
85
|
+
campaignKey: stored_data[:experiment_key],
|
|
86
|
+
userId: context.get_id
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
if stored_data[:experiment_id] == campaign_id
|
|
90
|
+
return evaluate_meg_personalization(campaign, stored_data, meg_group_winner_campaigns, group_id)
|
|
91
|
+
end
|
|
92
|
+
meg_group_winner_campaigns[group_id] = stored_data[:experiment_variation_id] != -1 ? "#{stored_data[:experiment_id]}_#{stored_data[:experiment_variation_id]}" : stored_data[:experiment_id]
|
|
93
|
+
return [false, nil]
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Pre-segmentation check
|
|
97
|
+
pre_segmentation_passed = CampaignDecisionService.new.get_pre_segmentation_decision(campaign, context)
|
|
98
|
+
|
|
99
|
+
if pre_segmentation_passed && group_id
|
|
100
|
+
winner_campaign = evaluate_groups(settings, feature, group_id, evaluated_feature_map, context, storage_service)
|
|
101
|
+
return evaluate_meg_campaign_winner(winner_campaign, campaign, context, meg_group_winner_campaigns, group_id)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
[pre_segmentation_passed, nil]
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Evaluate the MEG campaign
|
|
108
|
+
# @param group_winner_campaign_id [String] The ID of the MEG group winner campaign
|
|
109
|
+
# @param campaign [CampaignModel] The campaign to evaluate
|
|
110
|
+
# @param context [ContextModel] The context for the evaluation
|
|
111
|
+
# @param meg_group_winner_campaigns [Hash] The map of MEG group winner campaigns
|
|
112
|
+
# @param group_id [String] The ID of the MEG group
|
|
113
|
+
def self.evaluate_meg_campaign(group_winner_campaign_id, campaign, context, meg_group_winner_campaigns, group_id)
|
|
114
|
+
if campaign.get_type == CampaignTypeEnum::AB && group_winner_campaign_id == campaign.get_id
|
|
115
|
+
return [true, nil]
|
|
116
|
+
elsif campaign.get_type == CampaignTypeEnum::PERSONALIZE && group_winner_campaign_id == "#{campaign.get_id}_#{campaign.get_variations[0].get_id}"
|
|
117
|
+
return [true, nil]
|
|
118
|
+
end
|
|
119
|
+
[false, nil]
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def self.evaluate_meg_personalization(campaign, stored_data, meg_group_winner_campaigns, group_id)
|
|
123
|
+
if campaign.get_type == CampaignTypeEnum::PERSONALIZE
|
|
124
|
+
if stored_data[:experiment_variation_id] == campaign.get_variations[0].get_id
|
|
125
|
+
return [true, nil]
|
|
126
|
+
else
|
|
127
|
+
meg_group_winner_campaigns[group_id] = "#{stored_data[:experiment_id]}_#{stored_data[:experiment_variation_id]}"
|
|
128
|
+
return [false, nil]
|
|
129
|
+
end
|
|
130
|
+
else
|
|
131
|
+
return [true, nil]
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Evaluate the MEG campaign winner
|
|
136
|
+
# @param winner_campaign [CampaignModel] The winner campaign
|
|
137
|
+
# @param campaign [CampaignModel] The campaign to evaluate
|
|
138
|
+
# @param context [ContextModel] The context for the evaluation
|
|
139
|
+
# @param meg_group_winner_campaigns [Hash] The map of MEG group winner campaigns
|
|
140
|
+
# @param group_id [String] The ID of the MEG group
|
|
141
|
+
def self.evaluate_meg_campaign_winner(winner_campaign, campaign, context, meg_group_winner_campaigns, group_id)
|
|
142
|
+
if winner_campaign && winner_campaign.get_id == campaign.get_id
|
|
143
|
+
if winner_campaign.get_type == CampaignTypeEnum::AB
|
|
144
|
+
return [true, nil]
|
|
145
|
+
else
|
|
146
|
+
# if personalise then check if the requested variation is the winner
|
|
147
|
+
if winner_campaign.get_variations[0].get_id == campaign.get_variations[0].get_id
|
|
148
|
+
return [true, nil]
|
|
149
|
+
else
|
|
150
|
+
meg_group_winner_campaigns[group_id] = "#{winner_campaign.get_id}_#{winner_campaign.get_variations[0].get_id}"
|
|
151
|
+
return [false, nil]
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
elsif winner_campaign
|
|
155
|
+
if winner_campaign.get_type == CampaignTypeEnum::AB
|
|
156
|
+
meg_group_winner_campaigns[group_id] = winner_campaign.get_id
|
|
157
|
+
else
|
|
158
|
+
meg_group_winner_campaigns[group_id] = "#{winner_campaign.get_id}_#{winner_campaign.get_variations[0].get_id}"
|
|
159
|
+
end
|
|
160
|
+
return [false, nil]
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
meg_group_winner_campaigns[group_id] = -1
|
|
164
|
+
[false, nil]
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Evaluate the traffic and get the variation
|
|
168
|
+
# @param settings [SettingsModel] The settings for the VWO instance
|
|
169
|
+
# @param campaign [CampaignModel] The campaign to evaluate
|
|
170
|
+
# @param user_id [String] The ID of the user
|
|
171
|
+
def self.evaluate_traffic_and_get_variation(settings, campaign, context)
|
|
172
|
+
variation = CampaignDecisionService.new.get_variation_alloted(context, settings.get_account_id, campaign)
|
|
173
|
+
|
|
174
|
+
user_id = context.get_id
|
|
175
|
+
bucketing_seed = context.get_bucketing_seed
|
|
176
|
+
bucketing_id = bucketing_seed || user_id
|
|
177
|
+
display_user_id = bucketing_id != user_id ? "#{user_id} (Seed: #{bucketing_id})" : user_id
|
|
178
|
+
|
|
179
|
+
if variation.nil?
|
|
180
|
+
LoggerService.log(LogLevelEnum::INFO, "USER_CAMPAIGN_BUCKET_INFO", {
|
|
181
|
+
campaignKey: campaign.get_type == CampaignTypeEnum::AB ? campaign.get_key : "#{campaign.get_name}_#{campaign.get_rule_key}",
|
|
182
|
+
userId: display_user_id,
|
|
183
|
+
status: 'did not get any variation'
|
|
184
|
+
})
|
|
185
|
+
return nil
|
|
186
|
+
end
|
|
187
|
+
LoggerService.log(LogLevelEnum::INFO, "USER_CAMPAIGN_BUCKET_INFO", {
|
|
188
|
+
campaignKey: campaign.get_type == CampaignTypeEnum::AB ? campaign.get_key : "#{campaign.get_name}_#{campaign.get_rule_key}",
|
|
189
|
+
userId: display_user_id,
|
|
190
|
+
status: "got variation: #{variation.get_key}"
|
|
191
|
+
})
|
|
192
|
+
variation
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# Check if the campaign satisfies whitelisting
|
|
196
|
+
# @param campaign [CampaignModel] The campaign to evaluate
|
|
197
|
+
# @param context [ContextModel] The context for the evaluation
|
|
198
|
+
def self.check_campaign_whitelisting(campaign, context)
|
|
199
|
+
# Check if the campaign satisfies whitelisting
|
|
200
|
+
whitelisting_result = evaluate_whitelisting(campaign, context)
|
|
201
|
+
status = whitelisting_result ? StatusEnum::PASSED : StatusEnum::FAILED
|
|
202
|
+
variation_string = whitelisting_result ? whitelisting_result[:variation].get_key : ''
|
|
203
|
+
|
|
204
|
+
LoggerService.log(LogLevelEnum::INFO, "WHITELISTING_STATUS", {
|
|
205
|
+
userId: context.get_id,
|
|
206
|
+
campaignKey: campaign.get_type == CampaignTypeEnum::AB ? campaign.get_key : "#{campaign.get_name}_#{campaign.get_rule_key}",
|
|
207
|
+
status: status,
|
|
208
|
+
variationString: variation_string
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
whitelisting_result
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# Evaluate the whitelisting
|
|
215
|
+
# @param campaign [CampaignModel] The campaign to evaluate
|
|
216
|
+
# @param context [ContextModel] The context for the evaluation
|
|
217
|
+
def self.evaluate_whitelisting(campaign, context)
|
|
218
|
+
targeted_variations = []
|
|
219
|
+
|
|
220
|
+
campaign.get_variations.each do |variation|
|
|
221
|
+
next if DataTypeUtil.is_object(variation.get_segments) && variation.get_segments.empty?
|
|
222
|
+
if DataTypeUtil.is_object(variation.get_segments)
|
|
223
|
+
segmentation_result = SegmentationManager.instance.validate_segmentation(
|
|
224
|
+
variation.get_segments,
|
|
225
|
+
context.get_variation_targeting_variables
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
targeted_variations.push(clone_object(variation)) if segmentation_result
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# Determine the whitelisted variation
|
|
233
|
+
whitelisted_variation = nil
|
|
234
|
+
if targeted_variations.length > 1
|
|
235
|
+
scale_variation_weights(targeted_variations)
|
|
236
|
+
current_allocation = 0
|
|
237
|
+
|
|
238
|
+
targeted_variations.each do |variation|
|
|
239
|
+
step_factor = assign_range_values(variation, current_allocation)
|
|
240
|
+
current_allocation += step_factor
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
whitelisted_variation = CampaignDecisionService.new.get_variation(
|
|
244
|
+
targeted_variations,
|
|
245
|
+
DecisionMaker.new.calculate_bucket_value(CampaignUtil.get_bucketing_seed(context.get_bucketing_seed || context.get_id, campaign, nil))
|
|
246
|
+
)
|
|
247
|
+
else
|
|
248
|
+
whitelisted_variation = targeted_variations.first
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
return nil unless whitelisted_variation
|
|
252
|
+
|
|
253
|
+
{
|
|
254
|
+
variation: whitelisted_variation,
|
|
255
|
+
variation_name: whitelisted_variation.get_key,
|
|
256
|
+
variation_id: whitelisted_variation.get_id
|
|
257
|
+
}
|
|
258
|
+
end
|
|
259
|
+
end
|