vwo-fme-ruby-sdk 1.10.0 → 1.11.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a885f0996175388d1ad657e02b340d1a7adb40fba753175a26d0cdff815b6825
4
- data.tar.gz: 79ab835a2f57e7c6a9b5a2a1f2f9d2d0fbd077975d9647aec9370f485bbf7c0b
3
+ metadata.gz: 45e767e6d560045ec2ded7455611fb246ea6a5fa5790f43fa2cff3752c7c6bac
4
+ data.tar.gz: 72411b218f6457da494c7e57e09864bb29eb79675953bfb63c057a8d615ae71d
5
5
  SHA512:
6
- metadata.gz: 255ee85fb56815dbd0547e747c1f310c2977e403fe70c026eb44714bea40be95d31de92010c9255f65897dc87b8f931838185dfe89bc6bd6e6ad121de013c2c7
7
- data.tar.gz: c05a40227ba1bf1b5b9da1f033f2892a0e2cca42f2d52478300479a3d4f0edac6b6489f024cfab2d5850357260f75970fb025e785609db257ca5f05541047cb4
6
+ metadata.gz: b6fc602254dfd56bef13c00f70c0a12dfb6735b33f3303aa4138c9dce24ca14255d665257f7c0673ff53e1cd0fb3553ebf984f9395b02bd018ec63372e696b2d
7
+ data.tar.gz: 5d9d10c85f9c06cf8ffca33251939f3bf29c9c0023c8902a5a943877643af013b96d1eaa2e285016d9a2af83c5953e98c70d870ae08e9c32bd36b030cb4137bf
@@ -16,6 +16,7 @@
16
16
 
17
17
  "INVALID_PARAM": "Key:{key} passed to API:{apiName} is not of valid type. Got type:{type}, should be:{correctType}",
18
18
  "INVALID_CONTEXT": "Context should be an object and must contain a mandatory key - id, which is User ID",
19
+ "INVALID_BUCKETING_SEED": "bucketingSeed passed to API:{apiName} is invalid (got type:{type}). It must be a non-empty string. Falling back to userId for bucketing.",
19
20
 
20
21
  "FEATURE_NOT_FOUND": "Feature not found for the key:{featureKey}",
21
22
  "EVENT_NOT_FOUND": "Event:{eventName} not found in any of the features' metrics",
@@ -122,7 +122,7 @@ class FlagApi
122
122
 
123
123
  if rollout_rules_to_evaluate.any?
124
124
  passed_rollout_campaign = CampaignModel.new.model_from_dictionary(rollout_rules_to_evaluate.first)
125
- variation = DecisionUtil.evaluate_traffic_and_get_variation(settings, passed_rollout_campaign, context.get_id)
125
+ variation = DecisionUtil.evaluate_traffic_and_get_variation(settings, passed_rollout_campaign, context)
126
126
 
127
127
  if variation
128
128
  is_enabled = true
@@ -168,7 +168,7 @@ class FlagApi
168
168
 
169
169
  if experiment_rules_to_evaluate.any?
170
170
  campaign = CampaignModel.new.model_from_dictionary(experiment_rules_to_evaluate.first)
171
- variation = DecisionUtil.evaluate_traffic_and_get_variation(settings, campaign, context.get_id)
171
+ variation = DecisionUtil.evaluate_traffic_and_get_variation(settings, campaign, context)
172
172
 
173
173
  if variation
174
174
  is_enabled = true
@@ -17,7 +17,7 @@
17
17
  # Define the Constants module
18
18
  module Constants
19
19
  SDK_NAME = 'vwo-fme-ruby-sdk'.freeze
20
- SDK_VERSION = '1.10.0'.freeze
20
+ SDK_VERSION = '1.11.0'.freeze
21
21
 
22
22
  MAX_TRAFFIC_PERCENT = 100
23
23
  MAX_TRAFFIC_VALUE = 10_000
@@ -17,9 +17,9 @@ require_relative '../../utils/uuid_util'
17
17
  require_relative '../../services/settings_service'
18
18
 
19
19
  class ContextModel
20
- attr_accessor :id, :user_agent, :ip_address, :custom_variables, :variation_targeting_variables, :post_segmentation_variables, :vwo, :session_id, :uuid
20
+ attr_accessor :id, :user_agent, :ip_address, :custom_variables, :variation_targeting_variables, :post_segmentation_variables, :vwo, :session_id, :uuid, :bucketing_seed
21
21
 
22
- def initialize(id = nil, user_agent = nil, ip_address = nil, custom_variables = {}, variation_targeting_variables = {}, post_segmentation_variables = {}, vwo = nil, session_id = nil, uuid = nil)
22
+ def initialize(id = nil, user_agent = nil, ip_address = nil, custom_variables = {}, variation_targeting_variables = {}, post_segmentation_variables = {}, vwo = nil, session_id = nil, uuid = nil, bucketing_seed = nil)
23
23
  @id = id
24
24
  @user_agent = user_agent
25
25
  @ip_address = ip_address
@@ -29,6 +29,7 @@ class ContextModel
29
29
  @vwo = vwo
30
30
  @session_id = session_id
31
31
  @uuid = uuid
32
+ @bucketing_seed = bucketing_seed
32
33
  end
33
34
 
34
35
  # Creates a model instance from a hash (dictionary)
@@ -52,7 +53,8 @@ class ContextModel
52
53
  @variation_targeting_variables = context[:variationTargetingVariables] if context.key?(:variationTargetingVariables)
53
54
  @post_segmentation_variables = context[:postSegmentationVariables] if context.key?(:postSegmentationVariables)
54
55
  @vwo = ContextVWOModel.new.model_from_dictionary(context[:_vwo]) if context.key?(:_vwo)
55
-
56
+ @bucketing_seed = context[:bucketingSeed].to_s if context.key?(:bucketingSeed)
57
+
56
58
  # check if sessionId is present in context and should be non null and non empty
57
59
  if context.key?(:sessionId) && !context[:sessionId].nil? && !context[:sessionId].to_s.empty?
58
60
  @session_id = context[:sessionId].to_i
@@ -141,4 +143,12 @@ class ContextModel
141
143
  def set_uuid(uuid)
142
144
  @uuid = uuid
143
145
  end
146
+
147
+ def get_bucketing_seed
148
+ @bucketing_seed
149
+ end
150
+
151
+ def set_bucketing_seed(bucketing_seed)
152
+ @bucketing_seed = bucketing_seed
153
+ end
144
154
  end
@@ -30,20 +30,24 @@ class CampaignDecisionService
30
30
  # @param user_id [String] The ID of the user
31
31
  # @param campaign [CampaignModel] The campaign to check
32
32
  # @return [Boolean] True if the user is part of the campaign, false otherwise
33
- def is_user_part_of_campaign(user_id, campaign)
34
- return false if campaign.nil? || user_id.nil?
33
+ def is_user_part_of_campaign(context, campaign)
34
+ return false if campaign.nil? || context.nil?
35
+
36
+ user_id = context.get_id
37
+ bucketing_seed = context.get_bucketing_seed
38
+ bucketing_id = bucketing_seed || user_id
35
39
 
36
40
  is_rollout_or_personalize = [CampaignTypeEnum::ROLLOUT, CampaignTypeEnum::PERSONALIZE].include?(campaign.get_type)
37
41
  salt = is_rollout_or_personalize ? campaign.get_variations.first.get_salt : campaign.get_salt
38
42
  traffic_allocation = is_rollout_or_personalize ? campaign.get_variations.first.get_weight : campaign.get_traffic
39
43
 
40
- bucket_key = salt ? "#{salt}_#{user_id}" : "#{campaign.get_id}_#{user_id}"
44
+ bucket_key = salt ? "#{salt}_#{bucketing_id}" : "#{campaign.get_id}_#{bucketing_id}"
41
45
  value_assigned_to_user = DecisionMaker.new.get_bucket_value_for_user(bucket_key)
42
46
 
43
47
  is_user_part = value_assigned_to_user != 0 && value_assigned_to_user <= traffic_allocation
44
48
 
45
49
  LoggerService.log(LogLevelEnum::INFO, "USER_PART_OF_CAMPAIGN", {
46
- userId: user_id,
50
+ userId: bucketing_id != user_id ? "#{user_id} (Seed: #{bucketing_id})" : user_id,
47
51
  notPart: is_user_part ? '' : 'not',
48
52
  campaignKey: campaign.get_type == CampaignTypeEnum::AB ? campaign.get_key : "#{campaign.get_name}_#{campaign.get_rule_key}"
49
53
  })
@@ -72,19 +76,23 @@ class CampaignDecisionService
72
76
  # @param account_id [String] The ID of the account
73
77
  # @param campaign [CampaignModel] The campaign to bucket the user into
74
78
  # @return [VariationModel] The variation assigned to the user
75
- def bucket_user_to_variation(user_id, account_id, campaign)
76
- return nil if campaign.nil? || user_id.nil?
79
+ def bucket_user_to_variation(context, account_id, campaign)
80
+ return nil if campaign.nil? || context.nil?
81
+
82
+ user_id = context.get_id
83
+ bucketing_seed = context.get_bucketing_seed
84
+ bucketing_id = bucketing_seed || user_id
77
85
 
78
86
  multiplier = campaign.get_traffic ? 1 : nil
79
87
  percent_traffic = campaign.get_traffic
80
88
  salt = campaign.get_salt
81
- bucket_key = salt ? "#{salt}_#{account_id}_#{user_id}" : "#{campaign.get_id}_#{account_id}_#{user_id}"
82
-
89
+ bucket_key = salt ? "#{salt}_#{account_id}_#{bucketing_id}" : "#{campaign.get_id}_#{account_id}_#{bucketing_id}"
90
+
83
91
  hash_value = DecisionMaker.new.generate_hash_value(bucket_key)
84
92
  bucket_value = DecisionMaker.new.generate_bucket_value(hash_value, Constants::MAX_TRAFFIC_VALUE, multiplier)
85
93
 
86
94
  LoggerService.log(LogLevelEnum::DEBUG, "USER_BUCKET_TO_VARIATION", {
87
- userId: user_id,
95
+ userId: bucketing_id != user_id ? "#{user_id} (Seed: #{bucketing_id})" : user_id,
88
96
  campaignKey: campaign.get_key,
89
97
  percentTraffic: percent_traffic,
90
98
  bucketValue: bucket_value,
@@ -139,13 +147,13 @@ class CampaignDecisionService
139
147
  # @param account_id [String] The ID of the account
140
148
  # @param campaign [CampaignModel] The campaign to evaluate
141
149
  # @return [VariationModel] The variation assigned to the user
142
- def get_variation_alloted(user_id, account_id, campaign)
143
- is_user_part = is_user_part_of_campaign(user_id, campaign)
150
+ def get_variation_alloted(context, account_id, campaign)
151
+ is_user_part = is_user_part_of_campaign(context, campaign)
144
152
 
145
153
  if [CampaignTypeEnum::ROLLOUT, CampaignTypeEnum::PERSONALIZE].include?(campaign.get_type)
146
154
  return campaign.get_variations.first if is_user_part
147
155
  else
148
- return bucket_user_to_variation(user_id, account_id, campaign) if is_user_part
156
+ return bucket_user_to_variation(context, account_id, campaign) if is_user_part
149
157
  end
150
158
 
151
159
  nil
@@ -168,19 +168,25 @@ class DecisionUtil
168
168
  # @param settings [SettingsModel] The settings for the VWO instance
169
169
  # @param campaign [CampaignModel] The campaign to evaluate
170
170
  # @param user_id [String] The ID of the user
171
- def self.evaluate_traffic_and_get_variation(settings, campaign, user_id)
172
- variation = CampaignDecisionService.new.get_variation_alloted(user_id, settings.get_account_id, campaign)
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
+
173
179
  if variation.nil?
174
180
  LoggerService.log(LogLevelEnum::INFO, "USER_CAMPAIGN_BUCKET_INFO", {
175
181
  campaignKey: campaign.get_type == CampaignTypeEnum::AB ? campaign.get_key : "#{campaign.get_name}_#{campaign.get_rule_key}",
176
- userId: user_id,
182
+ userId: display_user_id,
177
183
  status: 'did not get any variation'
178
184
  })
179
185
  return nil
180
186
  end
181
187
  LoggerService.log(LogLevelEnum::INFO, "USER_CAMPAIGN_BUCKET_INFO", {
182
188
  campaignKey: campaign.get_type == CampaignTypeEnum::AB ? campaign.get_key : "#{campaign.get_name}_#{campaign.get_rule_key}",
183
- userId: user_id,
189
+ userId: display_user_id,
184
190
  status: "got variation: #{variation.get_key}"
185
191
  })
186
192
  variation
@@ -236,7 +242,7 @@ class DecisionUtil
236
242
 
237
243
  whitelisted_variation = CampaignDecisionService.new.get_variation(
238
244
  targeted_variations,
239
- DecisionMaker.new.calculate_bucket_value(CampaignUtil.get_bucketing_seed(context.get_id, campaign, nil))
245
+ DecisionMaker.new.calculate_bucket_value(CampaignUtil.get_bucketing_seed(context.get_bucketing_seed || context.get_id, campaign, nil))
240
246
  )
241
247
  else
242
248
  whitelisted_variation = targeted_variations.first
@@ -110,7 +110,7 @@ def is_rollout_rule_for_feature_passed(settings, feature, evaluated_feature_map,
110
110
 
111
111
  if rule_to_test_for_traffic
112
112
  campaign = CampaignModel.new.model_from_dictionary(rule_to_test_for_traffic)
113
- variation = evaluate_traffic_and_get_variation(settings, campaign, context.id)
113
+ variation = evaluate_traffic_and_get_variation(settings, campaign, context)
114
114
  if variation.is_a?(VariationModel) && !variation.nil? && variation.id.is_a?(Integer)
115
115
  evaluated_feature_map[feature.key] = {
116
116
  rollout_id: rule_to_test_for_traffic.id,
@@ -160,7 +160,7 @@ def get_eligible_campaigns(settings, campaign_map, context, storage_service)
160
160
  end
161
161
 
162
162
  # Check if user is eligible for the campaign
163
- if CampaignDecisionService.new.get_pre_segmentation_decision(campaign, context) && CampaignDecisionService.new.is_user_part_of_campaign(context.id, campaign)
163
+ if CampaignDecisionService.new.get_pre_segmentation_decision(campaign, context) && CampaignDecisionService.new.is_user_part_of_campaign(context, campaign)
164
164
  LoggerService.log(LogLevelEnum::INFO, "MEG_CAMPAIGN_ELIGIBLE", { campaignKey: campaign.key, userId: context.id })
165
165
 
166
166
  eligible_campaigns.push(campaign)
@@ -231,7 +231,7 @@ def normalize_weights_and_find_winning_campaign(shortlisted_campaigns, context,
231
231
 
232
232
  winner_campaign = CampaignDecisionService.new.get_variation(
233
233
  shortlisted_variations,
234
- DecisionMaker.new.calculate_bucket_value(CampaignUtil.get_bucketing_seed(context.id, nil, group_id))
234
+ DecisionMaker.new.calculate_bucket_value(CampaignUtil.get_bucketing_seed(context.get_bucketing_seed || context.get_id, nil, group_id))
235
235
  )
236
236
 
237
237
  if winner_campaign
@@ -315,7 +315,7 @@ def get_campaign_using_advanced_algo(settings, shortlisted_campaigns, context, c
315
315
  CampaignUtil.set_campaign_allocation(participating_campaign_list)
316
316
  winner_campaign = CampaignDecisionService.new.get_variation(
317
317
  participating_campaign_list,
318
- DecisionMaker.new.calculate_bucket_value(CampaignUtil.get_bucketing_seed(context.id, nil, group_id))
318
+ DecisionMaker.new.calculate_bucket_value(CampaignUtil.get_bucketing_seed(context.get_bucketing_seed || context.get_id, nil, group_id))
319
319
  )
320
320
  end
321
321
 
@@ -75,6 +75,15 @@ class VWOClient
75
75
  # add the uuid to the context copy
76
76
  context_copy[:uuid] = uuid
77
77
 
78
+ # Validate bucketingSeed: must be a non-empty, non-whitespace-only string
79
+ if context_copy.key?(:bucketingSeed)
80
+ seed = context_copy[:bucketingSeed]
81
+ if seed.nil? || seed.is_a?(Numeric) || seed.is_a?(Hash) || seed.is_a?(Array) || (seed.is_a?(String) && seed.strip.empty?)
82
+ LoggerService.log(LogLevelEnum::ERROR, "INVALID_BUCKETING_SEED", { apiName: api_name, type: seed.class.name })
83
+ context_copy.delete(:bucketingSeed)
84
+ end
85
+ end
86
+
78
87
  unless feature_key.is_a?(String) && !feature_key.empty?
79
88
  raise TypeError, 'feature_key should be a non-empty string'
80
89
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: vwo-fme-ruby-sdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.10.0
4
+ version: 1.11.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - VWO
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-03-06 00:00:00.000000000 Z
11
+ date: 2026-03-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: uuidtools