Skip to content

Conversation

FarhanAnjum-opti
Copy link
Contributor

@FarhanAnjum-opti FarhanAnjum-opti commented Sep 24, 2025

Summary

Decision Service methods to handle CMAB

Test plan

Added unit tests

Issues

FSSDK-11170

…ations over CMAB service decisions in DecisionService
Copy link
Contributor

@muzahidul-opti muzahidul-opti left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes look good to me. Added few comments.

@Nullable ODPManager odpManager
) {
@Nullable ODPManager odpManager,
@Nonnull CmabService cmabService
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like breaking change, can we make nullable?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Javascript and all other server side implementations have cmabService as a mandatory field (non null). It shouldn't be breaking.

private String cmabUUID;

public DecisionResponse(@Nullable T result, @Nonnull DecisionReasons reasons) {
public DecisionResponse(@Nullable T result, @Nonnull DecisionReasons reasons, @Nonnull boolean error, @Nullable String cmabUUID) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it impact the existing user?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, existing users can still use this class as they were. In that case cmabUUID will be set null and error will be false. Class behaviour will be same.

.replace("\"", "\\\"")
.replace("\n", "\\n")
.replace("\r", "\\r")
.replace("\t", "\\t");
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there something built-in that does this so we don't have to write our own?

Also, this doesn't cover every legal case for JSON escaping. And don't we escape \ with \? Why three?

Copy link
Contributor Author

@FarhanAnjum-opti FarhanAnjum-opti Oct 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The SDK supports multiple parsers (e.g., Jackson, Gson) but avoids hard-binding to any one to prevent dependency lock-in. Instead, we kept payload building minimal and manual, which ensures flexibility and avoids unnecessary dependencies.

Regarding triple ,
s.replace("\"", "\\""); would create a compilation error because the string literal ends prematurely.
s.replace("\"", "\\\""); is correct (produces the string \")

Copy link
Contributor

@muzahidul-opti muzahidul-opti left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Copy link
Contributor

@jaeopt jaeopt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few changes in API definitions. Can you refactor them first?
I need more time to review the cmab logic.

String flagKey = flagsWithoutForcedDecision.get(i).getKey();

if (error) {
OptimizelyDecision optimizelyDecision = OptimizelyDecision.newErrorDecision(flagKey, user, DecisionMessage.CMAB_ERROR.reason(experimentKey));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we always report CMAB error for any decision errors? Is this safe?


/**
* Returns a decision result ({@link OptimizelyDecision}) for a given flag key and a user context,
* skipping CMAB logic and using only traditional A/B testing.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add more comments about why we need this like -
"This will be called by mobile apps which will use non-blocking legacy ab-tests only (for backward compatibility with android-sdk)"

@Nonnull List<String> keys,
@Nonnull List<OptimizelyDecideOption> options,
boolean ignoreDefaultOptions) {
Map<String, OptimizelyDecision> decisionMap = new HashMap<>();
Copy link
Contributor

@jaeopt jaeopt Oct 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have full separate copies of decideForKeys and decideForKeysSync
It won't be easy to maintain these identical copies with small changes.
Can we consider refactor decideForKeysSync to reuse decideForKeys (or the other way). We can pass "needAsync=True" param to skip async ops (including CMAB).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same for decideSync and decideAllSync?

Comment on lines +220 to +229
/**
* Returns a decision result ({@link OptimizelyDecision}) for a given flag key and a user context,
* which contains all data required to deliver the flag. This method skips CMAB logic.
*
* @param key A flag key for which a decision will be made.
* @return A decision result.
*/
public OptimizelyDecision decideSync(@Nonnull String key) {
return decideSync(key, Collections.emptyList());
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can remove this - this is only for android-sdk and it can call decideSync(key, options) always.

return optimizely.decideForKeysSync(copy(), keys, options);
}

/**
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove this too

Comment on lines +289 to +297
/**
* Returns a decision result asynchronously for a given flag key and a user context.
*
* @param key A flag key for which a decision will be made.
* @param callback A callback to invoke when the decision is available.
*/
public void decideAsync(@Nonnull String key, @Nonnull OptimizelyDecisionCallback callback) {
decideAsync(key, callback, Collections.emptyList());
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove this too

Comment on lines +313 to +321
/**
* Returns decision results asynchronously for multiple flag keys.
*
* @param keys A list of flag keys for which decisions will be made.
* @param callback A callback to invoke when decisions are available.
*/
public void decideForKeysAsync(@Nonnull List<String> keys, @Nonnull OptimizelyDecisionsCallback callback) {
decideForKeysAsync(keys, callback, Collections.emptyList());
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove this too

Comment on lines +335 to +342
/**
* Returns decision results asynchronously for all active flag keys.
*
* @param callback A callback to invoke when decisions are available.
*/
public void decideAllAsync(@Nonnull OptimizelyDecisionsCallback callback) {
decideAllAsync(callback, Collections.emptyList());
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove this too

@Nullable UserProfileTracker userProfileTracker,
@Nullable DecisionReasons reasons) {
@Nullable DecisionReasons reasons,
@Nonnull boolean useCmab) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we change this param useCmab to needAsync (or similar)?
I understand the purpose of this is differentiate sync vs async not specific for cmab.
We can keep more it more generatic for future extension.

if (decisionMeetAudience.getResult()) {
String bucketingId = getBucketingId(user.getUserId(), user.getAttributes());
String cmabUUID = null;
if (useCmab && isCmabExperiment(experiment)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can consider to bring useCmab (needAsync flag) to decouple from cmab so can use to cover other async ops too.

* @return A {@link DecisionResponse} including the entity ID ("$" if bucketed to CMAB, null otherwise) and decision reasons
*/
@Nonnull
public DecisionResponse<String> bucketForCmab(@Nonnull Experiment experiment,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not see if we need this separate bucketing for cmab. We can reuse the current bucket().

DecisionReasons reasons = DefaultDecisionReasons.newInstance();

// Check if user is in CMAB traffic allocation
DecisionResponse<String> bucketResponse = bucketer.bucketForCmab(experiment, bucketingId, projectConfig);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't we reuse existing bucket() here?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants