8000 feat: Add ODP Segments support in Audience Evaluation (#474) · optimizely/java-sdk@6b9b965 · GitHub
[go: up one dir, main page]

Skip to content

Commit 6b9b965

Browse files
authored
feat: Add ODP Segments support in Audience Evaluation (#474)
## Summary 1. Added ODP Segments support in Audience evaluation 2. Adding logic to parse Integrations from datafile and make odp host and key available in projectConfig. ## Test plan - Manually tested thoroughly. - Added New unit tests. - Existing unit tests pass. - Existing Full stack compatibility tests pass. ## JIRA [OASIS-8382](https://optimizely.atlassian.net/browse/OASIS-8382)
1 parent 60b11f8 commit 6b9b965

33 files changed

+1184
-312
lines changed

.github/workflows/java.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ jobs:
4545

4646
test:
4747
if: startsWith(github.ref, 'refs/tags/') != true
48-
runs-on: ubuntu-18.04
48+
runs-on: macos-latest
4949
strategy:
5050
fail-fast: false
5151
matrix:

core-api/src/main/java/com/optimizely/ab/OptimizelyUserContext.java

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ public class OptimizelyUserContext {
3636
@Nonnull
3737
private final Map<String, Object> attributes;
3838

39+
private List<String> qualifiedSegments;
40+
3941
@Nonnull
4042
private final Optimizely optimizely;
4143

@@ -44,19 +46,14 @@ public class OptimizelyUserContext {
4446
public OptimizelyUserContext(@Nonnull Optimizely optimizely,
4547
@Nonnull String userId,
4648
@Nonnull Map<String, ?> attributes) {
47-
this.optimizely = optimizely;
48-
this.userId = userId;
49-
if (attributes != null) {
50-
this.attributes = Collections.synchronizedMap(new HashMap<>(attributes));
51-
} else {
52-
this.attributes = Collections.synchronizedMap(new HashMap<>());
53-
}
49+
this(optimizely, userId, attributes, Collections.EMPTY_MAP, null);
5450
}
5551

5652
public OptimizelyUserContext(@Nonnull Optimizely optimizely,
5753
@Nonnull String userId,
5854
@Nonnull Map<String, ?> attributes,
59-
@Nullable Map<String, OptimizelyForcedDecision> forcedDecisionsMap) {
55+
@Nullable Map<String, OptimizelyForcedDecision> forcedDecisionsMap,
56+
@Nullable List<String> qualifiedSegments) {
6057
this.optimizely = optimizely;
6158
this.userId = userId;
6259
if (attributes != null) {
@@ -65,8 +62,10 @@ public OptimizelyUserContext(@Nonnull Optimizely optimizely,
6562
this.attributes = Collections.synchronizedMap(new HashMap<>());
6663
}
6764
if (forcedDecisionsMap != null) {
68-
this.forcedDecisionsMap = new ConcurrentHashMap<>(forcedDecisionsMap);
65+
this.forcedDecisionsMap = new ConcurrentHashMap<>(forcedDecisionsMap);
6966
}
67+
68+
this.qualifiedSegments = Collections.synchronizedList( qualifiedSegments == null ? new LinkedList<>(): qualifiedSegments);
7069
}
7170

7271
public OptimizelyUserContext(@Nonnull Optimizely optimizely, @Nonnull String userId) {
@@ -86,7 +85,16 @@ public Optimizely getOptimizely() {
8685
}
8786

8887
public OptimizelyUserContext copy() {
89-
return new OptimizelyUserContext(optimizely, userId, attributes, forcedDecisionsMap);
88+
return new OptimizelyUserContext(optimizely, userId, attributes, forcedDecisionsMap, qualifiedSegments);
89+
}
90+
91+
/**
92+
* Returns true if the user is qualified for the given segment name
93+
* @param segment A String segment key which will be checked in the qualified segments list that if it exists then user is qualified.
94+
* @return boolean Is user qualified for a segment.
95+
*/
96+
public boolean isQualifiedFor(@Nonnull String segment) {
97+
return qualifiedSegments.contains(segment);
9098
}
9199

92100
/**
@@ -265,7 +273,14 @@ public boolean removeAllForcedDecisions() {
265273
return true;
266274
}
267275

276+
public List<String> getQualifiedSegments() {
277+
return qualifiedSegments;
278+
}
268279

280+
public void setQualifiedSegments(List<String> qualifiedSegments) {
281+
this.qualifiedSegments.clear();
282+
this.qualifiedSegments.addAll(qualifiedSegments);
283+
}
269284

270285
// Utils
271286

core-api/src/main/java/com/optimizely/ab/bucketing/DecisionService.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ public DecisionResponse<Variation> getVariation(@Nonnull Experiment experiment,
152152
}
153153
}
154154

155-
DecisionResponse<Boolean> decisionMeetAudience = ExperimentUtils.doesUserMeetAudienceConditions(projectConfig, experiment, user.getAttributes(), EXPERIMENT, experiment.getKey());
155+
DecisionResponse<Boolean> decisionMeetAudience = ExperimentUtils.doesUserMeetAudienceConditions(projectConfig, experiment, user, EXPERIMENT, experiment.getKey());
156156
reasons.merge(decisionMeetAudience.getReasons());
157157
if (decisionMeetAudience.getResult()) {
158158
String bucketingId = getBucketingId(user.getUserId(), user.getAttributes());
@@ -693,7 +693,7 @@ DecisionResponse<AbstractMap.SimpleEntry> getVariationFromDeliveryRule(@Nonnull
693693
DecisionResponse<Boolean> audienceDecisionResponse = ExperimentUtils.doesUserMeetAudienceConditions(
694694
projectConfig,
695695
rule,
696-
user.getAttributes(),
696+
user,
697697
RULE,
698698
String.valueOf(ruleIndex + 1)
699699
);

core-api/src/main/java/com/optimizely/ab/config/DatafileProjectConfig.java

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ public class DatafileProjectConfig implements ProjectConfig {
6363
private final boolean anonymizeIP;
6464
private final boolean sendFlagDecisions;
6565
private final Boolean botFiltering;
66+
private final String hostForODP;
67+
private final String publicKeyForODP;
6668
private final List<Attribute> attributes;
6769
private final List<Audience> audiences;
6870
private final List<Audience> typedAudiences;
@@ -71,6 +73,7 @@ public class DatafileProjectConfig implements ProjectConfig {
7173
private final List<FeatureFlag> featureFlags;
7274
private final List<Group> groups;
7375
private final List<Rollout> rollouts;
76+
private final List<Integration> integrations;
7477

7578
// key to entity mappings
7679
private final Map<String, Attribute> attributeKeyMapping;
@@ -121,6 +124,7 @@ public DatafileProjectConfig(String accountId, String projectId, String version,
121124
experiments,
122125
null,
123126
groups,
127+
null,
124128
null
125129
);
126130
}
@@ -142,8 +146,8 @@ public DatafileProjectConfig(String accountId,
142146
List<Experiment> experiments,
143147
List<FeatureFlag> featureFlags,
144148
List<Group> groups,
145-
List<Rollout> rollouts) {
146-
149+
List<Rollout> rollouts,
150+
List<Integration> integrations) {
147151
this.accountId = accountId;
148152
this.projectId = projectId;
149153
this.version = version;
@@ -182,6 +186,24 @@ public DatafileProjectConfig(String accountId,
182186
allExperiments.addAll(aggregateGroupExperiments(groups));
183187
this.experiments = Collections.unmodifiableList(allExperiments);
184188

189+
String publicKeyForODP = "";
190+
String hostForODP = "";
191+
if (integrations == null) {
192+
this.integrations = Collections.emptyList();
193+
} else {
194+
this.integrations = Collections.unmodifiableList(integrations);
195+
for (Integration integration: this.integrations) {
196+
if (integration.getKey().equals("odp")) {
197+
hostForODP = integration.getHost();
198+
publicKeyForODP = integration.getPublicKey();
199+
break;
200+
}
201+
}
202+
}
203+
204+
this.publicKeyForODP = publicKeyForODP;
205+
this.hostForODP = hostForODP;
206+
185207
Map<String, Experiment> variationIdToExperimentMap = new HashMap<String, Experiment>();
186208
for (Experiment experiment : this.experiments) {
187209
for (Variation variation : experiment.getVariations()) {
@@ -448,6 +470,11 @@ public List<Audience> getTypedAudiences() {
448470
return typedAudiences;
449471
}
450472

473+
@Override
474+
public List<Integration> getIntegrations() {
475+
return integrations;
476+
}
477+
451478
@Override
452479
public Audience getAudience(String audienceId) {
453480
return audienceIdMapping.get(audienceId);
@@ -524,6 +551,16 @@ public Variation getFlagVariationByKey(String flagKey, String variationKey) {
524551
return null;
525552
}
526553

554+
@Override
555+
public String getHostForODP() {
556+
return hostForODP;
557+
}
558+
559+
@Override
560+
public String getPublicKeyForODP() {
561+
return publicKeyForODP;
562+
}
563+
527564
@Override
528565
public String toString() {
529566
return "ProjectConfig{" +
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/**
2+
*
3+
* Copyright 2022, Optimizely and contributors
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package com.optimizely.ab.config;
18+
19+
import com.fasterxml.jackson.annotation.JsonCreator;
20+
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
21+
import com.fasterxml.jackson.annotation.JsonProperty;
22+
23+
import javax.annotation.Nonnull;
24+
import javax.annotation.Nullable;
25+
import javax.annotation.concurrent.Immutable;
26+
27+
/**
28+
* Represents the Optimizely Integration configuration.
29+
*
30+
* @see <a href="http://developers.optimizely.com/server/reference/index.html#json">Project JSON</a>
31+
*/
32+
@Immutable
33+
@JsonIgnoreProperties(ignoreUnknown = true)
34+
public class Integration {
35+
private final String key;
36+
private final String host;
37+
private final String publicKey;
38+
39+
@JsonCreator
40+
public Integration(@JsonProperty("key") String key,
41+
@JsonProperty("host") String host,
42+
@JsonProperty("publicKey") String publicKey) {
43+
this.key = key;
44+
this.host = host;
45+
this.publicKey = publicKey;
46+
}
47+
48+
@Nonnull
49+
public String getKey() {
50+
return key;
51+
}
52+
53+
@Nullable
54+
public String getHost() { return host; }
55+
56+
@Nullable
57+
public String getPublicKey() { return publicKey; }
58+
59+
@Override
60+
public String toString() {
61+
return "Integration{" +
62+
"key='" + key + '\'' +
63+
((this.host != null) ? (", host='" + host + '\'') : "") +
64+
((this.publicKey != null) ? (", publicKey='" + publicKey + '\'') : "") +
65+
'}';
66+
}
67+
}

core-api/src/main/java/com/optimizely/ab/config/ProjectConfig.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,8 @@ Experiment getExperimentForKey(@Nonnull String experimentKey,
8383

8484
List<Audience> getTypedAudiences();
8585

86+
List<Integration> getIntegrations();
87+
8688
Audience getAudience(String audienceId);
8789

8890
Map<String, Experiment> getExperimentKeyMapping();
@@ -107,6 +109,10 @@ Experiment getExperimentForKey(@Nonnull String experimentKey,
107109

108110
Variation getFlagVariationByKey(String flagKey, String variationKey);
109111

112+
String getHostForODP();
113+
114+
String getPublicKeyForODP();
115+
110116
@Override
111117
String toString();
112118

core-api/src/main/java/com/optimizely/ab/config/audience/AndCondition.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/**
22
*
3-
* Copyright 2016-2019, Optimizely and contributors
3+
* Copyright 2016-2019, 2022, Optimizely and contributors
44
*
55
* Licensed under the Apache License, Version 2.0 (the "License");
66
* you may not use this file except in compliance with the License.
@@ -16,6 +16,7 @@
1616
*/
1717
package com.optimizely.ab.config.audience;
1818

19+
import com.optimizely.ab.OptimizelyUserContext;
1920
import com.optimizely.ab.config.ProjectConfig;
2021

2122
import javax.annotation.Nonnull;
@@ -42,7 +43,7 @@ public List<Condition> getConditions() {
4243
}
4344

4445
@Nullable
45-
public Boolean evaluate(ProjectConfig config, Map<String, ?> attributes) {
46+
public Boolean evaluate(ProjectConfig config, OptimizelyUserContext user) {
4647
if (conditions == null) return null;
4748
boolean foundNull = false;
4849
// According to the matrix where:
@@ -53,7 +54,7 @@ public Boolean evaluate(ProjectConfig config, Map<String, ?> attributes) {
5354
// true and true is true
5455
// null and null is null
5556
for (Condition condition : conditions) {
56-
Boolean conditionEval = condition.evaluate(config, attributes);
57+
Boolean conditionEval = condition.evaluate(config, user);
5758
if (conditionEval == null) {
5859
foundNull = true;
5960
} else if (!conditionEval) { // false with nulls or trues is false.
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/**
2+
*
3+
* Copyright 2022, Optimizely and contributors
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package com.optimizely.ab.config.audience;
18+
19+
public enum AttributeType {
20+
CUSTOM_ATTRIBUTE("custom_attribute"),
21+
THIRD_PARTY_DIMENSION("third_party_dimension");
22+
23+
private final String key;
24+
25+
AttributeType(String key) {
26+
this.key = key;
27+
}
28+
29+
@Override
30+
public String toString() {
31+
return key;
32+
}
33+
}

core-api/src/main/java/com/optimizely/ab/config/audience/AudienceIdCondition.java

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
/**
22
*
3-
* Copyright 2018-2021, Optimizely and contributors
3+
* Copyright 2018-2022, Optimizely and contributors
44
*
55
* Licensed under the Apache License, Version 2.0 (the "License");
66
* you may not use this file except in compliance with the License.
77
* You may obtain a copy of the License at
88
*
9-
* http://www.apache.org/licenses/LICENSE-2.0
9+
* https://www.apache.org/licenses/LICENSE-2.0
1010
*
1111
* Unless required by applicable law or agreed to in writing, software
1212
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -19,6 +19,7 @@
1919
import com.fasterxml.jackson.annotation.JsonCreator;
2020
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
2121
import com.fasterxml.jackson.annotation.JsonProperty;
22+
import com.optimizely.ab.OptimizelyUserContext;
2223
import com.optimizely.ab.config.ProjectConfig;
2324
import com.optimizely.ab.internal.InvalidAudienceCondition;
2425
import org.slf4j.Logger;
@@ -71,7 +72,7 @@ public String getOperandOrId() {
7172

7273
@Nullable
7374
@Override
74-
public Boolean evaluate(ProjectConfig config, Map<String, ?> attributes) {
75+
public Boolean evaluate(ProjectConfig config, OptimizelyUserContext user) {
7576
7733 if (config != null) {
7677
audience = config.getAudienceIdMapping().get(audienceId);
7778
}
@@ -80,7 +81,7 @@ public Boolean evaluate(ProjectConfig config, Map<String, ?> attributes) {
8081
return null;
8182
}
8283
logger.debug("Starting to evaluate audience \"{}\" with conditions: {}.", audience.getId(), audience.getConditions());
83-
Boolean result = audience.getConditions().evaluate(config, attributes);
84+
Boolean result = audience.getConditions().evaluate(config, user);
8485
logger.debug("Audience \"{}\" evaluated to {}.", audience.getId(), result);
8586
return result;
8687
}

0 commit comments

Comments
 (0)
0