From 1aa5822556f5eff98cdb087420100c68fdb7fbb4 Mon Sep 17 00:00:00 2001 From: Ember Stevens Date: Wed, 7 Jun 2023 21:34:24 -0700 Subject: [PATCH 01/26] Updates daily flag count --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 33f60035..dffecaeb 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ ## LaunchDarkly overview -[LaunchDarkly](https://www.launchdarkly.com) is a feature management platform that serves over 100 billion feature flags daily to help teams build better software, faster. [Get started](https://docs.launchdarkly.com/home/getting-started) using LaunchDarkly today! +[LaunchDarkly](https://www.launchdarkly.com) is a feature management platform that serves trillions of feature flags daily to help teams build better software, faster. [Get started](https://docs.launchdarkly.com/home/getting-started) using LaunchDarkly today! [![Twitter Follow](https://img.shields.io/twitter/follow/launchdarkly.svg?style=social&label=Follow&maxAge=2592000)](https://twitter.com/intent/follow?screen_name=launchdarkly) @@ -18,7 +18,7 @@ Refer to the [SDK documentation](https://docs.launchdarkly.com/sdk/client-side/a ## Learn more -Check out our [documentation](https://docs.launchdarkly.com) for in-depth instructions on configuring and using LaunchDarkly. You can also head straight to the [complete reference guide for this SDK](https://docs.launchdarkly.com/sdk/client-side/android) or our [Javadocs](http://launchdarkly.github.io/android-client-sdk/). +Read our [documentation](https://docs.launchdarkly.com) for in-depth instructions on configuring and using LaunchDarkly. You can also head straight to the [complete reference guide for this SDK](https://docs.launchdarkly.com/sdk/client-side/android) or our [Javadocs](http://launchdarkly.github.io/android-client-sdk/). ## Testing From eb7d510969e131053931a9c8efd85e5f07ceed38 Mon Sep 17 00:00:00 2001 From: Ember Stevens Date: Wed, 7 Jun 2023 22:06:09 -0700 Subject: [PATCH 02/26] Removes check out --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index dffecaeb..affc2ff7 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ We encourage pull requests and other contributions from the community. Check out * Gradually roll out a feature to an increasing percentage of users, and track the effect that the feature has on key metrics (for instance, how likely is a user to complete a purchase if they have feature A versus feature B?). * Turn off a feature that you realize is causing performance problems in production, without needing to re-deploy, or even restart the application with a changed configuration file. * Grant access to certain features based on user attributes, like payment plan (eg: users on the ‘gold’ plan get access to more features than users in the ‘silver’ plan). Disable parts of your application to facilitate maintenance, without taking everything offline. -* LaunchDarkly provides feature flag SDKs for a wide variety of languages and technologies. Check out [our documentation](https://docs.launchdarkly.com/sdk) for a complete list. +* LaunchDarkly provides feature flag SDKs for a wide variety of languages and technologies. Read [our documentation](https://docs.launchdarkly.com/sdk) for a complete list. * Explore LaunchDarkly * [launchdarkly.com](https://www.launchdarkly.com/ "LaunchDarkly Main Website") for more information * [docs.launchdarkly.com](https://docs.launchdarkly.com/ "LaunchDarkly Documentation") for our documentation and SDK reference guides From 07d822a7edbf3ed9bb36c1f38e79ac2c2c0f943a Mon Sep 17 00:00:00 2001 From: Todd Anderson Date: Wed, 14 Jun 2023 15:59:04 -0500 Subject: [PATCH 03/26] Contexts autopopulated with application and device information without collision handling nor testing. --- .../android/AnonymousKeyContextModifier.java | 53 +++++++ .../sdk/android/ClientContextImpl.java | 5 +- .../sdk/android/ComponentsImpl.java | 11 +- .../sdk/android/ContextDecorator.java | 94 ------------- .../sdk/android/EnvironmentReporter.java | 130 ++++++++++++++++++ .../sdk/android/IContextModifier.java | 14 ++ .../launchdarkly/sdk/android/LDClient.java | 39 ++++-- .../launchdarkly/sdk/android/LDConfig.java | 5 +- .../com/launchdarkly/sdk/android/LDUtil.java | 1 + .../android/PersistentDataStoreWrapper.java | 71 ++++++++-- .../sdk/android/TelemetryContextModifier.java | 93 +++++++++++++ .../integrations/ApplicationInfoBuilder.java | 20 ++- .../android/subsystems/ApplicationInfo.java | 15 +- .../sdk/android/subsystems/ClientContext.java | 45 ++++-- ...a => AnonymousKeyContextModifierTest.java} | 46 +++---- 15 files changed, 471 insertions(+), 171 deletions(-) create mode 100644 launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/AnonymousKeyContextModifier.java delete mode 100644 launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/ContextDecorator.java create mode 100644 launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/EnvironmentReporter.java create mode 100644 launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/IContextModifier.java create mode 100644 launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/TelemetryContextModifier.java rename launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/{ContextDecoratorTest.java => AnonymousKeyContextModifierTest.java} (78%) diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/AnonymousKeyContextModifier.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/AnonymousKeyContextModifier.java new file mode 100644 index 00000000..7a3f62ad --- /dev/null +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/AnonymousKeyContextModifier.java @@ -0,0 +1,53 @@ +package com.launchdarkly.sdk.android; + +import androidx.annotation.NonNull; + +import com.launchdarkly.logging.LDLogger; +import com.launchdarkly.sdk.ContextMultiBuilder; +import com.launchdarkly.sdk.LDContext; + +final class AnonymousKeyContextModifier { + + @NonNull private final PersistentDataStoreWrapper persistentData; + private final boolean generateAnonymousKeys; + + public AnonymousKeyContextModifier( + @NonNull PersistentDataStoreWrapper persistentData, + boolean generateAnonymousKeys + ) { + this.persistentData = persistentData; + this.generateAnonymousKeys = generateAnonymousKeys; + } + + public LDContext modifyContext(LDContext context, LDLogger logger) { + if (!generateAnonymousKeys) { + return context; + } + if (context.isMultiple()) { + boolean hasAnyAnon = false; + for (int i = 0; i < context.getIndividualContextCount(); i++) { + if (context.getIndividualContext(i).isAnonymous()) { + hasAnyAnon = true; + break; + } + } + if (hasAnyAnon) { + ContextMultiBuilder builder = LDContext.multiBuilder(); + for (int i = 0; i < context.getIndividualContextCount(); i++) { + LDContext c = context.getIndividualContext(i); + builder.add(c.isAnonymous() ? singleKindContextWithGeneratedKey(c) : c); + } + return builder.build(); + } + } else if (context.isAnonymous()) { + return singleKindContextWithGeneratedKey(context); + } + return context; + } + + private LDContext singleKindContextWithGeneratedKey(LDContext context) { + return LDContext.builderFromContext(context) + .key(persistentData.getOrGenerateContextKey(context.getKind())) + .build(); + } +} diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/ClientContextImpl.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/ClientContextImpl.java index 80678cc2..334b5979 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/ClientContextImpl.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/ClientContextImpl.java @@ -54,16 +54,18 @@ static ClientContextImpl fromConfig( LDContext initialContext, LDLogger logger, PlatformState platformState, + EnvironmentReporter environmentReporter, TaskExecutor taskExecutor ) { boolean initiallyInBackground = platformState != null && !platformState.isForeground(); - ClientContext minimalContext = new ClientContext(mobileKey, config.applicationInfo, logger, config, + ClientContext minimalContext = new ClientContext(mobileKey, environmentReporter, logger, config, null, environmentName, config.isEvaluationReasons(), initialContext, null, initiallyInBackground, null, config.serviceEndpoints, config.isOffline()); HttpConfiguration httpConfig = config.http.build(minimalContext); ClientContext baseClientContext = new ClientContext( mobileKey, config.applicationInfo, + environmentReporter, logger, config, null, @@ -102,6 +104,7 @@ public static ClientContextImpl forDataSource( new ClientContext( baseClientContext.getMobileKey(), baseClientContext.getApplicationInfo(), + baseClientContext.getEnvironmentReporter(), baseClientContext.getBaseLogger(), baseClientContext.getConfig(), dataSourceUpdateSink, diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/ComponentsImpl.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/ComponentsImpl.java index 92df3580..91461aef 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/ComponentsImpl.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/ComponentsImpl.java @@ -211,12 +211,11 @@ public HttpConfiguration build(ClientContext clientContext) { Map headers = new HashMap<>(); headers.put("Authorization", LDUtil.AUTH_SCHEME + clientContext.getMobileKey()); headers.put("User-Agent", LDUtil.USER_AGENT_HEADER_VALUE); - if (clientContext.getApplicationInfo() != null) { - String tagHeader = LDUtil.applicationTagHeader(clientContext.getApplicationInfo(), - clientContext.getBaseLogger()); - if (!tagHeader.isEmpty()) { - headers.put("X-LaunchDarkly-Tags", tagHeader); - } + clientContext.getEnvironmentReporter().getApplicationInfo(); + String tagHeader = LDUtil.applicationTagHeader(clientContext.getEnvironmentReporter().getApplicationInfo(), + clientContext.getBaseLogger()); + if (!tagHeader.isEmpty()) { + headers.put("X-LaunchDarkly-Tags", tagHeader); } if (wrapperName != null) { String wrapperId = wrapperVersion == null ? wrapperName : (wrapperName + "/" + wrapperVersion); diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/ContextDecorator.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/ContextDecorator.java deleted file mode 100644 index d45283af..00000000 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/ContextDecorator.java +++ /dev/null @@ -1,94 +0,0 @@ -package com.launchdarkly.sdk.android; - -import androidx.annotation.NonNull; - -import com.launchdarkly.logging.LDLogger; -import com.launchdarkly.sdk.ContextKind; -import com.launchdarkly.sdk.ContextMultiBuilder; -import com.launchdarkly.sdk.LDContext; - -import java.util.HashMap; -import java.util.Map; -import java.util.UUID; - -final class ContextDecorator { - private static final String GENERATED_KEY_SHARED_PREFS_PREFIX = "anon-key-"; - - @NonNull private final PersistentDataStoreWrapper persistentData; - private final boolean generateAnonymousKeys; - - private Map cachedGeneratedKey = new HashMap<>(); - private Object generatedKeyLock = new Object(); - - public ContextDecorator( - @NonNull PersistentDataStoreWrapper persistentData, - boolean generateAnonymousKeys - ) { - this.persistentData = persistentData; - this.generateAnonymousKeys = generateAnonymousKeys; - } - - public LDContext decorateContext(LDContext context, LDLogger logger) { - if (!generateAnonymousKeys) { - return context; - } - if (context.isMultiple()) { - boolean hasAnyAnon = false; - for (int i = 0; i < context.getIndividualContextCount(); i++) { - if (context.getIndividualContext(i).isAnonymous()) { - hasAnyAnon = true; - break; - } - } - if (hasAnyAnon) { - ContextMultiBuilder builder = LDContext.multiBuilder(); - for (int i = 0; i < context.getIndividualContextCount(); i++) { - LDContext c = context.getIndividualContext(i); - builder.add(c.isAnonymous() ? singleKindContextWithGeneratedKey(c, logger) : c); - } - return builder.build(); - } - } else if (context.isAnonymous()) { - return singleKindContextWithGeneratedKey(context, logger); - } - return context; - } - - private LDContext singleKindContextWithGeneratedKey(LDContext context, LDLogger logger) { - return LDContext.builderFromContext(context) - .key(getOrCreateAutoContextKey(context.getKind(), logger)) - .build(); - } - - private String getOrCreateAutoContextKey(ContextKind contextKind, LDLogger logger) { - synchronized (generatedKeyLock) { - String key = cachedGeneratedKey.get(contextKind); - if (key != null) { - return key; - } - key = persistentData.getGeneratedContextKey(contextKind); - if (key != null) { - cachedGeneratedKey.put(contextKind, key); - return key; - } - final String generatedKey = UUID.randomUUID().toString(); - cachedGeneratedKey.put(contextKind, generatedKey); - - logger.info( - "Did not find a generated anonymous key for context kind \"{}\". Generating a new one: {}", - contextKind, generatedKey); - - // Updating persistent storage may be a blocking I/O call, so don't do it on the main - // thread. That part doesn't need to be done under this lock anyway - the fact that - // we've put it into the cachedGeneratedKey map already means any subsequent calls will - // get that value and not have to hit the persistent store. - new Thread(new Runnable() { - public void run() { - persistentData.setGeneratedContextKey(contextKind, generatedKey); - } - }).run(); - - return generatedKey; - } - } -} diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/EnvironmentReporter.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/EnvironmentReporter.java new file mode 100644 index 00000000..d57f6cb0 --- /dev/null +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/EnvironmentReporter.java @@ -0,0 +1,130 @@ +package com.launchdarkly.sdk.android; + +import android.app.Application; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.os.Build; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.launchdarkly.sdk.android.subsystems.ApplicationInfo; + +import java.util.Locale; + +// TODO: Come up with better name than EnvironmentReporter. This is confusing since LD has environments +// as a different concept +public class EnvironmentReporter { + + // TODO: Should we rename EnvironmentReporter to SDKEnvironmentReporter + private Application application; + private ApplicationInfo applicationInfo; + + /** + * Creates an environment reporter. + * @param application that represents the application environment this code is running in. + */ + public EnvironmentReporter(Application application) { + this.application = application; + } + + /** + * Sets the application info that this environment reporter will report when asked in the future. + * @param applicationInfo to report. + */ + public void setApplicationInfo(ApplicationInfo applicationInfo) { + this.applicationInfo = applicationInfo; + } + + /** + * Gets the {@link ApplicationInfo} for the application environment. If no {@link ApplicationInfo} + * has been provided via {@link #setApplicationInfo(ApplicationInfo)}, the {@link EnvironmentReporter} + * will collect application info automatically. + */ + @NonNull + public ApplicationInfo getApplicationInfo() { + // First priority is to return the application info that was provided manually. + // Second priority is to use application info fetched from the Android API. + if (applicationInfo == null) { + applicationInfo = new com.launchdarkly.sdk.android.subsystems.ApplicationInfo( + getApplicationID(), + getApplicationName(), + getApplicationVersion(), + getApplicationVersionName() + ); + } + + return applicationInfo; + } + + private String getApplicationID() { + return application.getPackageName(); + } + + private String getApplicationName() { + try { + PackageManager pm = application.getPackageManager(); + android.content.pm.ApplicationInfo ai = pm.getApplicationInfo( application.getPackageName(), 0); + return pm.getApplicationLabel(ai).toString(); + } catch (PackageManager.NameNotFoundException e) { + // TODO: investigate if this runtime exception can actually happen since we just + // got the package name just before. Current gut feeling says not possible, but + // those are famous last words. + throw new RuntimeException(e); + } + } + + private String getApplicationVersion() { + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + return String.valueOf(application.getPackageManager().getPackageInfo(application.getPackageName(), 0).getLongVersionCode()); + } else { + return String.valueOf(application.getPackageManager().getPackageInfo(application.getPackageName(), 0).versionCode); + } + } catch (PackageManager.NameNotFoundException e) { + // TODO: investigate if this runtime exception can actually happen since we just + // got the package name just before. Current gut feeling says not possible, but + // those are famous last words. + throw new RuntimeException(e); + } + } + + private String getApplicationVersionName() { + try { + PackageManager pm = application.getPackageManager(); + return pm.getPackageInfo( application.getPackageName(), 0).versionName; + } catch (PackageManager.NameNotFoundException e) { + // TODO: figure out if there is a way to get around this exception more elegantly. + throw new RuntimeException(e); + } + } + + public String getManufacturer() { + return Build.MANUFACTURER; + } + + public String getModel() { + return Build.MODEL; + } + + /** + * @return a BCP47 language tag representing the locale + */ + public String getLocale() { + Locale locale; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N){ + locale = application.getResources().getConfiguration().getLocales().get(0); + } else{ + locale = application.getResources().getConfiguration().locale; + } + return locale.toLanguageTag(); + } + + public String getOSFamily() { + return "Android"; + } + + public String getOSVersion() { + return Build.VERSION.RELEASE; + } +} diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/IContextModifier.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/IContextModifier.java new file mode 100644 index 00000000..c8aab349 --- /dev/null +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/IContextModifier.java @@ -0,0 +1,14 @@ +package com.launchdarkly.sdk.android; + +import com.launchdarkly.sdk.LDContext; + +public interface IContextModifier { + + /** + * Modifies the provided context and returns the resulting context. + * @param context + * @return + */ + public LDContext modifyContext(LDContext context); + +} diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDClient.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDClient.java index fceee94c..d016ed1f 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDClient.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDClient.java @@ -46,8 +46,11 @@ public class LDClient implements LDClientInterface, Closeable { // Will only be set once, during initialization, and the map is considered immutable. static volatile Map instances = null; private static volatile PlatformState sharedPlatformState; + + private static volatile EnvironmentReporter environmentReporter; private static volatile TaskExecutor sharedTaskExecutor; - private static volatile ContextDecorator contextDecorator; + private static volatile TelemetryContextModifier telemetryContextModifier; + private static volatile AnonymousKeyContextModifier anonymousKeyContextModifier; // A lock to ensure calls to `init()` are serialized. static Object initLock = new Object(); @@ -72,7 +75,7 @@ public class LDClient implements LDClientInterface, Closeable { * * @param application your Android application * @param config configuration used to set up the client - * @param context the initial evaluation context; see {@link LDClient} for more information + * @param inputContext the initial evaluation context; see {@link LDClient} for more information * about setting the context and optionally requesting a unique key for it * @return a {@link Future} which will complete once the client has been initialized * @see #init(Application, LDConfig, LDContext, int) @@ -81,7 +84,7 @@ public class LDClient implements LDClientInterface, Closeable { */ public static Future init(@NonNull Application application, @NonNull LDConfig config, - @NonNull LDContext context) { + @NonNull LDContext inputContext) { // As this is an externally facing API we should still check these, so we hide the linter // warnings @@ -94,16 +97,16 @@ public static Future init(@NonNull Application application, return new LDFailedFuture<>(new LaunchDarklyException("Client initialization requires a valid configuration")); } //noinspection ConstantConditions - if (context == null || !context.isValid()) { + if (inputContext == null || !inputContext.isValid()) { return new LDFailedFuture<>(new LaunchDarklyException("Client initialization requires a valid evaluation context (" - + (context == null ? "was null" : context.getError() + ")"))); + + (inputContext == null ? "was null" : inputContext.getError() + ")"))); } LDLogger logger = initSharedLogger(config); final LDAwaitFuture resultFuture = new LDAwaitFuture<>(); LDClient primaryClient; - LDContext actualContext; + LDContext modifiedContext; // Acquire the `initLock` to ensure that if `init()` is called multiple times, we will only // initialize the client(s) once. @@ -115,6 +118,8 @@ public static Future init(@NonNull Application application, sharedTaskExecutor = new AndroidTaskExecutor(application, logger); sharedPlatformState = new AndroidPlatformState(application, sharedTaskExecutor, logger); + environmentReporter = new EnvironmentReporter(application); + environmentReporter.setApplicationInfo(config.applicationInfo); PersistentDataStore store = config.getPersistentDataStore() == null ? new SharedPreferencesPersistentDataStore(application, logger) : @@ -123,11 +128,15 @@ public static Future init(@NonNull Application application, store, logger ); - contextDecorator = new ContextDecorator(persistentData, config.isGenerateAnonymousKeys()); + telemetryContextModifier = new TelemetryContextModifier(persistentData, environmentReporter); + anonymousKeyContextModifier = new AnonymousKeyContextModifier(persistentData, config.isGenerateAnonymousKeys()); + // TODO: seems like this migration should be happening before the PersistentDataStoreWrapper exists. Discuss. Migration.migrateWhenNeeded(store, logger); - actualContext = contextDecorator.decorateContext(context, logger); + + modifiedContext = telemetryContextModifier.modifyContext(inputContext); + modifiedContext = anonymousKeyContextModifier.modifyContext(modifiedContext, logger); // Create, but don't start, every LDClient instance final Map newInstances = new HashMap<>(); @@ -138,9 +147,10 @@ public static Future init(@NonNull Application application, try { final LDClient instance = new LDClient( sharedPlatformState, + environmentReporter, sharedTaskExecutor, persistentData.perEnvironmentData(mobileKey), - actualContext, + modifiedContext, config, mobileKey, envName @@ -179,7 +189,7 @@ public void onError(Throwable e) { // Start up all instances for (final LDClient instance : instances.values()) { if (instance.connectivityManager.startUp(completeWhenCounterZero)) { - instance.eventProcessor.recordIdentifyEvent(actualContext); + instance.eventProcessor.recordIdentifyEvent(modifiedContext); } } @@ -310,6 +320,7 @@ public static LDClient getForMobileKey(String keyName) throws LaunchDarklyExcept @VisibleForTesting protected LDClient( @NonNull final PlatformState platformState, + @NonNull final EnvironmentReporter environmentReporter, @NonNull final TaskExecutor taskExecutor, @NonNull PersistentDataStoreWrapper.PerEnvironmentData environmentStore, @NonNull LDContext initialContext, @@ -332,7 +343,7 @@ protected LDClient( FeatureFetcher fetcher = null; if (config.dataSource instanceof ComponentsImpl.DataSourceRequiresFeatureFetcher) { ClientContextImpl minimalContext = ClientContextImpl.fromConfig(config, mobileKey, - environmentName, null, initialContext, logger, platformState, taskExecutor + environmentName, null, initialContext, logger, platformState, environmentReporter, taskExecutor ); fetcher = new HttpFeatureFlagFetcher(minimalContext); } @@ -344,6 +355,7 @@ protected LDClient( initialContext, logger, platformState, + environmentReporter, taskExecutor ); @@ -393,7 +405,10 @@ public Future identify(LDContext context) { logger.warn("identify() was called with an invalid context: {}", context.getError()); return new LDFailedFuture<>(new LaunchDarklyException("Invalid context: " + context.getError())); } - return identifyInstances(contextDecorator.decorateContext(context, getSharedLogger())); + + LDContext modifiedContext = telemetryContextModifier.modifyContext(context); + modifiedContext = anonymousKeyContextModifier.modifyContext(modifiedContext, getSharedLogger()); + return identifyInstances(modifiedContext); } @Override diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDConfig.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDConfig.java index c0baa974..67c03ba3 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDConfig.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDConfig.java @@ -1,5 +1,7 @@ package com.launchdarkly.sdk.android; +import androidx.annotation.Nullable; + import com.launchdarkly.logging.LDLogAdapter; import com.launchdarkly.logging.LDLogLevel; import com.launchdarkly.logging.Logs; @@ -57,6 +59,7 @@ public class LDConfig { final ServiceEndpoints serviceEndpoints; + @Nullable final ApplicationInfo applicationInfo; final ComponentConfigurer dataSource; final ComponentConfigurer events; @@ -597,7 +600,7 @@ public LDConfig build() { .createServiceEndpoints(); ApplicationInfo applicationInfo = this.applicationInfoBuilder == null ? - Components.applicationInfo().createApplicationInfo() : + null : applicationInfoBuilder.createApplicationInfo(); return new LDConfig( diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDUtil.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDUtil.java index c23166c2..ff817a3b 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDUtil.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDUtil.java @@ -57,6 +57,7 @@ static String applicationTagHeader(ApplicationInfo applicationInfo, LDLogger log String[][] tags = { {"applicationId", "application-id", applicationInfo.getApplicationId()}, {"applicationVersion", "application-version", applicationInfo.getApplicationVersion()}, + {"applicationVersionName", "application-version-name", applicationInfo.getApplicationVersionName()} }; List parts = new ArrayList<>(); for (String[] row : tags) { diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/PersistentDataStoreWrapper.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/PersistentDataStoreWrapper.java index ca6e5e65..49876db0 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/PersistentDataStoreWrapper.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/PersistentDataStoreWrapper.java @@ -11,6 +11,7 @@ import java.util.HashMap; import java.util.Map; +import java.util.UUID; import java.util.concurrent.atomic.AtomicBoolean; /** @@ -49,6 +50,9 @@ static class SavedConnectionInfo { private static final String GLOBAL_NAMESPACE = "LaunchDarkly"; private static final String NAMESPACE_PREFIX = "LaunchDarkly_"; + + // TODO: We are now generating keys for non-anonymous contexts (the telemetry contexts). + // At the moment of writing this TODO, the generated keys are using the anonKey prefix. private static final String ANON_CONTEXT_KEY_PREFIX = "anonKey_"; private static final String ENVIRONMENT_METADATA_KEY = "index"; private static final String ENVIRONMENT_CONTEXT_DATA_KEY_PREFIX = "flags_"; @@ -60,6 +64,9 @@ static class SavedConnectionInfo { private final LDLogger logger; private final Object storeLock = new Object(); + + private Map generatedKeysCache = new HashMap<>(); + private final AtomicBoolean loggedStorageError = new AtomicBoolean(false); public PersistentDataStoreWrapper( @@ -83,17 +90,49 @@ public PerEnvironmentData perEnvironmentData(String mobileKey) { } /** - * Returns the cached anonymous key, if any, for the specified context kind (used when - * {@link LDConfig.Builder#generateAnonymousKeys(boolean)} is enabled). This is not in - * {@link PerEnvironmentData} because these generated keys are per device+context kind, not - * per environment. + * Returns the cached generated key if one exists or generates and saves a key for the + * specified context kind This is not in {@link PerEnvironmentData} because these generated + * keys are per device+context kind, not per environment. * * @param contextKind a context kind * @return the cached key, or null if there is none */ - public String getGeneratedContextKey(ContextKind contextKind) { - return tryGetValue(GLOBAL_NAMESPACE, - ANON_CONTEXT_KEY_PREFIX + contextKind.toString()); + public String getOrGenerateContextKey(ContextKind contextKind) { + synchronized (generatedKeysCache) { + // do we have a generated key in the cache? + String cachedKey = generatedKeysCache.get(contextKind); + if (cachedKey != null) { + return cachedKey; + } + + // do we have a generated key in persistence? + String persistedKey = tryGetValue(GLOBAL_NAMESPACE, ANON_CONTEXT_KEY_PREFIX + contextKind.toString()); + if (persistedKey != null) { + generatedKeysCache.put(contextKind, persistedKey); + return persistedKey; + } + + // don't have a generated key, so generate a key + final String generatedKey = UUID.randomUUID().toString(); + generatedKeysCache.put(contextKind, generatedKey); + + logger.info("Did not find a generated key for context kind \"{}\". Generating a new one: {}", contextKind, generatedKey); + + // Updating persistent storage may be a blocking I/O call, so don't do it on the main + // thread. That part doesn't need to be done under this lock anyway - the fact that + // we've put it into the cachedGeneratedKey map already means any subsequent calls will + // get that value and not have to hit the persistent store. + + // TODO: Revisit usage of new Thread instead of executor service. + new Thread(new Runnable() { + public void run() { + trySetValue(GLOBAL_NAMESPACE, + ANON_CONTEXT_KEY_PREFIX + contextKind.toString(), generatedKey); + } + }).run(); + + return generatedKey; + } } /** @@ -103,7 +142,7 @@ public String getGeneratedContextKey(ContextKind contextKind) { * per environment. * * @param contextKind a context kind - * @param key the generated key + * @param key the generated key */ public void setGeneratedContextKey(ContextKind contextKind, String key) { trySetValue(GLOBAL_NAMESPACE, @@ -141,7 +180,7 @@ public EnvironmentData getContextData(String hashedContextId) { * Stores flag data for a specific context, overwriting any previous data for that context. * * @param hashedContextId the hashed key of the context - * @param allData the flag data + * @param allData the flag data */ public void setContextData(String hashedContextId, EnvironmentData allData) { trySetValue(environmentNamespace, keyForContextId(hashedContextId), allData.toJson()); @@ -161,7 +200,8 @@ public void removeContextData(String hashedContextId) { * * @return a {@link ContextIndex} (never null; will be empty if none have been stored) */ - @NonNull public ContextIndex getIndex() { + @NonNull + public ContextIndex getIndex() { String serializedData = tryGetValue(environmentNamespace, ENVIRONMENT_METADATA_KEY); try { return serializedData == null ? new ContextIndex() : @@ -185,7 +225,8 @@ public void setIndex(@NonNull ContextIndex contextIndex) { * * @return a {@link SavedConnectionInfo} (never null; will be empty if none was stored) */ - @NonNull public SavedConnectionInfo getConnectionInfo() { + @NonNull + public SavedConnectionInfo getConnectionInfo() { Long lastSuccessTime = tryGetValueAsLong(environmentNamespace, ENVIRONMENT_LAST_SUCCESS_TIME_KEY); Long lastFailureTime = tryGetValueAsLong(environmentNamespace, ENVIRONMENT_LAST_FAILURE_TIME_KEY); String lastFailureJson = tryGetValue(environmentNamespace, ENVIRONMENT_LAST_FAILURE_KEY); @@ -251,10 +292,10 @@ private void trySetValues(String namespace, Map keysAndValues) { } private void maybeLogStoreError(Exception e) { - if (loggedStorageError.getAndSet(true)) { - return; - } - LDUtil.logExceptionAtErrorLevel(logger, e, "Failure in persistent data store"); + if (loggedStorageError.getAndSet(true)) { + return; + } + LDUtil.logExceptionAtErrorLevel(logger, e, "Failure in persistent data store"); } private Long tryGetValueAsLong(String namespace, String key) { diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/TelemetryContextModifier.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/TelemetryContextModifier.java new file mode 100644 index 00000000..adbcbd38 --- /dev/null +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/TelemetryContextModifier.java @@ -0,0 +1,93 @@ +package com.launchdarkly.sdk.android; + +import com.launchdarkly.sdk.ContextKind; +import com.launchdarkly.sdk.ContextMultiBuilder; +import com.launchdarkly.sdk.LDContext; +import com.launchdarkly.sdk.LDValue; +import com.launchdarkly.sdk.ObjectBuilder; + +import java.util.ArrayList; +import java.util.List; + +public class TelemetryContextModifier implements IContextModifier { + + // TODO: determine proper home for these constants. + private static final String LD_APPLICATION_KIND = "ld_application"; + private static final String LD_DEVICE_KIND = "ld_device"; + public static final String ATTR_ID = "id"; + public static final String ATTR_NAME = "name"; + public static final String ATTR_VERSION = "version"; + public static final String ATTR_VERSION_NAME = "versionName"; + public static final String ATTR_MANUFACTURER = "manufacturer"; + public static final String ATTR_MODEL = "model"; + public static final String ATTR_LOCALE = "locale"; + public static final String ATTR_OS = "os"; + public static final String ATTR_FAMILY = "family"; + + + private PersistentDataStoreWrapper persistentData; + private EnvironmentReporter environmentReporter; + + public TelemetryContextModifier(PersistentDataStoreWrapper persistentData, + EnvironmentReporter environmentReporter) { + this.persistentData = persistentData; + this.environmentReporter = environmentReporter; + } + + @Override + public LDContext modifyContext(LDContext context) { + + // TODO: update with collision detection logic + + // make cloned builder for context + ContextMultiBuilder builder = LDContext.multiBuilder(); + List contextList = new ArrayList<>(); + if (context.isMultiple()) { + for (int i = 0; i < context.getIndividualContextCount(); i++) { + contextList.add(context.getIndividualContext(i)); + } + } else { + contextList.add(context); + } + + contextList.add(makeLDApplicationKindContext()); + contextList.add(makeLDDeviceKindContext()); + + for (LDContext c : contextList) { + builder.add(c); + } + + return builder.build(); + } + + private LDContext makeLDApplicationKindContext() { + + // TODO: add logic for detecting if no properties are available and if no properties are available, exclude the context kind + + ContextKind ldApplicationKind = ContextKind.of(LD_APPLICATION_KIND); + String key = persistentData.getOrGenerateContextKey(ldApplicationKind); + + return LDContext.builder(ldApplicationKind, key) + .set(ATTR_ID, LDValue.of(environmentReporter.getApplicationInfo().getApplicationId())) + .set(ATTR_NAME, LDValue.of(environmentReporter.getApplicationInfo().getApplicationName())) + .set(ATTR_VERSION, LDValue.of(environmentReporter.getApplicationInfo().getApplicationVersion())) + .set(ATTR_VERSION_NAME, LDValue.of(environmentReporter.getApplicationInfo().getApplicationVersionName())) + .build(); + } + + private LDContext makeLDDeviceKindContext() { + ContextKind ldDeviceKind = ContextKind.of(LD_DEVICE_KIND); + String key = persistentData.getOrGenerateContextKey(ldDeviceKind); + + return LDContext.builder(ldDeviceKind, key) + .set(ATTR_MANUFACTURER, LDValue.of(environmentReporter.getManufacturer())) + .set(ATTR_MODEL, LDValue.of(environmentReporter.getModel())) + .set(ATTR_LOCALE, LDValue.of(environmentReporter.getLocale())) + .set(ATTR_OS, new ObjectBuilder() + .put(ATTR_FAMILY, environmentReporter.getOSFamily()) + .put(ATTR_VERSION, environmentReporter.getOSVersion()) + .build() + ) + .build(); + } +} \ No newline at end of file diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/ApplicationInfoBuilder.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/ApplicationInfoBuilder.java index 9295c931..6605c77a 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/ApplicationInfoBuilder.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/ApplicationInfoBuilder.java @@ -26,7 +26,9 @@ */ public final class ApplicationInfoBuilder { private String applicationId; + private String applicationName; private String applicationVersion; + private String applicationVersionName; /** * Create an empty ApplicationInfoBuilder. @@ -50,6 +52,11 @@ public ApplicationInfoBuilder applicationId(String applicationId) { return this; } + public ApplicationInfoBuilder applicationName(String applicationName) { + this.applicationName = applicationName; + return this; + } + /** * Sets a unique identifier representing the version of the application where the LaunchDarkly SDK * is running. @@ -61,8 +68,13 @@ public ApplicationInfoBuilder applicationId(String applicationId) { * @param applicationVersion the application version * @return the builder */ - public ApplicationInfoBuilder applicationVersion(String applicationVersion) { - this.applicationVersion = applicationVersion; + public ApplicationInfoBuilder applicationVersion(String version) { + this.applicationVersion = version; + return this; + } + + public ApplicationInfoBuilder applicationVersionName(String versionName) { + this.applicationVersionName = versionName; return this; } @@ -72,6 +84,8 @@ public ApplicationInfoBuilder applicationVersion(String applicationVersion) { * @return the configuration object */ public ApplicationInfo createApplicationInfo() { - return new ApplicationInfo(applicationId, applicationVersion); + // TODO: evaluate the risk of injecting a new parameter of the same type in the middle of + // an existing constructor. + return new ApplicationInfo(applicationId, applicationName, applicationVersion, applicationVersionName); } } diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/subsystems/ApplicationInfo.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/subsystems/ApplicationInfo.java index d66f20db..64438f60 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/subsystems/ApplicationInfo.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/subsystems/ApplicationInfo.java @@ -11,7 +11,9 @@ */ public final class ApplicationInfo { private String applicationId; + private String applicationName; private String applicationVersion; + private String applicationVersionName; /** * Used internally by the SDK to store application metadata. @@ -20,9 +22,12 @@ public final class ApplicationInfo { * @param applicationVersion the application version * @see ApplicationInfoBuilder */ - public ApplicationInfo(String applicationId, String applicationVersion) { + public ApplicationInfo(String applicationId, String applicationName, + String applicationVersion, String applicationVersionName) { this.applicationId = applicationId; + this.applicationName = applicationName; this.applicationVersion = applicationVersion; + this.applicationVersionName = applicationVersionName; } /** @@ -43,4 +48,12 @@ public String getApplicationId() { public String getApplicationVersion() { return applicationVersion; } + + public String getApplicationName() { + return applicationName; + } + + public String getApplicationVersionName() { + return applicationVersionName; + } } diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/subsystems/ClientContext.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/subsystems/ClientContext.java index 58ab0298..308c239e 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/subsystems/ClientContext.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/subsystems/ClientContext.java @@ -1,7 +1,10 @@ package com.launchdarkly.sdk.android.subsystems; +import androidx.annotation.Nullable; + import com.launchdarkly.logging.LDLogger; import com.launchdarkly.sdk.LDContext; +import com.launchdarkly.sdk.android.EnvironmentReporter; import com.launchdarkly.sdk.android.LDClient; import com.launchdarkly.sdk.android.LDConfig; import com.launchdarkly.sdk.android.interfaces.ServiceEndpoints; @@ -23,6 +26,9 @@ */ public class ClientContext { private final ApplicationInfo applicationInfo; + + private final EnvironmentReporter environmentReporter; + private final LDLogger baseLogger; private final LDConfig config; private final DataSourceUpdateSink dataSourceUpdateSink; @@ -55,6 +61,7 @@ public class ClientContext { public ClientContext( String mobileKey, ApplicationInfo applicationInfo, + EnvironmentReporter environmentReporter, LDLogger baseLogger, LDConfig config, DataSourceUpdateSink dataSourceUpdateSink, @@ -69,6 +76,7 @@ public ClientContext( ) { this.mobileKey = mobileKey; this.applicationInfo = applicationInfo; + this.environmentReporter = environmentReporter; this.baseLogger = baseLogger; this.config = config; this.dataSourceUpdateSink = dataSourceUpdateSink; @@ -101,6 +109,7 @@ public ClientContext( @Deprecated public ClientContext( String mobileKey, + EnvironmentReporter environmentReporter, LDLogger baseLogger, LDConfig config, DataSourceUpdateSink dataSourceUpdateSink, @@ -113,7 +122,7 @@ public ClientContext( ServiceEndpoints serviceEndpoints, boolean setOffline ) { - this(mobileKey, null, baseLogger, config, dataSourceUpdateSink, environmentName, + this(mobileKey, null, environmentReporter, baseLogger, config, dataSourceUpdateSink, environmentName, evaluationReasons, evaluationContext, http, inBackground, previouslyInBackground, serviceEndpoints, setOffline); } @@ -154,21 +163,22 @@ public ClientContext( serviceEndpoints, setOffline); } - protected ClientContext(ClientContext copyFrom) { + protected ClientContext(ClientContext copy) { this( - copyFrom.mobileKey, - copyFrom.applicationInfo, - copyFrom.baseLogger, - copyFrom.config, - copyFrom.dataSourceUpdateSink, - copyFrom.environmentName, - copyFrom.evaluationReasons, - copyFrom.evaluationContext, - copyFrom.http, - copyFrom.inBackground, - copyFrom.previouslyInBackground, - copyFrom.serviceEndpoints, - copyFrom.setOffline + copy.mobileKey, + copy.applicationInfo, + copy.environmentReporter, + copy.baseLogger, + copy.config, + copy.dataSourceUpdateSink, + copy.environmentName, + copy.evaluationReasons, + copy.evaluationContext, + copy.http, + copy.inBackground, + copy.previouslyInBackground, + copy.serviceEndpoints, + copy.setOffline ); } @@ -176,10 +186,15 @@ protected ClientContext(ClientContext copyFrom) { * The application metadata object. * @return the application metadata */ + @Nullable public ApplicationInfo getApplicationInfo() { return applicationInfo; } + public EnvironmentReporter getEnvironmentReporter() { + return environmentReporter; + } + /** * The base logger for the SDK. * @return a logger instance diff --git a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/ContextDecoratorTest.java b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/AnonymousKeyContextModifierTest.java similarity index 78% rename from launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/ContextDecoratorTest.java rename to launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/AnonymousKeyContextModifierTest.java index 1a346632..496f1460 100644 --- a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/ContextDecoratorTest.java +++ b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/AnonymousKeyContextModifierTest.java @@ -12,7 +12,7 @@ import org.junit.Rule; import org.junit.Test; -public class ContextDecoratorTest { +public class AnonymousKeyContextModifierTest { private static final ContextKind KIND1 = ContextKind.of("kind1"); private static final ContextKind KIND2 = ContextKind.of("kind2"); @@ -23,7 +23,7 @@ public class ContextDecoratorTest { public void singleKindNonAnonymousContextIsUnchanged() { LDContext context = LDContext.builder("key1").name("name").build(); assertEquals(context, - makeDecoratorWithoutPersistence().decorateContext(context, logging.logger)); + makeDecoratorWithoutPersistence().modifyContext(context, logging.logger)); logging.assertNothingLogged(); } @@ -31,7 +31,7 @@ public void singleKindNonAnonymousContextIsUnchanged() { public void singleKindAnonymousContextIsUnchangedIfConfigOptionIsNotSet() { LDContext context = LDContext.builder("key1").anonymous(true).name("name").build(); assertEquals(context, - makeDecoratorWithoutPersistence().decorateContext(context, logging.logger)); + makeDecoratorWithoutPersistence().modifyContext(context, logging.logger)); logging.assertNothingLogged(); } @@ -39,7 +39,7 @@ public void singleKindAnonymousContextIsUnchangedIfConfigOptionIsNotSet() { public void singleKindAnonymousContextGetsGeneratedKeyIfConfigOptionIsSet() { LDContext context = LDContext.builder("placeholder").anonymous(true).name("name").build(); LDContext transformed = makeDecoratorWithoutPersistence(true) - .decorateContext(context, logging.logger); + .modifyContext(context, logging.logger); assertContextHasBeenTransformedWithNewKey(context, transformed); logging.assertInfoLogged("Did not find a generated anonymous key for context kind \"user\""); } @@ -51,7 +51,7 @@ public void multiKindContextIsUnchangedIfNoIndividualContextsNeedGeneratedKey() LDContext multiContext = LDContext.createMulti(c1, c2); assertSame(multiContext, - makeDecoratorWithoutPersistence().decorateContext(multiContext, logging.logger)); + makeDecoratorWithoutPersistence().modifyContext(multiContext, logging.logger)); logging.assertNothingLogged(); } @@ -61,7 +61,7 @@ public void multiKindContextGetsGeneratedKeyForIndividualContext() { LDContext c2 = LDContext.builder("key2").kind(KIND2).anonymous(true).name("name2").build(); LDContext multiContext = LDContext.createMulti(c1, c2); LDContext transformedMulti = makeDecoratorWithoutPersistence(true) - .decorateContext(multiContext, logging.logger); + .modifyContext(multiContext, logging.logger); assertEquals(multiContext.getIndividualContextCount(), transformedMulti.getIndividualContextCount()); assertSame(multiContext.getIndividualContext(0), transformedMulti.getIndividualContext(0)); @@ -75,7 +75,7 @@ public void multiKindContextGetsSeparateGeneratedKeyForEachKind() { LDContext c2 = LDContext.builder("key2").kind(KIND2).anonymous(true).name("name2").build(); LDContext multiContext = LDContext.createMulti(c1, c2); LDContext transformedMulti = makeDecoratorWithoutPersistence(true) - .decorateContext(multiContext, logging.logger); + .modifyContext(multiContext, logging.logger); assertEquals(multiContext.getIndividualContextCount(), transformedMulti.getIndividualContextCount()); assertContextHasBeenTransformedWithNewKey( @@ -94,15 +94,15 @@ public void generatedKeysPersistPerKindIfPersistentStorageIsEnabled() { PersistentDataStore store = new InMemoryPersistentDataStore(); - ContextDecorator decorator1 = makeDecoratorWithPersistence(store, true); - LDContext transformedMultiA = decorator1.decorateContext(multiContext, logging.logger); + AnonymousKeyContextModifier decorator1 = makeDecoratorWithPersistence(store, true); + LDContext transformedMultiA = decorator1.modifyContext(multiContext, logging.logger); assertContextHasBeenTransformedWithNewKey( multiContext.getIndividualContext(0), transformedMultiA.getIndividualContext(0)); assertContextHasBeenTransformedWithNewKey( multiContext.getIndividualContext(1), transformedMultiA.getIndividualContext(1)); - ContextDecorator decorator2 = makeDecoratorWithPersistence(store, true); - LDContext transformedMultiB = decorator2.decorateContext(multiContext, logging.logger); + AnonymousKeyContextModifier decorator2 = makeDecoratorWithPersistence(store, true); + LDContext transformedMultiB = decorator2.modifyContext(multiContext, logging.logger); assertEquals(transformedMultiA, transformedMultiB); } @@ -112,14 +112,14 @@ public void generatedKeysAreReusedDuringLifetimeOfSdkEvenIfPersistentStorageIsDi LDContext c2 = LDContext.builder("key2").kind(KIND2).anonymous(true).name("name2").build(); LDContext multiContext = LDContext.createMulti(c1, c2); - ContextDecorator decorator = makeDecoratorWithoutPersistence(true); - LDContext transformedMultiA = decorator.decorateContext(multiContext, logging.logger); + AnonymousKeyContextModifier decorator = makeDecoratorWithoutPersistence(true); + LDContext transformedMultiA = decorator.modifyContext(multiContext, logging.logger); assertContextHasBeenTransformedWithNewKey( multiContext.getIndividualContext(0), transformedMultiA.getIndividualContext(0)); assertContextHasBeenTransformedWithNewKey( multiContext.getIndividualContext(1), transformedMultiA.getIndividualContext(1)); - LDContext transformedMultiB = decorator.decorateContext(multiContext, logging.logger); + LDContext transformedMultiB = decorator.modifyContext(multiContext, logging.logger); assertEquals(transformedMultiA, transformedMultiB); } @@ -129,15 +129,15 @@ public void generatedKeysAreNotReusedAcrossRestartsIfPersistentStorageIsDisabled LDContext c2 = LDContext.builder("key2").kind(KIND2).anonymous(true).name("name2").build(); LDContext multiContext = LDContext.createMulti(c1, c2); - ContextDecorator decorator1 = makeDecoratorWithoutPersistence(true); - LDContext transformedMultiA = decorator1.decorateContext(multiContext, logging.logger); + AnonymousKeyContextModifier decorator1 = makeDecoratorWithoutPersistence(true); + LDContext transformedMultiA = decorator1.modifyContext(multiContext, logging.logger); assertContextHasBeenTransformedWithNewKey( multiContext.getIndividualContext(0), transformedMultiA.getIndividualContext(0)); assertContextHasBeenTransformedWithNewKey( multiContext.getIndividualContext(1), transformedMultiA.getIndividualContext(1)); - ContextDecorator decorator2 = makeDecoratorWithoutPersistence(true); - LDContext transformedMultiB = decorator2.decorateContext(multiContext, logging.logger); + AnonymousKeyContextModifier decorator2 = makeDecoratorWithoutPersistence(true); + LDContext transformedMultiB = decorator2.modifyContext(multiContext, logging.logger); assertContextHasBeenTransformedWithNewKey( multiContext.getIndividualContext(0), transformedMultiB.getIndividualContext(0)); assertNotEquals(transformedMultiA.getIndividualContext(0).getKey(), @@ -147,17 +147,17 @@ public void generatedKeysAreNotReusedAcrossRestartsIfPersistentStorageIsDisabled assertNotEquals(transformedMultiA.getIndividualContext(1).getKey(), transformedMultiB.getIndividualContext(1).getKey()); } - private ContextDecorator makeDecoratorWithPersistence(PersistentDataStore store, - boolean generateAnonymousKeys) { + private AnonymousKeyContextModifier makeDecoratorWithPersistence(PersistentDataStore store, + boolean generateAnonymousKeys) { PersistentDataStoreWrapper persistentData = new PersistentDataStoreWrapper(store, LDLogger.none()); - return new ContextDecorator(persistentData, generateAnonymousKeys); + return new AnonymousKeyContextModifier(persistentData, generateAnonymousKeys); } - private ContextDecorator makeDecoratorWithoutPersistence(boolean generateAnonymousKeys) { + private AnonymousKeyContextModifier makeDecoratorWithoutPersistence(boolean generateAnonymousKeys) { return makeDecoratorWithPersistence(new NullPersistentDataStore(), generateAnonymousKeys); } - private ContextDecorator makeDecoratorWithoutPersistence() { + private AnonymousKeyContextModifier makeDecoratorWithoutPersistence() { return makeDecoratorWithoutPersistence(false); } From 1441a1fefbd3984cd3546adc1612448b4be2416c Mon Sep 17 00:00:00 2001 From: Todd Anderson Date: Wed, 21 Jun 2023 13:19:39 -0500 Subject: [PATCH 04/26] Added prioritizing sourcing of automatic env attributes and unit tests. --- .../EnvironmentReporterBuilderTest.java | 52 ++++++ .../sdk/android/AutoEnvContextModifier.java | 134 ++++++++++++++++ .../sdk/android/ClientContextImpl.java | 5 +- .../sdk/android/ComponentsImpl.java | 7 +- .../launchdarkly/sdk/android/EventUtil.java | 4 +- .../sdk/android/IContextModifier.java | 13 +- .../launchdarkly/sdk/android/LDClient.java | 34 ++-- .../sdk/android/LDPackageConsts.java | 7 + .../com/launchdarkly/sdk/android/LDUtil.java | 2 +- .../android/PersistentDataStoreWrapper.java | 28 ++-- .../sdk/android/TelemetryContextModifier.java | 93 ----------- .../AndroidEnvironmentReporter.java} | 122 +++++++-------- .../ApplicationInfoEnvironmentReporter.java | 24 +++ .../env/EnvironmentReporterBuilder.java | 70 +++++++++ .../env/EnvironmentReporterChainBase.java | 58 +++++++ .../sdk/android/env/IEnvironmentReporter.java | 48 ++++++ .../android/env/SDKEnvironmentReporter.java | 25 +++ .../integrations/ApplicationInfoBuilder.java | 22 ++- .../android/subsystems/ApplicationInfo.java | 10 ++ .../sdk/android/subsystems/ClientContext.java | 98 +----------- .../AnonymousKeyContextModifierTest.java | 1 - .../android/AutoEnvContextModifierTest.java | 148 ++++++++++++++++++ .../sdk/android/ConnectivityManagerTest.java | 5 + .../android/ContextDataManagerTestBase.java | 4 + .../sdk/android/DiagnosticConfigTest.java | 3 +- .../android/HttpConfigurationBuilderTest.java | 15 +- .../sdk/android/LDConfigTest.java | 12 +- .../sdk/android/MigrationTest.java | 4 +- .../PersistentDataStoreWrapperTest.java | 31 +++- .../sdk/android/PollingDataSourceTest.java | 6 +- .../sdk/android/StreamingDataSourceTest.java | 8 +- shared-test-code/build.gradle | 1 + .../sdk/android/MockComponents.java | 7 +- 33 files changed, 777 insertions(+), 324 deletions(-) create mode 100644 launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/EnvironmentReporterBuilderTest.java create mode 100644 launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/AutoEnvContextModifier.java create mode 100644 launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDPackageConsts.java delete mode 100644 launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/TelemetryContextModifier.java rename launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/{EnvironmentReporter.java => env/AndroidEnvironmentReporter.java} (57%) create mode 100644 launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/env/ApplicationInfoEnvironmentReporter.java create mode 100644 launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/env/EnvironmentReporterBuilder.java create mode 100644 launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/env/EnvironmentReporterChainBase.java create mode 100644 launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/env/IEnvironmentReporter.java create mode 100644 launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/env/SDKEnvironmentReporter.java create mode 100644 launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/AutoEnvContextModifierTest.java diff --git a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/EnvironmentReporterBuilderTest.java b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/EnvironmentReporterBuilderTest.java new file mode 100644 index 00000000..f88acf8d --- /dev/null +++ b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/EnvironmentReporterBuilderTest.java @@ -0,0 +1,52 @@ +package com.launchdarkly.sdk.android; + +import android.app.Application; + +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import com.launchdarkly.sdk.android.env.EnvironmentReporterBuilder; +import com.launchdarkly.sdk.android.env.IEnvironmentReporter; +import com.launchdarkly.sdk.android.integrations.ApplicationInfoBuilder; +import com.launchdarkly.sdk.android.subsystems.ApplicationInfo; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +public class EnvironmentReporterBuilderTest { + + /** + * Requirement 1.2.5.2 - Prioritized sourcing application info attributes + */ + @Test + public void prioritizesProvidedApplicationInfo() { + + Application application = ApplicationProvider.getApplicationContext(); + EnvironmentReporterBuilder builder1 = new EnvironmentReporterBuilder(); + builder1.enableCollectionFromPlatform(application); + IEnvironmentReporter reporter1 = builder1.build(); + ApplicationInfo reporter1Output = reporter1.getApplicationInfo(); + + EnvironmentReporterBuilder builder2 = new EnvironmentReporterBuilder(); + ApplicationInfo manualInfoInput = new ApplicationInfoBuilder().applicationId("manualAppID").createApplicationInfo(); + builder2.setApplicationInfo(manualInfoInput); + builder2.enableCollectionFromPlatform(application); + IEnvironmentReporter reporter2 = builder2.build(); + ApplicationInfo reporter2Output = reporter2.getApplicationInfo(); + + Assert.assertNotEquals(reporter1Output.getApplicationId(), reporter2Output.getApplicationId()); + Assert.assertEquals(manualInfoInput.getApplicationId(), reporter2Output.getApplicationId()); + } + + @Test + public void defaultsToSDKValues() { + IEnvironmentReporter reporter = new EnvironmentReporterBuilder().build(); + Assert.assertEquals(LDPackageConsts.SDK_NAME, reporter.getApplicationInfo().getApplicationId()); + Assert.assertEquals(LDPackageConsts.SDK_NAME, reporter.getApplicationInfo().getApplicationName()); + Assert.assertEquals(BuildConfig.VERSION_NAME, reporter.getApplicationInfo().getApplicationVersion()); + Assert.assertEquals(BuildConfig.VERSION_NAME, reporter.getApplicationInfo().getApplicationVersionName()); + + } +} diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/AutoEnvContextModifier.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/AutoEnvContextModifier.java new file mode 100644 index 00000000..55df02b6 --- /dev/null +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/AutoEnvContextModifier.java @@ -0,0 +1,134 @@ +package com.launchdarkly.sdk.android; + +import com.launchdarkly.sdk.ContextBuilder; +import com.launchdarkly.sdk.ContextKind; +import com.launchdarkly.sdk.ContextMultiBuilder; +import com.launchdarkly.sdk.LDContext; +import com.launchdarkly.sdk.LDValue; +import com.launchdarkly.sdk.ObjectBuilder; +import com.launchdarkly.sdk.android.env.IEnvironmentReporter; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Callable; + +/** + * An {@link IContextModifier} that will add a few context kinds with environmental + * information that is useful out of the box. + */ +public class AutoEnvContextModifier implements IContextModifier { + + static final String LD_APPLICATION_KIND = "ld_application"; + static final String LD_DEVICE_KIND = "ld_device"; + static final String ATTR_ID = "id"; + static final String ATTR_NAME = "name"; + static final String ATTR_VERSION = "version"; + static final String ATTR_VERSION_NAME = "versionName"; + static final String ATTR_MANUFACTURER = "manufacturer"; + static final String ATTR_MODEL = "model"; + static final String ATTR_LOCALE = "locale"; + static final String ATTR_OS = "os"; + static final String ATTR_FAMILY = "family"; + + private PersistentDataStoreWrapper persistentData; + private IEnvironmentReporter environmentReporter; + + public AutoEnvContextModifier(PersistentDataStoreWrapper persistentData, + IEnvironmentReporter environmentReporter) { + this.persistentData = persistentData; + this.environmentReporter = environmentReporter; + } + + @Override + public LDContext modifyContext(LDContext context) { + ContextMultiBuilder builder = LDContext.multiBuilder(); + builder.add(context); + + // iterate over telemetry context recipes, avoid overwriting customer contexts, add new contexts. + for (ContextRecipe recipe : makeRecipeList()) { + if (context.getIndividualContext(recipe.kind) == null) { + builder.add(makeLDContextFromRecipe(recipe)); + } + } + + return builder.build(); + } + + /** + * A {@link ContextRecipe} is a set of callables that will be executed for a given kind + * to generate the associated {@link LDContext}. The reason this class exists is to not make + * platform API calls until the context kind is needed. + */ + private static class ContextRecipe { + ContextKind kind; + Callable keyCallable; + Map> attributeCallables; + + /** + * @param kind that the recipe is for + * @param keyCallable that when invoked will retrieve a key for the context + * @param attributeCallables that when invoked will retrieve values for the attributes (map key) + */ + public ContextRecipe(ContextKind kind, Callable keyCallable, Map> attributeCallables) { + this.kind = kind; + this.keyCallable = keyCallable; + this.attributeCallables = attributeCallables; + } + } + + /** + * @param recipe to use + * @return a {@link LDContext} that was generated following the provided {@link ContextRecipe} + */ + private LDContext makeLDContextFromRecipe(ContextRecipe recipe) { + try { + // make builder, iterate over attributes in recipe, run LDValue callables to get values, add to builder + ContextBuilder builder = LDContext.builder(recipe.kind, recipe.keyCallable.call()); + for (Map.Entry> entry : recipe.attributeCallables.entrySet()) { + builder.set(entry.getKey(), entry.getValue().call()); + } + return builder.build(); + } catch (Exception e) { + // TODO: Java callable interface throws exception, come up with structure for closures to get LDValues that doesn't have exceptions. + throw new RuntimeException(e); + } + } + + /** + * @return a list of {@link ContextRecipe} that can be used to create contexts of the + * kind in the recipe. + */ + private List makeRecipeList() { + ContextKind ldApplicationKind = ContextKind.of(LD_APPLICATION_KIND); + Map> applicationCallables = new HashMap<>(); + applicationCallables.put(ATTR_ID, () -> LDValue.of(environmentReporter.getApplicationInfo().getApplicationId())); + applicationCallables.put(ATTR_NAME, () -> LDValue.of(environmentReporter.getApplicationInfo().getApplicationName())); + applicationCallables.put(ATTR_VERSION, () -> LDValue.of(environmentReporter.getApplicationInfo().getApplicationVersion())); + applicationCallables.put(ATTR_VERSION_NAME, () -> LDValue.of(environmentReporter.getApplicationInfo().getApplicationVersionName())); + + ContextKind ldDeviceKind = ContextKind.of(LD_DEVICE_KIND); + Map> deviceCallables = new HashMap<>(); + deviceCallables.put(ATTR_MANUFACTURER, () -> LDValue.of(environmentReporter.getManufacturer())); + deviceCallables.put(ATTR_MODEL, () -> LDValue.of(environmentReporter.getModel())); + deviceCallables.put(ATTR_LOCALE, () -> LDValue.of(environmentReporter.getLocale())); + deviceCallables.put(ATTR_OS, () -> new ObjectBuilder() + .put(ATTR_FAMILY, environmentReporter.getOSFamily()) + .put(ATTR_VERSION, environmentReporter.getOSVersion()) + .build()); + + return Arrays.asList( + new ContextRecipe( + ldApplicationKind, + () -> persistentData.getOrGenerateContextKey(ldApplicationKind), + applicationCallables + ), + new ContextRecipe( + ldDeviceKind, + () -> persistentData.getOrGenerateContextKey(ldDeviceKind), + deviceCallables + ) + ); + } +} \ No newline at end of file diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/ClientContextImpl.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/ClientContextImpl.java index 334b5979..7cf87a00 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/ClientContextImpl.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/ClientContextImpl.java @@ -2,6 +2,7 @@ import com.launchdarkly.logging.LDLogger; import com.launchdarkly.sdk.LDContext; +import com.launchdarkly.sdk.android.env.IEnvironmentReporter; import com.launchdarkly.sdk.android.subsystems.ClientContext; import com.launchdarkly.sdk.android.subsystems.HttpConfiguration; import com.launchdarkly.sdk.android.subsystems.DataSourceUpdateSink; @@ -54,7 +55,7 @@ static ClientContextImpl fromConfig( LDContext initialContext, LDLogger logger, PlatformState platformState, - EnvironmentReporter environmentReporter, + IEnvironmentReporter environmentReporter, TaskExecutor taskExecutor ) { boolean initiallyInBackground = platformState != null && !platformState.isForeground(); @@ -64,7 +65,6 @@ static ClientContextImpl fromConfig( HttpConfiguration httpConfig = config.http.build(minimalContext); ClientContext baseClientContext = new ClientContext( mobileKey, - config.applicationInfo, environmentReporter, logger, config, @@ -103,7 +103,6 @@ public static ClientContextImpl forDataSource( return new ClientContextImpl( new ClientContext( baseClientContext.getMobileKey(), - baseClientContext.getApplicationInfo(), baseClientContext.getEnvironmentReporter(), baseClientContext.getBaseLogger(), baseClientContext.getConfig(), diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/ComponentsImpl.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/ComponentsImpl.java index 91461aef..605d98c3 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/ComponentsImpl.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/ComponentsImpl.java @@ -211,9 +211,10 @@ public HttpConfiguration build(ClientContext clientContext) { Map headers = new HashMap<>(); headers.put("Authorization", LDUtil.AUTH_SCHEME + clientContext.getMobileKey()); headers.put("User-Agent", LDUtil.USER_AGENT_HEADER_VALUE); - clientContext.getEnvironmentReporter().getApplicationInfo(); - String tagHeader = LDUtil.applicationTagHeader(clientContext.getEnvironmentReporter().getApplicationInfo(), - clientContext.getBaseLogger()); + String tagHeader = LDUtil.applicationTagHeader( + clientContext.getEnvironmentReporter().getApplicationInfo(), + clientContext.getBaseLogger() + ); if (!tagHeader.isEmpty()) { headers.put("X-LaunchDarkly-Tags", tagHeader); } diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/EventUtil.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/EventUtil.java index b32421a4..1e5f4886 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/EventUtil.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/EventUtil.java @@ -58,9 +58,9 @@ static DiagnosticStore.SdkDiagnosticParams makeDiagnosticParams(ClientContext cl } return new DiagnosticStore.SdkDiagnosticParams( mobileKey, - "android-client-sdk", + LDPackageConsts.SDK_NAME, BuildConfig.VERSION_NAME, - "Android", + LDPackageConsts.SDK_PLATFORM_NAME, LDValue.buildObject().put("androidSDKVersion", Build.VERSION.SDK_INT).build(), headers, Collections.singletonList(configProperties.build()) diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/IContextModifier.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/IContextModifier.java index c8aab349..9b63996c 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/IContextModifier.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/IContextModifier.java @@ -2,13 +2,18 @@ import com.launchdarkly.sdk.LDContext; +/** + * Modifies contexts when invoked. + */ public interface IContextModifier { /** - * Modifies the provided context and returns the resulting context. - * @param context - * @return + * Modifies the provided context and returns a resulting context. May result in no changes at + * the discretion of the implementation. + * + * @param context to be modified + * @return another context that is the result of modification */ - public LDContext modifyContext(LDContext context); + LDContext modifyContext(LDContext context); } diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDClient.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDClient.java index d016ed1f..0fc6c122 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDClient.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDClient.java @@ -12,6 +12,8 @@ import com.launchdarkly.sdk.LDContext; import com.launchdarkly.sdk.LDUser; import com.launchdarkly.sdk.LDValue; +import com.launchdarkly.sdk.android.env.EnvironmentReporterBuilder; +import com.launchdarkly.sdk.android.env.IEnvironmentReporter; import com.launchdarkly.sdk.android.subsystems.Callback; import com.launchdarkly.sdk.android.DataModel.Flag; import com.launchdarkly.sdk.android.subsystems.EventProcessor; @@ -46,10 +48,9 @@ public class LDClient implements LDClientInterface, Closeable { // Will only be set once, during initialization, and the map is considered immutable. static volatile Map instances = null; private static volatile PlatformState sharedPlatformState; - - private static volatile EnvironmentReporter environmentReporter; + private static volatile IEnvironmentReporter environmentReporter; private static volatile TaskExecutor sharedTaskExecutor; - private static volatile TelemetryContextModifier telemetryContextModifier; + private static volatile AutoEnvContextModifier autoEnvContextModifier; private static volatile AnonymousKeyContextModifier anonymousKeyContextModifier; // A lock to ensure calls to `init()` are serialized. @@ -75,7 +76,7 @@ public class LDClient implements LDClientInterface, Closeable { * * @param application your Android application * @param config configuration used to set up the client - * @param inputContext the initial evaluation context; see {@link LDClient} for more information + * @param context the initial evaluation context; see {@link LDClient} for more information * about setting the context and optionally requesting a unique key for it * @return a {@link Future} which will complete once the client has been initialized * @see #init(Application, LDConfig, LDContext, int) @@ -84,7 +85,7 @@ public class LDClient implements LDClientInterface, Closeable { */ public static Future init(@NonNull Application application, @NonNull LDConfig config, - @NonNull LDContext inputContext) { + @NonNull LDContext context) { // As this is an externally facing API we should still check these, so we hide the linter // warnings @@ -97,9 +98,9 @@ public static Future init(@NonNull Application application, return new LDFailedFuture<>(new LaunchDarklyException("Client initialization requires a valid configuration")); } //noinspection ConstantConditions - if (inputContext == null || !inputContext.isValid()) { + if (context == null || !context.isValid()) { return new LDFailedFuture<>(new LaunchDarklyException("Client initialization requires a valid evaluation context (" - + (inputContext == null ? "was null" : inputContext.getError() + ")"))); + + (context == null ? "was null" : context.getError() + ")"))); } LDLogger logger = initSharedLogger(config); @@ -118,8 +119,14 @@ public static Future init(@NonNull Application application, sharedTaskExecutor = new AndroidTaskExecutor(application, logger); sharedPlatformState = new AndroidPlatformState(application, sharedTaskExecutor, logger); - environmentReporter = new EnvironmentReporter(application); - environmentReporter.setApplicationInfo(config.applicationInfo); + + EnvironmentReporterBuilder reporterBuilder = new EnvironmentReporterBuilder(); + reporterBuilder.setApplicationInfo(config.applicationInfo); + + // TODO: sc-204374 - Support opt-in/out + // reporterBuilder.enableCollectionFromPlatform(application); + + environmentReporter = reporterBuilder.build(); PersistentDataStore store = config.getPersistentDataStore() == null ? new SharedPreferencesPersistentDataStore(application, logger) : @@ -128,14 +135,13 @@ public static Future init(@NonNull Application application, store, logger ); - telemetryContextModifier = new TelemetryContextModifier(persistentData, environmentReporter); + autoEnvContextModifier = new AutoEnvContextModifier(persistentData, environmentReporter); anonymousKeyContextModifier = new AnonymousKeyContextModifier(persistentData, config.isGenerateAnonymousKeys()); - // TODO: seems like this migration should be happening before the PersistentDataStoreWrapper exists. Discuss. Migration.migrateWhenNeeded(store, logger); - modifiedContext = telemetryContextModifier.modifyContext(inputContext); + modifiedContext = autoEnvContextModifier.modifyContext(context); modifiedContext = anonymousKeyContextModifier.modifyContext(modifiedContext, logger); // Create, but don't start, every LDClient instance @@ -320,7 +326,7 @@ public static LDClient getForMobileKey(String keyName) throws LaunchDarklyExcept @VisibleForTesting protected LDClient( @NonNull final PlatformState platformState, - @NonNull final EnvironmentReporter environmentReporter, + @NonNull final IEnvironmentReporter environmentReporter, @NonNull final TaskExecutor taskExecutor, @NonNull PersistentDataStoreWrapper.PerEnvironmentData environmentStore, @NonNull LDContext initialContext, @@ -406,7 +412,7 @@ public Future identify(LDContext context) { return new LDFailedFuture<>(new LaunchDarklyException("Invalid context: " + context.getError())); } - LDContext modifiedContext = telemetryContextModifier.modifyContext(context); + LDContext modifiedContext = autoEnvContextModifier.modifyContext(context); modifiedContext = anonymousKeyContextModifier.modifyContext(modifiedContext, getSharedLogger()); return identifyInstances(modifiedContext); } diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDPackageConsts.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDPackageConsts.java new file mode 100644 index 00000000..f309292e --- /dev/null +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDPackageConsts.java @@ -0,0 +1,7 @@ +package com.launchdarkly.sdk.android; + +public class LDPackageConsts { + public static final String SDK_NAME = "android-client-sdk"; + public static final String SDK_PLATFORM_NAME = "Android"; + public static final String SDK_CLIENT_NAME = "AndroidClient"; +} diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDUtil.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDUtil.java index ff817a3b..0d8f4476 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDUtil.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDUtil.java @@ -30,7 +30,7 @@ class LDUtil { static final String AUTH_SCHEME = "api_key "; - static final String USER_AGENT_HEADER_VALUE = "AndroidClient/" + BuildConfig.VERSION_NAME; + static final String USER_AGENT_HEADER_VALUE = LDPackageConsts.SDK_CLIENT_NAME + "/" + BuildConfig.VERSION_NAME; static Callback noOpCallback() { return new Callback() { diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/PersistentDataStoreWrapper.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/PersistentDataStoreWrapper.java index 49876db0..d12574e0 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/PersistentDataStoreWrapper.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/PersistentDataStoreWrapper.java @@ -50,9 +50,6 @@ static class SavedConnectionInfo { private static final String GLOBAL_NAMESPACE = "LaunchDarkly"; private static final String NAMESPACE_PREFIX = "LaunchDarkly_"; - - // TODO: We are now generating keys for non-anonymous contexts (the telemetry contexts). - // At the moment of writing this TODO, the generated keys are using the anonKey prefix. private static final String ANON_CONTEXT_KEY_PREFIX = "anonKey_"; private static final String ENVIRONMENT_METADATA_KEY = "index"; private static final String ENVIRONMENT_CONTEXT_DATA_KEY_PREFIX = "flags_"; @@ -91,7 +88,7 @@ public PerEnvironmentData perEnvironmentData(String mobileKey) { /** * Returns the cached generated key if one exists or generates and saves a key for the - * specified context kind This is not in {@link PerEnvironmentData} because these generated + * specified context kind. This is not in {@link PerEnvironmentData} because these generated * keys are per device+context kind, not per environment. * * @param contextKind a context kind @@ -112,7 +109,7 @@ public String getOrGenerateContextKey(ContextKind contextKind) { return persistedKey; } - // don't have a generated key, so generate a key + // don't have a key, so generate a key final String generatedKey = UUID.randomUUID().toString(); generatedKeysCache.put(contextKind, generatedKey); @@ -123,13 +120,8 @@ public String getOrGenerateContextKey(ContextKind contextKind) { // we've put it into the cachedGeneratedKey map already means any subsequent calls will // get that value and not have to hit the persistent store. - // TODO: Revisit usage of new Thread instead of executor service. - new Thread(new Runnable() { - public void run() { - trySetValue(GLOBAL_NAMESPACE, - ANON_CONTEXT_KEY_PREFIX + contextKind.toString(), generatedKey); - } - }).run(); + new Thread(() -> trySetValue(GLOBAL_NAMESPACE, + ANON_CONTEXT_KEY_PREFIX + contextKind.toString(), generatedKey)).run(); return generatedKey; } @@ -142,7 +134,7 @@ public void run() { * per environment. * * @param contextKind a context kind - * @param key the generated key + * @param key the generated key */ public void setGeneratedContextKey(ContextKind contextKind, String key) { trySetValue(GLOBAL_NAMESPACE, @@ -180,7 +172,7 @@ public EnvironmentData getContextData(String hashedContextId) { * Stores flag data for a specific context, overwriting any previous data for that context. * * @param hashedContextId the hashed key of the context - * @param allData the flag data + * @param allData the flag data */ public void setContextData(String hashedContextId, EnvironmentData allData) { trySetValue(environmentNamespace, keyForContextId(hashedContextId), allData.toJson()); @@ -292,10 +284,10 @@ private void trySetValues(String namespace, Map keysAndValues) { } private void maybeLogStoreError(Exception e) { - if (loggedStorageError.getAndSet(true)) { - return; - } - LDUtil.logExceptionAtErrorLevel(logger, e, "Failure in persistent data store"); + if (loggedStorageError.getAndSet(true)) { + return; + } + LDUtil.logExceptionAtErrorLevel(logger, e, "Failure in persistent data store"); } private Long tryGetValueAsLong(String namespace, String key) { diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/TelemetryContextModifier.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/TelemetryContextModifier.java deleted file mode 100644 index adbcbd38..00000000 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/TelemetryContextModifier.java +++ /dev/null @@ -1,93 +0,0 @@ -package com.launchdarkly.sdk.android; - -import com.launchdarkly.sdk.ContextKind; -import com.launchdarkly.sdk.ContextMultiBuilder; -import com.launchdarkly.sdk.LDContext; -import com.launchdarkly.sdk.LDValue; -import com.launchdarkly.sdk.ObjectBuilder; - -import java.util.ArrayList; -import java.util.List; - -public class TelemetryContextModifier implements IContextModifier { - - // TODO: determine proper home for these constants. - private static final String LD_APPLICATION_KIND = "ld_application"; - private static final String LD_DEVICE_KIND = "ld_device"; - public static final String ATTR_ID = "id"; - public static final String ATTR_NAME = "name"; - public static final String ATTR_VERSION = "version"; - public static final String ATTR_VERSION_NAME = "versionName"; - public static final String ATTR_MANUFACTURER = "manufacturer"; - public static final String ATTR_MODEL = "model"; - public static final String ATTR_LOCALE = "locale"; - public static final String ATTR_OS = "os"; - public static final String ATTR_FAMILY = "family"; - - - private PersistentDataStoreWrapper persistentData; - private EnvironmentReporter environmentReporter; - - public TelemetryContextModifier(PersistentDataStoreWrapper persistentData, - EnvironmentReporter environmentReporter) { - this.persistentData = persistentData; - this.environmentReporter = environmentReporter; - } - - @Override - public LDContext modifyContext(LDContext context) { - - // TODO: update with collision detection logic - - // make cloned builder for context - ContextMultiBuilder builder = LDContext.multiBuilder(); - List contextList = new ArrayList<>(); - if (context.isMultiple()) { - for (int i = 0; i < context.getIndividualContextCount(); i++) { - contextList.add(context.getIndividualContext(i)); - } - } else { - contextList.add(context); - } - - contextList.add(makeLDApplicationKindContext()); - contextList.add(makeLDDeviceKindContext()); - - for (LDContext c : contextList) { - builder.add(c); - } - - return builder.build(); - } - - private LDContext makeLDApplicationKindContext() { - - // TODO: add logic for detecting if no properties are available and if no properties are available, exclude the context kind - - ContextKind ldApplicationKind = ContextKind.of(LD_APPLICATION_KIND); - String key = persistentData.getOrGenerateContextKey(ldApplicationKind); - - return LDContext.builder(ldApplicationKind, key) - .set(ATTR_ID, LDValue.of(environmentReporter.getApplicationInfo().getApplicationId())) - .set(ATTR_NAME, LDValue.of(environmentReporter.getApplicationInfo().getApplicationName())) - .set(ATTR_VERSION, LDValue.of(environmentReporter.getApplicationInfo().getApplicationVersion())) - .set(ATTR_VERSION_NAME, LDValue.of(environmentReporter.getApplicationInfo().getApplicationVersionName())) - .build(); - } - - private LDContext makeLDDeviceKindContext() { - ContextKind ldDeviceKind = ContextKind.of(LD_DEVICE_KIND); - String key = persistentData.getOrGenerateContextKey(ldDeviceKind); - - return LDContext.builder(ldDeviceKind, key) - .set(ATTR_MANUFACTURER, LDValue.of(environmentReporter.getManufacturer())) - .set(ATTR_MODEL, LDValue.of(environmentReporter.getModel())) - .set(ATTR_LOCALE, LDValue.of(environmentReporter.getLocale())) - .set(ATTR_OS, new ObjectBuilder() - .put(ATTR_FAMILY, environmentReporter.getOSFamily()) - .put(ATTR_VERSION, environmentReporter.getOSVersion()) - .build() - ) - .build(); - } -} \ No newline at end of file diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/EnvironmentReporter.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/env/AndroidEnvironmentReporter.java similarity index 57% rename from launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/EnvironmentReporter.java rename to launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/env/AndroidEnvironmentReporter.java index d57f6cb0..c8cb12f5 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/EnvironmentReporter.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/env/AndroidEnvironmentReporter.java @@ -1,70 +1,89 @@ -package com.launchdarkly.sdk.android; +package com.launchdarkly.sdk.android.env; import android.app.Application; -import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.os.Build; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; +import com.launchdarkly.sdk.android.LDPackageConsts; import com.launchdarkly.sdk.android.subsystems.ApplicationInfo; import java.util.Locale; -// TODO: Come up with better name than EnvironmentReporter. This is confusing since LD has environments -// as a different concept -public class EnvironmentReporter { +/** + * An {@link IEnvironmentReporter} that can fetch environment attributes from Android platform. + */ +class AndroidEnvironmentReporter extends EnvironmentReporterChainBase implements IEnvironmentReporter { - // TODO: Should we rename EnvironmentReporter to SDKEnvironmentReporter - private Application application; - private ApplicationInfo applicationInfo; + // application reference used to make API queries + @NonNull + private final Application application; /** - * Creates an environment reporter. - * @param application that represents the application environment this code is running in. + * @param application that represents the Android application this code is running in. */ - public EnvironmentReporter(Application application) { + public AndroidEnvironmentReporter(Application application) { this.application = application; } - /** - * Sets the application info that this environment reporter will report when asked in the future. - * @param applicationInfo to report. - */ - public void setApplicationInfo(ApplicationInfo applicationInfo) { - this.applicationInfo = applicationInfo; + @Override + @NonNull + public ApplicationInfo getApplicationInfo() { + return new ApplicationInfo( + getApplicationID(), + getApplicationName(), + getApplicationVersion(), + getApplicationVersionName() + ); } - /** - * Gets the {@link ApplicationInfo} for the application environment. If no {@link ApplicationInfo} - * has been provided via {@link #setApplicationInfo(ApplicationInfo)}, the {@link EnvironmentReporter} - * will collect application info automatically. - */ @NonNull - public ApplicationInfo getApplicationInfo() { - // First priority is to return the application info that was provided manually. - // Second priority is to use application info fetched from the Android API. - if (applicationInfo == null) { - applicationInfo = new com.launchdarkly.sdk.android.subsystems.ApplicationInfo( - getApplicationID(), - getApplicationName(), - getApplicationVersion(), - getApplicationVersionName() - ); + @Override + public String getManufacturer() { + return Build.MANUFACTURER; + } + + @NonNull + @Override + public String getModel() { + return Build.MODEL; + } + + @NonNull + @Override + public String getLocale() { + Locale locale; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + locale = application.getResources().getConfiguration().getLocales().get(0); + } else { + locale = application.getResources().getConfiguration().locale; } + return locale.toLanguageTag(); + } - return applicationInfo; + @NonNull + @Override + public String getOSFamily() { + return LDPackageConsts.SDK_PLATFORM_NAME; + } + + @NonNull + @Override + public String getOSVersion() { + return Build.VERSION.RELEASE; } + @NonNull private String getApplicationID() { return application.getPackageName(); } + @NonNull private String getApplicationName() { try { PackageManager pm = application.getPackageManager(); - android.content.pm.ApplicationInfo ai = pm.getApplicationInfo( application.getPackageName(), 0); + android.content.pm.ApplicationInfo ai = pm.getApplicationInfo(application.getPackageName(), 0); return pm.getApplicationLabel(ai).toString(); } catch (PackageManager.NameNotFoundException e) { // TODO: investigate if this runtime exception can actually happen since we just @@ -74,6 +93,7 @@ private String getApplicationName() { } } + @NonNull private String getApplicationVersion() { try { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { @@ -89,42 +109,14 @@ private String getApplicationVersion() { } } + @NonNull private String getApplicationVersionName() { try { PackageManager pm = application.getPackageManager(); - return pm.getPackageInfo( application.getPackageName(), 0).versionName; + return pm.getPackageInfo(application.getPackageName(), 0).versionName; } catch (PackageManager.NameNotFoundException e) { // TODO: figure out if there is a way to get around this exception more elegantly. throw new RuntimeException(e); } } - - public String getManufacturer() { - return Build.MANUFACTURER; - } - - public String getModel() { - return Build.MODEL; - } - - /** - * @return a BCP47 language tag representing the locale - */ - public String getLocale() { - Locale locale; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N){ - locale = application.getResources().getConfiguration().getLocales().get(0); - } else{ - locale = application.getResources().getConfiguration().locale; - } - return locale.toLanguageTag(); - } - - public String getOSFamily() { - return "Android"; - } - - public String getOSVersion() { - return Build.VERSION.RELEASE; - } } diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/env/ApplicationInfoEnvironmentReporter.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/env/ApplicationInfoEnvironmentReporter.java new file mode 100644 index 00000000..7ce24d2c --- /dev/null +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/env/ApplicationInfoEnvironmentReporter.java @@ -0,0 +1,24 @@ +package com.launchdarkly.sdk.android.env; + +import androidx.annotation.NonNull; + +import com.launchdarkly.sdk.android.subsystems.ApplicationInfo; + +/** + * An {@link IEnvironmentReporter} that reports the provided {@link ApplicationInfo} for {@link #getApplicationInfo()} + * and defers all other attributes to the next reporter in the chain. + */ +class ApplicationInfoEnvironmentReporter extends EnvironmentReporterChainBase implements IEnvironmentReporter { + + private ApplicationInfo applicationInfo; + + public ApplicationInfoEnvironmentReporter(ApplicationInfo applicationInfo) { + this.applicationInfo = applicationInfo; + } + + @NonNull + @Override + public ApplicationInfo getApplicationInfo() { + return applicationInfo; + } +} diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/env/EnvironmentReporterBuilder.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/env/EnvironmentReporterBuilder.java new file mode 100644 index 00000000..6c8fb03d --- /dev/null +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/env/EnvironmentReporterBuilder.java @@ -0,0 +1,70 @@ +package com.launchdarkly.sdk.android.env; + +import android.app.Application; + +import androidx.annotation.Nullable; + +import com.launchdarkly.sdk.android.subsystems.ApplicationInfo; + +import java.util.ArrayList; +import java.util.List; + +/** + * Builder for making an {@link IEnvironmentReporter} with various options. + */ +public class EnvironmentReporterBuilder { + + @Nullable + private Application application; + + @Nullable + private ApplicationInfo applicationInfo; + + /** + * Sets the application info that this environment reporter will report when asked in the future, + * overriding the automatically sourced {@link ApplicationInfo} + * + * @param applicationInfo to report. + */ + public void setApplicationInfo(ApplicationInfo applicationInfo) { + this.applicationInfo = applicationInfo; + } + + /** + * Enables automatically collecting attributes from the platform. + * + * @param application reference for platform calls + */ + public void enableCollectionFromPlatform(Application application) { + this.application = application; + } + + public IEnvironmentReporter build() { + /** + * Create chain of responsibility with the following priority order + * 1. {@link ApplicationInfoEnvironmentReporter} - holds customer override + * 2. {@link AndroidEnvironmentReporter} - Android platform API next + * 3. {@link SDKEnvironmentReporter} - Fallback is SDK constants + */ + List reporters = new ArrayList<>(); + + if (applicationInfo != null) { + reporters.add(new ApplicationInfoEnvironmentReporter(applicationInfo)); + } + + if (application != null) { + reporters.add(new AndroidEnvironmentReporter(application)); + } + + // always add fallback reporter + reporters.add(new SDKEnvironmentReporter()); + + // build chain of responsibility by iterating on all but last element + for (int i = 0; i < reporters.size() - 1; i++) { + reporters.get(i).setNext(reporters.get(i + 1)); + } + + // guaranteed non-empty since fallback reporter is always added + return reporters.get(0); + } +} diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/env/EnvironmentReporterChainBase.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/env/EnvironmentReporterChainBase.java new file mode 100644 index 00000000..2dd55ed1 --- /dev/null +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/env/EnvironmentReporterChainBase.java @@ -0,0 +1,58 @@ +package com.launchdarkly.sdk.android.env; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.launchdarkly.sdk.android.subsystems.ApplicationInfo; + +/** + * Base implementation for using {@link IEnvironmentReporter}s in a chain of responsibility pattern. + */ +class EnvironmentReporterChainBase implements IEnvironmentReporter { + + private static final String UNKNOWN = "unknown"; + + // the next reporter in the chain if there is one + @Nullable + protected EnvironmentReporterChainBase next; + + public void setNext(EnvironmentReporterChainBase next) { + this.next = next; + } + + @NonNull + @Override + public ApplicationInfo getApplicationInfo() { + return next != null ? next.getApplicationInfo() : new ApplicationInfo(UNKNOWN, UNKNOWN, UNKNOWN, UNKNOWN); + } + + @NonNull + @Override + public String getManufacturer() { + return next != null ? next.getManufacturer() : UNKNOWN; + } + + @NonNull + @Override + public String getModel() { + return next != null ? next.getModel() : UNKNOWN; + } + + @NonNull + @Override + public String getLocale() { + return next != null ? next.getLocale() : UNKNOWN; + } + + @NonNull + @Override + public String getOSFamily() { + return next != null ? next.getOSFamily() : UNKNOWN; + } + + @NonNull + @Override + public String getOSVersion() { + return next != null ? next.getOSVersion() : UNKNOWN; + } +} diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/env/IEnvironmentReporter.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/env/IEnvironmentReporter.java new file mode 100644 index 00000000..12da9c9d --- /dev/null +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/env/IEnvironmentReporter.java @@ -0,0 +1,48 @@ +package com.launchdarkly.sdk.android.env; + +import androidx.annotation.NonNull; + +import com.launchdarkly.sdk.android.subsystems.ApplicationInfo; + +/** + * Reports information about the software/hardware environment that the SDK is + * executing within. + */ +public interface IEnvironmentReporter { + + /** + * @return the {@link ApplicationInfo} for the application environment. + */ + @NonNull + ApplicationInfo getApplicationInfo(); + + /** + * @return the manufacturer of the device the application is running in + */ + @NonNull + String getManufacturer(); + + /** + * @return the model of the device the application is running in + */ + @NonNull + String getModel(); + + /** + * @return a BCP47 language tag representing the locale + */ + @NonNull + String getLocale(); + + /** + * @return the OS Family that this application is running in + */ + @NonNull + String getOSFamily(); + + /** + * @return the version of the OS that this application is running in + */ + @NonNull + String getOSVersion(); +} diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/env/SDKEnvironmentReporter.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/env/SDKEnvironmentReporter.java new file mode 100644 index 00000000..0bfd293a --- /dev/null +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/env/SDKEnvironmentReporter.java @@ -0,0 +1,25 @@ +package com.launchdarkly.sdk.android.env; + +import androidx.annotation.NonNull; + +import com.launchdarkly.sdk.android.BuildConfig; +import com.launchdarkly.sdk.android.LDPackageConsts; +import com.launchdarkly.sdk.android.subsystems.ApplicationInfo; + +/** + * An {@link IEnvironmentReporter} that reports static SDK information for {@link #getApplicationInfo()} + * and defers all other attributes to the next reporter in the chain. + */ +class SDKEnvironmentReporter extends EnvironmentReporterChainBase implements IEnvironmentReporter { + + @NonNull + @Override + public ApplicationInfo getApplicationInfo() { + return new ApplicationInfo( + LDPackageConsts.SDK_NAME, + LDPackageConsts.SDK_NAME, + BuildConfig.VERSION_NAME, + BuildConfig.VERSION_NAME + ); + } +} diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/ApplicationInfoBuilder.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/ApplicationInfoBuilder.java index 6605c77a..c2ddb823 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/ApplicationInfoBuilder.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/ApplicationInfoBuilder.java @@ -52,6 +52,16 @@ public ApplicationInfoBuilder applicationId(String applicationId) { return this; } + /** + * Sets a user friendly name for the application in which the LaunchDarkly SDK is running. + *

+ * This can be specified as any string value as long as it only uses the following characters: ASCII + * letters, ASCII digits, period, hyphen, underscore. A string containing any other characters will be + * ignored. + * + * @param applicationName the user friendly name + * @return the builder + */ public ApplicationInfoBuilder applicationName(String applicationName) { this.applicationName = applicationName; return this; @@ -65,7 +75,7 @@ public ApplicationInfoBuilder applicationName(String applicationName) { * letters, ASCII digits, period, hyphen, underscore. A string containing any other characters will be * ignored. * - * @param applicationVersion the application version + * @param version the application version * @return the builder */ public ApplicationInfoBuilder applicationVersion(String version) { @@ -73,6 +83,16 @@ public ApplicationInfoBuilder applicationVersion(String version) { return this; } + /** + * Sets a user friendly name for the version of the application in which the LaunchDarkly SDK is running. + *

+ * This can be specified as any string value as long as it only uses the following characters: ASCII + * letters, ASCII digits, period, hyphen, underscore. A string containing any other characters will be + * ignored. + * + * @param versionName the user friendly version name + * @return the builder + */ public ApplicationInfoBuilder applicationVersionName(String versionName) { this.applicationVersionName = versionName; return this; diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/subsystems/ApplicationInfo.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/subsystems/ApplicationInfo.java index 64438f60..83f8c7bd 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/subsystems/ApplicationInfo.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/subsystems/ApplicationInfo.java @@ -49,10 +49,20 @@ public String getApplicationVersion() { return applicationVersion; } + /** + * A user friendly name for the application in which the LaunchDarkly SDK is running. + * + * @return the friendly name of the application, or null + */ public String getApplicationName() { return applicationName; } + /** + * A user friendly name for the version of the application in which the LaunchDarkly SDK is running. + * + * @return the friendly name of the version, or null + */ public String getApplicationVersionName() { return applicationVersionName; } diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/subsystems/ClientContext.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/subsystems/ClientContext.java index 308c239e..570d4b49 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/subsystems/ClientContext.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/subsystems/ClientContext.java @@ -1,10 +1,8 @@ package com.launchdarkly.sdk.android.subsystems; -import androidx.annotation.Nullable; - import com.launchdarkly.logging.LDLogger; import com.launchdarkly.sdk.LDContext; -import com.launchdarkly.sdk.android.EnvironmentReporter; +import com.launchdarkly.sdk.android.env.IEnvironmentReporter; import com.launchdarkly.sdk.android.LDClient; import com.launchdarkly.sdk.android.LDConfig; import com.launchdarkly.sdk.android.interfaces.ServiceEndpoints; @@ -25,10 +23,7 @@ * @since 3.3.0 */ public class ClientContext { - private final ApplicationInfo applicationInfo; - - private final EnvironmentReporter environmentReporter; - + private final IEnvironmentReporter environmentReporter; private final LDLogger baseLogger; private final LDConfig config; private final DataSourceUpdateSink dataSourceUpdateSink; @@ -46,6 +41,7 @@ public class ClientContext { * Constructs an instance, specifying all properties. * * @param mobileKey see {@link #getMobileKey()} + * @param environmentReporter see {@link #getEnvironmentReporter()} * @param baseLogger see {@link #getBaseLogger()} * @param config see {@link #getConfig()} * @param dataSourceUpdateSink see {@link #getDataSourceUpdateSink()} @@ -60,8 +56,7 @@ public class ClientContext { */ public ClientContext( String mobileKey, - ApplicationInfo applicationInfo, - EnvironmentReporter environmentReporter, + IEnvironmentReporter environmentReporter, LDLogger baseLogger, LDConfig config, DataSourceUpdateSink dataSourceUpdateSink, @@ -75,7 +70,6 @@ public ClientContext( boolean setOffline ) { this.mobileKey = mobileKey; - this.applicationInfo = applicationInfo; this.environmentReporter = environmentReporter; this.baseLogger = baseLogger; this.config = config; @@ -90,83 +84,9 @@ public ClientContext( this.setOffline = setOffline; } - /** - * Deprecated constructor overload. - * - * @param mobileKey see {@link #getMobileKey()} - * @param baseLogger see {@link #getBaseLogger()} - * @param config see {@link #getConfig()} - * @param dataSourceUpdateSink see {@link #getDataSourceUpdateSink()} - * @param environmentName see {@link #getEnvironmentName()} - * @param evaluationReasons see {@link #isEvaluationReasons()} - * @param evaluationContext see {@link #getEvaluationContext()} - * @param http see {@link #getHttp()} - * @param inBackground see {@link #isInBackground()} - * @param serviceEndpoints see {@link #getServiceEndpoints()} - * @param setOffline see {@link #isSetOffline()} - * @deprecated use newer constructor - */ - @Deprecated - public ClientContext( - String mobileKey, - EnvironmentReporter environmentReporter, - LDLogger baseLogger, - LDConfig config, - DataSourceUpdateSink dataSourceUpdateSink, - String environmentName, - boolean evaluationReasons, - LDContext evaluationContext, - HttpConfiguration http, - boolean inBackground, - Boolean previouslyInBackground, - ServiceEndpoints serviceEndpoints, - boolean setOffline - ) { - this(mobileKey, null, environmentReporter, baseLogger, config, dataSourceUpdateSink, environmentName, - evaluationReasons, evaluationContext, http, inBackground, previouslyInBackground, - serviceEndpoints, setOffline); - } - - /** - * Deprecated constructor overload. - * - * @param mobileKey see {@link #getMobileKey()} - * @param baseLogger see {@link #getBaseLogger()} - * @param config see {@link #getConfig()} - * @param dataSourceUpdateSink see {@link #getDataSourceUpdateSink()} - * @param environmentName see {@link #getEnvironmentName()} - * @param evaluationReasons see {@link #isEvaluationReasons()} - * @param evaluationContext see {@link #getEvaluationContext()} - * @param http see {@link #getHttp()} - * @param inBackground see {@link #isInBackground()} - * @param serviceEndpoints see {@link #getServiceEndpoints()} - * @param setOffline see {@link #isSetOffline()} - * @deprecated use newer constructor - */ - @Deprecated - public ClientContext( - String mobileKey, - LDLogger baseLogger, - LDConfig config, - DataSourceUpdateSink dataSourceUpdateSink, - String environmentName, - boolean evaluationReasons, - LDContext evaluationContext, - HttpConfiguration http, - boolean inBackground, - ServiceEndpoints serviceEndpoints, - boolean setOffline - ) { - this(mobileKey, null, baseLogger, config, dataSourceUpdateSink, environmentName, - evaluationReasons, evaluationContext, http, inBackground, - null, - serviceEndpoints, setOffline); - } - protected ClientContext(ClientContext copy) { this( copy.mobileKey, - copy.applicationInfo, copy.environmentReporter, copy.baseLogger, copy.config, @@ -183,15 +103,9 @@ protected ClientContext(ClientContext copy) { } /** - * The application metadata object. - * @return the application metadata + * @return the {@link IEnvironmentReporter} for this client context */ - @Nullable - public ApplicationInfo getApplicationInfo() { - return applicationInfo; - } - - public EnvironmentReporter getEnvironmentReporter() { + public IEnvironmentReporter getEnvironmentReporter() { return environmentReporter; } diff --git a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/AnonymousKeyContextModifierTest.java b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/AnonymousKeyContextModifierTest.java index 496f1460..f468c7c9 100644 --- a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/AnonymousKeyContextModifierTest.java +++ b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/AnonymousKeyContextModifierTest.java @@ -41,7 +41,6 @@ public void singleKindAnonymousContextGetsGeneratedKeyIfConfigOptionIsSet() { LDContext transformed = makeDecoratorWithoutPersistence(true) .modifyContext(context, logging.logger); assertContextHasBeenTransformedWithNewKey(context, transformed); - logging.assertInfoLogged("Did not find a generated anonymous key for context kind \"user\""); } @Test diff --git a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/AutoEnvContextModifierTest.java b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/AutoEnvContextModifierTest.java new file mode 100644 index 00000000..2a4d280f --- /dev/null +++ b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/AutoEnvContextModifierTest.java @@ -0,0 +1,148 @@ +package com.launchdarkly.sdk.android; + +import com.launchdarkly.sdk.ContextKind; +import com.launchdarkly.sdk.LDContext; +import com.launchdarkly.sdk.ObjectBuilder; +import com.launchdarkly.sdk.android.env.EnvironmentReporterBuilder; + +import org.junit.Assert; +import org.junit.Test; + +public class AutoEnvContextModifierTest { + + /** + * Requirement 1.2.2.1 - Schema adherence + * Requirement 1.2.2.2 - Adding all context kinds + * Requirement 1.2.2.3 - Adding all attributes + * Requirement 1.2.2.5 - Schema version in _meta + */ + @Test + public void adheresToSchemaTest() { + PersistentDataStoreWrapper wrapper = TestUtil.makeSimplePersistentDataStoreWrapper(); + AutoEnvContextModifier underTest = new AutoEnvContextModifier( + wrapper, + new EnvironmentReporterBuilder().build() + ); + + LDContext input = LDContext.builder(ContextKind.of("aKind"), "aKey") + .set("dontOverwriteMeBro", "really bro").build(); + LDContext output = underTest.modifyContext(input); + + // it is important that we create this expected context after the code runs because there + // will be persistence side effects + ContextKind applicationKind = ContextKind.of(AutoEnvContextModifier.LD_APPLICATION_KIND); + LDContext expectedAppContext = LDContext.builder(applicationKind, wrapper.getOrGenerateContextKey(applicationKind)) + .set(AutoEnvContextModifier.ATTR_ID, LDPackageConsts.SDK_NAME) + .set(AutoEnvContextModifier.ATTR_NAME, LDPackageConsts.SDK_NAME) + .set(AutoEnvContextModifier.ATTR_VERSION, BuildConfig.VERSION_NAME) + .set(AutoEnvContextModifier.ATTR_VERSION_NAME, BuildConfig.VERSION_NAME) + .set("_meta", "0.1") + .build(); + + ContextKind deviceKind = ContextKind.of(AutoEnvContextModifier.LD_DEVICE_KIND); + LDContext expectedDeviceContext = LDContext.builder(deviceKind, wrapper.getOrGenerateContextKey(deviceKind)) + .set(AutoEnvContextModifier.ATTR_MANUFACTURER, "unknown") + .set(AutoEnvContextModifier.ATTR_MODEL, "unknown") + .set(AutoEnvContextModifier.ATTR_LOCALE, "unknown") + .set(AutoEnvContextModifier.ATTR_OS, new ObjectBuilder() + .put(AutoEnvContextModifier.ATTR_FAMILY, "unknown") + .put(AutoEnvContextModifier.ATTR_VERSION, "unknown") + .build()) + .set("_meta", "0.1") + .build(); + + LDContext expectedOutput = LDContext.multiBuilder().add(input).add(expectedAppContext).add(expectedDeviceContext).build(); + + Assert.assertEquals(expectedOutput, output); + + // TODO: Figure out how to set _meta + Assert.fail("_meta is not yet implemented."); + } + + /** + * Requirement 1.2.5.1 - Doesn't change customer provided data + */ + @Test + public void doesNotOverwriteCustomerDataTest() { + + PersistentDataStoreWrapper wrapper = TestUtil.makeSimplePersistentDataStoreWrapper(); + AutoEnvContextModifier underTest = new AutoEnvContextModifier( + wrapper, + new EnvironmentReporterBuilder().build() + ); + + LDContext input = LDContext.builder(ContextKind.of("ld_application"), "aKey") + .set("dontOverwriteMeBro", "really bro").build(); + LDContext output = underTest.modifyContext(input); + + // it is important that we create this expected context after the code runs because there + // will be persistence side effects + ContextKind deviceKind = ContextKind.of(AutoEnvContextModifier.LD_DEVICE_KIND); + LDContext expectedDeviceContext = LDContext.builder(deviceKind, wrapper.getOrGenerateContextKey(deviceKind)) + .set(AutoEnvContextModifier.ATTR_MANUFACTURER, "unknown") + .set(AutoEnvContextModifier.ATTR_MODEL, "unknown") + .set(AutoEnvContextModifier.ATTR_LOCALE, "unknown") + .set(AutoEnvContextModifier.ATTR_OS, new ObjectBuilder() + .put(AutoEnvContextModifier.ATTR_FAMILY, "unknown") + .put(AutoEnvContextModifier.ATTR_VERSION, "unknown") + .build()) + .set("_meta", "0.1") + .build(); + + LDContext expectedOutput = LDContext.multiBuilder().add(input).add(expectedDeviceContext).build(); + + Assert.assertEquals(expectedOutput, output); + + // TODO: Figure out how to set _meta + Assert.fail("_meta is not yet implemented."); + } + + /** + * Requirement 1.2.5.1 - Doesn't change customer provided data + */ + @Test + public void doesNotOverwriteCustomerDataMultiContextTest() { + + PersistentDataStoreWrapper wrapper = TestUtil.makeSimplePersistentDataStoreWrapper(); + AutoEnvContextModifier underTest = new AutoEnvContextModifier( + wrapper, + new EnvironmentReporterBuilder().build() + ); + + LDContext input1 = LDContext.builder(ContextKind.of("ld_application"), "aKey") + .set("dontOverwriteMeBro", "really bro").build(); + LDContext input2 = LDContext.builder(ContextKind.of("ld_device"), "anotherKey") + .set("AndDontOverwriteThisEither", "bro").build(); + LDContext multiContextInput = LDContext.multiBuilder().add(input1).add(input2).build(); + LDContext output = underTest.modifyContext(multiContextInput); + + // input and output should be the same + Assert.assertEquals(multiContextInput, output); + + // TODO: Figure out how to set _meta + //Assert.fail("_meta is not yet being checked."); + } + + /** + * Requirement 1.2.6.3 - Generated keys are consistent + */ + @Test + public void generatesConsistentKeysAcrossMultipleCalls() { + PersistentDataStoreWrapper wrapper = TestUtil.makeSimplePersistentDataStoreWrapper(); + AutoEnvContextModifier underTest = new AutoEnvContextModifier( + wrapper, + new EnvironmentReporterBuilder().build() + ); + + LDContext input = LDContext.builder(ContextKind.of("aKind"), "aKey") + .set("dontOverwriteMeBro", "really bro").build(); + + LDContext output1 = underTest.modifyContext(input); + String key1 = output1.getIndividualContext("ld_application").getKey(); + + LDContext output2 = underTest.modifyContext(input); + String key2 = output2.getIndividualContext("ld_application").getKey(); + + Assert.assertEquals(key1, key2); + } +} diff --git a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/ConnectivityManagerTest.java b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/ConnectivityManagerTest.java index 12fa08da..8a157d66 100644 --- a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/ConnectivityManagerTest.java +++ b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/ConnectivityManagerTest.java @@ -6,6 +6,8 @@ import com.launchdarkly.sdk.LDContext; import com.launchdarkly.sdk.LDValue; import com.launchdarkly.sdk.android.ConnectionInformation.ConnectionMode; +import com.launchdarkly.sdk.android.env.EnvironmentReporterBuilder; +import com.launchdarkly.sdk.android.env.IEnvironmentReporter; import com.launchdarkly.sdk.android.subsystems.Callback; import com.launchdarkly.sdk.android.subsystems.ClientContext; import com.launchdarkly.sdk.android.subsystems.ComponentConfigurer; @@ -69,6 +71,8 @@ public class ConnectivityManagerTest extends EasyMockSupport { private final TaskExecutor taskExecutor = new SimpleTestTaskExecutor(); private final MockPlatformState mockPlatformState = new MockPlatformState(); + private final IEnvironmentReporter environmentReporter = new EnvironmentReporterBuilder().build(); + private PersistentDataStoreWrapper.PerEnvironmentData environmentStore; private final BlockingQueue receivedClientContexts = new LinkedBlockingQueue<>(); private final BlockingQueue startedDataSources = new LinkedBlockingQueue<>(); @@ -107,6 +111,7 @@ private void createTestManager( CONTEXT, logging.logger, mockPlatformState, + environmentReporter, taskExecutor ); diff --git a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/ContextDataManagerTestBase.java b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/ContextDataManagerTestBase.java index 2cc86089..c0b077da 100644 --- a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/ContextDataManagerTestBase.java +++ b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/ContextDataManagerTestBase.java @@ -7,6 +7,8 @@ import static org.junit.Assert.fail; import com.launchdarkly.sdk.LDContext; +import com.launchdarkly.sdk.android.env.EnvironmentReporterBuilder; +import com.launchdarkly.sdk.android.env.IEnvironmentReporter; import com.launchdarkly.sdk.android.subsystems.ClientContext; import com.launchdarkly.sdk.android.subsystems.PersistentDataStore; @@ -20,6 +22,7 @@ public abstract class ContextDataManagerTestBase extends EasyMockSupport { protected final PersistentDataStore store; protected final PersistentDataStoreWrapper.PerEnvironmentData environmentStore; + protected final IEnvironmentReporter environmentReporter = new EnvironmentReporterBuilder().build(); protected final SimpleTestTaskExecutor taskExecutor = new SimpleTestTaskExecutor(); @Rule @@ -50,6 +53,7 @@ protected ContextDataManager createDataManager(int maxCachedContexts) { INITIAL_CONTEXT, logging.logger, null, + environmentReporter, taskExecutor ); return new ContextDataManager( diff --git a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/DiagnosticConfigTest.java b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/DiagnosticConfigTest.java index 7757fd6c..50608575 100644 --- a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/DiagnosticConfigTest.java +++ b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/DiagnosticConfigTest.java @@ -3,6 +3,7 @@ import com.launchdarkly.logging.LDLogger; import com.launchdarkly.sdk.LDValue; import com.launchdarkly.sdk.ObjectBuilder; +import com.launchdarkly.sdk.android.env.EnvironmentReporterBuilder; import com.launchdarkly.sdk.android.integrations.EventProcessorBuilder; import com.launchdarkly.sdk.android.integrations.StreamingDataSourceBuilder; import com.launchdarkly.sdk.android.subsystems.ClientContext; @@ -140,7 +141,7 @@ public void customDiagnosticConfigurationHttp() throws Exception { private static LDValue makeDiagnosticJson(LDConfig config) throws Exception { ClientContext clientContext = ClientContextImpl.fromConfig(config, "", "", - null, null, LDLogger.none(), null, null); + null, null, LDLogger.none(), null, new EnvironmentReporterBuilder().build(), null); DiagnosticStore.SdkDiagnosticParams params = EventUtil.makeDiagnosticParams(clientContext); DiagnosticStore diagnosticStore = new DiagnosticStore(params); MockDiagnosticEventSender mockSender = new MockDiagnosticEventSender(); diff --git a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/HttpConfigurationBuilderTest.java b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/HttpConfigurationBuilderTest.java index 559b0711..8b78441a 100644 --- a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/HttpConfigurationBuilderTest.java +++ b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/HttpConfigurationBuilderTest.java @@ -1,6 +1,6 @@ package com.launchdarkly.sdk.android; -import com.launchdarkly.sdk.android.subsystems.ApplicationInfo; +import com.launchdarkly.sdk.android.env.EnvironmentReporterBuilder; import com.launchdarkly.sdk.android.subsystems.ClientContext; import com.launchdarkly.sdk.android.subsystems.HttpConfiguration; @@ -14,13 +14,15 @@ public class HttpConfigurationBuilderTest { private static final String MOBILE_KEY = "mobile-key"; - private static final ClientContext BASIC_CONTEXT = new ClientContext(MOBILE_KEY, null, null, null, null, "", - false, null, null, false, null, null, false); + private static final ClientContext BASIC_CONTEXT = new ClientContext(MOBILE_KEY, new EnvironmentReporterBuilder().build(), + null, null, null, "", false, null, null, false, null, null, false); private static Map buildBasicHeaders() { Map ret = new HashMap<>(); ret.put("Authorization", LDUtil.AUTH_SCHEME + MOBILE_KEY); ret.put("User-Agent", LDUtil.USER_AGENT_HEADER_VALUE); + ret.put("X-LaunchDarkly-Tags", "application-id/" + LDPackageConsts.SDK_NAME + " application-version/" + + BuildConfig.VERSION_NAME + " application-version-name/" + BuildConfig.VERSION_NAME); return ret; } @@ -65,12 +67,11 @@ public void testWrapperWithVersion() { @Test public void testApplicationTags() { - ApplicationInfo info = new ApplicationInfo("authentication-service", "1.0.0"); - ClientContext contextWithTags = new ClientContext(MOBILE_KEY, info, null, null, null, - "", false, null, null, false, null, null, false); + ClientContext contextWithTags = new ClientContext(MOBILE_KEY, new EnvironmentReporterBuilder().build(), + null, null, null, "", false, null, null, false, null, null, false); HttpConfiguration hc = Components.httpConfiguration() .build(contextWithTags); - assertEquals("application-id/authentication-service application-version/1.0.0", + assertEquals("application-id/" + LDPackageConsts.SDK_NAME + " application-version/" + BuildConfig.VERSION_NAME + " application-version-name/" + BuildConfig.VERSION_NAME , toMap(hc.getDefaultHeaders()).get("X-LaunchDarkly-Tags")); } } diff --git a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/LDConfigTest.java b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/LDConfigTest.java index 991ade17..c85802f8 100644 --- a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/LDConfigTest.java +++ b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/LDConfigTest.java @@ -16,6 +16,7 @@ import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import com.launchdarkly.sdk.android.env.EnvironmentReporterBuilder; import com.launchdarkly.sdk.android.subsystems.ClientContext; public class LDConfigTest { @@ -73,11 +74,11 @@ Map headersToMap(Headers headers) { public void headersForEnvironment() { LDConfig config = new LDConfig.Builder().mobileKey("test-key").build(); ClientContext clientContext = ClientContextImpl.fromConfig(config, "test-key", "", - null, null, null, null, null); + null, null, null, null, new EnvironmentReporterBuilder().build(), null); Map headers = headersToMap( LDUtil.makeHttpProperties(clientContext).toHeadersBuilder().build() ); - assertEquals(2, headers.size()); + assertEquals(3, headers.size()); assertEquals(LDUtil.USER_AGENT_HEADER_VALUE, headers.get("user-agent")); assertEquals("api_key test-key", headers.get("authorization")); } @@ -94,14 +95,17 @@ public void headersForEnvironmentWithTransform() { })) .build(); ClientContext clientContext = ClientContextImpl.fromConfig(config, "test-key", "", - null, null, null, null, null); + null, null, null, null, new EnvironmentReporterBuilder().build(), null); expected.put("User-Agent", LDUtil.USER_AGENT_HEADER_VALUE); expected.put("Authorization", "api_key test-key"); + expected.put("X-LaunchDarkly-Tags", "application-id/" + LDPackageConsts.SDK_NAME + " application-version/" + + BuildConfig.VERSION_NAME + " application-version-name/" + BuildConfig.VERSION_NAME); Map headers = headersToMap( LDUtil.makeHttpProperties(clientContext).toHeadersBuilder().build() ); - assertEquals(2, headers.size()); + + assertEquals(3, headers.size()); assertEquals("api_key test-key, more", headers.get("authorization")); assertEquals("value", headers.get("new")); } diff --git a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/MigrationTest.java b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/MigrationTest.java index bcbe1530..13621b33 100644 --- a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/MigrationTest.java +++ b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/MigrationTest.java @@ -12,8 +12,6 @@ import org.junit.Rule; import org.junit.Test; -import java.util.Collections; - public class MigrationTest extends EasyMockSupport { private final PersistentDataStore store = new InMemoryPersistentDataStore(); @@ -63,7 +61,7 @@ public void migratesGeneratedAnonUserKey() { assertCurrentSchemaIdIsPresent(); PersistentDataStoreWrapper w = new PersistentDataStoreWrapper(store, logging.logger); - assertThat(w.getGeneratedContextKey(ContextKind.DEFAULT), equalTo(generatedKey)); + assertThat(w.getOrGenerateContextKey(ContextKind.DEFAULT), equalTo(generatedKey)); } private void assertCurrentSchemaIdIsPresent() { diff --git a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/PersistentDataStoreWrapperTest.java b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/PersistentDataStoreWrapperTest.java index 20b2729f..bb635961 100644 --- a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/PersistentDataStoreWrapperTest.java +++ b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/PersistentDataStoreWrapperTest.java @@ -13,8 +13,11 @@ import org.junit.Rule; import org.junit.Test; +import static org.easymock.EasyMock.anyString; +import static org.easymock.EasyMock.eq; import static org.easymock.EasyMock.expect; import static org.easymock.EasyMock.expectLastCall; +import static org.easymock.EasyMock.verify; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; @@ -205,38 +208,50 @@ public void setIndexWhenStoreThrowsException() { } @Test - public void getGeneratedContextKey() { + public void getOrGenerateContextKeyExisting() { expect(mockPersistentStore.getValue(EXPECTED_GLOBAL_NAMESPACE, EXPECTED_GENERATED_CONTEXT_KEY_PREFIX + "user")).andReturn("key1"); expect(mockPersistentStore.getValue(EXPECTED_GLOBAL_NAMESPACE, EXPECTED_GENERATED_CONTEXT_KEY_PREFIX + "org")).andReturn("key2"); replayAll(); - assertEquals("key1", wrapper.getGeneratedContextKey(ContextKind.DEFAULT)); - assertEquals("key2", wrapper.getGeneratedContextKey(ContextKind.of("org"))); + assertEquals("key1", wrapper.getOrGenerateContextKey(ContextKind.DEFAULT)); + assertEquals("key2", wrapper.getOrGenerateContextKey(ContextKind.of("org"))); verifyAll(); logging.assertNothingLogged(); } @Test - public void getGeneratedContextKeyNotFound() { + public void getOrGenerateContextKeyNotFound() { expect(mockPersistentStore.getValue(EXPECTED_GLOBAL_NAMESPACE, EXPECTED_GENERATED_CONTEXT_KEY_PREFIX + "user")).andReturn(null); + mockPersistentStore.setValue( + eq(EXPECTED_GLOBAL_NAMESPACE), + eq(EXPECTED_GENERATED_CONTEXT_KEY_PREFIX + "user"), + anyString() + ); + expectLastCall(); replayAll(); - assertNull( wrapper.getGeneratedContextKey(ContextKind.DEFAULT)); + assertNotNull( wrapper.getOrGenerateContextKey(ContextKind.DEFAULT)); verifyAll(); - logging.assertNothingLogged(); + logging.assertInfoLogged("Did not find a generated key for context kind \"user\""); } @Test - public void getGeneratedContextKeyWhenStoreThrowsException() { + public void getOrGenerateContextKeyWhenStoreThrowsException() { expect(mockPersistentStore.getValue(EXPECTED_GLOBAL_NAMESPACE, EXPECTED_GENERATED_CONTEXT_KEY_PREFIX + "user")) .andThrow(makeException()); + mockPersistentStore.setValue( + eq(EXPECTED_GLOBAL_NAMESPACE), + eq(EXPECTED_GENERATED_CONTEXT_KEY_PREFIX + "user"), + anyString() + ); + expectLastCall(); replayAll(); - assertNull( wrapper.getGeneratedContextKey(ContextKind.DEFAULT)); + assertNotNull( wrapper.getOrGenerateContextKey(ContextKind.DEFAULT)); verifyAll(); assertStoreErrorWasLogged(); } diff --git a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/PollingDataSourceTest.java b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/PollingDataSourceTest.java index bb43b319..32c6b641 100644 --- a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/PollingDataSourceTest.java +++ b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/PollingDataSourceTest.java @@ -7,6 +7,8 @@ import com.launchdarkly.sdk.LDContext; import com.launchdarkly.sdk.LDValue; +import com.launchdarkly.sdk.android.env.EnvironmentReporterBuilder; +import com.launchdarkly.sdk.android.env.IEnvironmentReporter; import com.launchdarkly.sdk.android.integrations.PollingDataSourceBuilder; import com.launchdarkly.sdk.android.subsystems.Callback; import com.launchdarkly.sdk.android.subsystems.ClientContext; @@ -29,6 +31,8 @@ public class PollingDataSourceTest extends EasyMockSupport { private final MockComponents.MockDataSourceUpdateSink dataSourceUpdateSink = new MockComponents.MockDataSourceUpdateSink(); private final MockFetcher fetcher = new MockFetcher(); private final MockPlatformState platformState = new MockPlatformState(); + + private final IEnvironmentReporter environmentReporter = new EnvironmentReporterBuilder().build(); private final SimpleTestTaskExecutor taskExecutor = new SimpleTestTaskExecutor(); @Rule @@ -37,7 +41,7 @@ public class PollingDataSourceTest extends EasyMockSupport { private ClientContext makeClientContext(boolean inBackground, Boolean previouslyInBackground) { ClientContext baseClientContext = ClientContextImpl.fromConfig( EMPTY_CONFIG, "", "", fetcher, CONTEXT, - logging.logger, platformState, taskExecutor); + logging.logger, platformState, environmentReporter, taskExecutor); return ClientContextImpl.forDataSource( baseClientContext, dataSourceUpdateSink, diff --git a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/StreamingDataSourceTest.java b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/StreamingDataSourceTest.java index 2c6a086c..d03f70b2 100644 --- a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/StreamingDataSourceTest.java +++ b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/StreamingDataSourceTest.java @@ -4,6 +4,8 @@ import static org.junit.Assert.assertTrue; import com.launchdarkly.sdk.LDContext; +import com.launchdarkly.sdk.android.env.EnvironmentReporterBuilder; +import com.launchdarkly.sdk.android.env.IEnvironmentReporter; import com.launchdarkly.sdk.android.subsystems.Callback; import com.launchdarkly.sdk.android.subsystems.ClientContext; import com.launchdarkly.sdk.android.subsystems.DataSource; @@ -26,12 +28,14 @@ public class StreamingDataSourceTest { private final MockComponents.MockDataSourceUpdateSink dataSourceUpdateSink = new MockComponents.MockDataSourceUpdateSink(); private final MockPlatformState platformState = new MockPlatformState(); + + private final IEnvironmentReporter environmentReporter = new EnvironmentReporterBuilder().build(); private final SimpleTestTaskExecutor taskExecutor = new SimpleTestTaskExecutor(); private ClientContext makeClientContext(boolean inBackground, Boolean previouslyInBackground) { ClientContext baseClientContext = ClientContextImpl.fromConfig( new LDConfig.Builder().build(), "", "", null, CONTEXT, - logging.logger, platformState, taskExecutor); + logging.logger, platformState, environmentReporter, taskExecutor); return ClientContextImpl.forDataSource( baseClientContext, dataSourceUpdateSink, @@ -48,7 +52,7 @@ private ClientContext makeClientContext(boolean inBackground, Boolean previously private ClientContext makeClientContextWithFetcher() { ClientContext baseClientContext = ClientContextImpl.fromConfig( new LDConfig.Builder().build(), "", "", makeFeatureFetcher(), CONTEXT, - logging.logger, platformState, taskExecutor); + logging.logger, platformState, environmentReporter, taskExecutor); return ClientContextImpl.forDataSource( baseClientContext, dataSourceUpdateSink, diff --git a/shared-test-code/build.gradle b/shared-test-code/build.gradle index 31bb69dd..44180c27 100644 --- a/shared-test-code/build.gradle +++ b/shared-test-code/build.gradle @@ -32,4 +32,5 @@ dependencies { implementation("androidx.annotation:annotation:${versions.androidAnnotation}") implementation("com.google.code.gson:gson:${versions.gson}") implementation("junit:junit:${versions.junit}") + implementation("org.easymock:easymock:4.3") } diff --git a/shared-test-code/src/main/java/com/launchdarkly/sdk/android/MockComponents.java b/shared-test-code/src/main/java/com/launchdarkly/sdk/android/MockComponents.java index 93bc6fe3..87dd008a 100644 --- a/shared-test-code/src/main/java/com/launchdarkly/sdk/android/MockComponents.java +++ b/shared-test-code/src/main/java/com/launchdarkly/sdk/android/MockComponents.java @@ -1,13 +1,18 @@ package com.launchdarkly.sdk.android; import static com.launchdarkly.sdk.android.AssertHelpers.requireValue; +import static org.easymock.EasyMock.createMock; +import static org.easymock.EasyMock.expect; +import static org.easymock.EasyMock.mock; +import static org.easymock.EasyMock.replay; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import com.launchdarkly.sdk.LDContext; +import com.launchdarkly.sdk.android.env.IEnvironmentReporter; +import com.launchdarkly.sdk.android.subsystems.ApplicationInfo; import com.launchdarkly.sdk.android.subsystems.Callback; import com.launchdarkly.sdk.android.subsystems.ClientContext; import com.launchdarkly.sdk.android.subsystems.ComponentConfigurer; From b62f48e597aef914f82b65129b71e27bcb49e031 Mon Sep 17 00:00:00 2001 From: Todd Anderson Date: Wed, 21 Jun 2023 16:25:14 -0500 Subject: [PATCH 05/26] Turning off automatic env attributes by default. Misc TODOs. --- .../sdk/android/AutoEnvContextModifier.java | 2 ++ .../com/launchdarkly/sdk/android/LDClient.java | 16 ++++++++++------ .../sdk/android/AutoEnvContextModifierTest.java | 3 ++- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/AutoEnvContextModifier.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/AutoEnvContextModifier.java index 55df02b6..9bd1f631 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/AutoEnvContextModifier.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/AutoEnvContextModifier.java @@ -51,6 +51,8 @@ public LDContext modifyContext(LDContext context) { if (context.getIndividualContext(recipe.kind) == null) { builder.add(makeLDContextFromRecipe(recipe)); } + + // TODO: log message when there is a collision } return builder.build(); diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDClient.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDClient.java index 0fc6c122..b1943f4f 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDClient.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDClient.java @@ -50,6 +50,8 @@ public class LDClient implements LDClientInterface, Closeable { private static volatile PlatformState sharedPlatformState; private static volatile IEnvironmentReporter environmentReporter; private static volatile TaskExecutor sharedTaskExecutor; + + // TODO: sc-204374 - Support opt-in/out private static volatile AutoEnvContextModifier autoEnvContextModifier; private static volatile AnonymousKeyContextModifier anonymousKeyContextModifier; @@ -135,14 +137,15 @@ public static Future init(@NonNull Application application, store, logger ); - autoEnvContextModifier = new AutoEnvContextModifier(persistentData, environmentReporter); + // TODO: sc-204374 - Support opt-in/out + // autoEnvContextModifier = new AutoEnvContextModifier(persistentData, environmentReporter); anonymousKeyContextModifier = new AnonymousKeyContextModifier(persistentData, config.isGenerateAnonymousKeys()); Migration.migrateWhenNeeded(store, logger); - - modifiedContext = autoEnvContextModifier.modifyContext(context); - modifiedContext = anonymousKeyContextModifier.modifyContext(modifiedContext, logger); + // TODO: sc-204374 - Support opt-in/out + // modifiedContext = autoEnvContextModifier.modifyContext(context); + modifiedContext = anonymousKeyContextModifier.modifyContext(context, logger); // Create, but don't start, every LDClient instance final Map newInstances = new HashMap<>(); @@ -412,8 +415,9 @@ public Future identify(LDContext context) { return new LDFailedFuture<>(new LaunchDarklyException("Invalid context: " + context.getError())); } - LDContext modifiedContext = autoEnvContextModifier.modifyContext(context); - modifiedContext = anonymousKeyContextModifier.modifyContext(modifiedContext, getSharedLogger()); + // TODO: sc-204374 - Support opt-in/out + // LDContext modifiedContext = autoEnvContextModifier.modifyContext(context); + LDContext modifiedContext = anonymousKeyContextModifier.modifyContext(context, getSharedLogger()); return identifyInstances(modifiedContext); } diff --git a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/AutoEnvContextModifierTest.java b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/AutoEnvContextModifierTest.java index 2a4d280f..779ede3c 100644 --- a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/AutoEnvContextModifierTest.java +++ b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/AutoEnvContextModifierTest.java @@ -12,9 +12,9 @@ public class AutoEnvContextModifierTest { /** * Requirement 1.2.2.1 - Schema adherence - * Requirement 1.2.2.2 - Adding all context kinds * Requirement 1.2.2.3 - Adding all attributes * Requirement 1.2.2.5 - Schema version in _meta + * Requirement 1.2.2.7 - Adding all context kinds */ @Test public void adheresToSchemaTest() { @@ -60,6 +60,7 @@ public void adheresToSchemaTest() { } /** + * Requirement 1.2.2.6 - Don't add kind if already exists * Requirement 1.2.5.1 - Doesn't change customer provided data */ @Test From cc44ac124d3da73c9554732d2bfe4058951641ee Mon Sep 17 00:00:00 2001 From: Todd Anderson Date: Wed, 21 Jun 2023 16:48:33 -0500 Subject: [PATCH 06/26] Adding some comments and updating signature of AnonymousKeyContextModifier --- .../android/AnonymousKeyContextModifier.java | 13 ++++++-- .../sdk/android/AutoEnvContextModifier.java | 8 +++-- .../launchdarkly/sdk/android/LDClient.java | 4 +-- .../AnonymousKeyContextModifierTest.java | 30 ++++++++----------- 4 files changed, 31 insertions(+), 24 deletions(-) diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/AnonymousKeyContextModifier.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/AnonymousKeyContextModifier.java index 7a3f62ad..f61a2a86 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/AnonymousKeyContextModifier.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/AnonymousKeyContextModifier.java @@ -6,11 +6,20 @@ import com.launchdarkly.sdk.ContextMultiBuilder; import com.launchdarkly.sdk.LDContext; -final class AnonymousKeyContextModifier { +/** + * A {@link IContextModifier} that will set the key of anonymous contexts to a randomly + * generated one. Generated keys are persisted and consistent for a given context kind + * across calls to {@link #modifyContext(LDContext)}. + */ +final class AnonymousKeyContextModifier implements IContextModifier { @NonNull private final PersistentDataStoreWrapper persistentData; private final boolean generateAnonymousKeys; + /** + * @param persistentData that will be used for storing/retrieving keys + * @param generateAnonymousKeys controls whether generated keys will be applied + */ public AnonymousKeyContextModifier( @NonNull PersistentDataStoreWrapper persistentData, boolean generateAnonymousKeys @@ -19,7 +28,7 @@ public AnonymousKeyContextModifier( this.generateAnonymousKeys = generateAnonymousKeys; } - public LDContext modifyContext(LDContext context, LDLogger logger) { + public LDContext modifyContext(LDContext context) { if (!generateAnonymousKeys) { return context; } diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/AutoEnvContextModifier.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/AutoEnvContextModifier.java index 9bd1f631..7a84b5fd 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/AutoEnvContextModifier.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/AutoEnvContextModifier.java @@ -32,9 +32,13 @@ public class AutoEnvContextModifier implements IContextModifier { static final String ATTR_OS = "os"; static final String ATTR_FAMILY = "family"; - private PersistentDataStoreWrapper persistentData; - private IEnvironmentReporter environmentReporter; + private final PersistentDataStoreWrapper persistentData; + private final IEnvironmentReporter environmentReporter; + /** + * @param persistentData for retrieving/storing generated context keys + * @param environmentReporter for retrieving attributes + */ public AutoEnvContextModifier(PersistentDataStoreWrapper persistentData, IEnvironmentReporter environmentReporter) { this.persistentData = persistentData; diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDClient.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDClient.java index b1943f4f..7db92a81 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDClient.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDClient.java @@ -145,7 +145,7 @@ public static Future init(@NonNull Application application, // TODO: sc-204374 - Support opt-in/out // modifiedContext = autoEnvContextModifier.modifyContext(context); - modifiedContext = anonymousKeyContextModifier.modifyContext(context, logger); + modifiedContext = anonymousKeyContextModifier.modifyContext(context); // Create, but don't start, every LDClient instance final Map newInstances = new HashMap<>(); @@ -417,7 +417,7 @@ public Future identify(LDContext context) { // TODO: sc-204374 - Support opt-in/out // LDContext modifiedContext = autoEnvContextModifier.modifyContext(context); - LDContext modifiedContext = anonymousKeyContextModifier.modifyContext(context, getSharedLogger()); + LDContext modifiedContext = anonymousKeyContextModifier.modifyContext(context); return identifyInstances(modifiedContext); } diff --git a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/AnonymousKeyContextModifierTest.java b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/AnonymousKeyContextModifierTest.java index f468c7c9..db5d784a 100644 --- a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/AnonymousKeyContextModifierTest.java +++ b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/AnonymousKeyContextModifierTest.java @@ -16,30 +16,25 @@ public class AnonymousKeyContextModifierTest { private static final ContextKind KIND1 = ContextKind.of("kind1"); private static final ContextKind KIND2 = ContextKind.of("kind2"); - @Rule - public LogCaptureRule logging = new LogCaptureRule(); - @Test public void singleKindNonAnonymousContextIsUnchanged() { LDContext context = LDContext.builder("key1").name("name").build(); assertEquals(context, - makeDecoratorWithoutPersistence().modifyContext(context, logging.logger)); - logging.assertNothingLogged(); + makeDecoratorWithoutPersistence().modifyContext(context)); } @Test public void singleKindAnonymousContextIsUnchangedIfConfigOptionIsNotSet() { LDContext context = LDContext.builder("key1").anonymous(true).name("name").build(); assertEquals(context, - makeDecoratorWithoutPersistence().modifyContext(context, logging.logger)); - logging.assertNothingLogged(); + makeDecoratorWithoutPersistence().modifyContext(context)); } @Test public void singleKindAnonymousContextGetsGeneratedKeyIfConfigOptionIsSet() { LDContext context = LDContext.builder("placeholder").anonymous(true).name("name").build(); LDContext transformed = makeDecoratorWithoutPersistence(true) - .modifyContext(context, logging.logger); + .modifyContext(context); assertContextHasBeenTransformedWithNewKey(context, transformed); } @@ -50,8 +45,7 @@ public void multiKindContextIsUnchangedIfNoIndividualContextsNeedGeneratedKey() LDContext multiContext = LDContext.createMulti(c1, c2); assertSame(multiContext, - makeDecoratorWithoutPersistence().modifyContext(multiContext, logging.logger)); - logging.assertNothingLogged(); + makeDecoratorWithoutPersistence().modifyContext(multiContext)); } @Test @@ -60,7 +54,7 @@ public void multiKindContextGetsGeneratedKeyForIndividualContext() { LDContext c2 = LDContext.builder("key2").kind(KIND2).anonymous(true).name("name2").build(); LDContext multiContext = LDContext.createMulti(c1, c2); LDContext transformedMulti = makeDecoratorWithoutPersistence(true) - .modifyContext(multiContext, logging.logger); + .modifyContext(multiContext); assertEquals(multiContext.getIndividualContextCount(), transformedMulti.getIndividualContextCount()); assertSame(multiContext.getIndividualContext(0), transformedMulti.getIndividualContext(0)); @@ -74,7 +68,7 @@ public void multiKindContextGetsSeparateGeneratedKeyForEachKind() { LDContext c2 = LDContext.builder("key2").kind(KIND2).anonymous(true).name("name2").build(); LDContext multiContext = LDContext.createMulti(c1, c2); LDContext transformedMulti = makeDecoratorWithoutPersistence(true) - .modifyContext(multiContext, logging.logger); + .modifyContext(multiContext); assertEquals(multiContext.getIndividualContextCount(), transformedMulti.getIndividualContextCount()); assertContextHasBeenTransformedWithNewKey( @@ -94,14 +88,14 @@ public void generatedKeysPersistPerKindIfPersistentStorageIsEnabled() { PersistentDataStore store = new InMemoryPersistentDataStore(); AnonymousKeyContextModifier decorator1 = makeDecoratorWithPersistence(store, true); - LDContext transformedMultiA = decorator1.modifyContext(multiContext, logging.logger); + LDContext transformedMultiA = decorator1.modifyContext(multiContext); assertContextHasBeenTransformedWithNewKey( multiContext.getIndividualContext(0), transformedMultiA.getIndividualContext(0)); assertContextHasBeenTransformedWithNewKey( multiContext.getIndividualContext(1), transformedMultiA.getIndividualContext(1)); AnonymousKeyContextModifier decorator2 = makeDecoratorWithPersistence(store, true); - LDContext transformedMultiB = decorator2.modifyContext(multiContext, logging.logger); + LDContext transformedMultiB = decorator2.modifyContext(multiContext); assertEquals(transformedMultiA, transformedMultiB); } @@ -112,13 +106,13 @@ public void generatedKeysAreReusedDuringLifetimeOfSdkEvenIfPersistentStorageIsDi LDContext multiContext = LDContext.createMulti(c1, c2); AnonymousKeyContextModifier decorator = makeDecoratorWithoutPersistence(true); - LDContext transformedMultiA = decorator.modifyContext(multiContext, logging.logger); + LDContext transformedMultiA = decorator.modifyContext(multiContext); assertContextHasBeenTransformedWithNewKey( multiContext.getIndividualContext(0), transformedMultiA.getIndividualContext(0)); assertContextHasBeenTransformedWithNewKey( multiContext.getIndividualContext(1), transformedMultiA.getIndividualContext(1)); - LDContext transformedMultiB = decorator.modifyContext(multiContext, logging.logger); + LDContext transformedMultiB = decorator.modifyContext(multiContext); assertEquals(transformedMultiA, transformedMultiB); } @@ -129,14 +123,14 @@ public void generatedKeysAreNotReusedAcrossRestartsIfPersistentStorageIsDisabled LDContext multiContext = LDContext.createMulti(c1, c2); AnonymousKeyContextModifier decorator1 = makeDecoratorWithoutPersistence(true); - LDContext transformedMultiA = decorator1.modifyContext(multiContext, logging.logger); + LDContext transformedMultiA = decorator1.modifyContext(multiContext); assertContextHasBeenTransformedWithNewKey( multiContext.getIndividualContext(0), transformedMultiA.getIndividualContext(0)); assertContextHasBeenTransformedWithNewKey( multiContext.getIndividualContext(1), transformedMultiA.getIndividualContext(1)); AnonymousKeyContextModifier decorator2 = makeDecoratorWithoutPersistence(true); - LDContext transformedMultiB = decorator2.modifyContext(multiContext, logging.logger); + LDContext transformedMultiB = decorator2.modifyContext(multiContext); assertContextHasBeenTransformedWithNewKey( multiContext.getIndividualContext(0), transformedMultiB.getIndividualContext(0)); assertNotEquals(transformedMultiA.getIndividualContext(0).getKey(), From 7fa4f9d0a89e9d47c8708aab6713a67683415640 Mon Sep 17 00:00:00 2001 From: Todd Anderson Date: Fri, 23 Jun 2023 12:06:07 -0500 Subject: [PATCH 07/26] Addressing remaining TODOs for auto attributes and updating some tests. --- .../sdk/android/AutoEnvContextModifier.java | 11 +++++-- .../env/AndroidEnvironmentReporter.java | 24 ++++++++------- .../android/env/SDKEnvironmentReporter.java | 2 +- .../integrations/ApplicationInfoBuilder.java | 4 +-- .../android/subsystems/ApplicationInfo.java | 20 +++++++------ .../android/AutoEnvContextModifierTest.java | 29 ++++++++++--------- .../sdk/android/LogCaptureRule.java | 5 ++++ 7 files changed, 55 insertions(+), 40 deletions(-) diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/AutoEnvContextModifier.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/AutoEnvContextModifier.java index 7a84b5fd..35d3fcf3 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/AutoEnvContextModifier.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/AutoEnvContextModifier.java @@ -1,5 +1,6 @@ package com.launchdarkly.sdk.android; +import com.launchdarkly.logging.LDLogger; import com.launchdarkly.sdk.ContextBuilder; import com.launchdarkly.sdk.ContextKind; import com.launchdarkly.sdk.ContextMultiBuilder; @@ -34,15 +35,18 @@ public class AutoEnvContextModifier implements IContextModifier { private final PersistentDataStoreWrapper persistentData; private final IEnvironmentReporter environmentReporter; + private final LDLogger logger; /** * @param persistentData for retrieving/storing generated context keys * @param environmentReporter for retrieving attributes */ public AutoEnvContextModifier(PersistentDataStoreWrapper persistentData, - IEnvironmentReporter environmentReporter) { + IEnvironmentReporter environmentReporter, + LDLogger logger) { this.persistentData = persistentData; this.environmentReporter = environmentReporter; + this.logger = logger; } @Override @@ -54,9 +58,10 @@ public LDContext modifyContext(LDContext context) { for (ContextRecipe recipe : makeRecipeList()) { if (context.getIndividualContext(recipe.kind) == null) { builder.add(makeLDContextFromRecipe(recipe)); + } else { + logger.warn("Unable to automatically add environment attributes for " + + "kind:{}. {} already exists.", recipe.kind, recipe.kind); } - - // TODO: log message when there is a collision } return builder.build(); diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/env/AndroidEnvironmentReporter.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/env/AndroidEnvironmentReporter.java index c8cb12f5..5d50b0fc 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/env/AndroidEnvironmentReporter.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/env/AndroidEnvironmentReporter.java @@ -32,8 +32,8 @@ public AndroidEnvironmentReporter(Application application) { public ApplicationInfo getApplicationInfo() { return new ApplicationInfo( getApplicationID(), - getApplicationName(), getApplicationVersion(), + getApplicationName(), getApplicationVersionName() ); } @@ -86,10 +86,10 @@ private String getApplicationName() { android.content.pm.ApplicationInfo ai = pm.getApplicationInfo(application.getPackageName(), 0); return pm.getApplicationLabel(ai).toString(); } catch (PackageManager.NameNotFoundException e) { - // TODO: investigate if this runtime exception can actually happen since we just - // got the package name just before. Current gut feeling says not possible, but - // those are famous last words. - throw new RuntimeException(e); + // We don't really expect this to ever happen since we just queried the platform + // for the application name and then immediately used it. Since the code has + // this logical path, the best we can do is defer to the next in the chain. + return super.getApplicationInfo().getApplicationName(); } } @@ -102,10 +102,10 @@ private String getApplicationVersion() { return String.valueOf(application.getPackageManager().getPackageInfo(application.getPackageName(), 0).versionCode); } } catch (PackageManager.NameNotFoundException e) { - // TODO: investigate if this runtime exception can actually happen since we just - // got the package name just before. Current gut feeling says not possible, but - // those are famous last words. - throw new RuntimeException(e); + // We don't really expect this to ever happen since we just queried the platform + // for the application name and then immediately used it. Since the code has + // this logical path, the best we can do is defer to the next in the chain. + return super.getApplicationInfo().getApplicationVersion(); } } @@ -115,8 +115,10 @@ private String getApplicationVersionName() { PackageManager pm = application.getPackageManager(); return pm.getPackageInfo(application.getPackageName(), 0).versionName; } catch (PackageManager.NameNotFoundException e) { - // TODO: figure out if there is a way to get around this exception more elegantly. - throw new RuntimeException(e); + // We don't really expect this to ever happen since we just queried the platform + // for the application name and then immediately used it. Since the code has + // this logical path, the best we can do is defer to the next in the chain. + return super.getApplicationInfo().getApplicationVersionName(); } } } diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/env/SDKEnvironmentReporter.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/env/SDKEnvironmentReporter.java index 0bfd293a..71d636f3 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/env/SDKEnvironmentReporter.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/env/SDKEnvironmentReporter.java @@ -16,9 +16,9 @@ class SDKEnvironmentReporter extends EnvironmentReporterChainBase implements IEn @Override public ApplicationInfo getApplicationInfo() { return new ApplicationInfo( - LDPackageConsts.SDK_NAME, LDPackageConsts.SDK_NAME, BuildConfig.VERSION_NAME, + LDPackageConsts.SDK_NAME, BuildConfig.VERSION_NAME ); } diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/ApplicationInfoBuilder.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/ApplicationInfoBuilder.java index c2ddb823..fc8ab2d0 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/ApplicationInfoBuilder.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/ApplicationInfoBuilder.java @@ -104,8 +104,6 @@ public ApplicationInfoBuilder applicationVersionName(String versionName) { * @return the configuration object */ public ApplicationInfo createApplicationInfo() { - // TODO: evaluate the risk of injecting a new parameter of the same type in the middle of - // an existing constructor. - return new ApplicationInfo(applicationId, applicationName, applicationVersion, applicationVersionName); + return new ApplicationInfo(applicationId, applicationVersion, applicationName, applicationVersionName); } } diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/subsystems/ApplicationInfo.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/subsystems/ApplicationInfo.java index 83f8c7bd..8b2e5570 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/subsystems/ApplicationInfo.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/subsystems/ApplicationInfo.java @@ -10,23 +10,25 @@ * @since 4.1.0 */ public final class ApplicationInfo { - private String applicationId; - private String applicationName; - private String applicationVersion; - private String applicationVersionName; + private final String applicationId; + private final String applicationName; + private final String applicationVersion; + private final String applicationVersionName; /** * Used internally by the SDK to store application metadata. * - * @param applicationId the application ID - * @param applicationVersion the application version + * @param applicationId the application ID + * @param applicationVersion the application version + * @param applicationName friendly name for the application + * @param applicationVersionName friendly name for the version * @see ApplicationInfoBuilder */ - public ApplicationInfo(String applicationId, String applicationName, - String applicationVersion, String applicationVersionName) { + public ApplicationInfo(String applicationId, String applicationVersion, + String applicationName, String applicationVersionName) { this.applicationId = applicationId; - this.applicationName = applicationName; this.applicationVersion = applicationVersion; + this.applicationName = applicationName; this.applicationVersionName = applicationVersionName; } diff --git a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/AutoEnvContextModifierTest.java b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/AutoEnvContextModifierTest.java index 779ede3c..b5711d55 100644 --- a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/AutoEnvContextModifierTest.java +++ b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/AutoEnvContextModifierTest.java @@ -1,15 +1,20 @@ package com.launchdarkly.sdk.android; +import com.launchdarkly.logging.LDLogger; import com.launchdarkly.sdk.ContextKind; import com.launchdarkly.sdk.LDContext; import com.launchdarkly.sdk.ObjectBuilder; import com.launchdarkly.sdk.android.env.EnvironmentReporterBuilder; import org.junit.Assert; +import org.junit.Rule; import org.junit.Test; public class AutoEnvContextModifierTest { + @Rule + public LogCaptureRule logging = new LogCaptureRule(); + /** * Requirement 1.2.2.1 - Schema adherence * Requirement 1.2.2.3 - Adding all attributes @@ -21,7 +26,8 @@ public void adheresToSchemaTest() { PersistentDataStoreWrapper wrapper = TestUtil.makeSimplePersistentDataStoreWrapper(); AutoEnvContextModifier underTest = new AutoEnvContextModifier( wrapper, - new EnvironmentReporterBuilder().build() + new EnvironmentReporterBuilder().build(), + LDLogger.none() ); LDContext input = LDContext.builder(ContextKind.of("aKind"), "aKey") @@ -54,14 +60,12 @@ public void adheresToSchemaTest() { LDContext expectedOutput = LDContext.multiBuilder().add(input).add(expectedAppContext).add(expectedDeviceContext).build(); Assert.assertEquals(expectedOutput, output); - - // TODO: Figure out how to set _meta - Assert.fail("_meta is not yet implemented."); } /** * Requirement 1.2.2.6 - Don't add kind if already exists * Requirement 1.2.5.1 - Doesn't change customer provided data + * Requirement 1.2.7.1 - Log warning when kind already exists */ @Test public void doesNotOverwriteCustomerDataTest() { @@ -69,7 +73,8 @@ public void doesNotOverwriteCustomerDataTest() { PersistentDataStoreWrapper wrapper = TestUtil.makeSimplePersistentDataStoreWrapper(); AutoEnvContextModifier underTest = new AutoEnvContextModifier( wrapper, - new EnvironmentReporterBuilder().build() + new EnvironmentReporterBuilder().build(), + logging.logger ); LDContext input = LDContext.builder(ContextKind.of("ld_application"), "aKey") @@ -93,9 +98,8 @@ public void doesNotOverwriteCustomerDataTest() { LDContext expectedOutput = LDContext.multiBuilder().add(input).add(expectedDeviceContext).build(); Assert.assertEquals(expectedOutput, output); - - // TODO: Figure out how to set _meta - Assert.fail("_meta is not yet implemented."); + logging.assertWarnLogged("Unable to automatically add environment attributes for " + + "kind:ld_application. ld_application already exists."); } /** @@ -107,7 +111,8 @@ public void doesNotOverwriteCustomerDataMultiContextTest() { PersistentDataStoreWrapper wrapper = TestUtil.makeSimplePersistentDataStoreWrapper(); AutoEnvContextModifier underTest = new AutoEnvContextModifier( wrapper, - new EnvironmentReporterBuilder().build() + new EnvironmentReporterBuilder().build(), + LDLogger.none() ); LDContext input1 = LDContext.builder(ContextKind.of("ld_application"), "aKey") @@ -119,9 +124,6 @@ public void doesNotOverwriteCustomerDataMultiContextTest() { // input and output should be the same Assert.assertEquals(multiContextInput, output); - - // TODO: Figure out how to set _meta - //Assert.fail("_meta is not yet being checked."); } /** @@ -132,7 +134,8 @@ public void generatesConsistentKeysAcrossMultipleCalls() { PersistentDataStoreWrapper wrapper = TestUtil.makeSimplePersistentDataStoreWrapper(); AutoEnvContextModifier underTest = new AutoEnvContextModifier( wrapper, - new EnvironmentReporterBuilder().build() + new EnvironmentReporterBuilder().build(), + LDLogger.none() ); LDContext input = LDContext.builder(ContextKind.of("aKind"), "aKey") diff --git a/shared-test-code/src/main/java/com/launchdarkly/sdk/android/LogCaptureRule.java b/shared-test-code/src/main/java/com/launchdarkly/sdk/android/LogCaptureRule.java index 5f8bb543..7a939dd1 100644 --- a/shared-test-code/src/main/java/com/launchdarkly/sdk/android/LogCaptureRule.java +++ b/shared-test-code/src/main/java/com/launchdarkly/sdk/android/LogCaptureRule.java @@ -48,6 +48,11 @@ public void assertInfoLogged(String messageSubstring) { containsString(messageSubstring))); } + public void assertWarnLogged(String messageSubstring) { + assertThat(logCapture.getMessageStrings(), + hasItems(allOf(containsString("WARN:")), + containsString(messageSubstring))); + } public void assertErrorLogged(String messageSubstring) { assertThat(logCapture.getMessageStrings(), hasItems(allOf(containsString("ERROR:")), From 0136dda8481ec39b15a4c471eb7b263f3495f06d Mon Sep 17 00:00:00 2001 From: Todd Anderson Date: Mon, 26 Jun 2023 10:23:33 -0500 Subject: [PATCH 08/26] Adding generated sources to Javadoc scope --- launchdarkly-android-client-sdk/build.gradle | 1 + .../com/launchdarkly/sdk/android/AutoEnvContextModifier.java | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/launchdarkly-android-client-sdk/build.gradle b/launchdarkly-android-client-sdk/build.gradle index bf565e1d..04f7c376 100644 --- a/launchdarkly-android-client-sdk/build.gradle +++ b/launchdarkly-android-client-sdk/build.gradle @@ -109,6 +109,7 @@ task javadoc(type: Javadoc) { source android.sourceSets.main.java.srcDirs // Include common library sources source configurations.commonDoc.collect { zipTree(it) } + source "$buildDir/generated/source" include("**/*.java") diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/AutoEnvContextModifier.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/AutoEnvContextModifier.java index 35d3fcf3..a451b77f 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/AutoEnvContextModifier.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/AutoEnvContextModifier.java @@ -38,8 +38,9 @@ public class AutoEnvContextModifier implements IContextModifier { private final LDLogger logger; /** - * @param persistentData for retrieving/storing generated context keys + * @param persistentData for retrieving/storing generated context keys * @param environmentReporter for retrieving attributes + * @param logger for logging messages */ public AutoEnvContextModifier(PersistentDataStoreWrapper persistentData, IEnvironmentReporter environmentReporter, From af8502689651d610ba273991c95e3e2f029534af Mon Sep 17 00:00:00 2001 From: "ld-repository-standards[bot]" <113625520+ld-repository-standards[bot]@users.noreply.github.com> Date: Mon, 26 Jun 2023 22:46:52 +0000 Subject: [PATCH 09/26] Add file CODEOWNERS --- CODEOWNERS | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 CODEOWNERS diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 00000000..7d0dac3c --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1,2 @@ +# Repository Maintainers +* @launchdarkly/team-sdk From dad79ceaed414e810abaeb7d28152fae2180b790 Mon Sep 17 00:00:00 2001 From: Todd Anderson Date: Wed, 5 Jul 2023 08:21:28 -0500 Subject: [PATCH 10/26] WIP --- .../launchdarkly/sdktest/Representations.java | 1 + .../launchdarkly/sdktest/SdkClientEntity.java | 2 + .../com/launchdarkly/sdktest/TestService.java | 3 +- .../sdk/android/AutoEnvContextModifier.java | 4 ++ .../launchdarkly/sdk/android/LDClient.java | 39 +++++++++---------- .../launchdarkly/sdk/android/LDConfig.java | 16 ++++++++ .../sdk/android/NoOpContextModifier.java | 14 +++++++ 7 files changed, 57 insertions(+), 22 deletions(-) create mode 100644 launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/NoOpContextModifier.java diff --git a/contract-tests/src/main/java/com/launchdarkly/sdktest/Representations.java b/contract-tests/src/main/java/com/launchdarkly/sdktest/Representations.java index b45a17fa..853dd464 100644 --- a/contract-tests/src/main/java/com/launchdarkly/sdktest/Representations.java +++ b/contract-tests/src/main/java/com/launchdarkly/sdktest/Representations.java @@ -71,6 +71,7 @@ public static class SdkConfigClientSideParams { LDUser initialUser; boolean evaluationReasons; boolean useReport; + boolean includeEnvironmentAttributes; } public static class CommandParams { diff --git a/contract-tests/src/main/java/com/launchdarkly/sdktest/SdkClientEntity.java b/contract-tests/src/main/java/com/launchdarkly/sdktest/SdkClientEntity.java index fbcb71f3..7b53a126 100644 --- a/contract-tests/src/main/java/com/launchdarkly/sdktest/SdkClientEntity.java +++ b/contract-tests/src/main/java/com/launchdarkly/sdktest/SdkClientEntity.java @@ -312,6 +312,8 @@ private LDConfig buildSdkConfig(SdkConfigParams params, LDLogAdapter logAdapter, Components.httpConfiguration().useReport(params.clientSide.useReport) ); + builder.includeMobileEnvironmentAttributes(params.clientSide.includeEnvironmentAttributes); + if (params.tags != null) { ApplicationInfoBuilder ab = Components.applicationInfo(); if (params.tags.applicationId != null) { diff --git a/contract-tests/src/main/java/com/launchdarkly/sdktest/TestService.java b/contract-tests/src/main/java/com/launchdarkly/sdktest/TestService.java index 69f9b55f..f7127daf 100644 --- a/contract-tests/src/main/java/com/launchdarkly/sdktest/TestService.java +++ b/contract-tests/src/main/java/com/launchdarkly/sdktest/TestService.java @@ -32,7 +32,8 @@ public class TestService extends NanoHTTPD { "service-endpoints", "singleton", "strongly-typed", - "tags" + "tags", + "auto-env-attributes" }; private static final String MIME_JSON = "application/json"; static final Gson gson = new GsonBuilder() diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/AutoEnvContextModifier.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/AutoEnvContextModifier.java index a451b77f..490a2320 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/AutoEnvContextModifier.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/AutoEnvContextModifier.java @@ -32,6 +32,8 @@ public class AutoEnvContextModifier implements IContextModifier { static final String ATTR_LOCALE = "locale"; static final String ATTR_OS = "os"; static final String ATTR_FAMILY = "family"; + static final String ENV_ATTRIBUTES_VERSION = "envAttributesVersion"; + static final String SPEC_VERSION = "0.1"; private final PersistentDataStoreWrapper persistentData; private final IEnvironmentReporter environmentReporter; @@ -115,6 +117,7 @@ private LDContext makeLDContextFromRecipe(ContextRecipe recipe) { private List makeRecipeList() { ContextKind ldApplicationKind = ContextKind.of(LD_APPLICATION_KIND); Map> applicationCallables = new HashMap<>(); + applicationCallables.put(ENV_ATTRIBUTES_VERSION, () -> LDValue.of(ENV_ATTRIBUTES_VERSION)); applicationCallables.put(ATTR_ID, () -> LDValue.of(environmentReporter.getApplicationInfo().getApplicationId())); applicationCallables.put(ATTR_NAME, () -> LDValue.of(environmentReporter.getApplicationInfo().getApplicationName())); applicationCallables.put(ATTR_VERSION, () -> LDValue.of(environmentReporter.getApplicationInfo().getApplicationVersion())); @@ -122,6 +125,7 @@ private List makeRecipeList() { ContextKind ldDeviceKind = ContextKind.of(LD_DEVICE_KIND); Map> deviceCallables = new HashMap<>(); + deviceCallables.put(ENV_ATTRIBUTES_VERSION, () -> LDValue.of(ENV_ATTRIBUTES_VERSION)); deviceCallables.put(ATTR_MANUFACTURER, () -> LDValue.of(environmentReporter.getManufacturer())); deviceCallables.put(ATTR_MODEL, () -> LDValue.of(environmentReporter.getModel())); deviceCallables.put(ATTR_LOCALE, () -> LDValue.of(environmentReporter.getLocale())); diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDClient.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDClient.java index 7db92a81..ee9b650c 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDClient.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDClient.java @@ -50,10 +50,8 @@ public class LDClient implements LDClientInterface, Closeable { private static volatile PlatformState sharedPlatformState; private static volatile IEnvironmentReporter environmentReporter; private static volatile TaskExecutor sharedTaskExecutor; - - // TODO: sc-204374 - Support opt-in/out - private static volatile AutoEnvContextModifier autoEnvContextModifier; - private static volatile AnonymousKeyContextModifier anonymousKeyContextModifier; + private static volatile IContextModifier autoEnvContextModifier; + private static volatile IContextModifier anonymousKeyContextModifier; // A lock to ensure calls to `init()` are serialized. static Object initLock = new Object(); @@ -122,14 +120,6 @@ public static Future init(@NonNull Application application, sharedTaskExecutor = new AndroidTaskExecutor(application, logger); sharedPlatformState = new AndroidPlatformState(application, sharedTaskExecutor, logger); - EnvironmentReporterBuilder reporterBuilder = new EnvironmentReporterBuilder(); - reporterBuilder.setApplicationInfo(config.applicationInfo); - - // TODO: sc-204374 - Support opt-in/out - // reporterBuilder.enableCollectionFromPlatform(application); - - environmentReporter = reporterBuilder.build(); - PersistentDataStore store = config.getPersistentDataStore() == null ? new SharedPreferencesPersistentDataStore(application, logger) : config.getPersistentDataStore(); @@ -137,15 +127,23 @@ public static Future init(@NonNull Application application, store, logger ); - // TODO: sc-204374 - Support opt-in/out - // autoEnvContextModifier = new AutoEnvContextModifier(persistentData, environmentReporter); - anonymousKeyContextModifier = new AnonymousKeyContextModifier(persistentData, config.isGenerateAnonymousKeys()); Migration.migrateWhenNeeded(store, logger); - // TODO: sc-204374 - Support opt-in/out - // modifiedContext = autoEnvContextModifier.modifyContext(context); - modifiedContext = anonymousKeyContextModifier.modifyContext(context); + EnvironmentReporterBuilder reporterBuilder = new EnvironmentReporterBuilder(); + reporterBuilder.setApplicationInfo(config.applicationInfo); + reporterBuilder.enableCollectionFromPlatform(application); + environmentReporter = reporterBuilder.build(); + + if (config.isIncludeEnvironmentAttributes()) { + autoEnvContextModifier = new AutoEnvContextModifier(persistentData, environmentReporter, logger); + } else { + autoEnvContextModifier = new NoOpContextModifier(); + } + anonymousKeyContextModifier = new AnonymousKeyContextModifier(persistentData, config.isGenerateAnonymousKeys()); + + modifiedContext = autoEnvContextModifier.modifyContext(context); + modifiedContext = anonymousKeyContextModifier.modifyContext(modifiedContext); // Create, but don't start, every LDClient instance final Map newInstances = new HashMap<>(); @@ -415,9 +413,8 @@ public Future identify(LDContext context) { return new LDFailedFuture<>(new LaunchDarklyException("Invalid context: " + context.getError())); } - // TODO: sc-204374 - Support opt-in/out - // LDContext modifiedContext = autoEnvContextModifier.modifyContext(context); - LDContext modifiedContext = anonymousKeyContextModifier.modifyContext(context); + LDContext modifiedContext = autoEnvContextModifier.modifyContext(context); // this may be a no-op modifier + modifiedContext = anonymousKeyContextModifier.modifyContext(modifiedContext); return identifyInstances(modifiedContext); } diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDConfig.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDConfig.java index 67c03ba3..f51d6d8f 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDConfig.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDConfig.java @@ -69,6 +69,7 @@ public class LDConfig { private final boolean disableBackgroundUpdating; private final boolean evaluationReasons; private final boolean generateAnonymousKeys; + private final boolean includeEnvironmentAttributes; private final LDLogAdapter logAdapter; private final String loggerName; private final int maxCachedContexts; @@ -87,6 +88,7 @@ public class LDConfig { boolean diagnosticOptOut, int maxCachedContexts, boolean generateAnonymousKeys, + boolean includeEnvironmentAttributes, PersistentDataStore persistentDataStore, LDLogAdapter logAdapter, String loggerName) { @@ -102,6 +104,7 @@ public class LDConfig { this.diagnosticOptOut = diagnosticOptOut; this.maxCachedContexts = maxCachedContexts; this.generateAnonymousKeys = generateAnonymousKeys; + this.includeEnvironmentAttributes = includeEnvironmentAttributes; this.persistentDataStore = persistentDataStore; this.logAdapter = logAdapter; this.loggerName = loggerName; @@ -137,6 +140,11 @@ int getMaxCachedContexts() { public boolean isGenerateAnonymousKeys() { return generateAnonymousKeys; } + // TODO: revisit this naming + public boolean isIncludeEnvironmentAttributes() { + return includeEnvironmentAttributes; + } + PersistentDataStore getPersistentDataStore() { return persistentDataStore; } LDLogAdapter getLogAdapter() { return logAdapter; } @@ -174,6 +182,8 @@ public static class Builder { private boolean generateAnonymousKeys; + private boolean includeEnvironmentAttributes; + private PersistentDataStore persistentDataStore; private LDLogAdapter logAdapter = defaultLogAdapter(); @@ -466,6 +476,11 @@ public LDConfig.Builder generateAnonymousKeys(boolean generateAnonymousKeys) { return this; } + public LDConfig.Builder includeMobileEnvironmentAttributes(boolean include) { + this.includeEnvironmentAttributes = include; + return this; + } + /** * Specifies a custom data store. Deliberately package-private-- currently this is only * configurable for tests. @@ -616,6 +631,7 @@ public LDConfig build() { diagnosticOptOut, maxCachedContexts, generateAnonymousKeys, + includeEnvironmentAttributes, persistentDataStore, actualLogAdapter, loggerName); diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/NoOpContextModifier.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/NoOpContextModifier.java new file mode 100644 index 00000000..be9d7cd1 --- /dev/null +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/NoOpContextModifier.java @@ -0,0 +1,14 @@ +package com.launchdarkly.sdk.android; + +import com.launchdarkly.sdk.LDContext; + +/** + * Context modifier that does nothing to the context. + */ +public class NoOpContextModifier implements IContextModifier { + + @Override + public LDContext modifyContext(LDContext context) { + return context; + } +} From 3874ef3299036075e9339a9cbeaa6a8786d81fa6 Mon Sep 17 00:00:00 2001 From: Todd Anderson Date: Mon, 24 Jul 2023 09:56:07 -0500 Subject: [PATCH 11/26] Removing TODO and adjusting logic related to environment builder. --- .../src/main/java/com/launchdarkly/sdk/android/LDClient.java | 4 +++- .../src/main/java/com/launchdarkly/sdk/android/LDConfig.java | 3 +-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDClient.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDClient.java index ee9b650c..60ad2fa6 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDClient.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDClient.java @@ -132,7 +132,9 @@ public static Future init(@NonNull Application application, EnvironmentReporterBuilder reporterBuilder = new EnvironmentReporterBuilder(); reporterBuilder.setApplicationInfo(config.applicationInfo); - reporterBuilder.enableCollectionFromPlatform(application); + if (config.isIncludeEnvironmentAttributes()) { + reporterBuilder.enableCollectionFromPlatform(application); + } environmentReporter = reporterBuilder.build(); if (config.isIncludeEnvironmentAttributes()) { diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDConfig.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDConfig.java index f51d6d8f..b46f3d42 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDConfig.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDConfig.java @@ -140,7 +140,6 @@ int getMaxCachedContexts() { public boolean isGenerateAnonymousKeys() { return generateAnonymousKeys; } - // TODO: revisit this naming public boolean isIncludeEnvironmentAttributes() { return includeEnvironmentAttributes; } @@ -182,7 +181,7 @@ public static class Builder { private boolean generateAnonymousKeys; - private boolean includeEnvironmentAttributes; + private boolean includeEnvironmentAttributes = true; private PersistentDataStore persistentDataStore; From 1e59f813b71bc29aeea1d1baf14567e3050c1816 Mon Sep 17 00:00:00 2001 From: Todd Anderson Date: Mon, 24 Jul 2023 10:18:28 -0500 Subject: [PATCH 12/26] Fixing incorrect default for includeEnvironmentAttributes --- .../src/main/java/com/launchdarkly/sdk/android/LDConfig.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDConfig.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDConfig.java index b46f3d42..52226e35 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDConfig.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDConfig.java @@ -181,7 +181,7 @@ public static class Builder { private boolean generateAnonymousKeys; - private boolean includeEnvironmentAttributes = true; + private boolean includeEnvironmentAttributes = false; private PersistentDataStore persistentDataStore; From 5026a892f71e4f71567e1db31e58918cdf42bfeb Mon Sep 17 00:00:00 2001 From: Todd Anderson Date: Mon, 24 Jul 2023 10:49:49 -0500 Subject: [PATCH 13/26] Resolving unit testing issue. --- .../launchdarkly/sdk/android/AutoEnvContextModifier.java | 4 ++-- .../sdk/android/AutoEnvContextModifierTest.java | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/AutoEnvContextModifier.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/AutoEnvContextModifier.java index 490a2320..badbf413 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/AutoEnvContextModifier.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/AutoEnvContextModifier.java @@ -117,7 +117,7 @@ private LDContext makeLDContextFromRecipe(ContextRecipe recipe) { private List makeRecipeList() { ContextKind ldApplicationKind = ContextKind.of(LD_APPLICATION_KIND); Map> applicationCallables = new HashMap<>(); - applicationCallables.put(ENV_ATTRIBUTES_VERSION, () -> LDValue.of(ENV_ATTRIBUTES_VERSION)); + applicationCallables.put(ENV_ATTRIBUTES_VERSION, () -> LDValue.of(SPEC_VERSION)); applicationCallables.put(ATTR_ID, () -> LDValue.of(environmentReporter.getApplicationInfo().getApplicationId())); applicationCallables.put(ATTR_NAME, () -> LDValue.of(environmentReporter.getApplicationInfo().getApplicationName())); applicationCallables.put(ATTR_VERSION, () -> LDValue.of(environmentReporter.getApplicationInfo().getApplicationVersion())); @@ -125,7 +125,7 @@ private List makeRecipeList() { ContextKind ldDeviceKind = ContextKind.of(LD_DEVICE_KIND); Map> deviceCallables = new HashMap<>(); - deviceCallables.put(ENV_ATTRIBUTES_VERSION, () -> LDValue.of(ENV_ATTRIBUTES_VERSION)); + deviceCallables.put(ENV_ATTRIBUTES_VERSION, () -> LDValue.of(SPEC_VERSION)); deviceCallables.put(ATTR_MANUFACTURER, () -> LDValue.of(environmentReporter.getManufacturer())); deviceCallables.put(ATTR_MODEL, () -> LDValue.of(environmentReporter.getModel())); deviceCallables.put(ATTR_LOCALE, () -> LDValue.of(environmentReporter.getLocale())); diff --git a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/AutoEnvContextModifierTest.java b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/AutoEnvContextModifierTest.java index b5711d55..624a4b6b 100644 --- a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/AutoEnvContextModifierTest.java +++ b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/AutoEnvContextModifierTest.java @@ -38,15 +38,16 @@ public void adheresToSchemaTest() { // will be persistence side effects ContextKind applicationKind = ContextKind.of(AutoEnvContextModifier.LD_APPLICATION_KIND); LDContext expectedAppContext = LDContext.builder(applicationKind, wrapper.getOrGenerateContextKey(applicationKind)) + .set(AutoEnvContextModifier.ENV_ATTRIBUTES_VERSION, "0.1") .set(AutoEnvContextModifier.ATTR_ID, LDPackageConsts.SDK_NAME) .set(AutoEnvContextModifier.ATTR_NAME, LDPackageConsts.SDK_NAME) .set(AutoEnvContextModifier.ATTR_VERSION, BuildConfig.VERSION_NAME) .set(AutoEnvContextModifier.ATTR_VERSION_NAME, BuildConfig.VERSION_NAME) - .set("_meta", "0.1") .build(); ContextKind deviceKind = ContextKind.of(AutoEnvContextModifier.LD_DEVICE_KIND); LDContext expectedDeviceContext = LDContext.builder(deviceKind, wrapper.getOrGenerateContextKey(deviceKind)) + .set(AutoEnvContextModifier.ENV_ATTRIBUTES_VERSION, "0.1") .set(AutoEnvContextModifier.ATTR_MANUFACTURER, "unknown") .set(AutoEnvContextModifier.ATTR_MODEL, "unknown") .set(AutoEnvContextModifier.ATTR_LOCALE, "unknown") @@ -54,7 +55,6 @@ public void adheresToSchemaTest() { .put(AutoEnvContextModifier.ATTR_FAMILY, "unknown") .put(AutoEnvContextModifier.ATTR_VERSION, "unknown") .build()) - .set("_meta", "0.1") .build(); LDContext expectedOutput = LDContext.multiBuilder().add(input).add(expectedAppContext).add(expectedDeviceContext).build(); @@ -85,6 +85,7 @@ public void doesNotOverwriteCustomerDataTest() { // will be persistence side effects ContextKind deviceKind = ContextKind.of(AutoEnvContextModifier.LD_DEVICE_KIND); LDContext expectedDeviceContext = LDContext.builder(deviceKind, wrapper.getOrGenerateContextKey(deviceKind)) + .set(AutoEnvContextModifier.ENV_ATTRIBUTES_VERSION, "0.1") .set(AutoEnvContextModifier.ATTR_MANUFACTURER, "unknown") .set(AutoEnvContextModifier.ATTR_MODEL, "unknown") .set(AutoEnvContextModifier.ATTR_LOCALE, "unknown") @@ -92,7 +93,6 @@ public void doesNotOverwriteCustomerDataTest() { .put(AutoEnvContextModifier.ATTR_FAMILY, "unknown") .put(AutoEnvContextModifier.ATTR_VERSION, "unknown") .build()) - .set("_meta", "0.1") .build(); LDContext expectedOutput = LDContext.multiBuilder().add(input).add(expectedDeviceContext).build(); From 80cd412d04a7e6b09c9cf601633267dcda97dca9 Mon Sep 17 00:00:00 2001 From: Todd Anderson Date: Wed, 26 Jul 2023 11:17:19 -0500 Subject: [PATCH 14/26] Added name to the OS object in ld_device recipe. --- .../launchdarkly/sdk/android/AutoEnvContextModifier.java | 1 + .../sdk/android/env/AndroidEnvironmentReporter.java | 6 ++++++ .../sdk/android/env/EnvironmentReporterChainBase.java | 6 ++++++ .../launchdarkly/sdk/android/env/IEnvironmentReporter.java | 6 ++++++ .../sdk/android/AutoEnvContextModifierTest.java | 2 ++ 5 files changed, 21 insertions(+) diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/AutoEnvContextModifier.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/AutoEnvContextModifier.java index badbf413..400a2be8 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/AutoEnvContextModifier.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/AutoEnvContextModifier.java @@ -131,6 +131,7 @@ private List makeRecipeList() { deviceCallables.put(ATTR_LOCALE, () -> LDValue.of(environmentReporter.getLocale())); deviceCallables.put(ATTR_OS, () -> new ObjectBuilder() .put(ATTR_FAMILY, environmentReporter.getOSFamily()) + .put(ATTR_NAME, environmentReporter.getOSName()) .put(ATTR_VERSION, environmentReporter.getOSVersion()) .build()); diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/env/AndroidEnvironmentReporter.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/env/AndroidEnvironmentReporter.java index 5d50b0fc..e28f2684 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/env/AndroidEnvironmentReporter.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/env/AndroidEnvironmentReporter.java @@ -68,6 +68,12 @@ public String getOSFamily() { return LDPackageConsts.SDK_PLATFORM_NAME; } + @NonNull + @Override + public String getOSName() { + return LDPackageConsts.SDK_PLATFORM_NAME + Build.VERSION.SDK_INT; + } + @NonNull @Override public String getOSVersion() { diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/env/EnvironmentReporterChainBase.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/env/EnvironmentReporterChainBase.java index 2dd55ed1..2e3a9570 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/env/EnvironmentReporterChainBase.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/env/EnvironmentReporterChainBase.java @@ -50,6 +50,12 @@ public String getOSFamily() { return next != null ? next.getOSFamily() : UNKNOWN; } + @NonNull + @Override + public String getOSName() { + return next != null ? next.getOSName() : UNKNOWN; + } + @NonNull @Override public String getOSVersion() { diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/env/IEnvironmentReporter.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/env/IEnvironmentReporter.java index 12da9c9d..58b64d5c 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/env/IEnvironmentReporter.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/env/IEnvironmentReporter.java @@ -40,6 +40,12 @@ public interface IEnvironmentReporter { @NonNull String getOSFamily(); + /** + * @return the name of the OS that this application is running in + */ + @NonNull + String getOSName(); + /** * @return the version of the OS that this application is running in */ diff --git a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/AutoEnvContextModifierTest.java b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/AutoEnvContextModifierTest.java index 624a4b6b..3aad29b6 100644 --- a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/AutoEnvContextModifierTest.java +++ b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/AutoEnvContextModifierTest.java @@ -53,6 +53,7 @@ public void adheresToSchemaTest() { .set(AutoEnvContextModifier.ATTR_LOCALE, "unknown") .set(AutoEnvContextModifier.ATTR_OS, new ObjectBuilder() .put(AutoEnvContextModifier.ATTR_FAMILY, "unknown") + .put(AutoEnvContextModifier.ATTR_NAME, "unknown") .put(AutoEnvContextModifier.ATTR_VERSION, "unknown") .build()) .build(); @@ -91,6 +92,7 @@ public void doesNotOverwriteCustomerDataTest() { .set(AutoEnvContextModifier.ATTR_LOCALE, "unknown") .set(AutoEnvContextModifier.ATTR_OS, new ObjectBuilder() .put(AutoEnvContextModifier.ATTR_FAMILY, "unknown") + .put(AutoEnvContextModifier.ATTR_NAME, "unknown") .put(AutoEnvContextModifier.ATTR_VERSION, "unknown") .build()) .build(); From add83048f4c88db48c6bb27134de79db7dcefef2 Mon Sep 17 00:00:00 2001 From: Todd Anderson Date: Thu, 27 Jul 2023 17:33:15 -0500 Subject: [PATCH 15/26] Adding deterministic ld_application key generation to recipe and also moved locale from ld_device to ld_application --- .../sdk/android/AutoEnvContextModifier.java | 9 ++++++--- .../android/AutoEnvContextModifierTest.java | 19 ++++++++++++------- .../launchdarkly/sdk/android/LDUtilTest.java | 15 +++++++++++++++ 3 files changed, 33 insertions(+), 10 deletions(-) create mode 100644 launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/LDUtilTest.java diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/AutoEnvContextModifier.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/AutoEnvContextModifier.java index 400a2be8..d156ecbd 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/AutoEnvContextModifier.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/AutoEnvContextModifier.java @@ -33,7 +33,7 @@ public class AutoEnvContextModifier implements IContextModifier { static final String ATTR_OS = "os"; static final String ATTR_FAMILY = "family"; static final String ENV_ATTRIBUTES_VERSION = "envAttributesVersion"; - static final String SPEC_VERSION = "0.1"; + static final String SPEC_VERSION = "0.3"; private final PersistentDataStoreWrapper persistentData; private final IEnvironmentReporter environmentReporter; @@ -122,13 +122,13 @@ private List makeRecipeList() { applicationCallables.put(ATTR_NAME, () -> LDValue.of(environmentReporter.getApplicationInfo().getApplicationName())); applicationCallables.put(ATTR_VERSION, () -> LDValue.of(environmentReporter.getApplicationInfo().getApplicationVersion())); applicationCallables.put(ATTR_VERSION_NAME, () -> LDValue.of(environmentReporter.getApplicationInfo().getApplicationVersionName())); + applicationCallables.put(ATTR_LOCALE, () -> LDValue.of(environmentReporter.getLocale())); ContextKind ldDeviceKind = ContextKind.of(LD_DEVICE_KIND); Map> deviceCallables = new HashMap<>(); deviceCallables.put(ENV_ATTRIBUTES_VERSION, () -> LDValue.of(SPEC_VERSION)); deviceCallables.put(ATTR_MANUFACTURER, () -> LDValue.of(environmentReporter.getManufacturer())); deviceCallables.put(ATTR_MODEL, () -> LDValue.of(environmentReporter.getModel())); - deviceCallables.put(ATTR_LOCALE, () -> LDValue.of(environmentReporter.getLocale())); deviceCallables.put(ATTR_OS, () -> new ObjectBuilder() .put(ATTR_FAMILY, environmentReporter.getOSFamily()) .put(ATTR_NAME, environmentReporter.getOSName()) @@ -138,7 +138,10 @@ private List makeRecipeList() { return Arrays.asList( new ContextRecipe( ldApplicationKind, - () -> persistentData.getOrGenerateContextKey(ldApplicationKind), + () -> LDUtil.urlSafeBase64Hash( + environmentReporter.getApplicationInfo().getApplicationId() + ":" + + environmentReporter.getApplicationInfo().getApplicationVersion() + ), applicationCallables ), new ContextRecipe( diff --git a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/AutoEnvContextModifierTest.java b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/AutoEnvContextModifierTest.java index 3aad29b6..eed7bc27 100644 --- a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/AutoEnvContextModifierTest.java +++ b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/AutoEnvContextModifierTest.java @@ -5,6 +5,7 @@ import com.launchdarkly.sdk.LDContext; import com.launchdarkly.sdk.ObjectBuilder; import com.launchdarkly.sdk.android.env.EnvironmentReporterBuilder; +import com.launchdarkly.sdk.android.env.IEnvironmentReporter; import org.junit.Assert; import org.junit.Rule; @@ -24,9 +25,10 @@ public class AutoEnvContextModifierTest { @Test public void adheresToSchemaTest() { PersistentDataStoreWrapper wrapper = TestUtil.makeSimplePersistentDataStoreWrapper(); + IEnvironmentReporter reporter = new EnvironmentReporterBuilder().build(); AutoEnvContextModifier underTest = new AutoEnvContextModifier( wrapper, - new EnvironmentReporterBuilder().build(), + reporter, LDLogger.none() ); @@ -37,20 +39,24 @@ public void adheresToSchemaTest() { // it is important that we create this expected context after the code runs because there // will be persistence side effects ContextKind applicationKind = ContextKind.of(AutoEnvContextModifier.LD_APPLICATION_KIND); - LDContext expectedAppContext = LDContext.builder(applicationKind, wrapper.getOrGenerateContextKey(applicationKind)) - .set(AutoEnvContextModifier.ENV_ATTRIBUTES_VERSION, "0.1") + String expectedApplicationKey = LDUtil.urlSafeBase64Hash( + reporter.getApplicationInfo().getApplicationId() + ":" + + reporter.getApplicationInfo().getApplicationVersion() + ); + LDContext expectedAppContext = LDContext.builder(applicationKind, expectedApplicationKey) + .set(AutoEnvContextModifier.ENV_ATTRIBUTES_VERSION, AutoEnvContextModifier.SPEC_VERSION) .set(AutoEnvContextModifier.ATTR_ID, LDPackageConsts.SDK_NAME) .set(AutoEnvContextModifier.ATTR_NAME, LDPackageConsts.SDK_NAME) .set(AutoEnvContextModifier.ATTR_VERSION, BuildConfig.VERSION_NAME) .set(AutoEnvContextModifier.ATTR_VERSION_NAME, BuildConfig.VERSION_NAME) + .set(AutoEnvContextModifier.ATTR_LOCALE, "unknown") .build(); ContextKind deviceKind = ContextKind.of(AutoEnvContextModifier.LD_DEVICE_KIND); LDContext expectedDeviceContext = LDContext.builder(deviceKind, wrapper.getOrGenerateContextKey(deviceKind)) - .set(AutoEnvContextModifier.ENV_ATTRIBUTES_VERSION, "0.1") + .set(AutoEnvContextModifier.ENV_ATTRIBUTES_VERSION, AutoEnvContextModifier.SPEC_VERSION) .set(AutoEnvContextModifier.ATTR_MANUFACTURER, "unknown") .set(AutoEnvContextModifier.ATTR_MODEL, "unknown") - .set(AutoEnvContextModifier.ATTR_LOCALE, "unknown") .set(AutoEnvContextModifier.ATTR_OS, new ObjectBuilder() .put(AutoEnvContextModifier.ATTR_FAMILY, "unknown") .put(AutoEnvContextModifier.ATTR_NAME, "unknown") @@ -86,10 +92,9 @@ public void doesNotOverwriteCustomerDataTest() { // will be persistence side effects ContextKind deviceKind = ContextKind.of(AutoEnvContextModifier.LD_DEVICE_KIND); LDContext expectedDeviceContext = LDContext.builder(deviceKind, wrapper.getOrGenerateContextKey(deviceKind)) - .set(AutoEnvContextModifier.ENV_ATTRIBUTES_VERSION, "0.1") + .set(AutoEnvContextModifier.ENV_ATTRIBUTES_VERSION, AutoEnvContextModifier.SPEC_VERSION) .set(AutoEnvContextModifier.ATTR_MANUFACTURER, "unknown") .set(AutoEnvContextModifier.ATTR_MODEL, "unknown") - .set(AutoEnvContextModifier.ATTR_LOCALE, "unknown") .set(AutoEnvContextModifier.ATTR_OS, new ObjectBuilder() .put(AutoEnvContextModifier.ATTR_FAMILY, "unknown") .put(AutoEnvContextModifier.ATTR_NAME, "unknown") diff --git a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/LDUtilTest.java b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/LDUtilTest.java new file mode 100644 index 00000000..53662e58 --- /dev/null +++ b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/LDUtilTest.java @@ -0,0 +1,15 @@ +package com.launchdarkly.sdk.android; + +import org.junit.Assert; +import org.junit.Test; + +public class LDUtilTest { + + @Test + public void testUrlSafeBase64Hash() { + String input = "hashThis!"; + String expectedOutput = "sfXg3HewbCAVNQLJzPZhnFKntWYvN0nAYyUWFGy24dQ="; + String output = LDUtil.urlSafeBase64Hash(input); + Assert.assertEquals(expectedOutput, output); + } +} From 1198f9f2a4b7deae328a7bf11ba40c1858d89cbd Mon Sep 17 00:00:00 2001 From: Todd Anderson Date: Mon, 31 Jul 2023 11:18:51 -0500 Subject: [PATCH 16/26] Added autoEnvAttributes required parameter to LDConfig. Adjusted related code and tests. --- .../launchdarkly/sdktest/SdkClientEntity.java | 6 +- .../launchdarkly/example/MainActivity.java | 3 +- .../sdk/android/LDClientEndToEndTest.java | 3 +- .../sdk/android/LDClientEvaluationTest.java | 3 +- .../sdk/android/LDClientEventTest.java | 3 +- .../sdk/android/LDClientLoggingTest.java | 15 ++-- .../sdk/android/LDClientTest.java | 15 ++-- .../android/MultiEnvironmentLDClientTest.java | 13 +-- .../sdk/android/TestDataWithLDClientTest.java | 3 +- .../launchdarkly/sdk/android/LDClient.java | 4 +- .../launchdarkly/sdk/android/LDConfig.java | 82 +++++++++++++------ .../sdk/android/ConnectivityManagerTest.java | 20 ++--- .../android/ContextDataManagerTestBase.java | 3 +- .../sdk/android/DiagnosticConfigTest.java | 13 +-- .../sdk/android/LDConfigTest.java | 41 +++++----- .../sdk/android/PollingDataSourceTest.java | 4 +- .../sdk/android/StreamingDataSourceTest.java | 5 +- .../sdk/android/TestDataTest.java | 4 +- 18 files changed, 142 insertions(+), 98 deletions(-) diff --git a/contract-tests/src/main/java/com/launchdarkly/sdktest/SdkClientEntity.java b/contract-tests/src/main/java/com/launchdarkly/sdktest/SdkClientEntity.java index 7b53a126..ca5de913 100644 --- a/contract-tests/src/main/java/com/launchdarkly/sdktest/SdkClientEntity.java +++ b/contract-tests/src/main/java/com/launchdarkly/sdktest/SdkClientEntity.java @@ -254,7 +254,9 @@ private ContextBuildResponse doContextConvert(ContextConvertParams params) { } private LDConfig buildSdkConfig(SdkConfigParams params, LDLogAdapter logAdapter, String tag) { - LDConfig.Builder builder = new LDConfig.Builder(); + LDConfig.Builder builder = new LDConfig.Builder(params.clientSide.includeEnvironmentAttributes ? + LDConfig.Builder.AutoEnvAttributes.Enabled : LDConfig.Builder.AutoEnvAttributes.Disabled); + builder.mobileKey(params.credential); builder.logAdapter(logAdapter).loggerName(tag + ".sdk"); @@ -312,8 +314,6 @@ private LDConfig buildSdkConfig(SdkConfigParams params, LDLogAdapter logAdapter, Components.httpConfiguration().useReport(params.clientSide.useReport) ); - builder.includeMobileEnvironmentAttributes(params.clientSide.includeEnvironmentAttributes); - if (params.tags != null) { ApplicationInfoBuilder ab = Components.applicationInfo(); if (params.tags.applicationId != null) { diff --git a/example/src/main/java/com/launchdarkly/example/MainActivity.java b/example/src/main/java/com/launchdarkly/example/MainActivity.java index dca5d469..684f85b3 100644 --- a/example/src/main/java/com/launchdarkly/example/MainActivity.java +++ b/example/src/main/java/com/launchdarkly/example/MainActivity.java @@ -19,6 +19,7 @@ import com.launchdarkly.sdk.android.LDAllFlagsListener; import com.launchdarkly.sdk.android.LDClient; import com.launchdarkly.sdk.android.LDConfig; +import com.launchdarkly.sdk.android.LDConfig.Builder.AutoEnvAttributes; import com.launchdarkly.sdk.android.LDFailure; import com.launchdarkly.sdk.android.LDStatusListener; @@ -68,7 +69,7 @@ public void onCreate(Bundle savedInstanceState) { setupOfflineSwitch(); setupListeners(); - LDConfig ldConfig = new LDConfig.Builder() + LDConfig ldConfig = new LDConfig.Builder(AutoEnvAttributes.Enabled) .mobileKey("MOBILE_KEY") .http( Components.httpConfiguration().useReport(false) diff --git a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/LDClientEndToEndTest.java b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/LDClientEndToEndTest.java index 7d00f8ab..f10b7cf4 100644 --- a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/LDClientEndToEndTest.java +++ b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/LDClientEndToEndTest.java @@ -14,6 +14,7 @@ import com.launchdarkly.sdk.LDContext; import com.launchdarkly.sdk.LDValue; import com.launchdarkly.sdk.android.DataModel.Flag; +import com.launchdarkly.sdk.android.LDConfig.Builder.AutoEnvAttributes; import com.launchdarkly.sdk.android.subsystems.PersistentDataStore; import org.junit.After; @@ -71,7 +72,7 @@ public void after() throws IOException { } private LDConfig.Builder baseConfig() { - return new LDConfig.Builder() + return new LDConfig.Builder(AutoEnvAttributes.Disabled) .mobileKey(MOBILE_KEY) .persistentDataStore(store) .diagnosticOptOut(true) diff --git a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/LDClientEvaluationTest.java b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/LDClientEvaluationTest.java index 4cb02736..99e80ffc 100644 --- a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/LDClientEvaluationTest.java +++ b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/LDClientEvaluationTest.java @@ -13,6 +13,7 @@ import com.launchdarkly.sdk.EvaluationReason; import com.launchdarkly.sdk.LDContext; import com.launchdarkly.sdk.LDValue; +import com.launchdarkly.sdk.android.LDConfig.Builder.AutoEnvAttributes; import com.launchdarkly.sdk.android.subsystems.ComponentConfigurer; import com.launchdarkly.sdk.android.subsystems.DataSource; @@ -171,7 +172,7 @@ private LDClient makeClientWithData(EnvironmentData data) { ComponentConfigurer dataSourceConfig = clientContext -> MockComponents.successfulDataSource(clientContext, data, ConnectionInformation.ConnectionMode.POLLING, null, null); - LDConfig config = new LDConfig.Builder() + LDConfig config = new LDConfig.Builder(AutoEnvAttributes.Disabled) .mobileKey(mobileKey) .dataSource(dataSourceConfig) .events(Components.noEvents()) diff --git a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/LDClientEventTest.java b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/LDClientEventTest.java index a2e22b7d..999c261e 100644 --- a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/LDClientEventTest.java +++ b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/LDClientEventTest.java @@ -12,6 +12,7 @@ import com.launchdarkly.sdk.LDValue; import com.launchdarkly.sdk.ObjectBuilder; import com.launchdarkly.sdk.android.DataModel.Flag; +import com.launchdarkly.sdk.android.LDConfig.Builder.AutoEnvAttributes; import com.launchdarkly.sdk.android.subsystems.PersistentDataStore; import com.launchdarkly.sdk.internal.GsonHelpers; import com.launchdarkly.sdk.json.JsonSerialization; @@ -257,7 +258,7 @@ private LDValue[] getEventsFromLastRequest(MockWebServer server, int expectedCou private LDConfig.Builder baseConfigBuilder(MockWebServer server) { HttpUrl baseUrl = server.url("/"); - return new LDConfig.Builder() + return new LDConfig.Builder(AutoEnvAttributes.Disabled) .mobileKey(mobileKey) .diagnosticOptOut(true) .serviceEndpoints(Components.serviceEndpoints().events(baseUrl.uri())); diff --git a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/LDClientLoggingTest.java b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/LDClientLoggingTest.java index 0ac9fe2e..fa1cb6fd 100644 --- a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/LDClientLoggingTest.java +++ b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/LDClientLoggingTest.java @@ -1,5 +1,9 @@ package com.launchdarkly.sdk.android; +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.assertNotEquals; + import android.app.Application; import androidx.test.core.app.ApplicationProvider; @@ -9,15 +13,12 @@ import com.launchdarkly.logging.LogCapture; import com.launchdarkly.logging.Logs; import com.launchdarkly.sdk.LDContext; +import com.launchdarkly.sdk.android.LDConfig.Builder.AutoEnvAttributes; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.CoreMatchers.containsString; -import static org.junit.Assert.assertNotEquals; - @RunWith(AndroidJUnit4.class) public class LDClientLoggingTest { @@ -34,7 +35,8 @@ public void setUp() { @Test public void customLogAdapterWithDefaultLevel() throws Exception { LogCapture logCapture = Logs.capture(); - LDConfig config = new LDConfig.Builder().mobileKey(mobileKey).offline(true).logAdapter(logCapture).build(); + LDConfig config = new LDConfig.Builder(AutoEnvAttributes.Disabled).mobileKey(mobileKey).offline(true) + .logAdapter(logCapture).build(); try (LDClient ldClient = LDClient.init(application, config, ldUser, 1)) { for (LogCapture.Message m: logCapture.getMessages()) { assertNotEquals(LDLogLevel.DEBUG, m.getLevel()); @@ -47,7 +49,8 @@ public void customLogAdapterWithDefaultLevel() throws Exception { @Test public void customLogAdapterWithDebugLevel() throws Exception { LogCapture logCapture = Logs.capture(); - LDConfig config = new LDConfig.Builder().mobileKey(mobileKey).offline(true).logAdapter(logCapture).logLevel(LDLogLevel.DEBUG).build(); + LDConfig config = new LDConfig.Builder(AutoEnvAttributes.Disabled).mobileKey(mobileKey).offline(true) + .logAdapter(logCapture).logLevel(LDLogLevel.DEBUG).build(); try (LDClient ldClient = LDClient.init(application, config, ldUser, 1)) { logCapture.requireMessage(LDLogLevel.DEBUG, 2000); } diff --git a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/LDClientTest.java b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/LDClientTest.java index 38c57510..2c7455da 100644 --- a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/LDClientTest.java +++ b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/LDClientTest.java @@ -1,5 +1,10 @@ package com.launchdarkly.sdk.android; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + import android.app.Application; import androidx.test.core.app.ApplicationProvider; @@ -8,6 +13,7 @@ import com.launchdarkly.sdk.LDContext; import com.launchdarkly.sdk.LDValue; import com.launchdarkly.sdk.android.DataModel.Flag; +import com.launchdarkly.sdk.android.LDConfig.Builder.AutoEnvAttributes; import com.launchdarkly.sdk.android.subsystems.PersistentDataStore; import org.junit.Before; @@ -18,11 +24,6 @@ import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; - @RunWith(AndroidJUnit4.class) public class LDClientTest { private static final String mobileKey = "test-mobile-key"; @@ -54,7 +55,7 @@ public void testOfflineClientReturnsDefaultsIfThereAreNoStoredFlags() throws Exc @Test public void testOfflineClientUsesStoredFlags() throws Exception { PersistentDataStore store = new InMemoryPersistentDataStore(); - LDConfig config = new LDConfig.Builder() + LDConfig config = new LDConfig.Builder(AutoEnvAttributes.Disabled) .mobileKey(mobileKey) .offline(true) .persistentDataStore(store) @@ -175,7 +176,7 @@ public void testInitBackgroundThread() throws Exception { } private LDConfig makeOfflineConfig() { - return new LDConfig.Builder() + return new LDConfig.Builder(AutoEnvAttributes.Disabled) .mobileKey(mobileKey) .offline(true) .build(); diff --git a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/MultiEnvironmentLDClientTest.java b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/MultiEnvironmentLDClientTest.java index a78ad357..902a8ed7 100644 --- a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/MultiEnvironmentLDClientTest.java +++ b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/MultiEnvironmentLDClientTest.java @@ -1,10 +1,16 @@ package com.launchdarkly.sdk.android; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.launchdarkly.sdk.LDContext; import com.launchdarkly.sdk.LDValue; +import com.launchdarkly.sdk.android.LDConfig.Builder.AutoEnvAttributes; import org.junit.Before; import org.junit.Rule; @@ -16,11 +22,6 @@ import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; - @RunWith(AndroidJUnit4.class) public class MultiEnvironmentLDClientTest { @@ -38,7 +39,7 @@ public void setUp() { secondaryKeys.put("test", "test"); secondaryKeys.put("test1", "test1"); - ldConfig = new LDConfig.Builder() + ldConfig = new LDConfig.Builder(AutoEnvAttributes.Disabled) .mobileKey("default-mobile-key") .offline(true) .secondaryMobileKeys(secondaryKeys) diff --git a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/TestDataWithLDClientTest.java b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/TestDataWithLDClientTest.java index 5afef34b..5bd7fd67 100644 --- a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/TestDataWithLDClientTest.java +++ b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/TestDataWithLDClientTest.java @@ -11,6 +11,7 @@ import com.launchdarkly.sdk.LDContext; import com.launchdarkly.sdk.LDValue; +import com.launchdarkly.sdk.android.LDConfig.Builder.AutoEnvAttributes; import com.launchdarkly.sdk.android.integrations.TestData; import org.junit.Before; @@ -26,7 +27,7 @@ public class TestDataWithLDClientTest { private Application application; public TestDataWithLDClientTest() { - config = new LDConfig.Builder().mobileKey("mobile-key") + config = new LDConfig.Builder(AutoEnvAttributes.Disabled).mobileKey("mobile-key") .dataSource(td) .events(Components.noEvents()) .build(); diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDClient.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDClient.java index 60ad2fa6..649e8c9e 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDClient.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDClient.java @@ -132,12 +132,12 @@ public static Future init(@NonNull Application application, EnvironmentReporterBuilder reporterBuilder = new EnvironmentReporterBuilder(); reporterBuilder.setApplicationInfo(config.applicationInfo); - if (config.isIncludeEnvironmentAttributes()) { + if (config.isAutoEnvAttributes()) { reporterBuilder.enableCollectionFromPlatform(application); } environmentReporter = reporterBuilder.build(); - if (config.isIncludeEnvironmentAttributes()) { + if (config.isAutoEnvAttributes()) { autoEnvContextModifier = new AutoEnvContextModifier(persistentData, environmentReporter, logger); } else { autoEnvContextModifier = new NoOpContextModifier(); diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDConfig.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDConfig.java index 52226e35..f8b525bb 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDConfig.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDConfig.java @@ -69,7 +69,7 @@ public class LDConfig { private final boolean disableBackgroundUpdating; private final boolean evaluationReasons; private final boolean generateAnonymousKeys; - private final boolean includeEnvironmentAttributes; + private final boolean autoEnvAttributes; private final LDLogAdapter logAdapter; private final String loggerName; private final int maxCachedContexts; @@ -88,7 +88,7 @@ public class LDConfig { boolean diagnosticOptOut, int maxCachedContexts, boolean generateAnonymousKeys, - boolean includeEnvironmentAttributes, + boolean autoEnvAttributes, PersistentDataStore persistentDataStore, LDLogAdapter logAdapter, String loggerName) { @@ -104,7 +104,7 @@ public class LDConfig { this.diagnosticOptOut = diagnosticOptOut; this.maxCachedContexts = maxCachedContexts; this.generateAnonymousKeys = generateAnonymousKeys; - this.includeEnvironmentAttributes = includeEnvironmentAttributes; + this.autoEnvAttributes = autoEnvAttributes; this.persistentDataStore = persistentDataStore; this.logAdapter = logAdapter; this.loggerName = loggerName; @@ -140,8 +140,8 @@ int getMaxCachedContexts() { public boolean isGenerateAnonymousKeys() { return generateAnonymousKeys; } - public boolean isIncludeEnvironmentAttributes() { - return includeEnvironmentAttributes; + public boolean isAutoEnvAttributes() { + return autoEnvAttributes; } PersistentDataStore getPersistentDataStore() { return persistentDataStore; } @@ -161,6 +161,26 @@ public boolean isIncludeEnvironmentAttributes() { * */ public static class Builder { + + /** + Enable / disable options for Auto Environment Attributes functionality. When enabled, the SDK will automatically + provide data about the mobile environment where the application is running. This data makes it simpler to target + your mobile customers based on application name or version, or on device characteristics including manufacturer, + model, operating system, locale, and so on. We recommend enabling this when you configure the SDK. See TKTK + for more documentation. + For example, consider a “dark mode” feature being added to an app. Versions 10 through 14 contain early, + incomplete versions of the feature. These versions are available to all customers, but the “dark mode” feature is only + enabled for testers. With version 15, the feature is considered complete. With Auto Environment Attributes enabled, + you can use targeting rules to enable "dark mode" for all customers who are using version 15 or greater, and ensure + that customers on previous versions don't use the earlier, unfinished version of the feature. + */ + public enum AutoEnvAttributes { + // Enables the Auto EnvironmentAttributes functionality. + Enabled, + // Disables the Auto EnvironmentAttributes functionality. + Disabled + } + private String mobileKey; private Map secondaryMobileKeys; @@ -181,7 +201,7 @@ public static class Builder { private boolean generateAnonymousKeys; - private boolean includeEnvironmentAttributes = false; + private boolean autoEnvAttributes = false; private PersistentDataStore persistentDataStore; @@ -189,13 +209,28 @@ public static class Builder { private String loggerName = DEFAULT_LOGGER_NAME; private LDLogLevel logLevel = null; + /** + * LDConfig.Builder constructor. Configurable values are all set to their default values. The client app can + * modify these values as desired. + * + * @param autoEnvAttributes - Enable / disable Auto Environment Attributes functionality. When enabled, the SDK + * will automatically provide data about the mobile environment where the application is + * running. This data makes it simpler to target your mobile customers based on + * application name or version, or on device characteristics including manufacturer, + * model, operating system, locale, and so on. We recommend enabling this when you + * configure the SDK. See TKTK for more documentation. + */ + public Builder(AutoEnvAttributes autoEnvAttributes) { + this.autoEnvAttributes = autoEnvAttributes == AutoEnvAttributes.Enabled; // mapping enum to boolean + } + /** * Sets the key for authenticating with LaunchDarkly. This is required unless you're using the client in offline mode. * * @param mobileKey Get this from the LaunchDarkly web app under Team Settings. * @return the builder */ - public LDConfig.Builder mobileKey(String mobileKey) { + public Builder mobileKey(String mobileKey) { if (secondaryMobileKeys != null && secondaryMobileKeys.containsValue(mobileKey)) { throw new IllegalArgumentException("The primary environment key cannot be in the secondary mobile keys."); } @@ -210,7 +245,7 @@ public LDConfig.Builder mobileKey(String mobileKey) { * @param secondaryMobileKeys A map of identifying names to unique mobile keys to access secondary environments * @return the builder */ - public LDConfig.Builder secondaryMobileKeys(Map secondaryMobileKeys) { + public Builder secondaryMobileKeys(Map secondaryMobileKeys) { if (secondaryMobileKeys == null) { this.secondaryMobileKeys = null; return this; @@ -304,7 +339,7 @@ public Builder applicationInfo(ApplicationInfoBuilder applicationInfoBuilder) { * @see Components#pollingDataSource() * @since 3.3.0 */ - public LDConfig.Builder dataSource(ComponentConfigurer dataSourceConfigurer) { + public Builder dataSource(ComponentConfigurer dataSourceConfigurer) { this.dataSource = dataSourceConfigurer; return this; } @@ -337,7 +372,7 @@ public LDConfig.Builder dataSource(ComponentConfigurer dataSourceCon * @see Components#sendEvents() * @see Components#noEvents() */ - public LDConfig.Builder events(ComponentConfigurer eventsConfigurer) { + public Builder events(ComponentConfigurer eventsConfigurer) { this.events = eventsConfigurer; return this; } @@ -370,7 +405,7 @@ public Builder http(ComponentConfigurer httpConfigurer) { * @param disableBackgroundUpdating true if the client should skip updating flags when in the background * @return the builder */ - public LDConfig.Builder disableBackgroundUpdating(boolean disableBackgroundUpdating) { + public Builder disableBackgroundUpdating(boolean disableBackgroundUpdating) { this.disableBackgroundUpdating = disableBackgroundUpdating; return this; } @@ -386,7 +421,7 @@ public LDConfig.Builder disableBackgroundUpdating(boolean disableBackgroundUpdat * @param offline true if the client should run in offline mode * @return the builder */ - public LDConfig.Builder offline(boolean offline) { + public Builder offline(boolean offline) { this.offline = offline; return this; } @@ -402,7 +437,7 @@ public LDConfig.Builder offline(boolean offline) { * @param evaluationReasons true if detail/reason information should be made available * @return the builder */ - public LDConfig.Builder evaluationReasons(boolean evaluationReasons) { + public Builder evaluationReasons(boolean evaluationReasons) { this.evaluationReasons = evaluationReasons; return this; } @@ -419,7 +454,7 @@ public LDConfig.Builder evaluationReasons(boolean evaluationReasons) { * @param diagnosticOptOut true if you want to opt out of sending any diagnostics data. * @return the builder */ - public LDConfig.Builder diagnosticOptOut(boolean diagnosticOptOut) { + public Builder diagnosticOptOut(boolean diagnosticOptOut) { this.diagnosticOptOut = diagnosticOptOut; return this; } @@ -437,7 +472,7 @@ public LDConfig.Builder diagnosticOptOut(boolean diagnosticOptOut) { * values represent allowing an unlimited number of cached contexts * @return the builder */ - public LDConfig.Builder maxCachedContexts(int maxCachedContexts) { + public Builder maxCachedContexts(int maxCachedContexts) { this.maxCachedContexts = maxCachedContexts; return this; } @@ -470,16 +505,11 @@ public LDConfig.Builder maxCachedContexts(int maxCachedContexts) { * @return the same builder * @since 4.0.0 */ - public LDConfig.Builder generateAnonymousKeys(boolean generateAnonymousKeys) { + public Builder generateAnonymousKeys(boolean generateAnonymousKeys) { this.generateAnonymousKeys = generateAnonymousKeys; return this; } - public LDConfig.Builder includeMobileEnvironmentAttributes(boolean include) { - this.includeEnvironmentAttributes = include; - return this; - } - /** * Specifies a custom data store. Deliberately package-private-- currently this is only * configurable for tests. @@ -487,7 +517,7 @@ public LDConfig.Builder includeMobileEnvironmentAttributes(boolean include) { * @param persistentDataStore the store implementation * @return the same builder */ - LDConfig.Builder persistentDataStore(PersistentDataStore persistentDataStore) { + Builder persistentDataStore(PersistentDataStore persistentDataStore) { this.persistentDataStore = persistentDataStore; return this; } @@ -520,7 +550,7 @@ LDConfig.Builder persistentDataStore(PersistentDataStore persistentDataStore) { * @see LDAndroidLogging * @see com.launchdarkly.logging.Logs */ - public LDConfig.Builder logAdapter(LDLogAdapter logAdapter) { + public Builder logAdapter(LDLogAdapter logAdapter) { this.logAdapter = logAdapter == null ? defaultLogAdapter() : logAdapter; return this; } @@ -558,7 +588,7 @@ public LDConfig.Builder logAdapter(LDLogAdapter logAdapter) { * @see #logAdapter(LDLogAdapter) * @see #loggerName(String) */ - public LDConfig.Builder logLevel(LDLogLevel logLevel) { + public Builder logLevel(LDLogLevel logLevel) { this.logLevel = logLevel; return this; } @@ -578,7 +608,7 @@ public LDConfig.Builder logLevel(LDLogLevel logLevel) { * @see #logAdapter(LDLogAdapter) * @see #logLevel(LDLogLevel) */ - public LDConfig.Builder loggerName(String loggerName) { + public Builder loggerName(String loggerName) { this.loggerName = loggerName == null ? DEFAULT_LOGGER_NAME : loggerName; return this; } @@ -630,7 +660,7 @@ public LDConfig build() { diagnosticOptOut, maxCachedContexts, generateAnonymousKeys, - includeEnvironmentAttributes, + autoEnvAttributes, persistentDataStore, actualLogAdapter, loggerName); diff --git a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/ConnectivityManagerTest.java b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/ConnectivityManagerTest.java index 8a157d66..9ec6a413 100644 --- a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/ConnectivityManagerTest.java +++ b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/ConnectivityManagerTest.java @@ -2,10 +2,19 @@ import static com.launchdarkly.sdk.android.TestUtil.requireNoMoreValues; import static com.launchdarkly.sdk.android.TestUtil.requireValue; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import androidx.annotation.NonNull; import com.launchdarkly.sdk.LDContext; import com.launchdarkly.sdk.LDValue; import com.launchdarkly.sdk.android.ConnectionInformation.ConnectionMode; +import com.launchdarkly.sdk.android.LDConfig.Builder.AutoEnvAttributes; import com.launchdarkly.sdk.android.env.EnvironmentReporterBuilder; import com.launchdarkly.sdk.android.env.IEnvironmentReporter; import com.launchdarkly.sdk.android.subsystems.Callback; @@ -31,15 +40,6 @@ import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; - -import androidx.annotation.NonNull; - public class ConnectivityManagerTest extends EasyMockSupport { // These tests use a mock PlatformState instead of AndroidPlatformState, so that we can test // the ConnectivityManager logic for how to behave under various network/foreground/background @@ -97,7 +97,7 @@ private void createTestManager( boolean backgroundDisabled, ComponentConfigurer dataSourceConfigurer ) { - LDConfig config = new LDConfig.Builder() + LDConfig config = new LDConfig.Builder(AutoEnvAttributes.Disabled) .mobileKey(MOBILE_KEY) .offline(setOffline) .disableBackgroundUpdating(backgroundDisabled) diff --git a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/ContextDataManagerTestBase.java b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/ContextDataManagerTestBase.java index c0b077da..d403a2ac 100644 --- a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/ContextDataManagerTestBase.java +++ b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/ContextDataManagerTestBase.java @@ -7,6 +7,7 @@ import static org.junit.Assert.fail; import com.launchdarkly.sdk.LDContext; +import com.launchdarkly.sdk.android.LDConfig.Builder.AutoEnvAttributes; import com.launchdarkly.sdk.android.env.EnvironmentReporterBuilder; import com.launchdarkly.sdk.android.env.IEnvironmentReporter; import com.launchdarkly.sdk.android.subsystems.ClientContext; @@ -46,7 +47,7 @@ protected static EnvironmentData makeFlagData(int index) { protected ContextDataManager createDataManager(int maxCachedContexts) { ClientContext clientContext = ClientContextImpl.fromConfig( - new LDConfig.Builder().build(), + new LDConfig.Builder(AutoEnvAttributes.Disabled).build(), "mobile-key", "", null, diff --git a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/DiagnosticConfigTest.java b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/DiagnosticConfigTest.java index 50608575..ed9507e4 100644 --- a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/DiagnosticConfigTest.java +++ b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/DiagnosticConfigTest.java @@ -3,6 +3,7 @@ import com.launchdarkly.logging.LDLogger; import com.launchdarkly.sdk.LDValue; import com.launchdarkly.sdk.ObjectBuilder; +import com.launchdarkly.sdk.android.LDConfig.Builder.AutoEnvAttributes; import com.launchdarkly.sdk.android.env.EnvironmentReporterBuilder; import com.launchdarkly.sdk.android.integrations.EventProcessorBuilder; import com.launchdarkly.sdk.android.integrations.StreamingDataSourceBuilder; @@ -33,7 +34,7 @@ public class DiagnosticConfigTest { private static final ScheduledExecutorService EXECUTOR = Executors.newSingleThreadScheduledExecutor(); @Test public void defaultDiagnosticConfiguration() throws Exception { - LDConfig ldConfig = new LDConfig.Builder().build(); + LDConfig ldConfig = new LDConfig.Builder(AutoEnvAttributes.Disabled).build(); LDValue diagnosticJson = makeDiagnosticJson(ldConfig); ObjectBuilder expected = makeExpectedDefaults(); Assert.assertEquals(expected.build(), diagnosticJson); @@ -43,7 +44,7 @@ public void defaultDiagnosticConfiguration() throws Exception { public void customDiagnosticConfigurationGeneral() throws Exception { HashMap secondaryKeys = new HashMap<>(1); secondaryKeys.put("secondary", "key"); - LDConfig ldConfig = new LDConfig.Builder() + LDConfig ldConfig = new LDConfig.Builder(AutoEnvAttributes.Disabled) .serviceEndpoints(Components.serviceEndpoints() .events("https://1.1.1.1") .polling("https://1.1.1.1") @@ -68,7 +69,7 @@ public void customDiagnosticConfigurationGeneral() throws Exception { @Test public void customDiagnosticConfigurationEvents() throws Exception { - LDConfig ldConfig = new LDConfig.Builder() + LDConfig ldConfig = new LDConfig.Builder(AutoEnvAttributes.Disabled) .events( Components.sendEvents() .allAttributesPrivate(true) @@ -89,7 +90,7 @@ public void customDiagnosticConfigurationEvents() throws Exception { @Test public void customDiagnosticConfigurationStreaming() throws Exception { - LDConfig ldConfig = new LDConfig.Builder() + LDConfig ldConfig = new LDConfig.Builder(AutoEnvAttributes.Disabled) .dataSource( Components.streamingDataSource() .backgroundPollIntervalMillis(900_000) @@ -106,7 +107,7 @@ public void customDiagnosticConfigurationStreaming() throws Exception { @Test public void customDiagnosticConfigurationPolling() throws Exception { - LDConfig ldConfig = new LDConfig.Builder() + LDConfig ldConfig = new LDConfig.Builder(AutoEnvAttributes.Disabled) .dataSource( Components.pollingDataSource() .backgroundPollIntervalMillis(900_000) @@ -124,7 +125,7 @@ public void customDiagnosticConfigurationPolling() throws Exception { @Test public void customDiagnosticConfigurationHttp() throws Exception { - LDConfig ldConfig = new LDConfig.Builder() + LDConfig ldConfig = new LDConfig.Builder(AutoEnvAttributes.Disabled) .http( Components.httpConfiguration() .connectTimeoutMillis(5_000) diff --git a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/LDConfigTest.java b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/LDConfigTest.java index c85802f8..f04104ab 100644 --- a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/LDConfigTest.java +++ b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/LDConfigTest.java @@ -1,5 +1,15 @@ package com.launchdarkly.sdk.android; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import com.launchdarkly.sdk.android.LDConfig.Builder.AutoEnvAttributes; +import com.launchdarkly.sdk.android.env.EnvironmentReporterBuilder; +import com.launchdarkly.sdk.android.subsystems.ClientContext; + import org.junit.Rule; import org.junit.Test; @@ -10,21 +20,12 @@ import okhttp3.Headers; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; - -import com.launchdarkly.sdk.android.env.EnvironmentReporterBuilder; -import com.launchdarkly.sdk.android.subsystems.ClientContext; - public class LDConfigTest { @Rule public LogCaptureRule logging = new LogCaptureRule(); @Test public void testBuilderDefaults() { - LDConfig config = new LDConfig.Builder().build(); + LDConfig config = new LDConfig.Builder(LDConfig.Builder.AutoEnvAttributes.Disabled).build(); assertFalse(config.isOffline()); assertFalse(config.isDisableBackgroundPolling()); @@ -36,25 +37,25 @@ public void testBuilderDefaults() { @Test public void testBuilderEvaluationReasons() { - LDConfig config = new LDConfig.Builder().evaluationReasons(true).build(); + LDConfig config = new LDConfig.Builder(AutoEnvAttributes.Disabled).evaluationReasons(true).build(); assertTrue(config.isEvaluationReasons()); } @Test public void testBuilderDiagnosticOptOut() { - LDConfig config = new LDConfig.Builder().diagnosticOptOut(true).build(); + LDConfig config = new LDConfig.Builder(AutoEnvAttributes.Disabled).diagnosticOptOut(true).build(); assertTrue(config.getDiagnosticOptOut()); } @Test public void testBuilderMaxCachedUsers() { - LDConfig config = new LDConfig.Builder().maxCachedContexts(0).build(); + LDConfig config = new LDConfig.Builder(AutoEnvAttributes.Disabled).maxCachedContexts(0).build(); assertEquals(0, config.getMaxCachedContexts()); - config = new LDConfig.Builder().maxCachedContexts(10).build(); + config = new LDConfig.Builder(AutoEnvAttributes.Disabled).maxCachedContexts(10).build(); assertEquals(10, config.getMaxCachedContexts()); - config = new LDConfig.Builder().maxCachedContexts(-1).build(); + config = new LDConfig.Builder(AutoEnvAttributes.Disabled).maxCachedContexts(-1).build(); assertEquals(-1, config.getMaxCachedContexts()); } @@ -72,7 +73,7 @@ Map headersToMap(Headers headers) { @Test public void headersForEnvironment() { - LDConfig config = new LDConfig.Builder().mobileKey("test-key").build(); + LDConfig config = new LDConfig.Builder(AutoEnvAttributes.Disabled).mobileKey("test-key").build(); ClientContext clientContext = ClientContextImpl.fromConfig(config, "test-key", "", null, null, null, null, new EnvironmentReporterBuilder().build(), null); Map headers = headersToMap( @@ -86,7 +87,7 @@ public void headersForEnvironment() { @Test public void headersForEnvironmentWithTransform() { HashMap expected = new HashMap<>(); - LDConfig config = new LDConfig.Builder().mobileKey("test-key") + LDConfig config = new LDConfig.Builder(AutoEnvAttributes.Disabled).mobileKey("test-key") .http(Components.httpConfiguration().headerTransform(headers -> { assertEquals(expected, headers); headers.remove("User-Agent"); @@ -112,7 +113,7 @@ public void headersForEnvironmentWithTransform() { @Test public void serviceEndpointsDefault() { - LDConfig config = new LDConfig.Builder().mobileKey("test-key").build(); + LDConfig config = new LDConfig.Builder(AutoEnvAttributes.Disabled).mobileKey("test-key").build(); assertEquals(StandardEndpoints.DEFAULT_STREAMING_BASE_URI, config.serviceEndpoints.getStreamingBaseUri()); assertEquals(StandardEndpoints.DEFAULT_POLLING_BASE_URI, @@ -123,7 +124,7 @@ public void serviceEndpointsDefault() { @Test public void serviceEndpointsBuilderNullIsSameAsDefault() { - LDConfig config = new LDConfig.Builder().mobileKey("test-key") + LDConfig config = new LDConfig.Builder(AutoEnvAttributes.Disabled).mobileKey("test-key") .serviceEndpoints( Components.serviceEndpoints().streaming("x") ) @@ -139,7 +140,7 @@ public void serviceEndpointsBuilderNullIsSameAsDefault() { @Test public void serviceEndpointsCustom() { - LDConfig config = new LDConfig.Builder().mobileKey("test-key") + LDConfig config = new LDConfig.Builder(AutoEnvAttributes.Disabled).mobileKey("test-key") .serviceEndpoints( Components.serviceEndpoints().streaming("http://uri1") .polling("http://uri2") diff --git a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/PollingDataSourceTest.java b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/PollingDataSourceTest.java index 32c6b641..c022a8c3 100644 --- a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/PollingDataSourceTest.java +++ b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/PollingDataSourceTest.java @@ -2,11 +2,11 @@ import static com.launchdarkly.sdk.android.AssertHelpers.requireNoMoreValues; import static com.launchdarkly.sdk.android.AssertHelpers.requireValue; - import static org.junit.Assert.assertEquals; import com.launchdarkly.sdk.LDContext; import com.launchdarkly.sdk.LDValue; +import com.launchdarkly.sdk.android.LDConfig.Builder.AutoEnvAttributes; import com.launchdarkly.sdk.android.env.EnvironmentReporterBuilder; import com.launchdarkly.sdk.android.env.IEnvironmentReporter; import com.launchdarkly.sdk.android.integrations.PollingDataSourceBuilder; @@ -26,7 +26,7 @@ public class PollingDataSourceTest extends EasyMockSupport { private static final LDContext CONTEXT = LDContext.create("context-key"); - private static final LDConfig EMPTY_CONFIG = new LDConfig.Builder().build(); + private static final LDConfig EMPTY_CONFIG = new LDConfig.Builder(AutoEnvAttributes.Disabled).build(); private final MockComponents.MockDataSourceUpdateSink dataSourceUpdateSink = new MockComponents.MockDataSourceUpdateSink(); private final MockFetcher fetcher = new MockFetcher(); diff --git a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/StreamingDataSourceTest.java b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/StreamingDataSourceTest.java index d03f70b2..8346dee5 100644 --- a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/StreamingDataSourceTest.java +++ b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/StreamingDataSourceTest.java @@ -4,6 +4,7 @@ import static org.junit.Assert.assertTrue; import com.launchdarkly.sdk.LDContext; +import com.launchdarkly.sdk.android.LDConfig.Builder.AutoEnvAttributes; import com.launchdarkly.sdk.android.env.EnvironmentReporterBuilder; import com.launchdarkly.sdk.android.env.IEnvironmentReporter; import com.launchdarkly.sdk.android.subsystems.Callback; @@ -34,7 +35,7 @@ public class StreamingDataSourceTest { private ClientContext makeClientContext(boolean inBackground, Boolean previouslyInBackground) { ClientContext baseClientContext = ClientContextImpl.fromConfig( - new LDConfig.Builder().build(), "", "", null, CONTEXT, + new LDConfig.Builder(AutoEnvAttributes.Disabled).build(), "", "", null, CONTEXT, logging.logger, platformState, environmentReporter, taskExecutor); return ClientContextImpl.forDataSource( baseClientContext, @@ -51,7 +52,7 @@ private ClientContext makeClientContext(boolean inBackground, Boolean previously // that has a fetcher private ClientContext makeClientContextWithFetcher() { ClientContext baseClientContext = ClientContextImpl.fromConfig( - new LDConfig.Builder().build(), "", "", makeFeatureFetcher(), CONTEXT, + new LDConfig.Builder(AutoEnvAttributes.Disabled).build(), "", "", makeFeatureFetcher(), CONTEXT, logging.logger, platformState, environmentReporter, taskExecutor); return ClientContextImpl.forDataSource( baseClientContext, diff --git a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/TestDataTest.java b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/TestDataTest.java index 557e1b73..783098fb 100644 --- a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/TestDataTest.java +++ b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/TestDataTest.java @@ -1,13 +1,13 @@ package com.launchdarkly.sdk.android; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; import com.launchdarkly.logging.LDLogger; import com.launchdarkly.sdk.ContextKind; import com.launchdarkly.sdk.EvaluationReason; import com.launchdarkly.sdk.LDContext; import com.launchdarkly.sdk.LDValue; +import com.launchdarkly.sdk.android.LDConfig.Builder.AutoEnvAttributes; import com.launchdarkly.sdk.android.integrations.TestData; import com.launchdarkly.sdk.android.subsystems.ClientContext; import com.launchdarkly.sdk.android.subsystems.DataSource; @@ -153,7 +153,7 @@ public void flagConfigByVariationByValue() { private void createAndStart() { ClientContext clientContext = new ClientContext("", null, LDLogger.none(), - new LDConfig.Builder().build(), updates, "", false, + new LDConfig.Builder(AutoEnvAttributes.Disabled).build(), updates, "", false, initialUser, null, false, null, null, false); DataSource ds = td.build(clientContext); AwaitableCallback callback = new AwaitableCallback<>(); From ff596c2e1bd37e1e92c405e1223e9569f35b7ee4 Mon Sep 17 00:00:00 2001 From: Todd Anderson Date: Mon, 31 Jul 2023 16:05:49 -0500 Subject: [PATCH 17/26] Adding LDClientEvent tests for AutoEnvAttributes Enabled/Disabled. --- .../sdk/android/LDClientEventTest.java | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/LDClientEventTest.java b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/LDClientEventTest.java index 999c261e..052a05e3 100644 --- a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/LDClientEventTest.java +++ b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/LDClientEventTest.java @@ -1,6 +1,8 @@ package com.launchdarkly.sdk.android; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import android.app.Application; @@ -242,6 +244,70 @@ public void testEventBufferFillsUp() throws IOException, InterruptedException { } } + @Test + public void testEventContainsAutoEnvAttributesWhenEnabled() throws Exception { + try (MockWebServer mockEventsServer = new MockWebServer()) { + mockEventsServer.start(); + // Enqueue a successful empty response + mockEventsServer.enqueue(new MockResponse()); + + HttpUrl baseUrl = mockEventsServer.url("/"); + LDConfig ldConfig = new LDConfig.Builder(AutoEnvAttributes.Enabled) + .mobileKey(mobileKey) + .diagnosticOptOut(true) + .serviceEndpoints(Components.serviceEndpoints() + .events(baseUrl.uri()) + .streaming(baseUrl.uri()) + .polling(baseUrl.uri()) + ) + .build(); + + // Don't wait as we are not set offline + try (LDClient ldClient = LDClient.init(application, ldConfig, ldUser, 0)){ + ldClient.track("test-event"); + ldClient.blockingFlush(); + + LDValue[] events = getEventsFromLastRequest(mockEventsServer, 2); + LDValue identifyEvent = events[0], customEvent = events[1]; + assertIdentifyEvent(identifyEvent, ldUser); + assertTrue(customEvent.get("contextKeys").toString().contains("ld_application")); + assertTrue(customEvent.get("contextKeys").toString().contains("ld_device")); + } + } + } + + @Test + public void testEventDoesNotContainAutoEnvAttributesWhenDisabled() throws Exception { + try (MockWebServer mockEventsServer = new MockWebServer()) { + mockEventsServer.start(); + // Enqueue a successful empty response + mockEventsServer.enqueue(new MockResponse()); + + HttpUrl baseUrl = mockEventsServer.url("/"); + LDConfig ldConfig = new LDConfig.Builder(AutoEnvAttributes.Disabled) + .mobileKey(mobileKey) + .diagnosticOptOut(true) + .serviceEndpoints(Components.serviceEndpoints() + .events(baseUrl.uri()) + .streaming(baseUrl.uri()) + .polling(baseUrl.uri()) + ) + .build(); + + // Don't wait as we are not set offline + try (LDClient ldClient = LDClient.init(application, ldConfig, ldUser, 0)){ + ldClient.track("test-event"); + ldClient.blockingFlush(); + + LDValue[] events = getEventsFromLastRequest(mockEventsServer, 2); + LDValue identifyEvent = events[0], customEvent = events[1]; + assertIdentifyEvent(identifyEvent, ldUser); + assertFalse(customEvent.get("contextKeys").toString().contains("ld_application")); + assertFalse(customEvent.get("contextKeys").toString().contains("ld_device")); + } + } + } + private LDValue[] getEventsFromLastRequest(MockWebServer server, int expectedCount) throws InterruptedException { RecordedRequest r = server.takeRequest(); assertEquals("POST", r.getMethod()); From ede22d830899cf40b68c360a55af906ff0e3e4be Mon Sep 17 00:00:00 2001 From: Todd Anderson Date: Thu, 3 Aug 2023 10:40:42 -0500 Subject: [PATCH 18/26] Deprecating LDUser and related functionality. --- .../java/com/launchdarkly/sdk/android/LDClient.java | 10 ++++++++++ .../launchdarkly/sdk/android/LDClientInterface.java | 3 +++ 2 files changed, 13 insertions(+) diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDClient.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDClient.java index fceee94c..bc081500 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDClient.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDClient.java @@ -200,7 +200,10 @@ public void onError(Throwable e) { * @return a {@link Future} which will complete once the client has been initialized * @see #init(Application, LDConfig, LDUser, int) * @see #init(Application, LDConfig, LDContext) + * + * @deprecated use {@link #init(Application, LDConfig, LDContext)} with {@link LDContext} */ + @Deprecated public static Future init(@NonNull Application application, @NonNull LDConfig config, @NonNull LDUser user) { @@ -257,7 +260,10 @@ public static LDClient init(Application application, LDConfig config, LDContext * @return the primary LDClient instance * @see #init(Application, LDConfig, LDUser) * @see #init(Application, LDConfig, LDContext, int) + * + * @deprecated use {@link #init(Application, LDConfig, LDContext, int)} with {@link LDContext} */ + @Deprecated public static LDClient init(Application application, LDConfig config, LDUser user, int startWaitSeconds) { return init(application, config, LDContext.fromUser(user), startWaitSeconds); } @@ -396,6 +402,10 @@ public Future identify(LDContext context) { return identifyInstances(contextDecorator.decorateContext(context, getSharedLogger())); } + /** + * @deprecated use {@link #identify(LDContext)} with {@link LDContext} + */ + @Deprecated @Override public Future identify(LDUser user) { return identify(LDContext.fromUser(user)); diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDClientInterface.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDClientInterface.java index 7f9be138..d8fe7a2e 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDClientInterface.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDClientInterface.java @@ -146,7 +146,10 @@ public interface LDClientInterface extends Closeable { * @return a Future whose success indicates the flag values for the new evaluation context have * been stored locally and are ready for use * @see #identify(LDContext) + * + * @deprecated use {@link #identify(LDContext)} with {@link LDContext} */ + @Deprecated Future identify(LDUser user); /** From e9378f68e2519f80bae958ffaf3cff500bdd3560 Mon Sep 17 00:00:00 2001 From: Todd Anderson Date: Thu, 3 Aug 2023 11:58:00 -0500 Subject: [PATCH 19/26] Updating java-sdk-common to use version with LDUser deprecation --- launchdarkly-android-client-sdk/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launchdarkly-android-client-sdk/build.gradle b/launchdarkly-android-client-sdk/build.gradle index bf565e1d..62401158 100644 --- a/launchdarkly-android-client-sdk/build.gradle +++ b/launchdarkly-android-client-sdk/build.gradle @@ -64,7 +64,7 @@ ext.versions = [ "jacksonCore": "2.10.5", "jacksonDatabind": "2.10.5.1", "junit": "4.13", - "launchdarklyJavaSdkCommon": "2.0.1", + "launchdarklyJavaSdkCommon": "2.1.0", "launchdarklyJavaSdkInternal": "1.0.0", "launchdarklyLogging": "1.1.1", "okhttp": "4.9.2", From 4742f962eb5edda66c1a31ce31f169b6827dcf70 Mon Sep 17 00:00:00 2001 From: Todd Anderson Date: Thu, 3 Aug 2023 14:47:55 -0500 Subject: [PATCH 20/26] Removing previously deprecated LDUser and related functionality. --- .../launchdarkly/sdktest/Representations.java | 3 - .../launchdarkly/sdktest/SdkClientEntity.java | 10 +-- ...HasherTest.java => ContextHasherTest.java} | 6 +- .../sdk/android/LDClientEventTest.java | 56 ++++++++--------- .../sdk/android/LDClientLoggingTest.java | 8 +-- .../sdk/android/LDClientTest.java | 18 +++--- .../android/MultiEnvironmentLDClientTest.java | 12 ++-- .../sdk/android/TestDataWithLDClientTest.java | 16 ++--- .../launchdarkly/sdk/android/EventUtil.java | 2 +- .../sdk/android/LDAllFlagsListener.java | 2 +- .../launchdarkly/sdk/android/LDClient.java | 62 ------------------- .../sdk/android/LDClientInterface.java | 20 ------ .../launchdarkly/sdk/android/LDConfig.java | 4 +- .../integrations/ApplicationInfoBuilder.java | 8 +-- .../integrations/EventProcessorBuilder.java | 13 ++-- .../HttpConfigurationBuilder.java | 4 +- .../android/subsystems/ApplicationInfo.java | 4 +- .../android/subsystems/EventProcessor.java | 1 - .../sdk/android/LDConfigTest.java | 2 +- .../sdk/android/TestDataTest.java | 44 ++++++------- 20 files changed, 99 insertions(+), 196 deletions(-) rename launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/{UserHasherTest.java => ContextHasherTest.java} (89%) diff --git a/contract-tests/src/main/java/com/launchdarkly/sdktest/Representations.java b/contract-tests/src/main/java/com/launchdarkly/sdktest/Representations.java index 853dd464..559dd558 100644 --- a/contract-tests/src/main/java/com/launchdarkly/sdktest/Representations.java +++ b/contract-tests/src/main/java/com/launchdarkly/sdktest/Representations.java @@ -3,7 +3,6 @@ import com.google.gson.annotations.SerializedName; import com.launchdarkly.sdk.EvaluationReason; import com.launchdarkly.sdk.LDContext; -import com.launchdarkly.sdk.LDUser; import com.launchdarkly.sdk.LDValue; import java.util.Map; @@ -68,7 +67,6 @@ public static class SdkConfigServiceEndpointParams { public static class SdkConfigClientSideParams { LDContext initialContext; - LDUser initialUser; boolean evaluationReasons; boolean useReport; boolean includeEnvironmentAttributes; @@ -109,7 +107,6 @@ public static class EvaluateAllFlagsResponse { public static class IdentifyEventParams { LDContext context; - LDUser user; } public static class CustomEventParams { diff --git a/contract-tests/src/main/java/com/launchdarkly/sdktest/SdkClientEntity.java b/contract-tests/src/main/java/com/launchdarkly/sdktest/SdkClientEntity.java index ca5de913..7044409b 100644 --- a/contract-tests/src/main/java/com/launchdarkly/sdktest/SdkClientEntity.java +++ b/contract-tests/src/main/java/com/launchdarkly/sdktest/SdkClientEntity.java @@ -62,9 +62,7 @@ public SdkClientEntity(Application application, CreateInstanceParams params, LDL long startWaitMs = params.configuration.startWaitTimeMs != null ? params.configuration.startWaitTimeMs.longValue() : 5000; Representations.SdkConfigClientSideParams clientSideParams = params.configuration.clientSide; - Future initFuture = clientSideParams.initialUser == null ? - LDClient.init(application, config, clientSideParams.initialContext) : - LDClient.init(application, config, clientSideParams.initialUser); + Future initFuture = LDClient.init(application, config, clientSideParams.initialContext); // Try to initialize client, but if it fails, keep going in case the test harness wants us to // work with an uninitialized client try { @@ -184,11 +182,7 @@ private EvaluateAllFlagsResponse doEvaluateAll(EvaluateAllFlagsParams params) { private void doIdentifyEvent(IdentifyEventParams params) { try { - if (params.user == null) { - client.identify(params.context).get(); - } else { - client.identify(params.user).get(); - } + client.identify(params.context).get(); } catch (ExecutionException | InterruptedException e) { throw new RuntimeException("Error waiting for identify", e); } diff --git a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/UserHasherTest.java b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/ContextHasherTest.java similarity index 89% rename from launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/UserHasherTest.java rename to launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/ContextHasherTest.java index fbd92b5f..db2922fd 100644 --- a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/UserHasherTest.java +++ b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/ContextHasherTest.java @@ -9,10 +9,10 @@ import static org.junit.Assert.assertNotEquals; @RunWith(AndroidJUnit4.class) -public class UserHasherTest { +public class ContextHasherTest { @Test - public void testUserHasherReturnsUniqueResults(){ + public void testContextHasherReturnsUniqueResults(){ ContextHasher contextHasher1 = new ContextHasher(); String input1 = "{'key':'userKey1'}"; @@ -22,7 +22,7 @@ public void testUserHasherReturnsUniqueResults(){ } @Test - public void testDifferentUserHashersReturnSameResults(){ + public void testDifferentContextHashersReturnSameResults(){ ContextHasher contextHasher1 = new ContextHasher(); ContextHasher contextHasher2 = new ContextHasher(); ContextHasher contextHasher3 = new ContextHasher(); diff --git a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/LDClientEventTest.java b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/LDClientEventTest.java index 052a05e3..db35d415 100644 --- a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/LDClientEventTest.java +++ b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/LDClientEventTest.java @@ -35,7 +35,7 @@ public class LDClientEventTest { // purpose of these tests is to validate event-related logic, not the store implementation. private static final String mobileKey = "test-mobile-key"; - private static final LDContext ldUser = LDContext.create("userKey"); + private static final LDContext ldContext = LDContext.create("userKey"); private Application application; @Before @@ -53,14 +53,14 @@ public void testTrack() throws IOException, InterruptedException { LDConfig ldConfig = baseConfigBuilder(mockEventsServer).build(); // Don't wait as we are not set offline - try (LDClient ldClient = LDClient.init(application, ldConfig, ldUser, 0)){ + try (LDClient ldClient = LDClient.init(application, ldConfig, ldContext, 0)){ ldClient.track("test-event"); ldClient.blockingFlush(); LDValue[] events = getEventsFromLastRequest(mockEventsServer, 2); LDValue identifyEvent = events[0], customEvent = events[1]; - assertIdentifyEvent(identifyEvent, ldUser); - assertCustomEvent(customEvent, ldUser, "test-event"); + assertIdentifyEvent(identifyEvent, ldContext); + assertCustomEvent(customEvent, ldContext, "test-event"); assertEquals(LDValue.ofNull(), customEvent.get("data")); assertEquals(LDValue.ofNull(), customEvent.get("metricValue")); } @@ -76,7 +76,7 @@ public void testTrackData() throws IOException, InterruptedException { LDConfig ldConfig = baseConfigBuilder(mockEventsServer).build(); // Don't wait as we are not set offline - try (LDClient client = LDClient.init(application, ldConfig, ldUser, 0)) { + try (LDClient client = LDClient.init(application, ldConfig, ldContext, 0)) { LDValue testData = LDValue.of("abc"); client.trackData("test-event", testData); @@ -84,8 +84,8 @@ public void testTrackData() throws IOException, InterruptedException { LDValue[] events = getEventsFromLastRequest(mockEventsServer, 2); LDValue identifyEvent = events[0], customEvent = events[1]; - assertIdentifyEvent(identifyEvent, ldUser); - assertCustomEvent(customEvent, ldUser, "test-event"); + assertIdentifyEvent(identifyEvent, ldContext); + assertCustomEvent(customEvent, ldContext, "test-event"); assertEquals(testData, customEvent.get("data")); assertEquals(LDValue.ofNull(), customEvent.get("metricValue")); } @@ -100,14 +100,14 @@ public void testTrackDataValueNull() throws IOException, InterruptedException { mockEventsServer.enqueue(new MockResponse()); LDConfig ldConfig = baseConfigBuilder(mockEventsServer).build(); - try (LDClient client = LDClient.init(application, ldConfig, ldUser, 0)) { + try (LDClient client = LDClient.init(application, ldConfig, ldContext, 0)) { client.trackData("test-event", null); client.blockingFlush(); LDValue[] events = getEventsFromLastRequest(mockEventsServer, 2); LDValue identifyEvent = events[0], customEvent = events[1]; - assertIdentifyEvent(identifyEvent, ldUser); - assertCustomEvent(customEvent, ldUser, "test-event"); + assertIdentifyEvent(identifyEvent, ldContext); + assertCustomEvent(customEvent, ldContext, "test-event"); assertEquals(LDValue.ofNull(), customEvent.get("data")); assertEquals(LDValue.ofNull(), customEvent.get("metricValue")); } @@ -122,14 +122,14 @@ public void testTrackDataValueOfNull() throws IOException, InterruptedException mockEventsServer.enqueue(new MockResponse()); LDConfig ldConfig = baseConfigBuilder(mockEventsServer).build(); - try (LDClient client = LDClient.init(application, ldConfig, ldUser, 0)) { + try (LDClient client = LDClient.init(application, ldConfig, ldContext, 0)) { client.trackData("test-event", LDValue.ofNull()); client.blockingFlush(); LDValue[] events = getEventsFromLastRequest(mockEventsServer, 2); LDValue identifyEvent = events[0], customEvent = events[1]; - assertIdentifyEvent(identifyEvent, ldUser); - assertCustomEvent(customEvent, ldUser, "test-event"); + assertIdentifyEvent(identifyEvent, ldContext); + assertCustomEvent(customEvent, ldContext, "test-event"); assertEquals(LDValue.ofNull(), customEvent.get("data")); assertEquals(LDValue.ofNull(), customEvent.get("metricValue")); } @@ -144,7 +144,7 @@ public void testTrackMetric() throws IOException, InterruptedException { mockEventsServer.enqueue(new MockResponse()); LDConfig ldConfig = baseConfigBuilder(mockEventsServer).build(); - try (LDClient client = LDClient.init(application, ldConfig, ldUser, 0)) { + try (LDClient client = LDClient.init(application, ldConfig, ldContext, 0)) { LDValue testData = LDValue.of("abc"); client.trackMetric("test-event", testData, 5.5); @@ -152,8 +152,8 @@ public void testTrackMetric() throws IOException, InterruptedException { LDValue[] events = getEventsFromLastRequest(mockEventsServer, 2); LDValue identifyEvent = events[0], customEvent = events[1]; - assertIdentifyEvent(identifyEvent, ldUser); - assertCustomEvent(customEvent, ldUser, "test-event"); + assertIdentifyEvent(identifyEvent, ldContext); + assertCustomEvent(customEvent, ldContext, "test-event"); assertEquals(testData, customEvent.get("data")); assertEquals(LDValue.of(5.5), customEvent.get("metricValue")); } @@ -172,19 +172,19 @@ public void variationFlagTrackReasonGeneratesEventWithReason() throws IOExceptio .variation(1).value(LDValue.of(true)).reason(EvaluationReason.off()) .trackEvents(true).trackReason(true).build(); PersistentDataStore store = new InMemoryPersistentDataStore(); - TestUtil.writeFlagUpdateToStore(store, mobileKey, ldUser, flag); + TestUtil.writeFlagUpdateToStore(store, mobileKey, ldContext, flag); LDConfig ldConfig = baseConfigBuilder(mockEventsServer) .persistentDataStore(store).build(); - try (LDClient client = LDClient.init(application, ldConfig, ldUser, 0)) { + try (LDClient client = LDClient.init(application, ldConfig, ldContext, 0)) { client.boolVariation("track-reason-flag", false); client.blockingFlush(); LDValue[] events = getEventsFromLastRequest(mockEventsServer, 3); LDValue identifyEvent = events[0], featureEvent = events[1], summaryEvent = events[2]; - assertIdentifyEvent(identifyEvent, ldUser); - assertFeatureEvent(featureEvent, ldUser); + assertIdentifyEvent(identifyEvent, ldContext); + assertFeatureEvent(featureEvent, ldContext); assertEquals(LDValue.of("track-reason-flag"), featureEvent.get("key")); assertEquals(LDValue.of(1), featureEvent.get("variation")); assertEquals(LDValue.of(true), featureEvent.get("value")); @@ -208,7 +208,7 @@ public void additionalHeadersIncludedInEventsRequest() throws IOException, Inter headers.put("Proxy-Authorization", "token"); headers.put("Authorization", "foo"); })).build(); - try (LDClient client = LDClient.init(application, ldConfig, ldUser, 0)) { + try (LDClient client = LDClient.init(application, ldConfig, ldContext, 0)) { client.blockingFlush(); } @@ -230,8 +230,8 @@ public void testEventBufferFillsUp() throws IOException, InterruptedException { .build(); // Don't wait as we are not set offline - try (LDClient client = LDClient.init(application, ldConfig, ldUser, 0)) { - client.identify(ldUser); + try (LDClient client = LDClient.init(application, ldConfig, ldContext, 0)) { + client.identify(ldContext); LDValue testData = LDValue.of("xyz"); client.trackData("test-event", testData); Thread.sleep(200); // let it drain the queue so the flush request isn't lost @@ -239,7 +239,7 @@ public void testEventBufferFillsUp() throws IOException, InterruptedException { // Verify that only the first event was sent and other events were dropped LDValue[] events = getEventsFromLastRequest(mockEventsServer, 1); - assertIdentifyEvent(events[0], ldUser); + assertIdentifyEvent(events[0], ldContext); } } } @@ -263,13 +263,13 @@ public void testEventContainsAutoEnvAttributesWhenEnabled() throws Exception { .build(); // Don't wait as we are not set offline - try (LDClient ldClient = LDClient.init(application, ldConfig, ldUser, 0)){ + try (LDClient ldClient = LDClient.init(application, ldConfig, ldContext, 0)){ ldClient.track("test-event"); ldClient.blockingFlush(); LDValue[] events = getEventsFromLastRequest(mockEventsServer, 2); LDValue identifyEvent = events[0], customEvent = events[1]; - assertIdentifyEvent(identifyEvent, ldUser); + assertIdentifyEvent(identifyEvent, ldContext); assertTrue(customEvent.get("contextKeys").toString().contains("ld_application")); assertTrue(customEvent.get("contextKeys").toString().contains("ld_device")); } @@ -295,13 +295,13 @@ public void testEventDoesNotContainAutoEnvAttributesWhenDisabled() throws Except .build(); // Don't wait as we are not set offline - try (LDClient ldClient = LDClient.init(application, ldConfig, ldUser, 0)){ + try (LDClient ldClient = LDClient.init(application, ldConfig, ldContext, 0)){ ldClient.track("test-event"); ldClient.blockingFlush(); LDValue[] events = getEventsFromLastRequest(mockEventsServer, 2); LDValue identifyEvent = events[0], customEvent = events[1]; - assertIdentifyEvent(identifyEvent, ldUser); + assertIdentifyEvent(identifyEvent, ldContext); assertFalse(customEvent.get("contextKeys").toString().contains("ld_application")); assertFalse(customEvent.get("contextKeys").toString().contains("ld_device")); } diff --git a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/LDClientLoggingTest.java b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/LDClientLoggingTest.java index fa1cb6fd..a081af04 100644 --- a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/LDClientLoggingTest.java +++ b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/LDClientLoggingTest.java @@ -24,12 +24,12 @@ public class LDClientLoggingTest { private static final String mobileKey = "test-mobile-key"; private Application application; - private LDContext ldUser; + private LDContext ldContext; @Before public void setUp() { application = ApplicationProvider.getApplicationContext(); - ldUser = LDContext.create("key"); + ldContext = LDContext.create("key"); } @Test @@ -37,7 +37,7 @@ public void customLogAdapterWithDefaultLevel() throws Exception { LogCapture logCapture = Logs.capture(); LDConfig config = new LDConfig.Builder(AutoEnvAttributes.Disabled).mobileKey(mobileKey).offline(true) .logAdapter(logCapture).build(); - try (LDClient ldClient = LDClient.init(application, config, ldUser, 1)) { + try (LDClient ldClient = LDClient.init(application, config, ldContext, 1)) { for (LogCapture.Message m: logCapture.getMessages()) { assertNotEquals(LDLogLevel.DEBUG, m.getLevel()); } @@ -51,7 +51,7 @@ public void customLogAdapterWithDebugLevel() throws Exception { LogCapture logCapture = Logs.capture(); LDConfig config = new LDConfig.Builder(AutoEnvAttributes.Disabled).mobileKey(mobileKey).offline(true) .logAdapter(logCapture).logLevel(LDLogLevel.DEBUG).build(); - try (LDClient ldClient = LDClient.init(application, config, ldUser, 1)) { + try (LDClient ldClient = LDClient.init(application, config, ldContext, 1)) { logCapture.requireMessage(LDLogLevel.DEBUG, 2000); } } diff --git a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/LDClientTest.java b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/LDClientTest.java index 2c7455da..18194366 100644 --- a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/LDClientTest.java +++ b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/LDClientTest.java @@ -27,7 +27,7 @@ @RunWith(AndroidJUnit4.class) public class LDClientTest { private static final String mobileKey = "test-mobile-key"; - private static final LDContext ldUser = LDContext.create("userKey"); + private static final LDContext ldContext = LDContext.create("userKey"); private Application application; @@ -38,7 +38,7 @@ public void setUp() { @Test public void testOfflineClientReturnsDefaultsIfThereAreNoStoredFlags() throws Exception { - try (LDClient ldClient = LDClient.init(application, makeOfflineConfig(), ldUser, 1)) { + try (LDClient ldClient = LDClient.init(application, makeOfflineConfig(), ldContext, 1)) { assertTrue("client was not initialized", ldClient.isInitialized()); assertTrue("client was offline", ldClient.isOffline()); @@ -63,9 +63,9 @@ public void testOfflineClientUsesStoredFlags() throws Exception { String flagKey = "flag-key", flagValue = "stored-value"; Flag flag = new FlagBuilder(flagKey).version(1).value(LDValue.of(flagValue)).build(); - TestUtil.writeFlagUpdateToStore(store, mobileKey, ldUser, flag); + TestUtil.writeFlagUpdateToStore(store, mobileKey, ldContext, flag); - try (LDClient ldClient = LDClient.init(application, config, ldUser, 1)) { + try (LDClient ldClient = LDClient.init(application, config, ldContext, 1)) { assertTrue("client was not initialized", ldClient.isInitialized()); assertTrue("client was offline", ldClient.isOffline()); @@ -75,7 +75,7 @@ public void testOfflineClientUsesStoredFlags() throws Exception { @Test public void givenDefaultsAreNullAndTestOfflineClientReturnsDefaults() throws Exception { - try (LDClient ldClient = LDClient.init(application, makeOfflineConfig(), ldUser, 1)) { + try (LDClient ldClient = LDClient.init(application, makeOfflineConfig(), ldContext, 1)) { assertTrue(ldClient.isInitialized()); assertTrue(ldClient.isOffline()); assertNull(ldClient.stringVariation("stringFlag", null)); @@ -88,7 +88,7 @@ public void testInitMissingApplication() throws Exception { ExecutionException actualFutureException = null; LaunchDarklyException actualProvidedException = null; - Future ldClientFuture = LDClient.init(null, makeOfflineConfig(), ldUser); + Future ldClientFuture = LDClient.init(null, makeOfflineConfig(), ldContext); try { ldClientFuture.get(); @@ -108,7 +108,7 @@ public void testInitMissingConfig() throws Exception { ExecutionException actualFutureException = null; LaunchDarklyException actualProvidedException = null; - Future ldClientFuture = LDClient.init(application, null, ldUser); + Future ldClientFuture = LDClient.init(application, null, ldContext); try { ldClientFuture.get(); @@ -147,7 +147,7 @@ public void testInitMissingContext() throws Exception { @Test public void testDoubleClose() throws IOException { - LDClient ldClient = LDClient.init(application, makeOfflineConfig(), ldUser, 1); + LDClient ldClient = LDClient.init(application, makeOfflineConfig(), ldContext, 1); ldClient.close(); ldClient.close(); } @@ -156,7 +156,7 @@ public void testDoubleClose() throws IOException { public void testInitBackgroundThread() throws Exception { Future backgroundComplete = new BackgroundThreadExecutor().newFixedThreadPool(1).submit(() -> { try { - try (LDClient ldClient = LDClient.init(application, makeOfflineConfig(), ldUser).get()) { + try (LDClient ldClient = LDClient.init(application, makeOfflineConfig(), ldContext).get()) { assertTrue(ldClient.isInitialized()); assertTrue(ldClient.isOffline()); diff --git a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/MultiEnvironmentLDClientTest.java b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/MultiEnvironmentLDClientTest.java index 902a8ed7..abf6e9d2 100644 --- a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/MultiEnvironmentLDClientTest.java +++ b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/MultiEnvironmentLDClientTest.java @@ -31,7 +31,7 @@ public class MultiEnvironmentLDClientTest { private LDClient ldClient; private Future ldClientFuture; private LDConfig ldConfig; - private LDContext ldUser; + private LDContext ldContext; @Before public void setUp() { @@ -47,12 +47,12 @@ public void setUp() { .loggerName(logging.loggerName) .build(); - ldUser = LDContext.create("userKey"); + ldContext = LDContext.create("userKey"); } @Test public void testOfflineClientReturnsDefaults() { - ldClient = LDClient.init(ApplicationProvider.getApplicationContext(), ldConfig, ldUser, 1); + ldClient = LDClient.init(ApplicationProvider.getApplicationContext(), ldConfig, ldContext, 1); assertTrue(ldClient.isInitialized()); assertTrue(ldClient.isOffline()); @@ -68,7 +68,7 @@ public void testOfflineClientReturnsDefaults() { @Test public void givenDefaultsAreNullAndTestOfflineClientReturnsDefaults() { - ldClient = LDClient.init(ApplicationProvider.getApplicationContext(), ldConfig, ldUser, 1); + ldClient = LDClient.init(ApplicationProvider.getApplicationContext(), ldConfig, ldContext, 1); assertTrue(ldClient.isInitialized()); assertTrue(ldClient.isOffline()); @@ -81,7 +81,7 @@ public void testInitMissingApplication() { ExecutionException actualFutureException = null; LaunchDarklyException actualProvidedException = null; - ldClientFuture = LDClient.init(null, ldConfig, ldUser); + ldClientFuture = LDClient.init(null, ldConfig, ldContext); try { ldClientFuture.get(); @@ -102,7 +102,7 @@ public void testInitMissingConfig() { ExecutionException actualFutureException = null; LaunchDarklyException actualProvidedException = null; - ldClientFuture = LDClient.init(ApplicationProvider.getApplicationContext(), null, ldUser); + ldClientFuture = LDClient.init(ApplicationProvider.getApplicationContext(), null, ldContext); try { ldClientFuture.get(); diff --git a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/TestDataWithLDClientTest.java b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/TestDataWithLDClientTest.java index 5bd7fd67..1d968bfd 100644 --- a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/TestDataWithLDClientTest.java +++ b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/TestDataWithLDClientTest.java @@ -22,7 +22,7 @@ public class TestDataWithLDClientTest { private final TestData td = TestData.dataSource(); private final LDConfig config; - private final LDContext user = LDContext.create("userkey"); + private final LDContext context = LDContext.create("userkey"); private Application application; @@ -39,7 +39,7 @@ public void setUp() { } private LDClient makeClient() throws Exception { - return LDClient.init(application, config, user, 5); + return LDClient.init(application, config, context, 5); } @Test @@ -76,17 +76,17 @@ public void canSetValuePerContext() throws Exception { .variation(0) .variationValueFunc(c -> c.getValue("favoriteColor")) ); - LDContext user1 = LDContext.create("user1"); - LDContext user2 = LDContext.builder("user2").set("favoriteColor", "green").build(); - LDContext user3 = LDContext.builder("user3").set("favoriteColor", "blue").build(); + LDContext context1 = LDContext.create("user1"); + LDContext context2 = LDContext.builder("user2").set("favoriteColor", "green").build(); + LDContext context3 = LDContext.builder("user3").set("favoriteColor", "blue").build(); - try (LDClient client = LDClient.init(application, config, user1, 5);) { + try (LDClient client = LDClient.init(application, config, context1, 5);) { assertEquals("red", client.stringVariation("flag", "")); - client.identify(user2).get(); + client.identify(context2).get(); assertEquals("green", client.stringVariation("flag", "")); - client.identify(user3).get(); + client.identify(context3).get(); assertEquals("blue", client.stringVariation("flag", "")); } } diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/EventUtil.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/EventUtil.java index 1e5f4886..e605de08 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/EventUtil.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/EventUtil.java @@ -48,7 +48,7 @@ static DiagnosticStore.SdkDiagnosticParams makeDiagnosticParams(ClientContext cl .put("backgroundPollingDisabled", config.isDisableBackgroundPolling()) .put("evaluationReasonsRequested", config.isEvaluationReasons()) .put("mobileKeyCount", config.getMobileKeys().size()) - .put("maxCachedUsers", config.getMaxCachedContexts()); + .put("maxCachedUsers", config.getMaxCachedContexts()); // Caution: maxCachedUsers used in production mergeComponentProperties(configProperties, config.dataSource); mergeComponentProperties(configProperties, config.events); mergeComponentProperties(configProperties, config.http); diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDAllFlagsListener.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDAllFlagsListener.java index ef6278f9..8ba9f353 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDAllFlagsListener.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDAllFlagsListener.java @@ -9,7 +9,7 @@ public interface LDAllFlagsListener { /** - * Called by the SDK whenever it receives an update for the stored flag values of the current user. + * Called by the SDK whenever it receives an update for the stored flag values of the current context. * * @param flagKey A list of flag keys which were created, updated, or deleted as part of the update. * This list may be empty if the update resulted in no changed flag values. diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDClient.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDClient.java index a0b8ed80..abce06da 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDClient.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDClient.java @@ -10,7 +10,6 @@ import com.launchdarkly.sdk.EvaluationDetail; import com.launchdarkly.sdk.EvaluationReason; import com.launchdarkly.sdk.LDContext; -import com.launchdarkly.sdk.LDUser; import com.launchdarkly.sdk.LDValue; import com.launchdarkly.sdk.android.env.EnvironmentReporterBuilder; import com.launchdarkly.sdk.android.env.IEnvironmentReporter; @@ -80,7 +79,6 @@ public class LDClient implements LDClientInterface, Closeable { * about setting the context and optionally requesting a unique key for it * @return a {@link Future} which will complete once the client has been initialized * @see #init(Application, LDConfig, LDContext, int) - * @see #init(Application, LDConfig, LDUser) * @since 3.0.0 */ public static Future init(@NonNull Application application, @@ -205,30 +203,6 @@ public void onError(Throwable e) { return resultFuture; } - /** - * Initializes the singleton/primary instance. - *

- * This is equivalent to {@link #init(Application, LDConfig, LDContext)}, but using the - * {@link LDUser} type instead of {@link LDContext}. - * - * @param application your Android application - * @param config configuration used to set up the client - * @param user the initial user attributes, which will be converted to an evaluation - * context; see {@link LDClient} for more information about setting the - * context and optionally requesting a unique key for it - * @return a {@link Future} which will complete once the client has been initialized - * @see #init(Application, LDConfig, LDUser, int) - * @see #init(Application, LDConfig, LDContext) - * - * @deprecated use {@link #init(Application, LDConfig, LDContext)} with {@link LDContext} - */ - @Deprecated - public static Future init(@NonNull Application application, - @NonNull LDConfig config, - @NonNull LDUser user) { - return init(application, config, LDContext.fromUser(user)); - } - /** * Initializes the singleton instance and blocks for up to startWaitSeconds seconds * until the client has been initialized. If the client does not initialize within @@ -243,7 +217,6 @@ public static Future init(@NonNull Application application, * @param startWaitSeconds maximum number of seconds to wait for the client to initialize * @return the primary LDClient instance * @see #init(Application, LDConfig, LDContext) - * @see #init(Application, LDConfig, LDUser, int) * @since 3.0.0 */ public static LDClient init(Application application, LDConfig config, LDContext context, int startWaitSeconds) { @@ -260,32 +233,6 @@ public static LDClient init(Application application, LDConfig config, LDContext } return instances.get(LDConfig.primaryEnvironmentName); } - - /** - * Initializes the singleton instance and blocks for up to startWaitSeconds seconds - * until the client has been initialized. If the client does not initialize within - * startWaitSeconds seconds, it is returned anyway and can be used, but may not - * have fetched the most recent feature flag values. - *

- * This is equivalent to {@link #init(Application, LDConfig, LDContext, int)}, but using the - * {@link LDUser} type instead of {@link LDContext}. - * - * @param application your Android application - * @param config configuration used to set up the client - * @param user the initial user attributes, which will be converted to an evaluation - * context; see {@link LDClient} for more information about setting the - * context and optionally requesting a unique key for it - * @param startWaitSeconds maximum number of seconds to wait for the client to initialize - * @return the primary LDClient instance - * @see #init(Application, LDConfig, LDUser) - * @see #init(Application, LDConfig, LDContext, int) - * - * @deprecated use {@link #init(Application, LDConfig, LDContext, int)} with {@link LDContext} - */ - @Deprecated - public static LDClient init(Application application, LDConfig config, LDUser user, int startWaitSeconds) { - return init(application, config, LDContext.fromUser(user), startWaitSeconds); - } /** * Returns the {@code LDClient} instance that was previously created with {@link #init(Application, LDConfig, LDContext, int)} @@ -426,15 +373,6 @@ public Future identify(LDContext context) { return identifyInstances(modifiedContext); } - /** - * @deprecated use {@link #identify(LDContext)} with {@link LDContext} - */ - @Deprecated - @Override - public Future identify(LDUser user) { - return identify(LDContext.fromUser(user)); - } - private @NonNull Map getInstancesIfTheyIncludeThisClient() { // Using this method ensures that 1. we are operating on an atomic snapshot of the // instances (in the unlikely case that they get closed & recreated right around now) and diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDClientInterface.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDClientInterface.java index d8fe7a2e..9acd178d 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDClientInterface.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDClientInterface.java @@ -4,7 +4,6 @@ import com.launchdarkly.sdk.EvaluationDetail; import com.launchdarkly.sdk.LDContext; -import com.launchdarkly.sdk.LDUser; import com.launchdarkly.sdk.LDValue; import java.io.Closeable; @@ -129,29 +128,10 @@ public interface LDClientInterface extends Closeable { * setting the context and optionally requesting a unique key for it * @return a Future whose success indicates the flag values for the new evaluation context have * been stored locally and are ready for use - * @see #identify(LDUser) * @since 3.0.0 */ Future identify(LDContext context); - /** - * Changes the current evaluation context, requests flags for that context from LaunchDarkly if we are online, - * and generates an analytics event to tell LaunchDarkly about the context. - *

- * This is equivalent to {@link #identify(LDContext)}, but using the {@link LDUser} type - * instead of {@link LDContext}. - * - * @param user the new user attributes, which will be converted to an evaluation context; see - * {@link LDClient} for more about setting the context and optionally requesting a unique key for it - * @return a Future whose success indicates the flag values for the new evaluation context have - * been stored locally and are ready for use - * @see #identify(LDContext) - * - * @deprecated use {@link #identify(LDContext)} with {@link LDContext} - */ - @Deprecated - Future identify(LDUser user); - /** * Sends all pending events to LaunchDarkly. */ diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDConfig.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDConfig.java index f8b525bb..a91fd3af 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDConfig.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDConfig.java @@ -7,7 +7,6 @@ import com.launchdarkly.logging.Logs; import com.launchdarkly.sdk.ContextKind; import com.launchdarkly.sdk.LDContext; -import com.launchdarkly.sdk.LDUser; import com.launchdarkly.sdk.android.integrations.ApplicationInfoBuilder; import com.launchdarkly.sdk.android.integrations.ServiceEndpointsBuilder; import com.launchdarkly.sdk.android.interfaces.ServiceEndpoints; @@ -498,8 +497,7 @@ public Builder maxCachedContexts(int maxCachedContexts) { * Every {@link LDContext} must always have a key, even if the key will later be overwritten * by the SDK, so if you use this functionality you must still provide a placeholder key. * This ensures that if the SDK configuration is changed so {@code generateAnonymousKeys} is - * no longer enabled, the SDK will still be able to use the context for evaluations. (The - * legacy {@link LDUser} type does allow a null key in this case.) + * no longer enabled, the SDK will still be able to use the context for evaluations. * * @param generateAnonymousKeys true to enable automatic anonymous key generation * @return the same builder diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/ApplicationInfoBuilder.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/ApplicationInfoBuilder.java index fc8ab2d0..c5d98b04 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/ApplicationInfoBuilder.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/ApplicationInfoBuilder.java @@ -53,13 +53,13 @@ public ApplicationInfoBuilder applicationId(String applicationId) { } /** - * Sets a user friendly name for the application in which the LaunchDarkly SDK is running. + * Sets a human friendly name for the application in which the LaunchDarkly SDK is running. *

* This can be specified as any string value as long as it only uses the following characters: ASCII * letters, ASCII digits, period, hyphen, underscore. A string containing any other characters will be * ignored. * - * @param applicationName the user friendly name + * @param applicationName the human friendly name * @return the builder */ public ApplicationInfoBuilder applicationName(String applicationName) { @@ -84,13 +84,13 @@ public ApplicationInfoBuilder applicationVersion(String version) { } /** - * Sets a user friendly name for the version of the application in which the LaunchDarkly SDK is running. + * Sets a human friendly name for the version of the application in which the LaunchDarkly SDK is running. *

* This can be specified as any string value as long as it only uses the following characters: ASCII * letters, ASCII digits, period, hyphen, underscore. A string containing any other characters will be * ignored. * - * @param versionName the user friendly version name + * @param versionName the human friendly version name * @return the builder */ public ApplicationInfoBuilder applicationVersionName(String versionName) { diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/EventProcessorBuilder.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/EventProcessorBuilder.java index add8f2a3..be373cac 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/EventProcessorBuilder.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/EventProcessorBuilder.java @@ -53,16 +53,14 @@ public abstract class EventProcessorBuilder implements ComponentConfigurer privateAttributes; /** - * Sets whether or not all optional user attributes should be hidden from LaunchDarkly. + * Sets whether or not all optional context attributes should be hidden from LaunchDarkly. *

- * If this is {@code true}, all user attribute values (other than the key) will be private, not just - * the attributes specified in {@link #privateAttributes(String...)} or on a per-user basis with - * {@link com.launchdarkly.sdk.LDUser.Builder} methods. By default, it is {@code false}. + * If this is {@code true}, all context attribute values (other than the key) will be private, not just + * the attributes specified in {@link #privateAttributes(String...)}. By default, it is {@code false}. * - * @param allAttributesPrivate true if all user attributes should be private + * @param allAttributesPrivate true if all context attributes should be private * @return the builder * @see #privateAttributes(String...) - * @see com.launchdarkly.sdk.LDUser.Builder */ public EventProcessorBuilder allAttributesPrivate(boolean allAttributesPrivate) { this.allAttributesPrivate = allAttributesPrivate; @@ -122,7 +120,7 @@ public EventProcessorBuilder flushIntervalMillis(int flushIntervalMillis) { *

* Any contexts sent to LaunchDarkly with this configuration active will have attributes with these * names removed. This is in addition to any attributes that were marked as private for an - * individual context with {@link com.launchdarkly.sdk.LDUser.Builder} methods. + * individual context with {@link com.launchdarkly.sdk.ContextBuilder} methods. *

* This method replaces any previous private attributes that were set on the same builder, rather * than adding to them. @@ -130,7 +128,6 @@ public EventProcessorBuilder flushIntervalMillis(int flushIntervalMillis) { * @param attributeNames a set of attribute names that will be removed from context data set to LaunchDarkly * @return the builder * @see #allAttributesPrivate(boolean) - * @see com.launchdarkly.sdk.LDUser.Builder */ public EventProcessorBuilder privateAttributes(String... attributeNames) { privateAttributes = new HashSet<>(); diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/HttpConfigurationBuilder.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/HttpConfigurationBuilder.java index 66bf25e1..ec384b5b 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/HttpConfigurationBuilder.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/HttpConfigurationBuilder.java @@ -67,8 +67,8 @@ public HttpConfigurationBuilder headerTransform(LDHeaderUpdater headerTransform) * Sets whether to use the HTTP REPORT method for feature flag requests. *

* By default, polling and streaming connections are made with the GET method, with the context - * data encoded into the request URI. Using REPORT allows the user data to be sent in the request - * body instead, which is somewhat more secure and efficient. + * data encoded into the request URI. Using REPORT allows the context data to be sent in the + * request body instead, which is somewhat more secure and efficient. *

* However, the REPORT method is not always supported by operating systems or network gateways. * Therefore it is disabled in the SDK by default. You can enable it if you know your code will diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/subsystems/ApplicationInfo.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/subsystems/ApplicationInfo.java index 8b2e5570..3363e954 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/subsystems/ApplicationInfo.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/subsystems/ApplicationInfo.java @@ -52,7 +52,7 @@ public String getApplicationVersion() { } /** - * A user friendly name for the application in which the LaunchDarkly SDK is running. + * A human friendly name for the application in which the LaunchDarkly SDK is running. * * @return the friendly name of the application, or null */ @@ -61,7 +61,7 @@ public String getApplicationName() { } /** - * A user friendly name for the version of the application in which the LaunchDarkly SDK is running. + * A human friendly name for the version of the application in which the LaunchDarkly SDK is running. * * @return the friendly name of the version, or null */ diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/subsystems/EventProcessor.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/subsystems/EventProcessor.java index 42053bad..d1c98af0 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/subsystems/EventProcessor.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/subsystems/EventProcessor.java @@ -3,7 +3,6 @@ import com.launchdarkly.sdk.EvaluationDetail; import com.launchdarkly.sdk.EvaluationReason; import com.launchdarkly.sdk.LDContext; -import com.launchdarkly.sdk.LDUser; import com.launchdarkly.sdk.LDValue; import java.io.Closeable; diff --git a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/LDConfigTest.java b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/LDConfigTest.java index f04104ab..92d99d8f 100644 --- a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/LDConfigTest.java +++ b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/LDConfigTest.java @@ -50,7 +50,7 @@ public void testBuilderDiagnosticOptOut() { } @Test - public void testBuilderMaxCachedUsers() { + public void testBuilderMaxCachedContexts() { LDConfig config = new LDConfig.Builder(AutoEnvAttributes.Disabled).maxCachedContexts(0).build(); assertEquals(0, config.getMaxCachedContexts()); config = new LDConfig.Builder(AutoEnvAttributes.Disabled).maxCachedContexts(10).build(); diff --git a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/TestDataTest.java b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/TestDataTest.java index 783098fb..e0b29cc4 100644 --- a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/TestDataTest.java +++ b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/TestDataTest.java @@ -20,7 +20,7 @@ import java.util.function.Function; public class TestDataTest { - private static final LDContext initialUser = LDContext.create("user0"); + private static final LDContext initialContext = LDContext.create("user0"); private static final EvaluationReason defaultReason = EvaluationReason.fallthrough(); private final TestData td = TestData.dataSource(); @@ -86,20 +86,20 @@ public void flagConfigBoolean() { verifyFlag(f -> f.variation(true), expectTrue); verifyFlag(f -> f.variation(false), expectFalse); - verifyFlag(f -> f.variation(false).variationForUser(initialUser.getKey(), true), expectTrue); - verifyFlag(f -> f.variation(true).variationForUser(initialUser.getKey(), false), expectFalse); + verifyFlag(f -> f.variation(false).variationForUser(initialContext.getKey(), true), expectTrue); + verifyFlag(f -> f.variation(true).variationForUser(initialContext.getKey(), false), expectFalse); - verifyFlag(f -> f.variation(false).variationForKey(null, initialUser.getKey(), true), expectTrue); - verifyFlag(f -> f.variation(false).variationForKey(ContextKind.DEFAULT, initialUser.getKey(), true), expectTrue); - verifyFlag(f -> f.variation(true).variationForKey(null, initialUser.getKey(), false), expectFalse); - verifyFlag(f -> f.variation(true).variationForKey(ContextKind.DEFAULT, initialUser.getKey(), false), expectFalse); - verifyFlag(f -> f.variation(true).variationForKey(ContextKind.of("other"), initialUser.getKey(), true), expectFalse); + verifyFlag(f -> f.variation(false).variationForKey(null, initialContext.getKey(), true), expectTrue); + verifyFlag(f -> f.variation(false).variationForKey(ContextKind.DEFAULT, initialContext.getKey(), true), expectTrue); + verifyFlag(f -> f.variation(true).variationForKey(null, initialContext.getKey(), false), expectFalse); + verifyFlag(f -> f.variation(true).variationForKey(ContextKind.DEFAULT, initialContext.getKey(), false), expectFalse); + verifyFlag(f -> f.variation(true).variationForKey(ContextKind.of("other"), initialContext.getKey(), true), expectFalse); - verifyFlag(f -> f.variation(false).variationFunc(c -> c.getKey().equals(initialUser.getKey())), expectTrue); - verifyFlag(f -> f.variation(true).variationFunc(c -> !c.getKey().equals(initialUser.getKey())), expectFalse); + verifyFlag(f -> f.variation(false).variationFunc(c -> c.getKey().equals(initialContext.getKey())), expectTrue); + verifyFlag(f -> f.variation(true).variationFunc(c -> !c.getKey().equals(initialContext.getKey())), expectFalse); // variationForUser/variationForKey takes precedence over variationFunc - verifyFlag(f -> f.variation(false).variationForUser(initialUser.getKey(), true) + verifyFlag(f -> f.variation(false).variationForUser(initialContext.getKey(), true) .variationFunc(c -> false), expectTrue); } @@ -114,16 +114,16 @@ public void flagConfigByVariationIndex() { verifyFlag(f -> f.variations(ab).variation(aIndex), expectA); verifyFlag(f -> f.variations(ab).variation(bIndex), expectB); - verifyFlag(f -> f.variations(ab).variation(aIndex).variationForUser(initialUser.getKey(), bIndex), expectB); - verifyFlag(f -> f.variations(ab).variation(bIndex).variationForUser(initialUser.getKey(), aIndex), expectA); + verifyFlag(f -> f.variations(ab).variation(aIndex).variationForUser(initialContext.getKey(), bIndex), expectB); + verifyFlag(f -> f.variations(ab).variation(bIndex).variationForUser(initialContext.getKey(), aIndex), expectA); verifyFlag(f -> f.variations(ab).variation(aIndex) - .variationIndexFunc(c -> c.getKey().equals(initialUser.getKey()) ? bIndex : null), expectB); + .variationIndexFunc(c -> c.getKey().equals(initialContext.getKey()) ? bIndex : null), expectB); verifyFlag(f -> f.variations(ab).variation(bIndex) - .variationIndexFunc(c -> c.getKey().equals(initialUser.getKey()) ? aIndex : null), expectA); + .variationIndexFunc(c -> c.getKey().equals(initialContext.getKey()) ? aIndex : null), expectA); // VariationForUser takes precedence over VariationFunc - verifyFlag(f -> f.variations(ab).variation(aIndex).variationForUser(initialUser.getKey(), bIndex) + verifyFlag(f -> f.variations(ab).variation(aIndex).variationForUser(initialContext.getKey(), bIndex) .variationIndexFunc(c -> aIndex), expectB); } @@ -138,23 +138,23 @@ public void flagConfigByVariationByValue() { verifyFlag(f -> f.variations(ab).variation(aVal), expectA); verifyFlag(f -> f.variations(ab).variation(bVal), expectB); - verifyFlag(f -> f.variations(ab).variation(aVal).variationForUser(initialUser.getKey(), bVal), expectB); - verifyFlag(f -> f.variations(ab).variation(bVal).variationForUser(initialUser.getKey(), aVal), expectA); + verifyFlag(f -> f.variations(ab).variation(aVal).variationForUser(initialContext.getKey(), bVal), expectB); + verifyFlag(f -> f.variations(ab).variation(bVal).variationForUser(initialContext.getKey(), aVal), expectA); verifyFlag(f -> f.variations(ab).variation(aIndex) - .variationIndexFunc(c -> c.getKey().equals(initialUser.getKey()) ? bIndex : null), expectB); + .variationIndexFunc(c -> c.getKey().equals(initialContext.getKey()) ? bIndex : null), expectB); verifyFlag(f -> f.variations(ab).variation(bIndex) - .variationIndexFunc(c -> c.getKey().equals(initialUser.getKey()) ? aIndex : null), expectA); + .variationIndexFunc(c -> c.getKey().equals(initialContext.getKey()) ? aIndex : null), expectA); // VariationForUser takes precedence over VariationFunc - verifyFlag(f -> f.variations(ab).variation(aIndex).variationForUser(initialUser.getKey(), bIndex) + verifyFlag(f -> f.variations(ab).variation(aIndex).variationForUser(initialContext.getKey(), bIndex) .variationIndexFunc(c -> aIndex), expectB); } private void createAndStart() { ClientContext clientContext = new ClientContext("", null, LDLogger.none(), new LDConfig.Builder(AutoEnvAttributes.Disabled).build(), updates, "", false, - initialUser, null, false, null, null, false); + initialContext, null, false, null, null, false); DataSource ds = td.build(clientContext); AwaitableCallback callback = new AwaitableCallback<>(); ds.start(callback); From dcca76317f93a669b4182229e6110c4eb7fcc3d1 Mon Sep 17 00:00:00 2001 From: Todd Anderson Date: Thu, 3 Aug 2023 15:28:26 -0500 Subject: [PATCH 21/26] Updating env attributes spec version to 1.0 for production release. --- .../com/launchdarkly/sdk/android/AutoEnvContextModifier.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/AutoEnvContextModifier.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/AutoEnvContextModifier.java index d156ecbd..984d0f5d 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/AutoEnvContextModifier.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/AutoEnvContextModifier.java @@ -33,7 +33,7 @@ public class AutoEnvContextModifier implements IContextModifier { static final String ATTR_OS = "os"; static final String ATTR_FAMILY = "family"; static final String ENV_ATTRIBUTES_VERSION = "envAttributesVersion"; - static final String SPEC_VERSION = "0.3"; + static final String SPEC_VERSION = "1.0"; private final PersistentDataStoreWrapper persistentData; private final IEnvironmentReporter environmentReporter; From 9b1d1ad6b89356fc20303d31789341418a3d16e9 Mon Sep 17 00:00:00 2001 From: Todd Anderson Date: Thu, 24 Aug 2023 13:47:05 -0500 Subject: [PATCH 22/26] fix: updating sanitization, validation, and fallback logic of Auto Env Attributes to be more consistent and user friendly. --- .../launchdarkly/sdktest/Representations.java | 2 + .../launchdarkly/sdktest/SdkClientEntity.java | 6 +++ launchdarkly-android-client-sdk/build.gradle | 2 + .../EnvironmentReporterBuilderTest.java | 11 ++++ .../sdk/android/AutoEnvContextModifier.java | 5 +- .../launchdarkly/sdk/android/LDConfig.java | 5 +- .../sdk/android/LDPackageConsts.java | 1 + .../com/launchdarkly/sdk/android/LDUtil.java | 50 +++++++++++++---- .../env/AndroidEnvironmentReporter.java | 39 ++++++++------ .../ApplicationInfoEnvironmentReporter.java | 4 ++ .../integrations/ApplicationInfoBuilder.java | 45 ++++++++++++++-- .../android/subsystems/ApplicationInfo.java | 10 ++++ .../android/AutoEnvContextModifierTest.java | 47 ++++++++++++++++ .../android/HttpConfigurationBuilderTest.java | 7 +-- .../sdk/android/LDConfigTest.java | 4 +- .../launchdarkly/sdk/android/LDUtilTest.java | 14 +++++ .../ApplicationInfoBuilderTest.java | 53 +++++++++++++++++++ 17 files changed, 266 insertions(+), 39 deletions(-) create mode 100644 launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/integrations/ApplicationInfoBuilderTest.java diff --git a/contract-tests/src/main/java/com/launchdarkly/sdktest/Representations.java b/contract-tests/src/main/java/com/launchdarkly/sdktest/Representations.java index 559dd558..f67b6589 100644 --- a/contract-tests/src/main/java/com/launchdarkly/sdktest/Representations.java +++ b/contract-tests/src/main/java/com/launchdarkly/sdktest/Representations.java @@ -56,7 +56,9 @@ public static class SdkConfigEventParams { public static class SdkConfigTagParams { String applicationId; + String applicationName; String applicationVersion; + String applicationVersionName; } public static class SdkConfigServiceEndpointParams { diff --git a/contract-tests/src/main/java/com/launchdarkly/sdktest/SdkClientEntity.java b/contract-tests/src/main/java/com/launchdarkly/sdktest/SdkClientEntity.java index 7044409b..f5c28cff 100644 --- a/contract-tests/src/main/java/com/launchdarkly/sdktest/SdkClientEntity.java +++ b/contract-tests/src/main/java/com/launchdarkly/sdktest/SdkClientEntity.java @@ -313,9 +313,15 @@ private LDConfig buildSdkConfig(SdkConfigParams params, LDLogAdapter logAdapter, if (params.tags.applicationId != null) { ab.applicationId(params.tags.applicationId); } + if (params.tags.applicationName != null) { + ab.applicationName(params.tags.applicationName); + } if (params.tags.applicationVersion != null) { ab.applicationVersion(params.tags.applicationVersion); } + if (params.tags.applicationVersionName != null) { + ab.applicationVersionName(params.tags.applicationVersionName); + } builder.applicationInfo(ab); } diff --git a/launchdarkly-android-client-sdk/build.gradle b/launchdarkly-android-client-sdk/build.gradle index 6bb8a18b..ecdd607e 100644 --- a/launchdarkly-android-client-sdk/build.gradle +++ b/launchdarkly-android-client-sdk/build.gradle @@ -59,6 +59,7 @@ configurations.all { ext {} ext.versions = [ "androidAnnotation": "1.2.0", + "androidAppcompat": "1.1.0", "eventsource": "3.0.0", "gson": "2.8.9", "jacksonCore": "2.10.5", @@ -82,6 +83,7 @@ dependencies { implementation("com.google.code.gson:gson:${versions.gson}") implementation("androidx.annotation:annotation:${versions.androidAnnotation}") + implementation("androidx.appcompat:appcompat:${versions.androidAppcompat}") implementation("com.launchdarkly:launchdarkly-java-sdk-internal:${versions.launchdarklyJavaSdkInternal}") implementation("com.launchdarkly:okhttp-eventsource:${versions.eventsource}") implementation("com.squareup.okhttp3:okhttp:${versions.okhttp}") diff --git a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/EnvironmentReporterBuilderTest.java b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/EnvironmentReporterBuilderTest.java index f88acf8d..8f682fd1 100644 --- a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/EnvironmentReporterBuilderTest.java +++ b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/EnvironmentReporterBuilderTest.java @@ -47,6 +47,17 @@ public void defaultsToSDKValues() { Assert.assertEquals(LDPackageConsts.SDK_NAME, reporter.getApplicationInfo().getApplicationName()); Assert.assertEquals(BuildConfig.VERSION_NAME, reporter.getApplicationInfo().getApplicationVersion()); Assert.assertEquals(BuildConfig.VERSION_NAME, reporter.getApplicationInfo().getApplicationVersionName()); + } + @Test + public void fallBackWhenIDMissing() { + EnvironmentReporterBuilder builder = new EnvironmentReporterBuilder(); + ApplicationInfo manualInfoInput = new ApplicationInfoBuilder().applicationName("imNotAnID!").createApplicationInfo(); + builder.setApplicationInfo(manualInfoInput); + IEnvironmentReporter reporter = builder.build(); + Assert.assertEquals(LDPackageConsts.SDK_NAME, reporter.getApplicationInfo().getApplicationId()); + Assert.assertEquals(LDPackageConsts.SDK_NAME, reporter.getApplicationInfo().getApplicationName()); + Assert.assertEquals(BuildConfig.VERSION_NAME, reporter.getApplicationInfo().getApplicationVersion()); + Assert.assertEquals(BuildConfig.VERSION_NAME, reporter.getApplicationInfo().getApplicationVersionName()); } } diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/AutoEnvContextModifier.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/AutoEnvContextModifier.java index 984d0f5d..13cc8816 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/AutoEnvContextModifier.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/AutoEnvContextModifier.java @@ -13,6 +13,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.concurrent.Callable; /** @@ -139,8 +140,8 @@ private List makeRecipeList() { new ContextRecipe( ldApplicationKind, () -> LDUtil.urlSafeBase64Hash( - environmentReporter.getApplicationInfo().getApplicationId() + ":" - + environmentReporter.getApplicationInfo().getApplicationVersion() + Objects.toString(environmentReporter.getApplicationInfo().getApplicationId(), "") + ":" + + Objects.toString(environmentReporter.getApplicationInfo().getApplicationVersion(), "") ), applicationCallables ), diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDConfig.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDConfig.java index a91fd3af..be5f1f96 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDConfig.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDConfig.java @@ -44,7 +44,6 @@ public class LDConfig { */ public static final int MIN_BACKGROUND_POLL_INTERVAL_MILLIS = 900_000; - static final String DEFAULT_LOGGER_NAME = "LaunchDarklySdk"; static final LDLogLevel DEFAULT_LOG_LEVEL = LDLogLevel.INFO; static final MediaType JSON = MediaType.parse("application/json; charset=utf-8"); @@ -205,7 +204,7 @@ public enum AutoEnvAttributes { private PersistentDataStore persistentDataStore; private LDLogAdapter logAdapter = defaultLogAdapter(); - private String loggerName = DEFAULT_LOGGER_NAME; + private String loggerName = LDPackageConsts.DEFAULT_LOGGER_NAME; private LDLogLevel logLevel = null; /** @@ -607,7 +606,7 @@ public Builder logLevel(LDLogLevel logLevel) { * @see #logLevel(LDLogLevel) */ public Builder loggerName(String loggerName) { - this.loggerName = loggerName == null ? DEFAULT_LOGGER_NAME : loggerName; + this.loggerName = loggerName == null ? LDPackageConsts.DEFAULT_LOGGER_NAME : loggerName; return this; } diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDPackageConsts.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDPackageConsts.java index f309292e..6210fe9e 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDPackageConsts.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDPackageConsts.java @@ -4,4 +4,5 @@ public class LDPackageConsts { public static final String SDK_NAME = "android-client-sdk"; public static final String SDK_PLATFORM_NAME = "Android"; public static final String SDK_CLIENT_NAME = "AndroidClient"; + public static final String DEFAULT_LOGGER_NAME = "LaunchDarklySdk"; } diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDUtil.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDUtil.java index 0d8f4476..1ae9fdf3 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDUtil.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDUtil.java @@ -3,6 +3,7 @@ import android.util.Base64; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import com.launchdarkly.logging.LDLogger; import com.launchdarkly.logging.LogValues; @@ -28,7 +29,7 @@ import okhttp3.Headers; -class LDUtil { +public class LDUtil { static final String AUTH_SCHEME = "api_key "; static final String USER_AGENT_HEADER_VALUE = LDPackageConsts.SDK_CLIENT_NAME + "/" + BuildConfig.VERSION_NAME; @@ -44,8 +45,41 @@ public void onError(Throwable error) { }; } - // Tag values must not be empty, and only contain letters, numbers, `.`, `_`, or `-`. - private static Pattern TAG_VALUE_REGEX = Pattern.compile("^[-a-zA-Z0-9._]+$"); + // Key, kind, tag, and several other system values must not be empty, contain only letters, + // numbers, `.`, `_`, or `-`. + private static final Pattern VALID_CHARS_REGEX = Pattern.compile("^[-a-zA-Z0-9._]+$"); + + /** + * @param s the string to validate + * @return null if valid, otherwise string describing issue + */ + @Nullable + public static String validateStringValue(@NonNull String s) { + if (s.isEmpty()) { + return "Empty string."; + } + + if (s.length() > 64) { + return "Longer than 64 characters."; + } + + if (!VALID_CHARS_REGEX.matcher(s).matches()) { + return "Contains invalid characters."; + } + return null; + } + + /** + * Replaces spaces with hyphens. In the future, if this function is made more generic, + * understand that customer data may already exist and changing this sanitization may + * have adverse consequences. + * + * @param s the string to sanitize + * @return the sanitized string + */ + public static String sanitizeSpaces(String s) { + return s.replace(' ', '-'); + } /** * Builds the "X-LaunchDarkly-Tags" HTTP header out of the configured application info. @@ -56,6 +90,7 @@ public void onError(Throwable error) { static String applicationTagHeader(ApplicationInfo applicationInfo, LDLogger logger) { String[][] tags = { {"applicationId", "application-id", applicationInfo.getApplicationId()}, + {"applicationName", "application-name", applicationInfo.getApplicationName()}, {"applicationVersion", "application-version", applicationInfo.getApplicationVersion()}, {"applicationVersionName", "application-version-name", applicationInfo.getApplicationVersionName()} }; @@ -67,12 +102,9 @@ static String applicationTagHeader(ApplicationInfo applicationInfo, LDLogger log if (tagVal == null) { continue; } - if (!TAG_VALUE_REGEX.matcher(tagVal).matches()) { - logger.warn("Value of ApplicationInfo.{} contained invalid characters and was discarded", javaKey); - continue; - } - if (tagVal.length() > 64) { - logger.warn("Value of ApplicationInfo.{} was longer than 64 characters and was discarded", javaKey); + String error = validateStringValue(tagVal); + if (error != null) { + logger.warn("Value of ApplicationInfo.{} was invalid. {}", javaKey, error); continue; } parts.add(tagKey + "/" + tagVal); diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/env/AndroidEnvironmentReporter.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/env/AndroidEnvironmentReporter.java index e28f2684..698ab64f 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/env/AndroidEnvironmentReporter.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/env/AndroidEnvironmentReporter.java @@ -7,6 +7,7 @@ import androidx.annotation.NonNull; import com.launchdarkly.sdk.android.LDPackageConsts; +import com.launchdarkly.sdk.android.integrations.ApplicationInfoBuilder; import com.launchdarkly.sdk.android.subsystems.ApplicationInfo; import java.util.Locale; @@ -30,12 +31,20 @@ public AndroidEnvironmentReporter(Application application) { @Override @NonNull public ApplicationInfo getApplicationInfo() { - return new ApplicationInfo( - getApplicationID(), - getApplicationVersion(), - getApplicationName(), - getApplicationVersionName() - ); + // use a builder here since it has sanitization and validation built in + ApplicationInfoBuilder builder = new ApplicationInfoBuilder(); + builder.applicationId(getApplicationID()); + builder.applicationVersion(getApplicationVersion()); + builder.applicationName(getApplicationName()); + builder.applicationVersionName(getApplicationVersionName()); + ApplicationInfo info = builder.createApplicationInfo(); + + // defer to super if required property applicationID is missing + if (info.getApplicationId() == null) { + info = super.getApplicationInfo(); + } + + return info; } @NonNull @@ -80,12 +89,10 @@ public String getOSVersion() { return Build.VERSION.RELEASE; } - @NonNull private String getApplicationID() { return application.getPackageName(); } - @NonNull private String getApplicationName() { try { PackageManager pm = application.getPackageManager(); @@ -99,7 +106,6 @@ private String getApplicationName() { } } - @NonNull private String getApplicationVersion() { try { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { @@ -109,22 +115,23 @@ private String getApplicationVersion() { } } catch (PackageManager.NameNotFoundException e) { // We don't really expect this to ever happen since we just queried the platform - // for the application name and then immediately used it. Since the code has - // this logical path, the best we can do is defer to the next in the chain. - return super.getApplicationInfo().getApplicationVersion(); + // for the application name and then immediately used it. It is best to set + // this to null, if enough properties are missing, we will fallback to the + // next source in the chain. + return null; } } - @NonNull private String getApplicationVersionName() { try { PackageManager pm = application.getPackageManager(); return pm.getPackageInfo(application.getPackageName(), 0).versionName; } catch (PackageManager.NameNotFoundException e) { // We don't really expect this to ever happen since we just queried the platform - // for the application name and then immediately used it. Since the code has - // this logical path, the best we can do is defer to the next in the chain. - return super.getApplicationInfo().getApplicationVersionName(); + // for the application name and then immediately used it. It is best to set + // this to null, if enough properties are missing, we will fallback to the + // next source in the chain. + return null; } } } diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/env/ApplicationInfoEnvironmentReporter.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/env/ApplicationInfoEnvironmentReporter.java index 7ce24d2c..5314a13c 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/env/ApplicationInfoEnvironmentReporter.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/env/ApplicationInfoEnvironmentReporter.java @@ -19,6 +19,10 @@ public ApplicationInfoEnvironmentReporter(ApplicationInfo applicationInfo) { @NonNull @Override public ApplicationInfo getApplicationInfo() { + // defer to super if required property applicationID is missing + if (applicationInfo.getApplicationId() == null) { + return super.getApplicationInfo(); + } return applicationInfo; } } diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/ApplicationInfoBuilder.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/ApplicationInfoBuilder.java index c5d98b04..5016e583 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/ApplicationInfoBuilder.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/ApplicationInfoBuilder.java @@ -1,6 +1,14 @@ package com.launchdarkly.sdk.android.integrations; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; +import androidx.core.util.Consumer; + +import com.launchdarkly.logging.LDLogger; import com.launchdarkly.sdk.android.Components; +import com.launchdarkly.sdk.android.LDAndroidLogging; +import com.launchdarkly.sdk.android.LDPackageConsts; +import com.launchdarkly.sdk.android.LDUtil; import com.launchdarkly.sdk.android.subsystems.ApplicationInfo; /** @@ -25,11 +33,18 @@ * @since 4.1.0 */ public final class ApplicationInfoBuilder { + @Nullable private String applicationId; + @Nullable private String applicationName; + @Nullable private String applicationVersion; + @Nullable private String applicationVersionName; + @VisibleForTesting + LDLogger logger = LDLogger.withAdapter(LDAndroidLogging.adapter(), ApplicationInfoBuilder.class.getName()); + /** * Create an empty ApplicationInfoBuilder. * @@ -48,7 +63,7 @@ public ApplicationInfoBuilder() {} * @return the builder */ public ApplicationInfoBuilder applicationId(String applicationId) { - this.applicationId = applicationId; + validatedThenChange(this.logger, s -> this.applicationId = s, applicationId); return this; } @@ -63,7 +78,7 @@ public ApplicationInfoBuilder applicationId(String applicationId) { * @return the builder */ public ApplicationInfoBuilder applicationName(String applicationName) { - this.applicationName = applicationName; + validatedThenChange(this.logger, s -> this.applicationName = s, applicationName); return this; } @@ -79,7 +94,7 @@ public ApplicationInfoBuilder applicationName(String applicationName) { * @return the builder */ public ApplicationInfoBuilder applicationVersion(String version) { - this.applicationVersion = version; + validatedThenChange(this.logger, s -> this.applicationVersion = s, version); return this; } @@ -94,7 +109,7 @@ public ApplicationInfoBuilder applicationVersion(String version) { * @return the builder */ public ApplicationInfoBuilder applicationVersionName(String versionName) { - this.applicationVersionName = versionName; + validatedThenChange(this.logger, s -> this.applicationVersionName = s, versionName); return this; } @@ -106,4 +121,26 @@ public ApplicationInfoBuilder applicationVersionName(String versionName) { public ApplicationInfo createApplicationInfo() { return new ApplicationInfo(applicationId, applicationVersion, applicationName, applicationVersionName); } + + /** + * @param propertySetter lambda for setting the property. Java is fun and has predefined + * functional interfaces. + * @param input the string that will be sanitized and validated then applied + */ + private void validatedThenChange(LDLogger logger, Consumer propertySetter, String input) { + if (input == null) { + propertySetter.accept(input); + return; + } + + String sanitized = LDUtil.sanitizeSpaces(input); + String error = LDUtil.validateStringValue(sanitized); + if (error != null) { + // TODO: make sure log includes property name + logger.warn(LDPackageConsts.DEFAULT_LOGGER_NAME, propertySetter.toString() + error); + return; + } + + propertySetter.accept(sanitized); + } } diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/subsystems/ApplicationInfo.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/subsystems/ApplicationInfo.java index 3363e954..99d43141 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/subsystems/ApplicationInfo.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/subsystems/ApplicationInfo.java @@ -1,5 +1,6 @@ package com.launchdarkly.sdk.android.subsystems; +import androidx.annotation.Nullable; import com.launchdarkly.sdk.android.integrations.ApplicationInfoBuilder; /** @@ -10,9 +11,14 @@ * @since 4.1.0 */ public final class ApplicationInfo { + + @Nullable private final String applicationId; + @Nullable private final String applicationName; + @Nullable private final String applicationVersion; + @Nullable private final String applicationVersionName; /** @@ -37,6 +43,7 @@ public ApplicationInfo(String applicationId, String applicationVersion, * * @return the application identifier, or null */ + @Nullable public String getApplicationId() { return applicationId; } @@ -47,6 +54,7 @@ public String getApplicationId() { * * @return the application version, or null */ + @Nullable public String getApplicationVersion() { return applicationVersion; } @@ -56,6 +64,7 @@ public String getApplicationVersion() { * * @return the friendly name of the application, or null */ + @Nullable public String getApplicationName() { return applicationName; } @@ -65,6 +74,7 @@ public String getApplicationName() { * * @return the friendly name of the version, or null */ + @Nullable public String getApplicationVersionName() { return applicationVersionName; } diff --git a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/AutoEnvContextModifierTest.java b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/AutoEnvContextModifierTest.java index eed7bc27..aab51c58 100644 --- a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/AutoEnvContextModifierTest.java +++ b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/AutoEnvContextModifierTest.java @@ -6,6 +6,7 @@ import com.launchdarkly.sdk.ObjectBuilder; import com.launchdarkly.sdk.android.env.EnvironmentReporterBuilder; import com.launchdarkly.sdk.android.env.IEnvironmentReporter; +import com.launchdarkly.sdk.android.subsystems.ApplicationInfo; import org.junit.Assert; import org.junit.Rule; @@ -156,4 +157,50 @@ public void generatesConsistentKeysAcrossMultipleCalls() { Assert.assertEquals(key1, key2); } + + /** + * Test that when only myID is supplied, hash is hash(myID:) and not hash(myId:null) + */ + @Test + public void generatedApplicationKeyWithVersionMissing() { + PersistentDataStoreWrapper wrapper = TestUtil.makeSimplePersistentDataStoreWrapper(); + ApplicationInfo info = new ApplicationInfo("myID", null, null, null); + EnvironmentReporterBuilder b = new EnvironmentReporterBuilder(); + b.setApplicationInfo(info); + IEnvironmentReporter reporter = b.build(); + AutoEnvContextModifier underTest = new AutoEnvContextModifier( + wrapper, + reporter, + LDLogger.none() + ); + + LDContext input = LDContext.builder(ContextKind.of("aKind"), "aKey").build(); + LDContext output = underTest.modifyContext(input); + + // it is important that we create this expected context after the code runs because there + // will be persistence side effects + ContextKind applicationKind = ContextKind.of(AutoEnvContextModifier.LD_APPLICATION_KIND); + String expectedApplicationKey = LDUtil.urlSafeBase64Hash(reporter.getApplicationInfo().getApplicationId() + ":"); + + LDContext expectedAppContext = LDContext.builder(applicationKind, expectedApplicationKey) + .set(AutoEnvContextModifier.ENV_ATTRIBUTES_VERSION, AutoEnvContextModifier.SPEC_VERSION) + .set(AutoEnvContextModifier.ATTR_ID, "myID") + .set(AutoEnvContextModifier.ATTR_LOCALE, "unknown") + .build(); + + ContextKind deviceKind = ContextKind.of(AutoEnvContextModifier.LD_DEVICE_KIND); + LDContext expectedDeviceContext = LDContext.builder(deviceKind, wrapper.getOrGenerateContextKey(deviceKind)) + .set(AutoEnvContextModifier.ENV_ATTRIBUTES_VERSION, AutoEnvContextModifier.SPEC_VERSION) + .set(AutoEnvContextModifier.ATTR_MANUFACTURER, "unknown") + .set(AutoEnvContextModifier.ATTR_MODEL, "unknown") + .set(AutoEnvContextModifier.ATTR_OS, new ObjectBuilder() + .put(AutoEnvContextModifier.ATTR_FAMILY, "unknown") + .put(AutoEnvContextModifier.ATTR_NAME, "unknown") + .put(AutoEnvContextModifier.ATTR_VERSION, "unknown") + .build()) + .build(); + + LDContext expectedOutput = LDContext.multiBuilder().add(input).add(expectedAppContext).add(expectedDeviceContext).build(); + Assert.assertEquals(expectedOutput, output); + } } diff --git a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/HttpConfigurationBuilderTest.java b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/HttpConfigurationBuilderTest.java index 8b78441a..b143c302 100644 --- a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/HttpConfigurationBuilderTest.java +++ b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/HttpConfigurationBuilderTest.java @@ -21,8 +21,8 @@ private static Map buildBasicHeaders() { Map ret = new HashMap<>(); ret.put("Authorization", LDUtil.AUTH_SCHEME + MOBILE_KEY); ret.put("User-Agent", LDUtil.USER_AGENT_HEADER_VALUE); - ret.put("X-LaunchDarkly-Tags", "application-id/" + LDPackageConsts.SDK_NAME + " application-version/" + - BuildConfig.VERSION_NAME + " application-version-name/" + BuildConfig.VERSION_NAME); + ret.put("X-LaunchDarkly-Tags", "application-id/" + LDPackageConsts.SDK_NAME + " application-name/" + LDPackageConsts.SDK_NAME + + " application-version/" + BuildConfig.VERSION_NAME + " application-version-name/" + BuildConfig.VERSION_NAME); return ret; } @@ -71,7 +71,8 @@ public void testApplicationTags() { null, null, null, "", false, null, null, false, null, null, false); HttpConfiguration hc = Components.httpConfiguration() .build(contextWithTags); - assertEquals("application-id/" + LDPackageConsts.SDK_NAME + " application-version/" + BuildConfig.VERSION_NAME + " application-version-name/" + BuildConfig.VERSION_NAME , + assertEquals("application-id/" + LDPackageConsts.SDK_NAME + " application-name/" + LDPackageConsts.SDK_NAME + + " application-version/" + BuildConfig.VERSION_NAME + " application-version-name/" + BuildConfig.VERSION_NAME , toMap(hc.getDefaultHeaders()).get("X-LaunchDarkly-Tags")); } } diff --git a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/LDConfigTest.java b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/LDConfigTest.java index 92d99d8f..f5328cd3 100644 --- a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/LDConfigTest.java +++ b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/LDConfigTest.java @@ -100,8 +100,8 @@ public void headersForEnvironmentWithTransform() { expected.put("User-Agent", LDUtil.USER_AGENT_HEADER_VALUE); expected.put("Authorization", "api_key test-key"); - expected.put("X-LaunchDarkly-Tags", "application-id/" + LDPackageConsts.SDK_NAME + " application-version/" + - BuildConfig.VERSION_NAME + " application-version-name/" + BuildConfig.VERSION_NAME); + expected.put("X-LaunchDarkly-Tags", "application-id/" + LDPackageConsts.SDK_NAME + " application-name/" + LDPackageConsts.SDK_NAME + + " application-version/" + BuildConfig.VERSION_NAME + " application-version-name/" + BuildConfig.VERSION_NAME); Map headers = headersToMap( LDUtil.makeHttpProperties(clientContext).toHeadersBuilder().build() ); diff --git a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/LDUtilTest.java b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/LDUtilTest.java index 53662e58..7761ecf6 100644 --- a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/LDUtilTest.java +++ b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/LDUtilTest.java @@ -12,4 +12,18 @@ public void testUrlSafeBase64Hash() { String output = LDUtil.urlSafeBase64Hash(input); Assert.assertEquals(expectedOutput, output); } + + @Test + public void testValidateStringValue() { + Assert.assertNotNull(LDUtil.validateStringValue("")); + Assert.assertNotNull(LDUtil.validateStringValue("0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEFwhoops")); + Assert.assertNotNull(LDUtil.validateStringValue("#@$%^&")); + Assert.assertNull(LDUtil.validateStringValue("a-Az-Z0-9._-")); + } + @Test + public void testSanitizeSpaces() { + Assert.assertEquals("", LDUtil.sanitizeSpaces("")); + Assert.assertEquals("--hello--", LDUtil.sanitizeSpaces(" hello ")); + Assert.assertEquals("world", LDUtil.sanitizeSpaces("world")); + } } diff --git a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/integrations/ApplicationInfoBuilderTest.java b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/integrations/ApplicationInfoBuilderTest.java new file mode 100644 index 00000000..e6a2aa16 --- /dev/null +++ b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/integrations/ApplicationInfoBuilderTest.java @@ -0,0 +1,53 @@ +package com.launchdarkly.sdk.android.integrations; + +import com.launchdarkly.logging.LDLogger; +import com.launchdarkly.sdk.android.subsystems.ApplicationInfo; + +import org.junit.Assert; +import org.junit.Test; + +public class ApplicationInfoBuilderTest { + + @Test + public void ignoresInvalidValues() { + ApplicationInfoBuilder b = new ApplicationInfoBuilder(); + b.logger = LDLogger.none(); + b.applicationId("im#invalid"); + b.applicationName("im#invalid"); + b.applicationVersion("im#invalid"); + b.applicationVersionName("im#invalid"); + ApplicationInfo info = b.createApplicationInfo(); + Assert.assertNull(info.getApplicationId()); + Assert.assertNull(info.getApplicationName()); + Assert.assertNull(info.getApplicationVersion()); + Assert.assertNull(info.getApplicationVersionName()); + } + + @Test + public void sanitizesValues() { + ApplicationInfoBuilder b = new ApplicationInfoBuilder(); + b.logger = LDLogger.none(); + b.applicationId("id has spaces"); + b.applicationName("name has spaces"); + b.applicationVersion("version has spaces"); + b.applicationVersionName("version name has spaces"); + ApplicationInfo info = b.createApplicationInfo(); + Assert.assertEquals("id-has-spaces", info.getApplicationId()); + Assert.assertEquals("name-has-spaces", info.getApplicationName()); + Assert.assertEquals("version-has-spaces", info.getApplicationVersion()); + Assert.assertEquals("version-name-has-spaces", info.getApplicationVersionName()); + } + + @Test + public void nullValueIsValid() { + ApplicationInfoBuilder b = new ApplicationInfoBuilder(); + b.logger = LDLogger.none(); + b.applicationId("myID"); // first non-null + ApplicationInfo info = b.createApplicationInfo(); + Assert.assertEquals("myID", info.getApplicationId()); + + b.applicationId(null); // now back to null + ApplicationInfo info2 = b.createApplicationInfo(); + Assert.assertNull(info2.getApplicationId()); + } +} From 3071d45a3c1144c47b06ec90dbd3bd88752055ae Mon Sep 17 00:00:00 2001 From: Todd Anderson Date: Thu, 24 Aug 2023 14:37:10 -0500 Subject: [PATCH 23/26] fix: shortening logger tag name --- .../sdk/android/integrations/ApplicationInfoBuilder.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/ApplicationInfoBuilder.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/ApplicationInfoBuilder.java index 5016e583..3047c9d2 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/ApplicationInfoBuilder.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/ApplicationInfoBuilder.java @@ -43,7 +43,7 @@ public final class ApplicationInfoBuilder { private String applicationVersionName; @VisibleForTesting - LDLogger logger = LDLogger.withAdapter(LDAndroidLogging.adapter(), ApplicationInfoBuilder.class.getName()); + LDLogger logger = LDLogger.withAdapter(LDAndroidLogging.adapter(), ApplicationInfoBuilder.class.getSimpleName()); /** * Create an empty ApplicationInfoBuilder. From 5f2bbf95108880920a287a7ef90e166b54cc5991 Mon Sep 17 00:00:00 2001 From: tanderson-ld <127344469+tanderson-ld@users.noreply.github.com> Date: Thu, 24 Aug 2023 16:08:05 -0500 Subject: [PATCH 24/26] Update launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/LDUtilTest.java Co-authored-by: Matthew M. Keeler --- .../src/test/java/com/launchdarkly/sdk/android/LDUtilTest.java | 1 + 1 file changed, 1 insertion(+) diff --git a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/LDUtilTest.java b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/LDUtilTest.java index 7761ecf6..01b1d411 100644 --- a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/LDUtilTest.java +++ b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/LDUtilTest.java @@ -20,6 +20,7 @@ public void testValidateStringValue() { Assert.assertNotNull(LDUtil.validateStringValue("#@$%^&")); Assert.assertNull(LDUtil.validateStringValue("a-Az-Z0-9._-")); } + @Test public void testSanitizeSpaces() { Assert.assertEquals("", LDUtil.sanitizeSpaces("")); From 68b1b7017c983c9314d04f505d95955c670304c3 Mon Sep 17 00:00:00 2001 From: Todd Anderson Date: Fri, 25 Aug 2023 10:57:03 -0500 Subject: [PATCH 25/26] feat:logging application info validation errors --- .../integrations/ApplicationInfoBuilder.java | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/ApplicationInfoBuilder.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/ApplicationInfoBuilder.java index 3047c9d2..be7e3fc1 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/ApplicationInfoBuilder.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/ApplicationInfoBuilder.java @@ -7,7 +7,6 @@ import com.launchdarkly.logging.LDLogger; import com.launchdarkly.sdk.android.Components; import com.launchdarkly.sdk.android.LDAndroidLogging; -import com.launchdarkly.sdk.android.LDPackageConsts; import com.launchdarkly.sdk.android.LDUtil; import com.launchdarkly.sdk.android.subsystems.ApplicationInfo; @@ -63,7 +62,7 @@ public ApplicationInfoBuilder() {} * @return the builder */ public ApplicationInfoBuilder applicationId(String applicationId) { - validatedThenChange(this.logger, s -> this.applicationId = s, applicationId); + validatedThenSet("applicationId", s -> this.applicationId = s, applicationId, this.logger); return this; } @@ -78,7 +77,7 @@ public ApplicationInfoBuilder applicationId(String applicationId) { * @return the builder */ public ApplicationInfoBuilder applicationName(String applicationName) { - validatedThenChange(this.logger, s -> this.applicationName = s, applicationName); + validatedThenSet("applicationName", s -> this.applicationName = s, applicationName, this.logger); return this; } @@ -94,7 +93,7 @@ public ApplicationInfoBuilder applicationName(String applicationName) { * @return the builder */ public ApplicationInfoBuilder applicationVersion(String version) { - validatedThenChange(this.logger, s -> this.applicationVersion = s, version); + validatedThenSet("applicationVersion", s -> this.applicationVersion = s, version, this.logger); return this; } @@ -109,7 +108,7 @@ public ApplicationInfoBuilder applicationVersion(String version) { * @return the builder */ public ApplicationInfoBuilder applicationVersionName(String versionName) { - validatedThenChange(this.logger, s -> this.applicationVersionName = s, versionName); + validatedThenSet("applicationVersionName", s -> this.applicationVersionName = s, versionName, this.logger); return this; } @@ -123,11 +122,13 @@ public ApplicationInfo createApplicationInfo() { } /** + * @param propertyName the name of the property being set. Used for logging. * @param propertySetter lambda for setting the property. Java is fun and has predefined * functional interfaces. * @param input the string that will be sanitized and validated then applied + * @param logger use for logging. Can you believe that!? */ - private void validatedThenChange(LDLogger logger, Consumer propertySetter, String input) { + private void validatedThenSet(String propertyName, Consumer propertySetter, String input, LDLogger logger) { if (input == null) { propertySetter.accept(input); return; @@ -136,8 +137,7 @@ private void validatedThenChange(LDLogger logger, Consumer propertySette String sanitized = LDUtil.sanitizeSpaces(input); String error = LDUtil.validateStringValue(sanitized); if (error != null) { - // TODO: make sure log includes property name - logger.warn(LDPackageConsts.DEFAULT_LOGGER_NAME, propertySetter.toString() + error); + logger.warn("Issue setting {} value '{}'. {}", propertyName, sanitized, error); return; } From 58d73eeca7f9276812967a1609741a0b41d87ca9 Mon Sep 17 00:00:00 2001 From: Louis Chan <91093020+louis-launchdarkly@users.noreply.github.com> Date: Tue, 19 Sep 2023 11:39:01 -0700 Subject: [PATCH 26/26] fix: Change the ld_application id generation logic (#310) We have discussed and agreed to modify the logic to not include the version as the context key generation. We will release this as a bug fix to make sure customers don't stuck on a version with old logic available. --- .../launchdarkly/sdk/android/AutoEnvContextModifier.java | 3 +-- .../sdk/android/AutoEnvContextModifierTest.java | 7 ++----- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/AutoEnvContextModifier.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/AutoEnvContextModifier.java index 13cc8816..499f46f5 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/AutoEnvContextModifier.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/AutoEnvContextModifier.java @@ -140,8 +140,7 @@ private List makeRecipeList() { new ContextRecipe( ldApplicationKind, () -> LDUtil.urlSafeBase64Hash( - Objects.toString(environmentReporter.getApplicationInfo().getApplicationId(), "") + ":" - + Objects.toString(environmentReporter.getApplicationInfo().getApplicationVersion(), "") + Objects.toString(environmentReporter.getApplicationInfo().getApplicationId(), "") ), applicationCallables ), diff --git a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/AutoEnvContextModifierTest.java b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/AutoEnvContextModifierTest.java index aab51c58..642a990e 100644 --- a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/AutoEnvContextModifierTest.java +++ b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/AutoEnvContextModifierTest.java @@ -40,10 +40,7 @@ public void adheresToSchemaTest() { // it is important that we create this expected context after the code runs because there // will be persistence side effects ContextKind applicationKind = ContextKind.of(AutoEnvContextModifier.LD_APPLICATION_KIND); - String expectedApplicationKey = LDUtil.urlSafeBase64Hash( - reporter.getApplicationInfo().getApplicationId() + ":" - + reporter.getApplicationInfo().getApplicationVersion() - ); + String expectedApplicationKey = LDUtil.urlSafeBase64Hash(reporter.getApplicationInfo().getApplicationId()); LDContext expectedAppContext = LDContext.builder(applicationKind, expectedApplicationKey) .set(AutoEnvContextModifier.ENV_ATTRIBUTES_VERSION, AutoEnvContextModifier.SPEC_VERSION) .set(AutoEnvContextModifier.ATTR_ID, LDPackageConsts.SDK_NAME) @@ -180,7 +177,7 @@ public void generatedApplicationKeyWithVersionMissing() { // it is important that we create this expected context after the code runs because there // will be persistence side effects ContextKind applicationKind = ContextKind.of(AutoEnvContextModifier.LD_APPLICATION_KIND); - String expectedApplicationKey = LDUtil.urlSafeBase64Hash(reporter.getApplicationInfo().getApplicationId() + ":"); + String expectedApplicationKey = LDUtil.urlSafeBase64Hash(reporter.getApplicationInfo().getApplicationId()); LDContext expectedAppContext = LDContext.builder(applicationKind, expectedApplicationKey) .set(AutoEnvContextModifier.ENV_ATTRIBUTES_VERSION, AutoEnvContextModifier.SPEC_VERSION)