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: Adds support for client-side prerequisite events #279

Merged
merged 3 commits into from
Oct 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ public class TestService extends NanoHTTPD {
"tags",
"auto-env-attributes",
"inline-context",
"anonymous-redaction"
"anonymous-redaction",
"client-prereq-events"
};
private static final String MIME_JSON = "application/json";
static final Gson gson = new GsonBuilder()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,48 @@ public void variationFlagTrackReasonGeneratesEventWithReason() throws IOExceptio
}
}

@Test
public void flagEvaluationWithPrereqProducesPrereqEvents() throws IOException, InterruptedException {
try (MockWebServer mockEventsServer = new MockWebServer()) {
mockEventsServer.start();
// Enqueue a successful empty response
mockEventsServer.enqueue(new MockResponse());

// Setup flag store with test flag
Flag flagA = new FlagBuilder("flagA").version(1)
.variation(1).value(LDValue.of(true)).reason(EvaluationReason.targetMatch()).build();
Flag flagAB = new FlagBuilder("flagAB").prerequisites(new String[]{"flagA"}).version(1)
.variation(1).value(LDValue.of(true)).reason(EvaluationReason.targetMatch()).build();
Flag flagAC = new FlagBuilder("flagAC").prerequisites(new String[]{"flagA"}).version(1)
.variation(1).value(LDValue.of(true)).reason(EvaluationReason.targetMatch()).build();
Flag flagABD = new FlagBuilder("flagABD").prerequisites(new String[]{"flagAB"}).version(1)
.variation(1).value(LDValue.of(true)).reason(EvaluationReason.targetMatch()).build();
PersistentDataStore store = new InMemoryPersistentDataStore();
TestUtil.writeFlagUpdateToStore(store, mobileKey, ldContext, flagA);
TestUtil.writeFlagUpdateToStore(store, mobileKey, ldContext, flagAB);
TestUtil.writeFlagUpdateToStore(store, mobileKey, ldContext, flagAC);
TestUtil.writeFlagUpdateToStore(store, mobileKey, ldContext, flagABD);
LDConfig ldConfig = baseConfigBuilder(mockEventsServer)
.persistentDataStore(store).build();

try (LDClient client = LDClient.init(application, ldConfig, ldContext, 0)) {
assertTrue(client.boolVariation("flagA", false));
assertTrue(client.boolVariation("flagAB", false));
assertTrue(client.boolVariation("flagAC", false));
assertTrue(client.boolVariation("flagABD", false));
client.blockingFlush();

LDValue[] events = getEventsFromLastRequest(mockEventsServer, 2);
LDValue summaryEvent = events[1];
assertSummaryEvent(summaryEvent);
assertEquals(LDValue.of(4), summaryEvent.get("features").get("flagA").get("counters").get(0).get("count"));
tanderson-ld marked this conversation as resolved.
Show resolved Hide resolved
assertEquals(LDValue.of(2), summaryEvent.get("features").get("flagAB").get("counters").get(0).get("count"));
assertEquals(LDValue.of(1), summaryEvent.get("features").get("flagAC").get("counters").get(0).get("count"));
assertEquals(LDValue.of(1), summaryEvent.get("features").get("flagABD").get("counters").get(0).get("count"));
}
}
}

@Test
public void additionalHeadersIncludedInEventsRequest() throws IOException, InterruptedException {
try (MockWebServer mockEventsServer = new MockWebServer()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ public static final class Flag {
private final Boolean trackEvents;
private final Boolean trackReason;
private final Long debugEventsUntilDate;
private final String[] prerequisites;
private final Boolean deleted;

private Flag(
Expand All @@ -51,6 +52,7 @@ private Flag(
boolean trackEvents,
boolean trackReason,
Long debugEventsUntilDate,
String[] prerequisites,
boolean deleted
) {
this.key = key;
Expand All @@ -62,6 +64,7 @@ private Flag(
this.trackEvents = trackEvents ? Boolean.TRUE : null;
this.trackReason = trackReason ? Boolean.TRUE : null;
this.debugEventsUntilDate = debugEventsUntilDate;
this.prerequisites = prerequisites;
this.deleted = deleted ? Boolean.TRUE : null;
}

Expand All @@ -76,6 +79,7 @@ private Flag(
* @param trackReason true if events must include evaluation reasons
* @param debugEventsUntilDate non-null if debugging is enabled
* @param reason evaluation reason of the result, or null if not available
* @param prerequisites flag keys of prerequisites
*/
public Flag(
@NonNull String key,
Expand All @@ -86,9 +90,10 @@ public Flag(
boolean trackEvents,
boolean trackReason,
@Nullable Long debugEventsUntilDate,
@Nullable EvaluationReason reason
@Nullable EvaluationReason reason,
@Nullable String[] prerequisites
) {
this(key, value, version, flagVersion, variation, reason, trackEvents, trackReason, debugEventsUntilDate, false);
this(key, value, version, flagVersion, variation, reason, trackEvents, trackReason, debugEventsUntilDate, prerequisites, false);
}

/**
Expand All @@ -97,7 +102,7 @@ public Flag(
* @return a placeholder {@link Flag} to represent a deleted flag
*/
public static Flag deletedItemPlaceholder(@NonNull String key, int version) {
return new Flag(key, null, version, null, null, null, false, false, null, true);
return new Flag(key, null, version, null, null, null, false, false, null, null, true);
}

String getKey() {
Expand All @@ -122,6 +127,7 @@ Integer getVariation() {
return variation;
}

@Nullable
EvaluationReason getReason() {
return reason;
}
Expand All @@ -132,6 +138,7 @@ boolean isTrackEvents() {

boolean isTrackReason() { return trackReason != null && trackReason.booleanValue(); }

@Nullable
Long getDebugEventsUntilDate() {
return debugEventsUntilDate;
}
Expand All @@ -140,6 +147,11 @@ int getVersionForEvents() {
return flagVersion == null ? version : flagVersion.intValue();
}

@Nullable
String[] getPrerequisites() {
return prerequisites;
}

boolean isDeleted() {
return deleted != null && deleted.booleanValue();
}
Expand All @@ -161,6 +173,7 @@ public boolean equals(Object other) {
trackEvents == o.trackEvents &&
trackReason == o.trackReason &&
Objects.equals(debugEventsUntilDate, o.debugEventsUntilDate) &&
Objects.equals(prerequisites, o.prerequisites) &&
deleted == o.deleted;
}
return false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ public static EnvironmentData fromJson(String json) throws SerializationExceptio
if (f.getKey() == null) {
f = new Flag(e.getKey(), f.getValue(), f.getVersion(), f.getFlagVersion(),
f.getVariation(), f.isTrackEvents(), f.isTrackReason(), f.getDebugEventsUntilDate(),
f.getReason());
f.getReason(), f.getPrerequisites());
dataMap.put(e.getKey(), f);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -518,6 +518,7 @@ private <T> EvaluationDetail<T> convertDetailType(EvaluationDetail<LDValue> deta
return EvaluationDetail.fromValue(converter.toType(detail.getValue()), detail.getVariationIndex(), detail.getReason());
}

// TODO: when implementing hooks support in the future, verify prerequisite evaluations do not trigger the evaluation hooks
private EvaluationDetail<LDValue> variationDetailInternal(@NonNull String key, @NonNull LDValue defaultValue, boolean checkType, boolean needsReason) {
LDContext context = clientContextImpl.getEvaluationContext();
Flag flag = contextDataManager.getNonDeletedFlag(key); // returns null for nonexistent *or* deleted flag
Expand All @@ -530,6 +531,13 @@ private EvaluationDetail<LDValue> variationDetailInternal(@NonNull String key, @
null, defaultValue, false, null);
result = EvaluationDetail.fromValue(defaultValue, EvaluationDetail.NO_VARIATION, EvaluationReason.error(EvaluationReason.ErrorKind.FLAG_NOT_FOUND));
} else {
if (flag.getPrerequisites() != null) {
// recurse on prerequisites to emulate prereq evaluations occurring with desirable side effects such as events for prereqs
for (String prereqKey : flag.getPrerequisites()) {
variationDetailInternal(prereqKey, LDValue.ofNull(), false, false);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

For reviewers: Is LDValue.ofNull() an appropriate default value for prereq evals?

tanderson-ld marked this conversation as resolved.
Show resolved Hide resolved
}
}

LDValue value = flag.getValue();
int variation = flag.getVariation() == null ? EvaluationDetail.NO_VARIATION : flag.getVariation();
if (value.isNull()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -509,7 +509,7 @@ Flag createFlag(int version, LDContext context) {
EvaluationReason reason = targetedVariation == null ? EvaluationReason.fallthrough() :
EvaluationReason.targetMatch();
return new Flag(key, value, version, null, variation,
false, false, null, reason);
false, false, null, reason, null);
}

private static int variationForBoolean(boolean value) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
import java.io.IOException;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
Expand Down Expand Up @@ -568,24 +569,23 @@ public void refreshDataSourceWhileInBackgroundWithBackgroundPollingDisabled() {
@Test
public void notifyListenersWhenStatusChanges() throws Exception {
createTestManager(false, false, makeSuccessfulDataSourceFactory());

awaitStartUp();

LDStatusListener mockListener = mock(LDStatusListener.class);
// expected initial connection
// expected initial connection mode
mockListener.onConnectionModeChanged(anyObject(ConnectionInformation.class));
// expected second connection after identify
// expected second connection mode after identify
mockListener.onConnectionModeChanged(anyObject(ConnectionInformation.class));
expectLastCall();
replayAll();

AwaitableCallback<Void> identifyListenersCalled = new AwaitableCallback<>();
CountDownLatch latch = new CountDownLatch(2);
tanderson-ld marked this conversation as resolved.
Show resolved Hide resolved
connectivityManager.registerStatusListener(mockListener);
connectivityManager.registerStatusListener(new LDStatusListener() {
@Override
public void onConnectionModeChanged(ConnectionInformation connectionInformation) {
// since the callback system is on another thread, need to use awaitable callback
identifyListenersCalled.onSuccess(null);
latch.countDown();
}

@Override
Expand All @@ -597,7 +597,7 @@ public void onInternalFailure(LDFailure ldFailure) {
LDContext context2 = LDContext.create("context2");
contextDataManager.switchToContext(context2);
connectivityManager.switchToContext(context2, new AwaitableCallback<>());
identifyListenersCalled.await();
latch.await(500, TimeUnit.MILLISECONDS);

verifyAll();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import static com.launchdarkly.sdk.android.AssertHelpers.assertJsonEqual;
import static org.hamcrest.CoreMatchers.hasItems;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
Expand Down Expand Up @@ -54,14 +55,15 @@ public void toJson() {
.trackEvents(true)
.trackReason(true)
.debugEventsUntilDate(1000L)
.prerequisites(new String[]{"flagA", "flagB"})
.build();
Flag flag2 = new FlagBuilder("flag2").version(200).value(false).build();
EnvironmentData data = new DataSetBuilder().add(flag1).add(flag2).build();
String json = data.toJson();

String expectedJson = "{" +
"\"flag1\":{\"key\":\"flag1\",\"version\":100,\"flagVersion\":222,\"value\":true," +
"\"variation\":1,\"reason\":{\"kind\":\"OFF\"},\"trackEvents\":true," +
"\"variation\":1,\"prerequisites\":[\"flagA\",\"flagB\"],\"reason\":{\"kind\":\"OFF\"},\"trackEvents\":true," +
"\"trackReason\":true,\"debugEventsUntilDate\":1000}," +
"\"flag2\":{\"key\":\"flag2\",\"version\":200,\"value\":false}" +
"}";
Expand All @@ -72,7 +74,7 @@ public void toJson() {
public void fromJson() throws Exception {
String json = "{" +
"\"flag1\":{\"key\":\"flag1\",\"version\":100,\"flagVersion\":222,\"value\":true," +
"\"variation\":1,\"reason\":{\"kind\":\"OFF\"},\"trackEvents\":true," +
"\"variation\":1,\"prerequisites\":[\"flagA\",\"flagB\"],\"reason\":{\"kind\":\"OFF\"},\"trackEvents\":true," +
"\"trackReason\":true,\"debugEventsUntilDate\":1000}," +
"\"flag2\":{\"key\":\"flag2\",\"version\":200,\"value\":false}" +
"}";
Expand All @@ -87,6 +89,7 @@ public void fromJson() throws Exception {
assertEquals(Integer.valueOf(222), flag1.getFlagVersion());
assertEquals(LDValue.of(true), flag1.getValue());
assertEquals(Integer.valueOf(1), flag1.getVariation());
assertArrayEquals(new String[]{"flagA", "flagB"}, flag1.getPrerequisites());
assertTrue(flag1.isTrackEvents());
assertTrue(flag1.isTrackReason());
assertEquals(Long.valueOf(1000), flag1.getDebugEventsUntilDate());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import static com.launchdarkly.sdk.internal.GsonHelpers.gsonInstance;

import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
Expand All @@ -20,6 +21,7 @@
import java.util.List;
import java.util.Map;

import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
Expand Down Expand Up @@ -206,6 +208,23 @@ public void trackReasonDefaultWhenOmitted() {
assertFalse(r.isTrackReason());
}

@Test
public void prerequisitesIsSerialized() {
final Flag r = new FlagBuilder("flag").prerequisites(new String[]{"flagB", "flagC"}).build();
final JsonObject json = gson.toJsonTree(r).getAsJsonObject();
final JsonArray array = json.getAsJsonArray("prerequisites");
assertEquals(2, array.size());
assertEquals("flagB", array.get(0).getAsString());
assertEquals("flagC", array.get(1).getAsString());
}

@Test
public void prerequisitesIsDeserialized() {
final String jsonStr = "{\"version\": 99, \"prerequisites\": [\"flagA\",\"flagB\"]}";
final Flag r = gson.fromJson(jsonStr, Flag.class);
assertArrayEquals(new String[]{"flagA","flagB"}, r.getPrerequisites());
}

@Test
public void debugEventsUntilDateIsSerialized() {
final long date = 12345L;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ public class PersistentDataStoreWrapperTest extends EasyMockSupport {
private static final String EXPECTED_INDEX_KEY = "index";
private static final String EXPECTED_GENERATED_CONTEXT_KEY_PREFIX = "anonKey_";
private static final Flag FLAG = new Flag("flagkey", LDValue.of(true), 1,
null, 0, false, false, null, null);
null, 0, false, false, null, null, null);

private final PersistentDataStore mockPersistentStore;
private final PersistentDataStoreWrapper wrapper;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public DataSetBuilder add(Flag flag) {

public DataSetBuilder add(String flagKey, int version, LDValue value, int variation) {
return add(new Flag(flagKey, value, version, null, variation,
false, false, null, null));
false, false, null, null, null));
}

public DataSetBuilder add(String flagKey, LDValue value, int variation) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ public final class FlagBuilder {
private boolean trackReason = false;
private Long debugEventsUntilDate = null;
private EvaluationReason reason = null;
private String[] prerequisites = null;

public FlagBuilder(@NonNull String key) {
this.key = key;
Expand Down Expand Up @@ -66,7 +67,12 @@ public FlagBuilder reason(EvaluationReason reason) {
return this;
}

public FlagBuilder prerequisites(String[] prerequisites) {
this.prerequisites = prerequisites;
return this;
}

public Flag build() {
return new Flag(key, value, version, flagVersion, variation, trackEvents, trackReason, debugEventsUntilDate, reason);
return new Flag(key, value, version, flagVersion, variation, trackEvents, trackReason, debugEventsUntilDate, reason, prerequisites);
}
}