Skip to content

Commit

Permalink
Version 2.7.0 (#67)
Browse files Browse the repository at this point in the history
### Added
- The new configuration option `setEvaluationReasons(true)` causes LaunchDarkly to report information about how each feature flag value was determined; you can access this information with the new client methods `boolVariationDetail`, `stringVariationDetail`, etc. The new methods return an object that contains both the flag value and a "reason" object which will tell you, for instance, if the user was individually targeted for the flag or was matched by one of the flag's rules, or if the flag returned the default value due to an error. For more information, see the SDK Reference Guide on [evaluation reasons](https://docs.launchdarkly.com/docs/evaluation-reasons).
- The new client method `getVersion()` returns the version string of the SDK.
### Fixed
- Bug causing `boolVariation`, `intVariation`, and `floatVariation` to always return `null` if `fallback` argument was `null`.
- Potential issue where environment versions for flag updates could compare incorrectly due to floating point coercion.
- Summary events for unknown flags (flags evaluated without any stored value, variation, or version) now include the returned value as intended.
- Inaccurate events caused by data for flag version and variation being unsynchronized with flag value.
- Bug causing some events to be dropped from summary counts due to data race in sending and updating summary events.
- Potential `ClassCastException` crash on some devices due to old version of OkHttp.
- Improved documentation comments throughout.
- Crash on migration when no primary mobile key is specified.
### Removed
- CircleCI V1 config file
  • Loading branch information
gwhelanLD committed Apr 2, 2019
1 parent 507f6a6 commit 64bbb2f
Show file tree
Hide file tree
Showing 84 changed files with 4,454 additions and 2,452 deletions.
15 changes: 15 additions & 0 deletions CHANGELOG.md
Expand Up @@ -2,6 +2,21 @@


All notable changes to the LaunchDarkly Android SDK will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org).
## [2.7.0] - 2019-04-02
### Added
- The new configuration option `setEvaluationReasons(true)` causes LaunchDarkly to report information about how each feature flag value was determined; you can access this information with the new client methods `boolVariationDetail`, `stringVariationDetail`, etc. The new methods return an object that contains both the flag value and a "reason" object which will tell you, for instance, if the user was individually targeted for the flag or was matched by one of the flag's rules, or if the flag returned the default value due to an error. For more information, see the SDK Reference Guide on [evaluation reasons](https://docs.launchdarkly.com/docs/evaluation-reasons).
- The new client method `getVersion()` returns the version string of the SDK.
### Fixed
- Bug causing `boolVariation`, `intVariation`, and `floatVariation` to always return `null` if `fallback` argument was `null`.
- Potential issue where environment versions for flag updates could compare incorrectly due to floating point coercion.
- Summary events for unknown flags (flags evaluated without any stored value, variation, or version) now include the returned value as intended.
- Inaccurate events caused by data for flag version and variation being unsynchronized with flag value.
- Bug causing some events to be dropped from summary counts due to data race in sending and updating summary events.
- Potential `ClassCastException` crash on some devices due to old version of OkHttp.
- Improved documentation comments throughout.
- Crash on migration when no primary mobile key is specified.
### Removed
- CircleCI V1 config file
## [2.6.0] - 2019-01-22
### Added
- Support for connecting to multiple environments through LDClient interface.
Expand Down
2 changes: 1 addition & 1 deletion README.md
Expand Up @@ -8,7 +8,7 @@ Check out the included example app, or follow things here:
1. Declare this dependency:

```
compile 'com.launchdarkly:launchdarkly-android-client:2.6.0'
compile 'com.launchdarkly:launchdarkly-android-client:2.7.0'
```
1. In your application configure and initialize the client:

Expand Down
36 changes: 0 additions & 36 deletions circle.yml

This file was deleted.

17 changes: 14 additions & 3 deletions launchdarkly-android-client/build.gradle
Expand Up @@ -7,7 +7,7 @@ apply plugin: 'io.codearte.nexus-staging'

allprojects {
group = 'com.launchdarkly'
version = '2.6.0'
version = '2.7.0'
sourceCompatibility = 1.7
targetCompatibility = 1.7
}
Expand All @@ -29,7 +29,17 @@ android {
versionName version
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
consumerProguardFiles 'consumer-proguard-rules.pro'

// The following argument makes the Android Test Orchestrator run its
// "pm clear" command after each test invocation. This command ensures
// that the app's state is completely cleared between tests.
testInstrumentationRunnerArguments clearPackageData: 'true'
}

testOptions {
execution 'ANDROID_TEST_ORCHESTRATOR'
}

lintOptions {
// TODO: fix things and set this to true
abortOnError false
Expand Down Expand Up @@ -57,10 +67,10 @@ android {

ext {
supportVersion = "26.0.1"
okhttpVersion = "3.6.0"
okhttpVersion = "3.9.1"
eventsourceVersion = "1.8.0"
gsonVersion = "2.8.2"
testRunnerVersion = "0.5"
testRunnerVersion = "1.0.2"
}

dependencies {
Expand All @@ -80,6 +90,7 @@ dependencies {
implementation "com.android.support:support-annotations:$supportVersion"
androidTestImplementation "com.android.support.test:runner:$testRunnerVersion"
androidTestImplementation "com.android.support.test:rules:$testRunnerVersion"
androidTestUtil "com.android.support.test:orchestrator:$testRunnerVersion"
androidTestImplementation 'org.hamcrest:hamcrest-library:1.3'
androidTestImplementation 'org.easymock:easymock:3.4'
androidTestImplementation 'junit:junit:4.12'
Expand Down
@@ -0,0 +1,54 @@
package com.launchdarkly.android;

import android.support.test.runner.AndroidJUnit4;

import com.google.gson.Gson;
import com.launchdarkly.android.flagstore.Flag;
import com.launchdarkly.android.flagstore.FlagBuilder;
import com.launchdarkly.android.gson.GsonCache;
import com.launchdarkly.android.response.DeleteFlagResponse;

import org.junit.Test;
import org.junit.runner.RunWith;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;

@RunWith(AndroidJUnit4.class)
public class DeleteFlagResponseTest {

private static final Gson gson = GsonCache.getGson();

@Test
public void deleteFlagResponseKeyIsDeserialized() {
final String jsonStr = "{\"key\": \"flag\"}";
final DeleteFlagResponse delete = gson.fromJson(jsonStr, DeleteFlagResponse.class);
assertEquals("flag", delete.flagToUpdate());
}

@Test
public void testUpdateFlag() {
// Create delete flag responses from json to verify version is deserialized
final String jsonNoVersion = "{\"key\": \"flag\"}";
final String jsonLowVersion = "{\"key\": \"flag\", \"version\": 50}";
final String jsonHighVersion = "{\"key\": \"flag\", \"version\": 100}";
final DeleteFlagResponse deleteNoVersion = gson.fromJson(jsonNoVersion, DeleteFlagResponse.class);
final DeleteFlagResponse deleteLowVersion = gson.fromJson(jsonLowVersion, DeleteFlagResponse.class);
final DeleteFlagResponse deleteHighVersion = gson.fromJson(jsonHighVersion, DeleteFlagResponse.class);
final Flag flagNoVersion = new FlagBuilder("flag").build();
final Flag flagLowVersion = new FlagBuilder("flag").version(50).build();
final Flag flagHighVersion = new FlagBuilder("flag").version(100).build();

assertNull(deleteNoVersion.updateFlag(null));
assertNull(deleteNoVersion.updateFlag(flagNoVersion));
assertNull(deleteNoVersion.updateFlag(flagLowVersion));
assertNull(deleteNoVersion.updateFlag(flagHighVersion));
assertNull(deleteLowVersion.updateFlag(null));
assertNull(deleteLowVersion.updateFlag(flagNoVersion));
assertEquals(flagLowVersion, deleteLowVersion.updateFlag(flagLowVersion));
assertEquals(flagHighVersion, deleteLowVersion.updateFlag(flagHighVersion));
assertNull(deleteHighVersion.updateFlag(null));
assertNull(deleteHighVersion.updateFlag(flagNoVersion));
assertNull(deleteHighVersion.updateFlag(flagLowVersion));
}
}
@@ -0,0 +1,66 @@
package com.launchdarkly.android;

import com.google.gson.Gson;
import com.google.gson.JsonElement;

import org.junit.Test;

import static org.junit.Assert.assertEquals;

public class EvaluationReasonTest {
private static final Gson gson = new LDConfig.Builder().build().getFilteredEventGson();

@Test
public void testOffReasonSerialization() {
EvaluationReason reason = EvaluationReason.off();
String json = "{\"kind\":\"OFF\"}";
assertJsonEqual(json, gson.toJson(reason));
assertEquals("OFF", reason.toString());
}

@Test
public void testFallthroughSerialization() {
EvaluationReason reason = EvaluationReason.fallthrough();
String json = "{\"kind\":\"FALLTHROUGH\"}";
assertJsonEqual(json, gson.toJson(reason));
assertEquals("FALLTHROUGH", reason.toString());
}

@Test
public void testTargetMatchSerialization() {
EvaluationReason reason = EvaluationReason.targetMatch();
String json = "{\"kind\":\"TARGET_MATCH\"}";
assertJsonEqual(json, gson.toJson(reason));
assertEquals("TARGET_MATCH", reason.toString());
}

@Test
public void testRuleMatchSerialization() {
EvaluationReason reason = EvaluationReason.ruleMatch(1, "id");
String json = "{\"kind\":\"RULE_MATCH\",\"ruleIndex\":1,\"ruleId\":\"id\"}";
assertJsonEqual(json, gson.toJson(reason));
assertEquals("RULE_MATCH(1,id)", reason.toString());
}

@Test
public void testPrerequisiteFailedSerialization() {
EvaluationReason reason = EvaluationReason.prerequisiteFailed("key");
String json = "{\"kind\":\"PREREQUISITE_FAILED\",\"prerequisiteKey\":\"key\"}";
assertJsonEqual(json, gson.toJson(reason));
assertEquals("PREREQUISITE_FAILED(key)", reason.toString());
}

@Test
public void testErrorSerialization() {
EvaluationReason reason = EvaluationReason.error(EvaluationReason.ErrorKind.EXCEPTION);
String json = "{\"kind\":\"ERROR\",\"errorKind\":\"EXCEPTION\"}";
assertJsonEqual(json, gson.toJson(reason));
assertEquals("ERROR(EXCEPTION)", reason.toString());
}

private void assertJsonEqual(String expectedString, String actualString) {
JsonElement expected = gson.fromJson(expectedString, JsonElement.class);
JsonElement actual = gson.fromJson(actualString, JsonElement.class);
assertEquals(expected, actual);
}
}
Expand Up @@ -192,7 +192,7 @@ public void testUserObjectRemovedFromFeatureEvent() {

LDUser user = builder.build();

final FeatureRequestEvent event = new FeatureRequestEvent("key1", user.getKeyAsString(), JsonNull.INSTANCE, JsonNull.INSTANCE, -1, -1);
final FeatureRequestEvent event = new FeatureRequestEvent("key1", user.getKeyAsString(), JsonNull.INSTANCE, JsonNull.INSTANCE, -1, -1, null);

Assert.assertNull(event.user);
Assert.assertEquals(user.getKeyAsString(), event.userKey);
Expand All @@ -205,7 +205,7 @@ public void testFullUserObjectIncludedInFeatureEvent() {

LDUser user = builder.build();

final FeatureRequestEvent event = new FeatureRequestEvent("key1", user, JsonNull.INSTANCE, JsonNull.INSTANCE, -1, -1);
final FeatureRequestEvent event = new FeatureRequestEvent("key1", user, JsonNull.INSTANCE, JsonNull.INSTANCE, -1, -1, null);

Assert.assertEquals(user, event.user);
Assert.assertNull(event.userKey);
Expand Down Expand Up @@ -244,16 +244,39 @@ public void testOptionalFieldsAreExcludedAppropriately() {

LDUser user = builder.build();

final FeatureRequestEvent hasVersionEvent = new FeatureRequestEvent("key1", user, JsonNull.INSTANCE, JsonNull.INSTANCE, 5, -1);
final FeatureRequestEvent hasVariationEvent = new FeatureRequestEvent("key1", user, JsonNull.INSTANCE, JsonNull.INSTANCE, -1, 20);
final EvaluationReason reason = EvaluationReason.fallthrough();

final FeatureRequestEvent hasVersionEvent = new FeatureRequestEvent("key1", user, JsonNull.INSTANCE, JsonNull.INSTANCE, 5, null, null);
final FeatureRequestEvent hasVariationEvent = new FeatureRequestEvent("key1", user, JsonNull.INSTANCE, JsonNull.INSTANCE, -1, 20, null);
final FeatureRequestEvent hasReasonEvent = new FeatureRequestEvent("key1", user, JsonNull.INSTANCE, JsonNull.INSTANCE, 5, 20, reason);

Assert.assertEquals(5, hasVersionEvent.version, 0.0f);
Assert.assertNull(hasVersionEvent.variation);
Assert.assertNull(hasVersionEvent.reason);

Assert.assertEquals(20, hasVariationEvent.variation, 0);
Assert.assertNull(hasVariationEvent.version);
Assert.assertNull(hasVariationEvent.reason);

Assert.assertEquals(5, hasReasonEvent.version, 0);
Assert.assertEquals(20, hasReasonEvent.variation, 0);
Assert.assertEquals(reason, hasReasonEvent.reason);
}

@Test
public void reasonIsSerialized() {
LDUser.Builder builder = new LDUser.Builder("1")
.email("email@server.net");
LDUser user = builder.build();
final EvaluationReason reason = EvaluationReason.fallthrough();

final FeatureRequestEvent hasReasonEvent = new FeatureRequestEvent("key1", user, JsonNull.INSTANCE, JsonNull.INSTANCE, 5, 20, reason);

LDConfig config = new LDConfig.Builder()
.build();
JsonElement jsonElement = config.getFilteredEventGson().toJsonTree(hasReasonEvent);

JsonElement expected = config.getFilteredEventGson().fromJson("{\"kind\":\"FALLTHROUGH\"}", JsonElement.class);
Assert.assertEquals(expected, jsonElement.getAsJsonObject().get("reason"));
}
}
Expand Up @@ -12,6 +12,7 @@
import org.junit.Test;
import org.junit.runner.RunWith;

import java.io.IOException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;

Expand Down Expand Up @@ -46,7 +47,7 @@ public void setUp() {
@UiThreadTest
// Not testing UI things, but we need to simulate the UI so the Foreground class is happy.
@Test
public void TestOfflineClientReturnsFallbacks() {
public void testOfflineClientReturnsFallbacks() {
ldClient = LDClient.init(activityTestRule.getActivity().getApplication(), ldConfig, ldUser, 1);
ldClient.clearSummaryEventSharedPreferences();

Expand All @@ -68,7 +69,7 @@ public void TestOfflineClientReturnsFallbacks() {
@UiThreadTest
// Not testing UI things, but we need to simulate the UI so the Foreground class is happy.
@Test
public void GivenFallbacksAreNullAndTestOfflineClientReturnsFallbacks() {
public void givenFallbacksAreNullAndTestOfflineClientReturnsFallbacks() {
ldClient = LDClient.init(activityTestRule.getActivity().getApplication(), ldConfig, ldUser, 1);
ldClient.clearSummaryEventSharedPreferences();

Expand All @@ -86,10 +87,11 @@ public void GivenFallbacksAreNullAndTestOfflineClientReturnsFallbacks() {

@UiThreadTest
@Test
public void TestInitMissingApplication() {
public void testInitMissingApplication() {
ExecutionException actualFutureException = null;
LaunchDarklyException actualProvidedException = null;

//noinspection ConstantConditions
ldClientFuture = LDClient.init(null, ldConfig, ldUser);

try {
Expand All @@ -108,10 +110,11 @@ public void TestInitMissingApplication() {

@UiThreadTest
@Test
public void TestInitMissingConfig() {
public void testInitMissingConfig() {
ExecutionException actualFutureException = null;
LaunchDarklyException actualProvidedException = null;

//noinspection ConstantConditions
ldClientFuture = LDClient.init(activityTestRule.getActivity().getApplication(), null, ldUser);

try {
Expand All @@ -130,10 +133,11 @@ public void TestInitMissingConfig() {

@UiThreadTest
@Test
public void TestInitMissingUser() {
public void testInitMissingUser() {
ExecutionException actualFutureException = null;
LaunchDarklyException actualProvidedException = null;

//noinspection ConstantConditions
ldClientFuture = LDClient.init(activityTestRule.getActivity().getApplication(), ldConfig, null);

try {
Expand All @@ -149,4 +153,12 @@ public void TestInitMissingUser() {
assertThat(actualProvidedException, instanceOf(LaunchDarklyException.class));
assertTrue("No future task to run", ldClientFuture.isDone());
}

@UiThreadTest
@Test
public void testDoubleClose() throws IOException {
ldClient = LDClient.init(activityTestRule.getActivity().getApplication(), ldConfig, ldUser, 1);
ldClient.close();
ldClient.close();
}
}

0 comments on commit 64bbb2f

Please sign in to comment.