Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Integrated ODPManager with UserContext and Optimizely client #490

Merged
merged 11 commits into from
Nov 21, 2022

Conversation

zashraf1985
Copy link
Contributor

@zashraf1985 zashraf1985 commented Oct 27, 2022

Summary

This PR integrates ODP functionality with UserContext and Optimizely client to make it available for users. There are two ways to integrate ODPManager.

  1. ODPManager.Builder.
  2. Use default instance from OptimizelyFactory.

Test plan

  1. Manually tested thoroughly
  2. Will add unit tests after the first review passes.

Issues

FSSDK-8389, FS-8683

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.

I reviewed the high level first and will look at more details again. The builder pattern for ODP configuration looks good. See comments for some changes suggested.

@@ -319,10 +322,16 @@ public static Optimizely newDefaultInstance(ProjectConfigManager configManager,
.withNotificationCenter(notificationCenter)
.build();

ODPApiManager defaultODPApiManager = new DefaultODPApiManager();
ODPManager odpManager = ODPManager.builder()
.withApiManager(defaultODPApiManager)
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 let it use DefaultODPApiManager by default?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We cannot do that because DefaultODPApiManager implements http calls and is only available in the http-client project. The Core SDK does not know about its implementation. This is exactly how we inject polling datafile manager and batch event processor.

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.

All changes look great! A few more changes suggested.
I also want to discuss a couple of possible extensions to support Android after wrapping up this PR.

core-api/src/main/java/com/optimizely/ab/Optimizely.java Outdated Show resolved Hide resolved
core-api/src/main/java/com/optimizely/ab/Optimizely.java Outdated Show resolved Hide resolved
core-api/src/main/java/com/optimizely/ab/Optimizely.java Outdated Show resolved Hide resolved
Comment on lines 322 to 326
ODPManager odpManager = optimizely.getODPManager();
if (odpManager != null) {
List<ODPSegmentOption> segmentOptions = new ArrayList<>();
if (ignoreCache) {
segmentOptions.add(ODPSegmentOption.IGNORE_CACHE);
}
if (resetCache) {
segmentOptions.add(ODPSegmentOption.RESET_CACHE);
synchronized (odpManager) {
setQualifiedSegments(odpManager.getSegmentManager().getQualifiedSegments(userId, segmentOptions));
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
ODPManager odpManager = optimizely.getODPManager();
if (odpManager != null) {
List<ODPSegmentOption> segmentOptions = new ArrayList<>();
if (ignoreCache) {
segmentOptions.add(ODPSegmentOption.IGNORE_CACHE);
}
if (resetCache) {
segmentOptions.add(ODPSegmentOption.RESET_CACHE);
synchronized (odpManager) {
setQualifiedSegments(odpManager.getSegmentManager().getQualifiedSegments(userId, segmentOptions));
}
setQualifiedSegments(optimizely.fetchQualifiedSegments(userId))

This is the only place we have dependency on ODPManager in OptimizelyUserContext. Can we move this logic to Optimizely too?

Copy link
Contributor Author

@zashraf1985 zashraf1985 Nov 4, 2022

Choose a reason for hiding this comment

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

I will do this but i just wanted to avoid method drilling. I do not want us to end up creating multiple levels of simple wrappers that do not add any value in between. This is why i preferred exposing ODPEventManager and ODPSegmentManager directly as getters. One option is to add wrappers for each ODP related function in the Optimizely client. Another option is to let userContext use ODPManager directly so that we do not need to keep adding wrappers as we add functionality to ODPManager. This wrapper mechanism gets specially tricky when we provide multiple overrides of methods. I would suggest to keep it like this and let userContext access ODPManager through a getter in optimizely client and call its methods direclty.. what do you say?

Copy link
Contributor

Choose a reason for hiding this comment

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

@zashraf1985 it looks like a style preference with tradeoffs. I'll leave it to you.


String query = String.format("query($userId: String, $audiences: [String]) {customer(%s: $userId) {audiences(subset: $audiences) {edges {node {name state}}}}}", userKey);
String variables = String.format("{\"userId\": \"%s\", \"audiences\": [%s]}", userValue, segmentsString);
String requestPayload = String.format("{\"query\": \"%s\", \"variables\": %s}", query, variables);
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 no gain at all in Java with this new format. I guess you're trying to avoid use JSON parser here :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

There is still a gain. We are now using graphQL json format which takes variables separately which means if someone wants to inject anything in the query, it wont work because we are not concatenating variables directly in the query, we are passing them as variables.

@zashraf1985 zashraf1985 removed their assignment Nov 9, 2022
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.

Async solution looks good to me. A few more small changes suggested.

ODPManager odpManager = getODPManager();
if (odpManager != null) {
odpManager.getEventManager().identifyUser(userId);
}
}

public void identifyUser(@Nullable String vuid, String userId) {
public void identifyUser(@Nullable String vuid, @Nullable String userId) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Wondering if need this api here. If we can determine if userId is vuid or fsUserId, we do not need this call here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

@@ -299,21 +300,16 @@ public void setQualifiedSegments(List<String> qualifiedSegments) {

/**
* Fetch all qualified segments for the user context.
*
* <p>
* The segments fetched will be saved and can be accessed at any time by calling {@link #getQualifiedSegments()}.
*/
public void fetchQualifiedSegments() {
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 return boolean (true/false for success/failure) for blocking fetchSegment calls. Clients can check the status before continuing with decide call?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

ODPManager odpManager = optimizely.getODPManager();
if (odpManager == null) {
logger.error("Audience segments fetch failed (ODP is not enabled");
callback.invokeCallback();
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 also pass the status boolean (success/failure) in the callback parameter.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

logger.error("Audience segments fetch failed (ODP is not enabled");
callback.invokeCallback();
} else {
odpManager.getSegmentManager().getQualifiedSegments(userId, segments -> {
Copy link
Contributor

Choose a reason for hiding this comment

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

If we want to keep this direct call to odpManager, I suggest to make "optimizely.identifyUser" call also direct call (for consistency). I still prefer moving all to optimizely :) I'm concerned about the increasing complexity of UserContext.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Removed the direct call and made a wrapper in optimizely

setQualifiedSegments(segments);
}
callback.invokeCallback();
}, segmentOptions);
Copy link
Contributor

Choose a reason for hiding this comment

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

This async solution looks good!


@FunctionalInterface
public interface ODPSegmentCallback {
void invokeCallback();
Copy link
Contributor

Choose a reason for hiding this comment

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

This name sounds redundant. What about onCompleted or onFetchComplemeted?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

changed it to onCompleted


@FunctionalInterface
public interface ODPSegmentCallback {
void invokeCallback();
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
void invokeCallback();
void onCompleted(bool status);

Comment on lines 25 to 26
Integer getMaxSize();
Integer getCacheTimeoutSeconds();
Copy link
Contributor

Choose a reason for hiding this comment

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

Why adding these to Cache interface? We do not care about these when clients implement their own cache with own size and timeout.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I needed these for some unit tests. I cant figure out any other way to verify some tests

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I removed these methods and tests checking for these values.

@@ -106,15 +106,66 @@ public List<String> getQualifiedSegments(ODPUserKey userKey, String userValue, L
return qualifiedSegments;
}

public void getQualifiedSegments(ODPUserKey userKey, String userValue, ODPSegmentFetchCallback callback, List<ODPSegmentOption> options) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Looks good!

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.

LGTM!

@zashraf1985 zashraf1985 merged commit b16dff5 into master Nov 21, 2022
@zashraf1985 zashraf1985 deleted the zeeshan/ats-integration branch November 21, 2022 19:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants