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.
Files changed (88) hide show
  1. checksums.yaml +7 -0
  2. data/lib/resources/debug_messages.json +21 -0
  3. data/lib/resources/error_messages.json +63 -0
  4. data/lib/resources/info_messages.json +43 -0
  5. data/lib/resources/warn_messages.json +1 -0
  6. data/lib/vwo.rb +40 -0
  7. data/lib/wingify/api/get_flag.rb +244 -0
  8. data/lib/wingify/api/set_attribute.rb +57 -0
  9. data/lib/wingify/api/track_event.rb +80 -0
  10. data/lib/wingify/constants/constants.rb +106 -0
  11. data/lib/wingify/decorators/storage_decorator.rb +82 -0
  12. data/lib/wingify/enums/api_enum.rb +23 -0
  13. data/lib/wingify/enums/campaign_type_enum.rb +19 -0
  14. data/lib/wingify/enums/debug_category_enum.rb +20 -0
  15. data/lib/wingify/enums/decision_types_enum.rb +18 -0
  16. data/lib/wingify/enums/event_enum.rb +22 -0
  17. data/lib/wingify/enums/headers_enum.rb +20 -0
  18. data/lib/wingify/enums/hooks_enum.rb +17 -0
  19. data/lib/wingify/enums/http_method_enum.rb +18 -0
  20. data/lib/wingify/enums/log_level_enum.rb +21 -0
  21. data/lib/wingify/enums/log_level_to_number.rb +27 -0
  22. data/lib/wingify/enums/status_enum.rb +19 -0
  23. data/lib/wingify/enums/storage_enum.rb +22 -0
  24. data/lib/wingify/enums/url_enum.rb +22 -0
  25. data/lib/wingify/models/campaign/campaign_model.rb +192 -0
  26. data/lib/wingify/models/campaign/feature_model.rb +111 -0
  27. data/lib/wingify/models/campaign/impact_campaign_model.rb +38 -0
  28. data/lib/wingify/models/campaign/metric_model.rb +44 -0
  29. data/lib/wingify/models/campaign/rule_model.rb +56 -0
  30. data/lib/wingify/models/campaign/variable_model.rb +51 -0
  31. data/lib/wingify/models/campaign/variation_model.rb +137 -0
  32. data/lib/wingify/models/gateway_service_model.rb +39 -0
  33. data/lib/wingify/models/schemas/settings_schema_validation.rb +104 -0
  34. data/lib/wingify/models/settings/settings_model.rb +101 -0
  35. data/lib/wingify/models/storage/storage_data_model.rb +44 -0
  36. data/lib/wingify/models/user/context_model.rb +154 -0
  37. data/lib/wingify/models/user/context_vwo_model.rb +38 -0
  38. data/lib/wingify/models/user/get_flag_response.rb +50 -0
  39. data/lib/wingify/models/vwo_options_model.rb +129 -0
  40. data/lib/wingify/packages/decision_maker/decision_maker.rb +60 -0
  41. data/lib/wingify/packages/logger/core/log_manager.rb +92 -0
  42. data/lib/wingify/packages/logger/core/transport_manager.rb +76 -0
  43. data/lib/wingify/packages/logger/log_message_builder.rb +70 -0
  44. data/lib/wingify/packages/logger/logger.rb +38 -0
  45. data/lib/wingify/packages/logger/transports/console_transport.rb +49 -0
  46. data/lib/wingify/packages/network_layer/client/network_client.rb +276 -0
  47. data/lib/wingify/packages/network_layer/handlers/request_handler.rb +37 -0
  48. data/lib/wingify/packages/network_layer/manager/network_manager.rb +145 -0
  49. data/lib/wingify/packages/network_layer/models/global_request_model.rb +105 -0
  50. data/lib/wingify/packages/network_layer/models/request_model.rb +179 -0
  51. data/lib/wingify/packages/network_layer/models/response_model.rb +62 -0
  52. data/lib/wingify/packages/segmentation_evaluator/core/segmentation_manager.rb +77 -0
  53. data/lib/wingify/packages/segmentation_evaluator/enums/segment_operand_regex_enum.rb +29 -0
  54. data/lib/wingify/packages/segmentation_evaluator/enums/segment_operand_value_enum.rb +26 -0
  55. data/lib/wingify/packages/segmentation_evaluator/enums/segment_operator_value_enum.rb +33 -0
  56. data/lib/wingify/packages/segmentation_evaluator/evaluators/segment_evaluator.rb +218 -0
  57. data/lib/wingify/packages/segmentation_evaluator/evaluators/segment_operand_evaluator.rb +415 -0
  58. data/lib/wingify/packages/segmentation_evaluator/utils/segment_util.rb +44 -0
  59. data/lib/wingify/packages/storage/connector.rb +26 -0
  60. data/lib/wingify/packages/storage/storage.rb +47 -0
  61. data/lib/wingify/services/batch_event_queue.rb +179 -0
  62. data/lib/wingify/services/campaign_decision_service.rb +161 -0
  63. data/lib/wingify/services/hooks_service.rb +51 -0
  64. data/lib/wingify/services/logger_service.rb +114 -0
  65. data/lib/wingify/services/settings_service.rb +178 -0
  66. data/lib/wingify/services/storage_service.rb +66 -0
  67. data/lib/wingify/utils/batch_event_dispatcher_util.rb +178 -0
  68. data/lib/wingify/utils/brand_context.rb +33 -0
  69. data/lib/wingify/utils/brand_util.rb +53 -0
  70. data/lib/wingify/utils/campaign_util.rb +284 -0
  71. data/lib/wingify/utils/data_type_util.rb +105 -0
  72. data/lib/wingify/utils/debugger_service_util.rb +40 -0
  73. data/lib/wingify/utils/decision_util.rb +259 -0
  74. data/lib/wingify/utils/event_util.rb +55 -0
  75. data/lib/wingify/utils/function_util.rb +141 -0
  76. data/lib/wingify/utils/gateway_service_util.rb +101 -0
  77. data/lib/wingify/utils/impression_util.rb +66 -0
  78. data/lib/wingify/utils/log_message_util.rb +42 -0
  79. data/lib/wingify/utils/meg_util.rb +357 -0
  80. data/lib/wingify/utils/network_util.rb +503 -0
  81. data/lib/wingify/utils/rule_evaluation_util.rb +57 -0
  82. data/lib/wingify/utils/settings_util.rb +38 -0
  83. data/lib/wingify/utils/usage_stats_util.rb +119 -0
  84. data/lib/wingify/utils/uuid_util.rb +96 -0
  85. data/lib/wingify/wingify_builder.rb +261 -0
  86. data/lib/wingify/wingify_client.rb +227 -0
  87. data/lib/wingify.rb +117 -0
  88. 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