Skip to content

Commit 79d0bb7

Browse files
Kaustubh22327rohitesh-wingify
authored andcommitted
feat: custom bucketing seed
1 parent 701593e commit 79d0bb7

12 files changed

Lines changed: 542 additions & 37 deletions

File tree

CHANGELOG.md

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,40 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8-
## [1.19.0] - 2025-02-18
8+
## [1.20.0] - 2026-03-03
9+
10+
### Added
11+
12+
- Added support for custom bucketing seed on the `VWOContext` so that bucketing can be driven by a caller-provided seed instead of the raw `userId`, with automatic fallback to `userId` when the seed is not set.
13+
14+
Example usage:
15+
16+
```java
17+
import com.vwo.VWO;
18+
import com.vwo.models.user.VWOContext;
19+
import com.vwo.models.user.GetFlag;
20+
import com.vwo.models.user.VWOInitOptions;
21+
import java.util.HashMap;
22+
import java.util.Map;
23+
24+
VWOInitOptions options = new VWOInitOptions();
25+
options.setAccountId(123456);
26+
options.setSdkKey("32-alpha-numeric-sdk-key");
27+
28+
VWO vwoClient = VWO.init(options);
29+
30+
// Use custom bucketing seed so different userIds can share bucketing
31+
VWOContext context = new VWOContext();
32+
context.setId("user-123");
33+
context.setBucketingSeed("shared-seed-1");
34+
35+
GetFlag flag = vwoClient.getFlag("feature-key", context);
36+
37+
// If bucketingSeed is invalid (non-string, empty or whitespace-only),
38+
// the SDK logs INVALID_PARAM for bucketingSeed and falls back to userId.
39+
```
40+
41+
## [1.19.0] - 2026-02-18
942
### Added
1043

1144
- Added session management capabilities to enable integration with VWO's web client testing campaigns. The SDK now automatically generates and manages session IDs to connect server-side feature flag decisions with client-side user sessions.
@@ -90,7 +123,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
90123
VWO instance = VWO.init(vwoInitOptions);
91124
```
92125

93-
## [1.17.0] - 2025-01-19
126+
## [1.17.0] - 2026-01-19
94127

95128
### Added
96129

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ limitations under the License. -->
1919

2020
<groupId>com.vwo.sdk</groupId>
2121
<artifactId>vwo-fme-java-sdk</artifactId>
22-
<version>1.19.0</version>
22+
<version>1.20.0</version>
2323
<packaging>jar</packaging>
2424

2525
<name>VWO FME Java SDK</name>

src/main/java/com/vwo/VWOClient.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,15 @@ public GetFlag getFlag(String featureKey, VWOContext context) {
123123
if (context == null || context.getId() == null || context.getId().isEmpty()) {
124124
throw new IllegalArgumentException("User ID is required");
125125
}
126+
127+
if (!DataTypeUtil.isNull(context.getBucketingSeed())) {
128+
if (!DataTypeUtil.isString(context.getBucketingSeed())
129+
|| context.getBucketingSeed().trim().isEmpty()) {
130+
vwoBuilder.getLoggerService().log(LogLevelEnum.ERROR, "INVALID_BUCKETING_SEED", new HashMap<String, Object>() {{
131+
}});
132+
context.setBucketingSeed(null);
133+
}
134+
}
126135
// get UUID from context
127136
uuid = this.getUUIDFromContext(context, apiName);
128137

@@ -141,6 +150,8 @@ public GetFlag getFlag(String featureKey, VWOContext context) {
141150
if (this.options.getIsAliasingEnabled()) {
142151
context.setId(UserIdUtil.getUserId(context.getId(), serviceContainer));
143152
serviceContainer.setUuid(context.getId(), true);
153+
// regenerate uuid with the resolved userId (handles case where aliasing changed the userId)
154+
uuid = this.getUUIDFromContext(context, apiName);
144155
}
145156
serviceContainer.setUuid(uuid, false);
146157

src/main/java/com/vwo/api/GetFlagAPI.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ public static GetFlag getFlag(String featureKey, VWOContext context, ServiceCont
181181
// Evaluate the passed rollout rule traffic and get the variation
182182
if (!rolloutRulesToEvaluate.isEmpty()) {
183183
Campaign passedRolloutCampaign = rolloutRulesToEvaluate.get(0);
184-
Variation variation = evaluateTrafficAndGetVariation(serviceContainer, passedRolloutCampaign, context.getId());
184+
Variation variation = evaluateTrafficAndGetVariation(serviceContainer, passedRolloutCampaign, context);
185185
if (variation != null) {
186186
isFlagEnabled = true;
187187
variablesToReturn = variation.getVariables();
@@ -256,7 +256,7 @@ public static GetFlag getFlag(String featureKey, VWOContext context, ServiceCont
256256
// Evaluate the passed experiment rule traffic and get the variation
257257
if (!experimentRulesToEvaluate.isEmpty()) {
258258
Campaign campaign = experimentRulesToEvaluate.get(0);
259-
Variation variation = evaluateTrafficAndGetVariation(serviceContainer, campaign, context.getId());
259+
Variation variation = evaluateTrafficAndGetVariation(serviceContainer, campaign, context);
260260
if (variation != null) {
261261
isFlagEnabled = true;
262262
variablesToReturn = variation.getVariables();

src/main/java/com/vwo/constants/Constants.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ public class Constants {
3232
public static final int DEFAULT_REQUEST_TIME_INTERVAL = 600; // 10 * 60(secs) = 600 secs i.e. 10 minutes
3333
public static final int DEFAULT_EVENTS_PER_REQUEST = 100;
3434
public static final String SDK_NAME = "vwo-fme-java-sdk";
35-
public static final String SDK_VERSION = "1.19.0";
35+
public static final String SDK_VERSION = "1.20.0";
3636
public static final long SETTINGS_EXPIRY = 10000000;
3737
public static final long SETTINGS_TIMEOUT = 50000;
3838

src/main/java/com/vwo/models/user/VWOContext.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ public class VWOContext {
2727
private long sessionId = generateSessionId();
2828
private Boolean useIdForWeb = false;
2929
private Map<String, ?> customVariables = new HashMap<>();
30+
private String bucketingSeed;
3031

3132
private Map<String, ?> variationTargetingVariables = new HashMap<>();
3233

@@ -95,4 +96,12 @@ public Boolean isUseIdForWeb() {
9596
public void setUseIdForWeb(Boolean useIdForWeb) {
9697
this.useIdForWeb = useIdForWeb;
9798
}
99+
100+
public String getBucketingSeed() {
101+
return bucketingSeed;
102+
}
103+
104+
public void setBucketingSeed(String bucketingSeed) {
105+
this.bucketingSeed = bucketingSeed;
106+
}
98107
}

src/main/java/com/vwo/services/CampaignDecisionService.java

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -27,18 +27,22 @@
2727

2828
import java.util.*;
2929

30+
import static com.vwo.utils.CampaignUtil.getBucketingId;
31+
import static com.vwo.utils.CampaignUtil.getUserIdForLogging;
32+
3033
public class CampaignDecisionService {
3134

3235
/**
3336
* This method is used to check if the user is part of the campaign.
34-
* @param userId User ID for which the check is to be performed.
37+
* @param context VWOContext object containing the user context.
3538
* @param campaign CampaignModel object containing the campaign settings.
3639
* @return boolean value indicating if the user is part of the campaign.
3740
*/
38-
public boolean isUserPartOfCampaign(String userId, Campaign campaign, ServiceContainer serviceContainer) {
39-
if (campaign == null || userId == null) {
41+
public boolean isUserPartOfCampaign(VWOContext context, Campaign campaign, ServiceContainer serviceContainer) {
42+
if (campaign == null || context == null || context.getId()==null) {
4043
return false;
4144
}
45+
String bucketingId = getBucketingId(context);
4246
double trafficAllocation;
4347
// Check if the campaign is of type ROLLOUT or PERSONALIZE
4448
// If yes, set the traffic allocation to the weight of the first variation
@@ -52,14 +56,14 @@ public boolean isUserPartOfCampaign(String userId, Campaign campaign, ServiceCon
5256

5357
// Generate bucket key using salt if available, otherwise use campaign ID
5458
String bucketKey = (salt != null && !salt.isEmpty()) ?
55-
salt + "_" + userId :
56-
campaign.getId() + "_" + userId;
59+
salt + "_" + bucketingId :
60+
campaign.getId() + "_" + bucketingId;
5761

5862
int valueAssignedToUser = new DecisionMaker().getBucketValueForUser(bucketKey);
5963
boolean isUserPart = valueAssignedToUser != 0 && valueAssignedToUser <= trafficAllocation;
6064

6165
serviceContainer.getLoggerService().log(LogLevelEnum.INFO, "USER_PART_OF_CAMPAIGN", new HashMap<String, Object>() {{
62-
put("userId", userId);
66+
put("userId", getUserIdForLogging(context));
6367
put("notPart", isUserPart? "" : "not");
6468
put("campaignKey", campaign.getType().equals(CampaignTypeEnum.AB.getValue()) ? campaign.getKey() : campaign.getName() + "_" + campaign.getRuleKey());
6569
}});
@@ -96,15 +100,16 @@ public Variation checkInRange(Variation variation, int bucketValue) {
96100

97101
/**
98102
* This method is used to bucket the user to a variation based on the bucket value.
99-
* @param userId User ID for which the bucketing is to be performed.
103+
* @param context VWOContext object containing the user context.
100104
* @param accountId Account ID for which the bucketing is to be performed.
101105
* @param campaign CampaignModel object containing the campaign settings.
102106
* @return VariationModel object containing the variation allotted to the user.
103107
*/
104-
public Variation bucketUserToVariation(String userId, String accountId, Campaign campaign, ServiceContainer serviceContainer) {
105-
if (campaign == null || userId == null) {
108+
public Variation bucketUserToVariation(VWOContext context, String accountId, Campaign campaign, ServiceContainer serviceContainer) {
109+
if (campaign == null || context == null || context.getId()==null) {
106110
return null;
107111
}
112+
String bucketingId = getBucketingId(context);
108113

109114
int multiplier = campaign.getPercentTraffic() != 0 ? 1 : 0;
110115
int percentTraffic = campaign.getPercentTraffic();
@@ -113,15 +118,15 @@ public Variation bucketUserToVariation(String userId, String accountId, Campaign
113118
String bucketKey;
114119
// if salt is not null and not empty, use salt else use campaign id
115120
if (salt != null && !salt.isEmpty()) {
116-
bucketKey = salt + "_" + accountId + "_" + userId;
121+
bucketKey = salt + "_" + accountId + "_" + bucketingId;
117122
} else {
118-
bucketKey = campaign.getId() + "_" + accountId + "_" + userId;
123+
bucketKey = campaign.getId() + "_" + accountId + "_" + bucketingId;
119124
}
120125
long hashValue = new DecisionMaker().generateHashValue(bucketKey);
121126
int bucketValue = new DecisionMaker().generateBucketValue(hashValue, Constants.MAX_TRAFFIC_VALUE, multiplier);
122127

123128
serviceContainer.getLoggerService().log(LogLevelEnum.DEBUG, "USER_BUCKET_TO_VARIATION", new HashMap<String, Object>() {{
124-
put("userId", userId);
129+
put("userId", getUserIdForLogging(context));
125130
put("campaignKey", campaign.getRuleKey());
126131
put("percentTraffic", String.valueOf(percentTraffic));
127132
put("bucketValue", String.valueOf(bucketValue));
@@ -168,17 +173,17 @@ public boolean getPreSegmentationDecision(Campaign campaign, VWOContext context,
168173

169174
/**
170175
* This method is used to get the variation allotted to the user in the campaign.
171-
* @param userId User ID for which the variation is to be allotted.
176+
* @param context VWOContext containing the user context.
172177
* @param accountId Account ID for which the variation is to be allotted.
173178
* @param campaign CampaignModel object containing the campaign settings.
174179
* @return VariationModel object containing the variation allotted to the user.
175180
*/
176-
public Variation getVariationAllotted(String userId, String accountId, Campaign campaign, ServiceContainer serviceContainer) {
177-
boolean isUserPart = isUserPartOfCampaign(userId, campaign, serviceContainer);
181+
public Variation getVariationAllotted(VWOContext context, String accountId, Campaign campaign, ServiceContainer serviceContainer) {
182+
boolean isUserPart = isUserPartOfCampaign(context, campaign, serviceContainer);
178183
if (Objects.equals(campaign.getType(), CampaignTypeEnum.ROLLOUT.getValue()) || Objects.equals(campaign.getType(), CampaignTypeEnum.PERSONALIZE.getValue())) {
179184
return isUserPart ? campaign.getVariations().get(0) : null;
180185
} else {
181-
return isUserPart ? bucketUserToVariation(userId, accountId, campaign, serviceContainer) : null;
186+
return isUserPart ? bucketUserToVariation(context, accountId, campaign, serviceContainer) : null;
182187
}
183188
}
184189
}

src/main/java/com/vwo/utils/CampaignUtil.java

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import com.vwo.constants.Constants;
1919
import com.vwo.enums.CampaignTypeEnum;
2020
import com.vwo.models.*;
21+
import com.vwo.models.user.VWOContext;
2122
import com.vwo.packages.logger.enums.LogLevelEnum;
2223
import com.vwo.services.LoggerService;
2324

@@ -403,4 +404,39 @@ public static String getCampaignTypeFromCampaignId(Settings settings, int campai
403404
.findFirst()
404405
.orElse("");
405406
}
407+
408+
/**
409+
* Returns the bucketing ID for a user based on the custom bucketing seed configuration.
410+
* @param context A VWOContext object containing user information.
411+
* @return The identifier to be used for bucketing (either the CustomBucketingSeed or the UserID).
412+
*/
413+
public static String getBucketingId(VWOContext context) {
414+
if (context == null || context.getId()==null) {
415+
return "";
416+
}
417+
String userId = context.getId();
418+
String bucketingSeed = context.getBucketingSeed();
419+
420+
// Simple logic: if bucketingSeed is provided and not empty, use it; otherwise fallback to userId
421+
if (bucketingSeed != null && !bucketingSeed.isEmpty()) {
422+
return bucketingSeed;
423+
}
424+
425+
return userId;
426+
}
427+
428+
/**
429+
* Returns the formatted user ID for logging purposes.
430+
* If a custom bucketing seed is used, it appends the seed to the user ID.
431+
* @param context A VWOContext object containing user information.
432+
* @return The formatted user ID string.
433+
*/
434+
public static String getUserIdForLogging(VWOContext context) {
435+
String userId = context.getId();
436+
String bucketingId = getBucketingId(context);
437+
if (!bucketingId.equals(userId)) {
438+
return userId + " (Seed: " + bucketingId + ")";
439+
}
440+
return userId;
441+
}
406442
}

src/main/java/com/vwo/utils/DecisionUtil.java

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ public static Map<String, Object> checkWhitelistingAndPreSeg(
8282
}
8383
} else {
8484
serviceContainer.getLoggerService().log(LogLevelEnum.INFO, "WHITELISTING_SKIP", new HashMap<String, Object>() {{
85-
put("userId", context.getId());
85+
put("userId", getUserIdForLogging(context));
8686
put("campaignKey", campaign.getRuleKey());
8787
}});
8888
}
@@ -252,27 +252,27 @@ public static Map<String, Object> checkWhitelistingAndPreSeg(
252252
* This method is used to evaluate the traffic for a given campaign and get the variation.
253253
* @param serviceContainer ServiceContainer object containing the service container.
254254
* @param campaign CampaignModel object containing the campaign settings.
255-
* @param userId String containing the user ID.
255+
* @param context VWOContext object containing user information.
256256
* @return VariationModel object containing the variation details.
257257
*/
258258
public static Variation evaluateTrafficAndGetVariation(
259259
ServiceContainer serviceContainer,
260260
Campaign campaign,
261-
String userId) {
261+
VWOContext context) {
262262

263263
// Get the variation allotted to the user
264-
Variation variation = new CampaignDecisionService().getVariationAllotted(userId, serviceContainer.getVWOInitOptions().getAccountId().toString(), campaign, serviceContainer);
264+
Variation variation = new CampaignDecisionService().getVariationAllotted(context, serviceContainer.getVWOInitOptions().getAccountId().toString(), campaign, serviceContainer);
265265
if (variation == null) {
266266
serviceContainer.getLoggerService().log(LogLevelEnum.INFO, "USER_CAMPAIGN_BUCKET_INFO", new HashMap<String, Object>() {{
267-
put("userId", userId);
267+
put("userId", getUserIdForLogging(context));
268268
put("campaignKey", campaign.getType().equals(CampaignTypeEnum.AB.getValue()) ? campaign.getKey() : campaign.getName() + "_" + campaign.getRuleKey());
269269
put("status", "did not get any variation");
270270
}});
271271
return null;
272272
}
273273

274274
serviceContainer.getLoggerService().log(LogLevelEnum.INFO, "USER_CAMPAIGN_BUCKET_INFO", new HashMap<String, Object>() {{
275-
put("userId", userId);
275+
put("userId", getUserIdForLogging(context));
276276
put("campaignKey", campaign.getType().equals(CampaignTypeEnum.AB.getValue()) ? campaign.getKey() : campaign.getName() + "_" + campaign.getRuleKey());
277277
put("status", "got variation: " + variation.getName());
278278
}});
@@ -290,7 +290,7 @@ private static Map<String, Object> checkCampaignWhitelisting(Campaign campaign,
290290
StatusEnum status = whitelistingResult != null ? StatusEnum.PASSED : StatusEnum.FAILED;
291291
String variationString = whitelistingResult != null ? (String) whitelistingResult.get("variationName") : "";
292292
serviceContainer.getLoggerService().log(LogLevelEnum.INFO, "WHITELISTING_STATUS", new HashMap<String, Object>() {{
293-
put("userId", context.getId());
293+
put("userId", getUserIdForLogging(context));
294294
put("campaignKey", campaign.getType().equals(CampaignTypeEnum.AB.getValue()) ? campaign.getKey() : campaign.getName() + "_" + campaign.getRuleKey());
295295
put("status", status.getStatus());
296296
put("variationString", variationString);
@@ -310,7 +310,7 @@ private static Map<String, Object> evaluateWhitelisting(Campaign campaign, VWOCo
310310
for (Variation variation : campaign.getVariations()) {
311311
if (variation.getSegments() != null && variation.getSegments().isEmpty()) {
312312
serviceContainer.getLoggerService().log(LogLevelEnum.INFO, "WHITELISTING_SKIP", new HashMap<String, Object>() {{
313-
put("userId", context.getId());
313+
put("userId", getUserIdForLogging(context));
314314
put("campaignKey", campaign.getType().equals(CampaignTypeEnum.AB.getValue()) ? campaign.getKey() : campaign.getName() + "_" + campaign.getRuleKey());
315315
put("variation", !variation.getName().isEmpty() ? "for variation: " + variation.getName() : "");
316316
}});
@@ -337,7 +337,7 @@ private static Map<String, Object> evaluateWhitelisting(Campaign campaign, VWOCo
337337
stepFactor = assignRangeValues(variation, currentAllocation);
338338
currentAllocation += stepFactor;
339339
}
340-
whitelistedVariation = new CampaignDecisionService().getVariation(targetedVariations, new DecisionMaker().calculateBucketValue(getBucketingSeed(context.getId(), campaign, null)));
340+
whitelistedVariation = new CampaignDecisionService().getVariation(targetedVariations, new DecisionMaker().calculateBucketValue(getBucketingSeed(getBucketingId(context), campaign, null)));
341341
} else if (targetedVariations.size() == 1) {
342342
whitelistedVariation = targetedVariations.get(0);
343343
}

0 commit comments

Comments
 (0)