@@ -128,6 +128,49 @@ private DecisionResponse<Variation> bucketToVariation(@Nonnull ExperimentCore ex
128128 return new DecisionResponse (null , reasons );
129129 }
130130
131+ /**
132+ * Determines CMAB traffic allocation for a user based on hashed value from murmurhash3.
133+ * This method handles bucketing users into CMAB (Contextual Multi-Armed Bandit) experiments.
134+ */
135+ @ Nonnull
136+ private DecisionResponse <String > bucketToEntityForCmab (@ Nonnull Experiment experiment ,
137+ @ Nonnull String bucketingId ) {
138+ DecisionReasons reasons = DefaultDecisionReasons .newInstance ();
139+
140+ // "salt" the bucket id using the experiment id
141+ String experimentId = experiment .getId ();
142+ String experimentKey = experiment .getKey ();
143+ String combinedBucketId = bucketingId + experimentId ;
144+
145+ // Handle CMAB traffic allocation
146+ TrafficAllocation cmabTrafficAllocation = new TrafficAllocation ("$" , experiment .getCmab ().getTrafficAllocation ());
147+ List <TrafficAllocation > trafficAllocations = java .util .Collections .singletonList (cmabTrafficAllocation );
148+
149+ String cmabMessage = reasons .addInfo ("Using CMAB traffic allocation for experiment \" %s\" ." , experimentKey );
150+ logger .debug (cmabMessage );
151+
152+ int hashCode = MurmurHash3 .murmurhash3_x86_32 (combinedBucketId , 0 , combinedBucketId .length (), MURMUR_HASH_SEED );
153+ int bucketValue = generateBucketValue (hashCode );
154+ logger .debug ("Assigned bucket {} to user with bucketingId \" {}\" when bucketing to a variation." , bucketValue , bucketingId );
155+
156+ String bucketedEntityId = bucketToEntity (bucketValue , trafficAllocations );
157+ if (bucketedEntityId != null ) {
158+ if ("$" .equals (bucketedEntityId )) {
159+ String message = reasons .addInfo ("User with bucketingId \" %s\" is bucketed into CMAB for experiment \" %s\" ." , bucketingId , experimentKey );
160+ logger .info (message );
161+ } else {
162+ // This shouldn't happen in CMAB since we only have "$" entity, but handle gracefully
163+ String message = reasons .addInfo ("User with bucketingId \" %s\" is bucketed into entity \" %s\" for experiment \" %s\" ." , bucketingId , bucketedEntityId , experimentKey );
164+ logger .info (message );
165+ }
166+ } else {
167+ String message = reasons .addInfo ("User with bucketingId \" %s\" is not bucketed into CMAB for experiment \" %s\" ." , bucketingId , experimentKey );
168+ logger .info (message );
169+ }
170+
171+ return new DecisionResponse <>(bucketedEntityId , reasons );
172+ }
173+
131174 /**
132175 * Assign a {@link Variation} of an {@link Experiment} to a user based on hashed value from murmurhash3.
133176 *
@@ -177,6 +220,54 @@ public DecisionResponse<Variation> bucket(@Nonnull ExperimentCore experiment,
177220 return new DecisionResponse <>(decisionResponse .getResult (), reasons );
178221 }
179222
223+ /**
224+ * Assign a user to CMAB traffic for an experiment based on hashed value from murmurhash3.
225+ * This method handles CMAB (Contextual Multi-Armed Bandit) traffic allocation.
226+ *
227+ * @param experiment The CMAB Experiment in which the user is to be bucketed.
228+ * @param bucketingId string A customer-assigned value used to create the key for the murmur hash.
229+ * @param projectConfig The current projectConfig
230+ * @return A {@link DecisionResponse} including the entity ID ("$" if bucketed to CMAB, null otherwise) and decision reasons
231+ */
232+ @ Nonnull
233+ public DecisionResponse <String > bucketForCmab (@ Nonnull Experiment experiment ,
234+ @ Nonnull String bucketingId ,
235+ @ Nonnull ProjectConfig projectConfig ) {
236+
237+ DecisionReasons reasons = DefaultDecisionReasons .newInstance ();
238+
239+ // ---------- Handle Group Logic (same as regular bucket method) ----------
240+ String groupId = experiment .getGroupId ();
241+ if (!groupId .isEmpty ()) {
242+ Group experimentGroup = projectConfig .getGroupIdMapping ().get (groupId );
243+
244+ if (experimentGroup .getPolicy ().equals (Group .RANDOM_POLICY )) {
245+ Experiment bucketedExperiment = bucketToExperiment (experimentGroup , bucketingId , projectConfig );
246+ if (bucketedExperiment == null ) {
247+ String message = reasons .addInfo ("User with bucketingId \" %s\" is not in any experiment of group %s." , bucketingId , experimentGroup .getId ());
248+ logger .info (message );
249+ return new DecisionResponse <>(null , reasons );
250+ }
251+
252+ if (!bucketedExperiment .getId ().equals (experiment .getId ())) {
253+ String message = reasons .addInfo ("User with bucketingId \" %s\" is not in experiment \" %s\" of group %s." , bucketingId , experiment .getKey (),
254+ experimentGroup .getId ());
255+ logger .info (message );
256+ return new DecisionResponse <>(null , reasons );
257+ }
258+
259+ String message = reasons .addInfo ("User with bucketingId \" %s\" is in experiment \" %s\" of group %s." , bucketingId , experiment .getKey (),
260+ experimentGroup .getId ());
261+ logger .info (message );
262+ }
263+ }
264+
265+ // ---------- Use CMAB-aware bucketToEntity ----------
266+ DecisionResponse <String > decisionResponse = bucketToEntityForCmab (experiment , bucketingId );
267+ reasons .merge (decisionResponse .getReasons ());
268+ return new DecisionResponse <>(decisionResponse .getResult (), reasons );
269+ }
270+
180271 //======== Helper methods ========//
181272
182273 /**
0 commit comments