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 c62c90a7..677c5328 100644 --- a/contract-tests/src/main/java/com/launchdarkly/sdktest/SdkClientEntity.java +++ b/contract-tests/src/main/java/com/launchdarkly/sdktest/SdkClientEntity.java @@ -9,6 +9,7 @@ import com.launchdarkly.sdk.android.LDClient; import com.launchdarkly.sdk.android.LDConfig; +import com.launchdarkly.sdk.android.integrations.ApplicationInfoBuilder; import com.launchdarkly.sdk.android.integrations.EventProcessorBuilder; import com.launchdarkly.sdk.android.integrations.PollingDataSourceBuilder; import com.launchdarkly.sdk.android.integrations.ServiceEndpointsBuilder; @@ -247,6 +248,17 @@ private LDConfig buildSdkConfig(SdkConfigParams params, LDLogAdapter logAdapter, Components.httpConfiguration().useReport(params.clientSide.useReport) ); + if (params.tags != null) { + ApplicationInfoBuilder ab = Components.applicationInfo(); + if (params.tags.applicationId != null) { + ab.applicationId(params.tags.applicationId); + } + if (params.tags.applicationVersion != null) { + ab.applicationVersion(params.tags.applicationVersion); + } + builder.applicationInfo(ab); + } + if (params.serviceEndpoints != null) { if (params.serviceEndpoints.streaming != null) { endpoints.streaming(params.serviceEndpoints.streaming); 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 e4bd626e..69f9b55f 100644 --- a/contract-tests/src/main/java/com/launchdarkly/sdktest/TestService.java +++ b/contract-tests/src/main/java/com/launchdarkly/sdktest/TestService.java @@ -32,6 +32,7 @@ public class TestService extends NanoHTTPD { "service-endpoints", "singleton", "strongly-typed", + "tags" }; 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/ClientContextImpl.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/ClientContextImpl.java index 718b8dee..fa9ddb03 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,19 +54,20 @@ static ClientContextImpl fromConfig( SummaryEventStore summaryEventStore, LDLogger logger ) { - ClientContext minimalContext = new ClientContext(null, mobileKey, logger, config, + ClientContext minimalContext = new ClientContext(null, config.applicationInfo, logger, config, environmentName, config.isEvaluationReasons(), null, config.isOffline(), - config.serviceEndpoints); + mobileKey, config.serviceEndpoints); HttpConfiguration httpConfig = config.http.build(minimalContext); ClientContext baseClientContext = new ClientContext( application, - mobileKey, + config.applicationInfo, logger, config, environmentName, config.isEvaluationReasons(), httpConfig, config.isOffline(), + mobileKey, config.serviceEndpoints ); return new ClientContextImpl(baseClientContext, diagnosticStore, sharedEventClient, summaryEventStore); diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/Components.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/Components.java index 771a18a0..532bdffc 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/Components.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/Components.java @@ -2,6 +2,7 @@ import static com.launchdarkly.sdk.android.ComponentsImpl.NULL_EVENT_PROCESSOR_FACTORY; +import com.launchdarkly.sdk.android.integrations.ApplicationInfoBuilder; import com.launchdarkly.sdk.android.integrations.EventProcessorBuilder; import com.launchdarkly.sdk.android.integrations.HttpConfigurationBuilder; import com.launchdarkly.sdk.android.integrations.PollingDataSourceBuilder; @@ -25,6 +26,28 @@ public abstract class Components { private Components() {} + /** + * Returns a configuration builder for the SDK's application metadata. + *
+ * Passing this to {@link LDConfig.Builder#applicationInfo(com.launchdarkly.sdk.android.integrations.ApplicationInfoBuilder)}, + * after setting any desired properties on the builder, applies this configuration to the SDK. + *
+ * LDConfig config = new LDConfig.Builder()
+ * .applicationInfo(
+ * Components.applicationInfo()
+ * .applicationId("authentication-service")
+ * .applicationVersion("1.0.0")
+ * )
+ * .build();
+ *
+ *
+ * @return a builder object
+ * @see LDConfig.Builder#applicationInfo(com.launchdarkly.sdk.android.integrations.ApplicationInfoBuilder)
+ */
+ public static ApplicationInfoBuilder applicationInfo() {
+ return new ApplicationInfoBuilder();
+ }
+
/**
* Returns a configuration builder for the SDK's networking configuration.
*
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 286cca17..2fd40bd6 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
@@ -132,6 +132,13 @@ public HttpConfiguration build(ClientContext clientContext) {
Map
+ * This object is normally a configuration builder obtained from {@link Components#applicationInfo()},
+ * which has methods for setting individual metadata properties.
+ *
+ * @param applicationInfoBuilder a configuration builder object returned by {@link Components#applicationInfo()}
+ * @return the builder
+ * @since 3.3.0
+ */
+ public Builder applicationInfo(ApplicationInfoBuilder applicationInfoBuilder) {
+ this.applicationInfoBuilder = applicationInfoBuilder;
+ return this;
+ }
+
/**
* Sets the configuration of the component that receives feature flag data from LaunchDarkly.
*
@@ -1274,11 +1296,16 @@ public LDConfig build() {
Components.serviceEndpoints().polling(pollUri).streaming(streamUri).events(eventsUri).build() :
this.serviceEndpointsBuilder.build();
+ ApplicationInfo applicationInfo = this.applicationInfoBuilder == null ?
+ Components.applicationInfo().createApplicationInfo() :
+ applicationInfoBuilder.createApplicationInfo();
+
return new LDConfig(
mobileKeys,
pollUri,
eventsUri,
streamUri,
+ applicationInfo,
dataSourceConfig,
eventsConfig,
httpConfig,
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 12520d92..247c6776 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
@@ -19,14 +19,18 @@
import com.launchdarkly.sdk.LDUser;
import com.launchdarkly.sdk.LDValue;
import com.launchdarkly.sdk.UserAttribute;
+import com.launchdarkly.sdk.android.subsystems.ApplicationInfo;
import com.launchdarkly.sdk.android.subsystems.HttpConfiguration;
import java.io.IOException;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
+import java.util.List;
import java.util.Map;
import java.util.Set;
+import java.util.regex.Pattern;
import okhttp3.Headers;
@@ -34,6 +38,41 @@ class LDUtil {
static final String AUTH_SCHEME = "api_key ";
static final String USER_AGENT_HEADER_VALUE = "AndroidClient/" + BuildConfig.VERSION_NAME;
+ // Tag values must not be empty, and only contain letters, numbers, `.`, `_`, or `-`.
+ private static Pattern TAG_VALUE_REGEX = Pattern.compile("^[-a-zA-Z0-9._]+$");
+
+ /**
+ * Builds the "X-LaunchDarkly-Tags" HTTP header out of the configured application info.
+ *
+ * @param applicationInfo the application metadata
+ * @return a space-separated string of tags, e.g. "application-id/authentication-service application-version/1.0.0"
+ */
+ static String applicationTagHeader(ApplicationInfo applicationInfo, LDLogger logger) {
+ String[][] tags = {
+ {"applicationId", "application-id", applicationInfo.getApplicationId()},
+ {"applicationVersion", "application-version", applicationInfo.getApplicationVersion()},
+ };
+ List
+ * Application metadata may be used in LaunchDarkly analytics or other product features, but does not affect feature flag evaluations.
+ *
+ * If you want to set non-default values for any of these fields, create a builder with
+ * {@link Components#applicationInfo()}, change its properties with the methods of this class,
+ * and pass it to {@link com.launchdarkly.sdk.android.LDConfig.Builder#applicationInfo(ApplicationInfoBuilder)}:
+ *
+ *
+ * @since 3.3.0
+ */
+public final class ApplicationInfoBuilder {
+ private String applicationId;
+ private String applicationVersion;
+
+ /**
+ * Create an empty ApplicationInfoBuilder.
+ *
+ * @see Components#applicationInfo()
+ */
+ public ApplicationInfoBuilder() {}
+
+ /**
+ * Sets a unique identifier representing the application where 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 applicationId the application identifier
+ * @return the builder
+ */
+ public ApplicationInfoBuilder applicationId(String applicationId) {
+ this.applicationId = applicationId;
+ return this;
+ }
+
+ /**
+ * Sets a unique identifier representing the version of the application where 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 applicationVersion the application version
+ * @return the builder
+ */
+ public ApplicationInfoBuilder applicationVersion(String applicationVersion) {
+ this.applicationVersion = applicationVersion;
+ return this;
+ }
+
+ /**
+ * Called internally by the SDK to create the configuration object.
+ *
+ * @return the configuration object
+ */
+ public ApplicationInfo createApplicationInfo() {
+ return new ApplicationInfo(applicationId, applicationVersion);
+ }
+}
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
new file mode 100644
index 00000000..9ef1dfdc
--- /dev/null
+++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/subsystems/ApplicationInfo.java
@@ -0,0 +1,46 @@
+package com.launchdarkly.sdk.android.subsystems;
+
+import com.launchdarkly.sdk.android.integrations.ApplicationInfoBuilder;
+
+/**
+ * Encapsulates the SDK's application metadata.
+ *
+ * See {@link ApplicationInfoBuilder} for more details on these properties.
+ *
+ * @since 3.3.0
+ */
+public final class ApplicationInfo {
+ private String applicationId;
+ private String applicationVersion;
+
+ /**
+ * Used internally by the SDK to store application metadata.
+ *
+ * @param applicationId the application ID
+ * @param applicationVersion the application version
+ * @see ApplicationInfoBuilder
+ */
+ public ApplicationInfo(String applicationId, String applicationVersion) {
+ this.applicationId = applicationId;
+ this.applicationVersion = applicationVersion;
+ }
+
+ /**
+ * A unique identifier representing the application where the LaunchDarkly SDK is running.
+ *
+ * @return the application identifier, or null
+ */
+ public String getApplicationId() {
+ return applicationId;
+ }
+
+ /**
+ * A unique identifier representing the version of the application where the
+ * LaunchDarkly SDK is running.
+ *
+ * @return the application version, or null
+ */
+ public String getApplicationVersion() {
+ return applicationVersion;
+ }
+}
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 7d99e00b..2b108a65 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
@@ -25,7 +25,8 @@
* @since 3.3.0
*/
public class ClientContext {
- private final Application application;
+ private final Application androidApplication;
+ private final ApplicationInfo applicationInfo;
private final LDLogger baseLogger;
private final LDConfig config;
private final boolean evaluationReasons;
@@ -36,37 +37,40 @@ public class ClientContext {
private final ServiceEndpoints serviceEndpoints;
public ClientContext(
- Application application,
- String mobileKey,
+ Application androidApplication,
+ ApplicationInfo applicationInfo,
LDLogger baseLogger,
LDConfig config,
String environmentName,
boolean evaluationReasons,
HttpConfiguration http,
boolean initiallySetOffline,
+ String mobileKey,
ServiceEndpoints serviceEndpoints
) {
- this.application = application;
- this.mobileKey = mobileKey;
+ this.androidApplication = androidApplication;
+ this.applicationInfo = applicationInfo;
this.baseLogger = baseLogger;
this.config = config;
this.environmentName = environmentName;
this.evaluationReasons = evaluationReasons;
this.http = http;
this.initiallySetOffline = initiallySetOffline;
+ this.mobileKey = mobileKey;
this.serviceEndpoints = serviceEndpoints;
}
protected ClientContext(ClientContext copyFrom) {
this(
- copyFrom.application,
- copyFrom.mobileKey,
+ copyFrom.androidApplication,
+ copyFrom.applicationInfo,
copyFrom.baseLogger,
copyFrom.config,
copyFrom.environmentName,
copyFrom.evaluationReasons,
copyFrom.http,
copyFrom.initiallySetOffline,
+ copyFrom.mobileKey,
copyFrom.serviceEndpoints
);
}
@@ -76,7 +80,15 @@ protected ClientContext(ClientContext copyFrom) {
* @return the application
*/
public Application getApplication() {
- return application;
+ return androidApplication;
+ }
+
+ /**
+ * The application metadata object.
+ * @return the application metadata
+ */
+ public ApplicationInfo getApplicationInfo() {
+ return applicationInfo;
}
/**
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
new file mode 100644
index 00000000..aea0c51e
--- /dev/null
+++ b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/HttpConfigurationBuilderTest.java
@@ -0,0 +1,94 @@
+package com.launchdarkly.sdk.android;
+
+import com.launchdarkly.sdk.android.Components;
+import com.launchdarkly.sdk.android.LDUtil;
+import com.launchdarkly.sdk.android.subsystems.ApplicationInfo;
+import com.launchdarkly.sdk.android.subsystems.ClientContext;
+import com.launchdarkly.sdk.android.subsystems.HttpConfiguration;
+
+import org.junit.Test;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.Proxy;
+import java.net.Socket;
+import java.net.UnknownHostException;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+import java.time.Duration;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.net.SocketFactory;
+import javax.net.ssl.SSLSocketFactory;
+import javax.net.ssl.X509TrustManager;
+
+import static com.launchdarkly.sdk.android.integrations.HttpConfigurationBuilder.DEFAULT_CONNECT_TIMEOUT_MILLIS;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+
+public class HttpConfigurationBuilderTest {
+ private static final String MOBILE_KEY = "mobile-key";
+ private static final ClientContext BASIC_CONTEXT = new ClientContext(null, null, null, null,
+ "", false, null, false, MOBILE_KEY, null);
+
+ private static Map
+ *
+ * LDConfig config = new LDConfig.Builder()
+ * .applicationInfo(
+ * Components.applicationInfo()
+ * .applicationId("authentication-service")
+ * .applicationVersion("1.0.0")
+ * )
+ * .build();
+ *