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.
Files changed (76) hide show
  1. checksums.yaml +4 -4
  2. data/lib/vwo/api/get_flag.rb +236 -0
  3. data/lib/vwo/api/set_attribute.rb +57 -0
  4. data/lib/vwo/api/track_event.rb +77 -0
  5. data/lib/vwo/constants/constants.rb +54 -0
  6. data/lib/vwo/decorators/storage_decorator.rb +86 -0
  7. data/lib/vwo/{utils/logger_helper.rb → enums/api_enum.rb} +5 -10
  8. data/lib/vwo/enums/campaign_type_enum.rb +19 -0
  9. data/lib/vwo/enums/decision_types_enum.rb +18 -0
  10. data/lib/vwo/enums/event_enum.rb +19 -0
  11. data/lib/vwo/enums/headers_enum.rb +20 -0
  12. data/lib/vwo/enums/hooks_enum.rb +17 -0
  13. data/lib/vwo/enums/http_method_enum.rb +18 -0
  14. data/lib/vwo/enums/log_level_enum.rb +21 -0
  15. data/lib/vwo/enums/status_enum.rb +19 -0
  16. data/lib/vwo/enums/storage_enum.rb +22 -0
  17. data/lib/vwo/enums/url_enum.rb +21 -0
  18. data/lib/vwo/models/campaign/campaign_model.rb +192 -0
  19. data/lib/vwo/models/campaign/feature_model.rb +111 -0
  20. data/lib/vwo/models/campaign/impact_campaign_model.rb +38 -0
  21. data/lib/vwo/models/campaign/metric_model.rb +44 -0
  22. data/lib/vwo/models/campaign/rule_model.rb +56 -0
  23. data/lib/vwo/models/campaign/variable_model.rb +51 -0
  24. data/lib/vwo/models/campaign/variation_model.rb +137 -0
  25. data/lib/vwo/models/gateway_service_model.rb +39 -0
  26. data/lib/vwo/models/schemas/settings_schema_validation.rb +102 -0
  27. data/lib/vwo/models/settings/settings_model.rb +85 -0
  28. data/lib/vwo/models/storage/storage_data_model.rb +44 -0
  29. data/lib/vwo/models/user/context_model.rb +100 -0
  30. data/lib/vwo/models/user/context_vwo_model.rb +38 -0
  31. data/lib/vwo/{utils/feature_flag_response.rb → models/user/get_flag_response.rb} +14 -14
  32. data/lib/vwo/models/vwo_options_model.rb +107 -0
  33. data/lib/vwo/packages/decision_maker/decision_maker.rb +60 -0
  34. data/lib/vwo/packages/logger/core/log_manager.rb +90 -0
  35. data/lib/vwo/packages/logger/core/transport_manager.rb +87 -0
  36. data/lib/vwo/packages/logger/log_message_builder.rb +70 -0
  37. data/lib/vwo/packages/logger/logger.rb +38 -0
  38. data/lib/vwo/packages/logger/transports/console_transport.rb +49 -0
  39. data/lib/vwo/packages/network_layer/client/network_client.rb +107 -0
  40. data/lib/vwo/packages/network_layer/handlers/request_handler.rb +37 -0
  41. data/lib/vwo/packages/network_layer/manager/network_manager.rb +78 -0
  42. data/lib/vwo/packages/network_layer/models/global_request_model.rb +105 -0
  43. data/lib/vwo/packages/network_layer/models/request_model.rb +145 -0
  44. data/lib/vwo/packages/network_layer/models/response_model.rb +45 -0
  45. data/lib/vwo/packages/segmentation_evaluator/core/segmentation_manager.rb +76 -0
  46. data/lib/vwo/packages/segmentation_evaluator/enums/segment_operand_regex_enum.rb +29 -0
  47. data/lib/vwo/packages/segmentation_evaluator/enums/segment_operand_value_enum.rb +26 -0
  48. data/lib/vwo/packages/segmentation_evaluator/enums/segment_operator_value_enum.rb +30 -0
  49. data/lib/vwo/packages/segmentation_evaluator/evaluators/segment_evaluator.rb +210 -0
  50. data/lib/vwo/packages/segmentation_evaluator/evaluators/segment_operand_evaluator.rb +198 -0
  51. data/lib/vwo/packages/segmentation_evaluator/utils/segment_util.rb +44 -0
  52. data/lib/vwo/{constants.rb → packages/storage/connector.rb} +12 -10
  53. data/lib/vwo/packages/storage/storage.rb +45 -0
  54. data/lib/vwo/services/campaign_decision_service.rb +153 -0
  55. data/lib/vwo/services/hooks_service.rb +51 -0
  56. data/lib/vwo/services/logger_service.rb +83 -0
  57. data/lib/vwo/services/settings_service.rb +120 -0
  58. data/lib/vwo/services/storage_service.rb +65 -0
  59. data/lib/vwo/utils/campaign_util.rb +249 -0
  60. data/lib/vwo/utils/data_type_util.rb +105 -0
  61. data/lib/vwo/utils/decision_util.rb +253 -0
  62. data/lib/vwo/utils/function_util.rb +123 -0
  63. data/lib/vwo/utils/gateway_service_util.rb +101 -0
  64. data/lib/vwo/utils/impression_util.rb +49 -0
  65. data/lib/vwo/utils/log_message_util.rb +42 -0
  66. data/lib/vwo/utils/meg_util.rb +350 -0
  67. data/lib/vwo/utils/network_util.rb +235 -0
  68. data/lib/vwo/utils/rule_evaluation_util.rb +57 -0
  69. data/lib/vwo/utils/settings_util.rb +38 -0
  70. data/lib/vwo/utils/url_util.rb +46 -0
  71. data/lib/vwo/utils/uuid_util.rb +55 -0
  72. data/lib/vwo/vwo_builder.rb +156 -11
  73. data/lib/vwo/vwo_client.rb +163 -113
  74. data/lib/vwo.rb +49 -31
  75. metadata +187 -9
  76. 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