-
Notifications
You must be signed in to change notification settings - Fork 32
feat: Integrated ODPManager with UserContext and Optimizely client #490
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
Changes from 9 commits
525ad99
5d3cb21
0a1bf21
0c74a18
42b2425
79263fc
e573a6a
6d88b86
68aa5ff
29f2b09
49a9686
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -28,6 +28,8 @@ | |
import com.optimizely.ab.event.internal.*; | ||
import com.optimizely.ab.event.internal.payload.EventBatch; | ||
import com.optimizely.ab.notification.*; | ||
import com.optimizely.ab.odp.ODPEvent; | ||
import com.optimizely.ab.odp.ODPManager; | ||
import com.optimizely.ab.optimizelyconfig.OptimizelyConfig; | ||
import com.optimizely.ab.optimizelyconfig.OptimizelyConfigManager; | ||
import com.optimizely.ab.optimizelyconfig.OptimizelyConfigService; | ||
|
@@ -96,6 +98,9 @@ public class Optimizely implements AutoCloseable { | |
@Nullable | ||
private final UserProfileService userProfileService; | ||
|
||
@Nullable | ||
private final ODPManager odpManager; | ||
|
||
private Optimizely(@Nonnull EventHandler eventHandler, | ||
@Nonnull EventProcessor eventProcessor, | ||
@Nonnull ErrorHandler errorHandler, | ||
|
@@ -104,7 +109,8 @@ private Optimizely(@Nonnull EventHandler eventHandler, | |
@Nonnull ProjectConfigManager projectConfigManager, | ||
@Nullable OptimizelyConfigManager optimizelyConfigManager, | ||
@Nonnull NotificationCenter notificationCenter, | ||
@Nonnull List<OptimizelyDecideOption> defaultDecideOptions | ||
@Nonnull List<OptimizelyDecideOption> defaultDecideOptions, | ||
@Nullable ODPManager odpManager | ||
) { | ||
this.eventHandler = eventHandler; | ||
this.eventProcessor = eventProcessor; | ||
|
@@ -115,6 +121,15 @@ private Optimizely(@Nonnull EventHandler eventHandler, | |
this.optimizelyConfigManager = optimizelyConfigManager; | ||
this.notificationCenter = notificationCenter; | ||
this.defaultDecideOptions = defaultDecideOptions; | ||
this.odpManager = odpManager; | ||
|
||
if (odpManager != null) { | ||
odpManager.getEventManager().start(); | ||
if (getProjectConfig() != null) { | ||
updateODPSettings(); | ||
} | ||
addUpdateConfigNotificationHandler(configNotification -> { updateODPSettings(); }); | ||
} | ||
} | ||
|
||
/** | ||
|
@@ -128,8 +143,6 @@ public boolean isValid() { | |
return getProjectConfig() != null; | ||
} | ||
|
||
|
||
|
||
/** | ||
* Checks if eventHandler {@link EventHandler} and projectConfigManager {@link ProjectConfigManager} | ||
* are Closeable {@link Closeable} and calls close on them. | ||
|
@@ -141,6 +154,9 @@ public void close() { | |
tryClose(eventProcessor); | ||
tryClose(eventHandler); | ||
tryClose(projectConfigManager); | ||
if (odpManager != null) { | ||
tryClose(odpManager); | ||
} | ||
} | ||
|
||
//======== activate calls ========// | ||
|
@@ -674,9 +690,9 @@ public OptimizelyJSON getFeatureVariableJSON(@Nonnull String featureKey, | |
*/ | ||
@Nullable | ||
public OptimizelyJSON getFeatureVariableJSON(@Nonnull String featureKey, | ||
@Nonnull String variableKey, | ||
@Nonnull String userId, | ||
@Nonnull Map<String, ?> attributes) { | ||
@Nonnull String variableKey, | ||
@Nonnull String userId, | ||
@Nonnull Map<String, ?> attributes) { | ||
|
||
return getFeatureVariableValueForType( | ||
featureKey, | ||
|
@@ -688,10 +704,10 @@ public OptimizelyJSON getFeatureVariableJSON(@Nonnull String featureKey, | |
|
||
@VisibleForTesting | ||
<T> T getFeatureVariableValueForType(@Nonnull String featureKey, | ||
@Nonnull String variableKey, | ||
@Nonnull String userId, | ||
@Nonnull Map<String, ?> attributes, | ||
@Nonnull String variableType) { | ||
@Nonnull String variableKey, | ||
@Nonnull String userId, | ||
@Nonnull Map<String, ?> attributes, | ||
@Nonnull String variableType) { | ||
if (featureKey == null) { | ||
logger.warn("The featureKey parameter must be nonnull."); | ||
return null; | ||
|
@@ -878,7 +894,7 @@ public OptimizelyJSON getAllFeatureVariables(@Nonnull String featureKey, | |
} | ||
} else { | ||
logger.info("User \"{}\" was not bucketed into any variation for feature flag \"{}\". " + | ||
"The default values are being returned.", userId, featureKey); | ||
"The default values are being returned.", userId, featureKey); | ||
} | ||
|
||
Map<String, Object> valuesMap = new HashMap<String, Object>(); | ||
|
@@ -1142,7 +1158,7 @@ public OptimizelyConfig getOptimizelyConfig() { | |
* @param userId The user ID to be used for bucketing. | ||
* @param attributes: A map of attribute names to current user attribute values. | ||
* @return An OptimizelyUserContext associated with this OptimizelyClient. | ||
*/ | ||
*/ | ||
public OptimizelyUserContext createUserContext(@Nonnull String userId, | ||
@Nonnull Map<String, ?> attributes) { | ||
if (userId == null) { | ||
|
@@ -1413,6 +1429,41 @@ public <T> int addNotificationHandler(Class<T> clazz, NotificationHandler<T> han | |
return notificationCenter.addNotificationHandler(clazz, handler); | ||
} | ||
|
||
@Nullable | ||
public ODPManager getODPManager() { | ||
zashraf1985 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return odpManager; | ||
} | ||
|
||
public void sendODPEvent(@Nullable String type, @Nonnull String action, @Nullable Map<String, String> identifiers, @Nullable Map<String, Object> data) { | ||
if (odpManager != null) { | ||
ODPEvent event = new ODPEvent(type, action, identifiers, data); | ||
odpManager.getEventManager().sendEvent(event); | ||
} else { | ||
logger.error("ODP event send failed (ODP is not enabled)"); | ||
} | ||
} | ||
|
||
public void identifyUser(@Nonnull String userId) { | ||
ODPManager odpManager = getODPManager(); | ||
if (odpManager != null) { | ||
odpManager.getEventManager().identifyUser(userId); | ||
} | ||
} | ||
|
||
public void identifyUser(@Nullable String vuid, @Nullable String userId) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done |
||
ODPManager odpManager = getODPManager(); | ||
if (odpManager != null) { | ||
odpManager.getEventManager().identifyUser(vuid, userId); | ||
} | ||
zashraf1985 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
private void updateODPSettings() { | ||
if (odpManager != null && getProjectConfig() != null) { | ||
ProjectConfig projectConfig = getProjectConfig(); | ||
odpManager.updateSettings(projectConfig.getHostForODP(), projectConfig.getPublicKeyForODP(), projectConfig.getAllSegments()); | ||
} | ||
} | ||
|
||
//======== Builder ========// | ||
|
||
/** | ||
|
@@ -1467,6 +1518,7 @@ public static class Builder { | |
private UserProfileService userProfileService; | ||
private NotificationCenter notificationCenter; | ||
private List<OptimizelyDecideOption> defaultDecideOptions; | ||
private ODPManager odpManager; | ||
|
||
// For backwards compatibility | ||
private AtomicProjectConfigManager fallbackConfigManager = new AtomicProjectConfigManager(); | ||
|
@@ -1562,6 +1614,11 @@ public Builder withDefaultDecideOptions(List<OptimizelyDecideOption> defaultDeci | |
return this; | ||
} | ||
|
||
public Builder withODPManager(ODPManager odpManager) { | ||
zashraf1985 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
this.odpManager = odpManager; | ||
return this; | ||
} | ||
|
||
// Helper functions for making testing easier | ||
protected Builder withBucketing(Bucketer bucketer) { | ||
this.bucketer = bucketer; | ||
|
@@ -1636,7 +1693,7 @@ public Optimizely build() { | |
defaultDecideOptions = Collections.emptyList(); | ||
} | ||
|
||
return new Optimizely(eventHandler, eventProcessor, errorHandler, decisionService, userProfileService, projectConfigManager, optimizelyConfigManager, notificationCenter, defaultDecideOptions); | ||
return new Optimizely(eventHandler, eventProcessor, errorHandler, decisionService, userProfileService, projectConfigManager, optimizelyConfigManager, notificationCenter, defaultDecideOptions, odpManager); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -16,7 +16,10 @@ | |
*/ | ||
package com.optimizely.ab; | ||
|
||
import com.optimizely.ab.config.Variation; | ||
import com.optimizely.ab.odp.ODPManager; | ||
import com.optimizely.ab.odp.ODPSegmentCallback; | ||
import com.optimizely.ab.odp.ODPSegmentManager; | ||
import com.optimizely.ab.odp.ODPSegmentOption; | ||
import com.optimizely.ab.optimizelydecision.*; | ||
import org.slf4j.Logger; | ||
import org.slf4j.LoggerFactory; | ||
|
@@ -54,6 +57,15 @@ public OptimizelyUserContext(@Nonnull Optimizely optimizely, | |
@Nonnull Map<String, ?> attributes, | ||
@Nullable Map<String, OptimizelyForcedDecision> forcedDecisionsMap, | ||
@Nullable List<String> qualifiedSegments) { | ||
this(optimizely, userId, attributes, forcedDecisionsMap, qualifiedSegments, true); | ||
} | ||
|
||
public OptimizelyUserContext(@Nonnull Optimizely optimizely, | ||
@Nonnull String userId, | ||
@Nonnull Map<String, ?> attributes, | ||
@Nullable Map<String, OptimizelyForcedDecision> forcedDecisionsMap, | ||
@Nullable List<String> qualifiedSegments, | ||
@Nullable Boolean shouldIdentifyUser) { | ||
this.optimizely = optimizely; | ||
this.userId = userId; | ||
if (attributes != null) { | ||
|
@@ -66,6 +78,10 @@ public OptimizelyUserContext(@Nonnull Optimizely optimizely, | |
} | ||
|
||
this.qualifiedSegments = Collections.synchronizedList( qualifiedSegments == null ? new LinkedList<>(): qualifiedSegments); | ||
|
||
if (shouldIdentifyUser == null || shouldIdentifyUser) { | ||
optimizely.identifyUser(userId); | ||
} | ||
} | ||
|
||
public OptimizelyUserContext(@Nonnull Optimizely optimizely, @Nonnull String userId) { | ||
|
@@ -85,7 +101,7 @@ public Optimizely getOptimizely() { | |
} | ||
|
||
public OptimizelyUserContext copy() { | ||
return new OptimizelyUserContext(optimizely, userId, attributes, forcedDecisionsMap, qualifiedSegments); | ||
return new OptimizelyUserContext(optimizely, userId, attributes, forcedDecisionsMap, qualifiedSegments, false); | ||
} | ||
|
||
/** | ||
|
@@ -282,6 +298,67 @@ public void setQualifiedSegments(List<String> qualifiedSegments) { | |
this.qualifiedSegments.addAll(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() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we return boolean (true/false for success/failure) for blocking There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done |
||
fetchQualifiedSegments(Collections.emptyList()); | ||
} | ||
|
||
/** | ||
* 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()}. | ||
* | ||
* @param segmentOptions A set of options for fetching qualified segments. | ||
*/ | ||
public void fetchQualifiedSegments(@Nonnull List<ODPSegmentOption> segmentOptions) { | ||
ODPManager odpManager = optimizely.getODPManager(); | ||
if (odpManager != null) { | ||
synchronized (odpManager) { | ||
setQualifiedSegments(odpManager.getSegmentManager().getQualifiedSegments(userId, segmentOptions)); | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* Fetch all qualified segments for the user context in a non-blocking manner. This method will fetch segments | ||
* in a separate thread and invoke the provided callback when results are available. | ||
* <p> | ||
* The segments fetched will be saved and can be accessed at any time by calling {@link #getQualifiedSegments()}. | ||
* | ||
* @param callback A callback to invoke when results are available. | ||
* @param segmentOptions A set of options for fetching qualified segments. | ||
*/ | ||
public void fetchQualifiedSegments(ODPSegmentCallback callback, List<ODPSegmentOption> segmentOptions) { | ||
ODPManager odpManager = optimizely.getODPManager(); | ||
if (odpManager == null) { | ||
logger.error("Audience segments fetch failed (ODP is not enabled"); | ||
callback.invokeCallback(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done |
||
} else { | ||
odpManager.getSegmentManager().getQualifiedSegments(userId, segments -> { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Removed the direct call and made a wrapper in |
||
if (segments != null) { | ||
setQualifiedSegments(segments); | ||
} | ||
callback.invokeCallback(); | ||
}, segmentOptions); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This async solution looks good! |
||
} | ||
} | ||
|
||
/** | ||
* Fetch all qualified segments for the user context in a non-blocking manner. This method will fetch segments | ||
* in a separate thread and invoke the provided callback when results are available. | ||
* <p> | ||
* The segments fetched will be saved and can be accessed at any time by calling {@link #getQualifiedSegments()}. | ||
* | ||
* @param callback A callback to invoke when results are available. | ||
*/ | ||
public void fetchQualifiedSegments(ODPSegmentCallback callback) { | ||
fetchQualifiedSegments(callback, Collections.emptyList()); | ||
} | ||
|
||
// Utils | ||
|
||
@Override | ||
|
@@ -309,5 +386,4 @@ public String toString() { | |
", attributes='" + attributes + '\'' + | ||
'}'; | ||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -24,6 +24,7 @@ | |
import javax.annotation.Nullable; | ||
import java.util.List; | ||
import java.util.Map; | ||
import java.util.Set; | ||
|
||
/** | ||
* ProjectConfig is an interface capturing the experiment, variation and feature definitions. | ||
|
@@ -69,6 +70,8 @@ Experiment getExperimentForKey(@Nonnull String experimentKey, | |
|
||
List<Experiment> getExperiments(); | ||
|
||
Set<String> getAllSegments(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Wondering if this addition to the interface will require changes to all existing customization. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If users are using their own ProjectConfigManagers? yes! But this is the only way to add more functionality to ProjectConfig. I believe almost no one will be customizing the whole ProjectConfigManager. If there are a handful of customers who are even doing it, ATS will go out as a major version change anyway. We can mention this in the release notes as a breaking change. What do you think? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @zashraf1985 We'll go for a major version change if we have to. We better keep all the breaking changes required. |
||
|
||
List<Experiment> getExperimentsForEventKey(String eventKey); | ||
|
||
List<FeatureFlag> getFeatureFlags(); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -22,4 +22,6 @@ public interface Cache<T> { | |
void save(String key, T value); | ||
T lookup(String key); | ||
void reset(); | ||
Integer getMaxSize(); | ||
Integer getCacheTimeoutSeconds(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I removed these methods and tests checking for these values. |
||
} |
Uh oh!
There was an error while loading. Please reload this page.