Skip to content

Commit dcbf9b0

Browse files
authored
feat(flag-decisions): Add support for sending flag decisions along with decision metadata. (#405)
1 parent d3ff6cd commit dcbf9b0

18 files changed

+369
-47
lines changed

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

Lines changed: 56 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -216,31 +216,65 @@ private Variation activate(@Nullable ProjectConfig projectConfig,
216216
return null;
217217
}
218218

219-
sendImpression(projectConfig, experiment, userId, copiedAttributes, variation);
219+
sendImpression(projectConfig, experiment, userId, copiedAttributes, variation, "experiment");
220220

221221
return variation;
222222
}
223223

224+
/**
225+
* Creates and sends impression event.
226+
*
227+
* @param projectConfig the current projectConfig
228+
* @param experiment the experiment user bucketed into and dispatch an impression event
229+
* @param userId the ID of the user
230+
* @param filteredAttributes the attributes of the user
231+
* @param variation the variation that was returned from activate.
232+
* @param ruleType It can either be experiment in case impression event is sent from activate or it's feature-test or rollout
233+
*/
224234
private void sendImpression(@Nonnull ProjectConfig projectConfig,
225235
@Nonnull Experiment experiment,
226236
@Nonnull String userId,
227237
@Nonnull Map<String, ?> filteredAttributes,
228-
@Nonnull Variation variation) {
229-
if (!experiment.isRunning()) {
230-
logger.info("Experiment has \"Launched\" status so not dispatching event during activation.");
231-
return;
232-
}
238+
@Nonnull Variation variation,
239+
@Nonnull String ruleType) {
240+
sendImpression(projectConfig, experiment, userId, filteredAttributes, variation, "", ruleType);
241+
}
242+
243+
/**
244+
* Creates and sends impression event.
245+
*
246+
* @param projectConfig the current projectConfig
247+
* @param experiment the experiment user bucketed into and dispatch an impression event
248+
* @param userId the ID of the user
249+
* @param filteredAttributes the attributes of the user
250+
* @param variation the variation that was returned from activate.
251+
* @param flagKey It can either be empty if ruleType is experiment or it's feature key in case ruleType is feature-test or rollout
252+
* @param ruleType It can either be experiment in case impression event is sent from activate or it's feature-test or rollout
253+
*/
254+
private void sendImpression(@Nonnull ProjectConfig projectConfig,
255+
@Nonnull Experiment experiment,
256+
@Nonnull String userId,
257+
@Nonnull Map<String, ?> filteredAttributes,
258+
@Nonnull Variation variation,
259+
@Nonnull String flagKey,
260+
@Nonnull String ruleType) {
233261

234262
UserEvent userEvent = UserEventFactory.createImpressionEvent(
235263
projectConfig,
236264
experiment,
237265
variation,
238266
userId,
239-
filteredAttributes);
267+
filteredAttributes,
268+
flagKey,
269+
ruleType);
240270

271+
if (userEvent == null) {
272+
return;
273+
}
241274
eventProcessor.process(userEvent);
242-
logger.info("Activating user \"{}\" in experiment \"{}\".", userId, experiment.getKey());
243-
275+
if (experiment != null) {
276+
logger.info("Activating user \"{}\" in experiment \"{}\".", userId, experiment.getKey());
277+
}
244278
// Kept For backwards compatibility.
245279
// This notification is deprecated and the new DecisionNotifications
246280
// are sent via their respective method calls.
@@ -386,16 +420,22 @@ private Boolean isFeatureEnabled(@Nonnull ProjectConfig projectConfig,
386420
FeatureDecision featureDecision = decisionService.getVariationForFeature(featureFlag, userId, copiedAttributes, projectConfig);
387421
Boolean featureEnabled = false;
388422
SourceInfo sourceInfo = new RolloutSourceInfo();
423+
if (featureDecision.decisionSource != null) {
424+
decisionSource = featureDecision.decisionSource;
425+
}
426+
sendImpression(
427+
projectConfig,
428+
featureDecision.experiment,
429+
userId,
430+
copiedAttributes,
431+
featureDecision.variation,
432+
featureKey,
433+
decisionSource.toString());
389434

390435
if (featureDecision.variation != null) {
436+
// This information is only necessary for feature tests.
437+
// For rollouts experiments and variations are an implementation detail only.
391438
if (featureDecision.decisionSource.equals(FeatureDecision.DecisionSource.FEATURE_TEST)) {
392-
sendImpression(
393-
projectConfig,
394-
featureDecision.experiment,
395-
userId,
396-
copiedAttributes,
397-
featureDecision.variation);
398-
decisionSource = featureDecision.decisionSource;
399439
sourceInfo = new FeatureTestSourceInfo(featureDecision.experiment.getKey(), featureDecision.variation.getKey());
400440
} else {
401441
logger.info("The user \"{}\" is not included in an experiment for feature \"{}\".",

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ public class DatafileProjectConfig implements ProjectConfig {
6161
private final String revision;
6262
private final String version;
6363
private final boolean anonymizeIP;
64+
private final boolean sendFlagDecisions;
6465
private final Boolean botFiltering;
6566
private final List<Attribute> attributes;
6667
private final List<Audience> audiences;
@@ -103,6 +104,7 @@ public DatafileProjectConfig(String accountId, String projectId, String version,
103104
this(
104105
accountId,
105106
anonymizeIP,
107+
false,
106108
null,
107109
projectId,
108110
revision,
@@ -121,6 +123,7 @@ public DatafileProjectConfig(String accountId, String projectId, String version,
121123
// v4 constructor
122124
public DatafileProjectConfig(String accountId,
123125
boolean anonymizeIP,
126+
boolean sendFlagDecisions,
124127
Boolean botFiltering,
125128
String projectId,
126129
String revision,
@@ -139,6 +142,7 @@ public DatafileProjectConfig(String accountId,
139142
this.version = version;
140143
this.revision = revision;
141144
this.anonymizeIP = anonymizeIP;
145+
this.sendFlagDecisions = sendFlagDecisions;
142146
this.botFiltering = botFiltering;
143147

144148
this.attributes = Collections.unmodifiableList(attributes);
@@ -322,6 +326,9 @@ public String getRevision() {
322326
return revision;
323327
}
324328

329+
@Override
330+
public boolean getSendFlagDecisions() { return sendFlagDecisions; }
331+
325332
@Override
326333
public boolean getAnonymizeIP() {
327334
return anonymizeIP;

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

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

5656
String getRevision();
5757

58+
boolean getSendFlagDecisions();
59+
5860
boolean getAnonymizeIP();
5961

6062
Boolean getBotFiltering();

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/**
22
*
3-
* Copyright 2016-2019, Optimizely and contributors
3+
* Copyright 2016-2020, 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.
@@ -83,9 +83,11 @@ public ProjectConfig deserialize(JsonElement json, Type typeOfT, JsonDeserializa
8383
anonymizeIP = jsonObject.get("anonymizeIP").getAsBoolean();
8484
}
8585

86+
8687
List<FeatureFlag> featureFlags = null;
8788
List<Rollout> rollouts = null;
8889
Boolean botFiltering = null;
90+
boolean sendFlagDecisions = false;
8991
if (datafileVersion >= Integer.parseInt(DatafileProjectConfig.Version.V4.toString())) {
9092
Type featureFlagsType = new TypeToken<List<FeatureFlag>>() {
9193
}.getType();
@@ -95,11 +97,14 @@ public ProjectConfig deserialize(JsonElement json, Type typeOfT, JsonDeserializa
9597
rollouts = context.deserialize(jsonObject.get("rollouts").getAsJsonArray(), rolloutsType);
9698
if (jsonObject.has("botFiltering"))
9799
botFiltering = jsonObject.get("botFiltering").getAsBoolean();
100+
if (jsonObject.has("sendFlagDecisions"))
101+
sendFlagDecisions = jsonObject.get("sendFlagDecisions").getAsBoolean();
98102
}
99103

100104
return new DatafileProjectConfig(
101105
accountId,
102106
anonymizeIP,
107+
sendFlagDecisions,
103108
botFiltering,
104109
projectId,
105110
revision,

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/**
22
*
3-
* Copyright 2016-2019, Optimizely and contributors
3+
* Copyright 2016-2020, 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.
@@ -64,17 +64,22 @@ public DatafileProjectConfig deserialize(JsonParser parser, DeserializationConte
6464
List<FeatureFlag> featureFlags = null;
6565
List<Rollout> rollouts = null;
6666
Boolean botFiltering = null;
67+
boolean sendFlagDecisions = false;
6768
if (datafileVersion >= Integer.parseInt(DatafileProjectConfig.Version.V4.toString())) {
6869
featureFlags = JacksonHelpers.arrayNodeToList(node.get("featureFlags"), FeatureFlag.class, codec);
6970
rollouts = JacksonHelpers.arrayNodeToList(node.get("rollouts"), Rollout.class, codec);
7071
if (node.hasNonNull("botFiltering")) {
7172
botFiltering = node.get("botFiltering").asBoolean();
7273
}
74+
if (node.hasNonNull("sendFlagDecisions")) {
75+
sendFlagDecisions = node.get("sendFlagDecisions").asBoolean();
76+
}
7377
}
7478

7579
return new DatafileProjectConfig(
7680
accountId,
7781
anonymizeIP,
82+
sendFlagDecisions,
7883
botFiltering,
7984
projectId,
8085
revision,

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,16 +73,20 @@ public ProjectConfig parseProjectConfig(@Nonnull String json) throws ConfigParse
7373
List<FeatureFlag> featureFlags = null;
7474
List<Rollout> rollouts = null;
7575
Boolean botFiltering = null;
76+
boolean sendFlagDecisions = false;
7677
if (datafileVersion >= Integer.parseInt(ProjectConfig.Version.V4.toString())) {
7778
featureFlags = parseFeatureFlags(rootObject.getJSONArray("featureFlags"));
7879
rollouts = parseRollouts(rootObject.getJSONArray("rollouts"));
7980
if (rootObject.has("botFiltering"))
8081
botFiltering = rootObject.getBoolean("botFiltering");
82+
if (rootObject.has("sendFlagDecisions"))
83+
sendFlagDecisions = rootObject.getBoolean("sendFlagDecisions");
8184
}
8285

8386
return new DatafileProjectConfig(
8487
accountId,
8588
anonymizeIP,
89+
sendFlagDecisions,
8690
botFiltering,
8791
projectId,
8892
revision,

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,16 +80,20 @@ public ProjectConfig parseProjectConfig(@Nonnull String json) throws ConfigParse
8080
List<FeatureFlag> featureFlags = null;
8181
List<Rollout> rollouts = null;
8282
Boolean botFiltering = null;
83+
boolean sendFlagDecisions = false;
8384
if (datafileVersion >= Integer.parseInt(DatafileProjectConfig.Version.V4.toString())) {
8485
featureFlags = parseFeatureFlags((JSONArray) rootObject.get("featureFlags"));
8586
rollouts = parseRollouts((JSONArray) rootObject.get("rollouts"));
8687
if (rootObject.containsKey("botFiltering"))
8788
botFiltering = (Boolean) rootObject.get("botFiltering");
89+
if (rootObject.containsKey("sendFlagDecisions"))
90+
sendFlagDecisions = (Boolean) rootObject.get("sendFlagDecisions");
8891
}
8992

9093
return new DatafileProjectConfig(
9194
accountId,
9295
anonymizeIP,
96+
sendFlagDecisions,
9397
botFiltering,
9498
projectId,
9599
revision,

core-api/src/main/java/com/optimizely/ab/event/internal/EventFactory.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/**
22
*
3-
* Copyright 2016-2019, Optimizely and contributors
3+
* Copyright 2016-2020, 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.
@@ -99,6 +99,7 @@ private static Visitor createVisitor(ImpressionEvent impressionEvent) {
9999
.setCampaignId(impressionEvent.getLayerId())
100100
.setExperimentId(impressionEvent.getExperimentId())
101101
.setVariationId(impressionEvent.getVariationId())
102+
.setMetadata(impressionEvent.getMetadata())
102103
.setIsCampaignHoldback(false)
103104
.build();
104105

core-api/src/main/java/com/optimizely/ab/event/internal/ImpressionEvent.java

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/**
22
*
3-
* Copyright 2019, Optimizely and contributors
3+
* Copyright 2019-2020, 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,8 @@
1616
*/
1717
package com.optimizely.ab.event.internal;
1818

19+
import com.optimizely.ab.event.internal.payload.DecisionMetadata;
20+
1921
import java.util.StringJoiner;
2022

2123
/**
@@ -28,19 +30,22 @@ public class ImpressionEvent extends BaseEvent implements UserEvent {
2830
private final String experimentKey;
2931
private final String variationKey;
3032
private final String variationId;
33+
private final DecisionMetadata metadata;
3134

3235
private ImpressionEvent(UserContext userContext,
3336
String layerId,
3437
String experimentId,
3538
String experimentKey,
3639
String variationKey,
37-
String variationId) {
40+
String variationId,
41+
DecisionMetadata metadata) {
3842
this.userContext = userContext;
3943
this.layerId = layerId;
4044
this.experimentId = experimentId;
4145
this.experimentKey = experimentKey;
4246
this.variationKey = variationKey;
4347
this.variationId = variationId;
48+
this.metadata = metadata;
4449
}
4550

4651
@Override
@@ -68,6 +73,8 @@ public String getVariationId() {
6873
return variationId;
6974
}
7075

76+
public DecisionMetadata getMetadata() { return metadata; }
77+
7178
public static class Builder {
7279

7380
private UserContext userContext;
@@ -76,6 +83,7 @@ public static class Builder {
7683
private String experimentKey;
7784
private String variationKey;
7885
private String variationId;
86+
private DecisionMetadata metadata;
7987

8088
public Builder withUserContext(UserContext userContext) {
8189
this.userContext = userContext;
@@ -107,8 +115,13 @@ public Builder withVariationId(String variationId) {
107115
return this;
108116
}
109117

118+
public Builder withMetadata(DecisionMetadata metadata) {
119+
this.metadata = metadata;
120+
return this;
121+
}
122+
110123
public ImpressionEvent build() {
111-
return new ImpressionEvent(userContext, layerId, experimentId, experimentKey, variationKey, variationId);
124+
return new ImpressionEvent(userContext, layerId, experimentId, experimentKey, variationKey, variationId, metadata);
112125
}
113126
}
114127

0 commit comments

Comments
 (0)