diff --git a/.circleci/config.yml b/.circleci/config.yml index 37d487759..3f2bc8df9 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -80,10 +80,11 @@ jobs: ./gradlew jacocoTestReport mkdir -p coverage/ cp -r build/reports/jacoco/test/* ./coverage - - run: mkdir -p ~/junit/ - run: name: Save test results - command: find . -type f -regex ".*/build/test-results/.*xml" -exec cp {} ~/junit/ \; + command: | + mkdir -p ~/junit/ + find . -type f -regex ".*/build/test-results/.*xml" -exec cp {} ~/junit/ \; when: always - run: make build-contract-tests @@ -126,7 +127,8 @@ jobs: name: save test results command: | mkdir .\junit - cp build/test-results/test/*.xml junit + cp build/test-results/test/*.xml junit -ErrorAction SilentlyContinue + when: always - store_test_results: path: .\junit - store_artifacts: diff --git a/contract-tests/service/src/main/java/sdktest/Representations.java b/contract-tests/service/src/main/java/sdktest/Representations.java index 60cf2b2a4..c30e538d9 100644 --- a/contract-tests/service/src/main/java/sdktest/Representations.java +++ b/contract-tests/service/src/main/java/sdktest/Representations.java @@ -25,6 +25,7 @@ public static class SdkConfigParams { SdkConfigStreamParams streaming; SdkConfigEventParams events; SdkConfigBigSegmentsParams bigSegments; + SdkConfigTagParams tags; } public static class SdkConfigStreamParams { @@ -49,6 +50,11 @@ public static class SdkConfigBigSegmentsParams { Long statusPollIntervalMs; Long staleAfterMs; } + + public static class SdkConfigTagParams { + String applicationId; + String applicationVersion; + } public static class CommandParams { String command; diff --git a/contract-tests/service/src/main/java/sdktest/SdkClientEntity.java b/contract-tests/service/src/main/java/sdktest/SdkClientEntity.java index a3f43fd88..9bc473ad1 100644 --- a/contract-tests/service/src/main/java/sdktest/SdkClientEntity.java +++ b/contract-tests/service/src/main/java/sdktest/SdkClientEntity.java @@ -8,6 +8,7 @@ import com.launchdarkly.sdk.server.FlagsStateOption; import com.launchdarkly.sdk.server.LDClient; import com.launchdarkly.sdk.server.LDConfig; +import com.launchdarkly.sdk.server.integrations.ApplicationInfoBuilder; import com.launchdarkly.sdk.server.integrations.BigSegmentsConfigurationBuilder; import com.launchdarkly.sdk.server.integrations.EventProcessorBuilder; import com.launchdarkly.sdk.server.integrations.StreamingDataSourceBuilder; @@ -235,6 +236,17 @@ private LDConfig buildSdkConfig(SdkConfigParams params) { } builder.bigSegments(bsb); } + + 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); + } return builder.build(); } diff --git a/contract-tests/service/src/main/java/sdktest/TestService.java b/contract-tests/service/src/main/java/sdktest/TestService.java index af6647879..68eed1456 100644 --- a/contract-tests/service/src/main/java/sdktest/TestService.java +++ b/contract-tests/service/src/main/java/sdktest/TestService.java @@ -26,7 +26,8 @@ public class TestService { "all-flags-client-side-only", "all-flags-details-only-for-tracked-flags", "all-flags-with-reasons", - "big-segments" + "big-segments", + "tags" }; static final Gson gson = new GsonBuilder().serializeNulls().create(); diff --git a/src/main/java/com/launchdarkly/sdk/server/ClientContextImpl.java b/src/main/java/com/launchdarkly/sdk/server/ClientContextImpl.java index 1cdf659fe..8004b36f9 100644 --- a/src/main/java/com/launchdarkly/sdk/server/ClientContextImpl.java +++ b/src/main/java/com/launchdarkly/sdk/server/ClientContextImpl.java @@ -1,5 +1,6 @@ package com.launchdarkly.sdk.server; +import com.launchdarkly.sdk.server.interfaces.ApplicationInfo; import com.launchdarkly.sdk.server.interfaces.BasicConfiguration; import com.launchdarkly.sdk.server.interfaces.ClientContext; import com.launchdarkly.sdk.server.interfaces.HttpConfiguration; @@ -51,7 +52,8 @@ private ClientContextImpl( ScheduledExecutorService sharedExecutor, DiagnosticAccumulator diagnosticAccumulator ) { - this.basicConfiguration = new BasicConfiguration(sdkKey, configuration.offline, configuration.threadPriority); + ApplicationInfo applicationInfo = configuration.applicationInfoBuilder.createApplicationInfo(); + this.basicConfiguration = new BasicConfiguration(sdkKey, configuration.offline, configuration.threadPriority, applicationInfo); this.httpConfiguration = configuration.httpConfigFactory.createHttpConfiguration(basicConfiguration); this.loggingConfiguration = configuration.loggingConfigFactory.createLoggingConfiguration(basicConfiguration); diff --git a/src/main/java/com/launchdarkly/sdk/server/Components.java b/src/main/java/com/launchdarkly/sdk/server/Components.java index bede125f5..171406f39 100644 --- a/src/main/java/com/launchdarkly/sdk/server/Components.java +++ b/src/main/java/com/launchdarkly/sdk/server/Components.java @@ -11,6 +11,7 @@ import com.launchdarkly.sdk.server.ComponentsImpl.PersistentDataStoreBuilderImpl; import com.launchdarkly.sdk.server.ComponentsImpl.PollingDataSourceBuilderImpl; import com.launchdarkly.sdk.server.ComponentsImpl.StreamingDataSourceBuilderImpl; +import com.launchdarkly.sdk.server.integrations.ApplicationInfoBuilder; import com.launchdarkly.sdk.server.integrations.BigSegmentsConfigurationBuilder; import com.launchdarkly.sdk.server.integrations.EventProcessorBuilder; import com.launchdarkly.sdk.server.integrations.HttpConfigurationBuilder; @@ -320,4 +321,27 @@ public static HttpAuthentication httpBasicAuthentication(String username, String public static LoggingConfigurationBuilder logging() { return new LoggingConfigurationBuilderImpl(); } + + /** + * Returns a configuration builder for the SDK's application metadata. + *
+ * Passing this to {@link LDConfig.Builder#applicationInfo(com.launchdarkly.sdk.server.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
+ * @since 5.8.0
+ * @see LDConfig.Builder#applicationInfo(com.launchdarkly.sdk.server.integrations.ApplicationInfoBuilder)
+ */
+ public static ApplicationInfoBuilder applicationInfo() {
+ return new ApplicationInfoBuilder();
+ }
}
diff --git a/src/main/java/com/launchdarkly/sdk/server/ComponentsImpl.java b/src/main/java/com/launchdarkly/sdk/server/ComponentsImpl.java
index 33ddaa4fb..86aab22c2 100644
--- a/src/main/java/com/launchdarkly/sdk/server/ComponentsImpl.java
+++ b/src/main/java/com/launchdarkly/sdk/server/ComponentsImpl.java
@@ -253,6 +253,12 @@ public HttpConfiguration createHttpConfiguration(BasicConfiguration basicConfigu
ImmutableMap.Builder+ * This object is normally a configuration builder obtained from {@link Components#applicationInfo()}, + * which has methods for setting individual logging-related properties. + * + * @param applicationInfoBuilder a configuration builder object returned by {@link Components#applicationInfo()} + * @return the builder + * @since 5.8.0 + */ + public Builder applicationInfo(ApplicationInfoBuilder applicationInfoBuilder) { + this.applicationInfoBuilder = applicationInfoBuilder; + return this; + } + /** * Sets the configuration of the SDK's Big Segments feature. *
diff --git a/src/main/java/com/launchdarkly/sdk/server/Util.java b/src/main/java/com/launchdarkly/sdk/server/Util.java
index fc6a4cc02..990e12343 100644
--- a/src/main/java/com/launchdarkly/sdk/server/Util.java
+++ b/src/main/java/com/launchdarkly/sdk/server/Util.java
@@ -1,5 +1,7 @@
package com.launchdarkly.sdk.server;
+import com.launchdarkly.sdk.server.Loggers;
+import com.launchdarkly.sdk.server.interfaces.ApplicationInfo;
import com.launchdarkly.sdk.server.interfaces.HttpAuthentication;
import com.launchdarkly.sdk.server.interfaces.HttpConfiguration;
@@ -13,7 +15,10 @@
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.time.Duration;
+import java.util.ArrayList;
+import java.util.List;
import java.util.Map;
+import java.util.regex.Pattern;
import java.util.concurrent.TimeUnit;
import static com.google.common.collect.Iterables.transform;
@@ -207,4 +212,35 @@ static URI concatenateUriPath(URI baseUri, String path) {
String addPath = path.startsWith("/") ? path.substring(1) : path;
return URI.create(uriStr + (uriStr.endsWith("/") ? "" : "/") + addPath);
}
+
+ // Tag values must not be empty, and only contain letters, numbers, `.`, `_`, or `-`.
+ private static Pattern TAG_VALUE_REGEX = Pattern.compile("^[\\w.-]+$");
+
+ /**
+ * 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) {
+ 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.server.LDConfig.Builder#applicationInfo(ApplicationInfoBuilder)}:
+ *
+ *
+ * @since 5.8.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/src/main/java/com/launchdarkly/sdk/server/interfaces/ApplicationInfo.java b/src/main/java/com/launchdarkly/sdk/server/interfaces/ApplicationInfo.java
new file mode 100644
index 000000000..e64ed6c0b
--- /dev/null
+++ b/src/main/java/com/launchdarkly/sdk/server/interfaces/ApplicationInfo.java
@@ -0,0 +1,46 @@
+package com.launchdarkly.sdk.server.interfaces;
+
+import com.launchdarkly.sdk.server.integrations.ApplicationInfoBuilder;
+
+/**
+ * Encapsulates the SDK's application metadata.
+ *
+ * See {@link ApplicationInfoBuilder} for more details on these properties.
+ *
+ * @since 5.8.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/src/main/java/com/launchdarkly/sdk/server/interfaces/BasicConfiguration.java b/src/main/java/com/launchdarkly/sdk/server/interfaces/BasicConfiguration.java
index 926cf3f13..3a19a9737 100644
--- a/src/main/java/com/launchdarkly/sdk/server/interfaces/BasicConfiguration.java
+++ b/src/main/java/com/launchdarkly/sdk/server/interfaces/BasicConfiguration.java
@@ -9,18 +9,33 @@ public final class BasicConfiguration {
private final String sdkKey;
private final boolean offline;
private final int threadPriority;
-
+ private final ApplicationInfo applicationInfo;
+
/**
* Constructs an instance.
- *
+ *
* @param sdkKey the SDK key
* @param offline true if the SDK was configured to be completely offline
* @param threadPriority the thread priority that should be used for any worker threads created by SDK components
+ * @param applicationInfo metadata about the application using this SDK
*/
- public BasicConfiguration(String sdkKey, boolean offline, int threadPriority) {
+ public BasicConfiguration(String sdkKey, boolean offline, int threadPriority, ApplicationInfo applicationInfo) {
this.sdkKey = sdkKey;
this.offline = offline;
this.threadPriority = threadPriority;
+ this.applicationInfo = applicationInfo;
+ }
+
+ /**
+ * Constructs an instance.
+ *
+ * @param sdkKey the SDK key
+ * @param offline true if the SDK was configured to be completely offline
+ * @param threadPriority the thread priority that should be used for any worker threads created by SDK components
+ */
+ @Deprecated
+ public BasicConfiguration(String sdkKey, boolean offline, int threadPriority) {
+ this(sdkKey, offline, threadPriority, null);
}
/**
@@ -51,4 +66,14 @@ public boolean isOffline() {
public int getThreadPriority() {
return threadPriority;
}
+
+ /**
+ * The metadata about the application using this SDK.
+ *
+ * @return the application info
+ * @see com.launchdarkly.sdk.server.LDConfig.Builder#applicationInfo(com.launchdarkly.sdk.server.integrations.ApplicationInfoBuilder)
+ */
+ public ApplicationInfo getApplicationInfo() {
+ return applicationInfo;
+ }
}
diff --git a/src/test/java/com/launchdarkly/sdk/server/ClientContextImplTest.java b/src/test/java/com/launchdarkly/sdk/server/ClientContextImplTest.java
index 1268e5378..026576a5b 100644
--- a/src/test/java/com/launchdarkly/sdk/server/ClientContextImplTest.java
+++ b/src/test/java/com/launchdarkly/sdk/server/ClientContextImplTest.java
@@ -133,7 +133,7 @@ public void packagePrivatePropertiesHaveDefaultsIfContextIsNotOurImplementation(
private static final class SomeOtherContextImpl implements ClientContext {
public BasicConfiguration getBasic() {
- return new BasicConfiguration(SDK_KEY, false, Thread.MIN_PRIORITY);
+ return new BasicConfiguration(SDK_KEY, false, Thread.MIN_PRIORITY, null);
}
public HttpConfiguration getHttp() {
diff --git a/src/test/java/com/launchdarkly/sdk/server/DefaultFeatureRequestorTest.java b/src/test/java/com/launchdarkly/sdk/server/DefaultFeatureRequestorTest.java
index 2c3f52587..4844a5e0f 100644
--- a/src/test/java/com/launchdarkly/sdk/server/DefaultFeatureRequestorTest.java
+++ b/src/test/java/com/launchdarkly/sdk/server/DefaultFeatureRequestorTest.java
@@ -42,7 +42,7 @@ private DefaultFeatureRequestor makeRequestor(HttpServer server, LDConfig config
}
private HttpConfiguration makeHttpConfig(LDConfig config) {
- return config.httpConfigFactory.createHttpConfiguration(new BasicConfiguration(sdkKey, false, 0));
+ return config.httpConfigFactory.createHttpConfiguration(new BasicConfiguration(sdkKey, false, 0, null));
}
private void verifyExpectedData(FeatureRequestor.AllData data) {
diff --git a/src/test/java/com/launchdarkly/sdk/server/LDConfigTest.java b/src/test/java/com/launchdarkly/sdk/server/LDConfigTest.java
index 0afba1f6b..3794e990a 100644
--- a/src/test/java/com/launchdarkly/sdk/server/LDConfigTest.java
+++ b/src/test/java/com/launchdarkly/sdk/server/LDConfigTest.java
@@ -26,7 +26,7 @@
@SuppressWarnings("javadoc")
public class LDConfigTest {
- private static final BasicConfiguration BASIC_CONFIG = new BasicConfiguration("", false, 0);
+ private static final BasicConfiguration BASIC_CONFIG = new BasicConfiguration("", false, 0, null);
@Test
public void defaults() {
diff --git a/src/test/java/com/launchdarkly/sdk/server/UtilTest.java b/src/test/java/com/launchdarkly/sdk/server/UtilTest.java
index 6cf5b6fb3..2a9b91151 100644
--- a/src/test/java/com/launchdarkly/sdk/server/UtilTest.java
+++ b/src/test/java/com/launchdarkly/sdk/server/UtilTest.java
@@ -1,5 +1,6 @@
package com.launchdarkly.sdk.server;
+import com.launchdarkly.sdk.server.interfaces.ApplicationInfo;
import com.launchdarkly.sdk.server.interfaces.HttpAuthentication;
import com.launchdarkly.sdk.server.interfaces.HttpConfiguration;
@@ -90,4 +91,16 @@ public void describeDuration() {
assertEquals("1 minute", Util.describeDuration(Duration.ofMillis(60000)));
assertEquals("2 minutes", Util.describeDuration(Duration.ofMillis(120000)));
}
+
+ @Test
+ public void applicationTagHeader() {
+ assertEquals("", Util.applicationTagHeader(new ApplicationInfo(null, null)));
+ assertEquals("application-id/foo", Util.applicationTagHeader(new ApplicationInfo("foo", null)));
+ assertEquals("application-version/1.0.0", Util.applicationTagHeader(new ApplicationInfo(null, "1.0.0")));
+ assertEquals("application-id/foo application-version/1.0.0", Util.applicationTagHeader(new ApplicationInfo("foo", "1.0.0")));
+ // Values with invalid characters get discarded
+ assertEquals("", Util.applicationTagHeader(new ApplicationInfo("invalid name", "lol!")));
+ // Empty values get discarded
+ assertEquals("", Util.applicationTagHeader(new ApplicationInfo("", "")));
+ }
}
diff --git a/src/test/java/com/launchdarkly/sdk/server/integrations/ApplicationInfoBuilderTest.java b/src/test/java/com/launchdarkly/sdk/server/integrations/ApplicationInfoBuilderTest.java
new file mode 100644
index 000000000..9b9977e06
--- /dev/null
+++ b/src/test/java/com/launchdarkly/sdk/server/integrations/ApplicationInfoBuilderTest.java
@@ -0,0 +1,27 @@
+package com.launchdarkly.sdk.server.integrations;
+
+import com.launchdarkly.sdk.server.Components;
+import com.launchdarkly.sdk.server.interfaces.ApplicationInfo;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+@SuppressWarnings("javadoc")
+public class ApplicationInfoBuilderTest {
+ @Test
+ public void infoBuilder() {
+ ApplicationInfo i1 = Components.applicationInfo()
+ .createApplicationInfo();
+ assertNull(i1.getApplicationId());
+ assertNull(i1.getApplicationVersion());
+
+ ApplicationInfo i2 = Components.applicationInfo()
+ .applicationId("authentication-service")
+ .applicationVersion("1.0.0")
+ .createApplicationInfo();
+ assertEquals("authentication-service", i2.getApplicationId());
+ assertEquals("1.0.0", i2.getApplicationVersion());
+ }
+}
diff --git a/src/test/java/com/launchdarkly/sdk/server/integrations/HttpConfigurationBuilderTest.java b/src/test/java/com/launchdarkly/sdk/server/integrations/HttpConfigurationBuilderTest.java
index 6b2b6bea6..06783edf8 100644
--- a/src/test/java/com/launchdarkly/sdk/server/integrations/HttpConfigurationBuilderTest.java
+++ b/src/test/java/com/launchdarkly/sdk/server/integrations/HttpConfigurationBuilderTest.java
@@ -2,6 +2,7 @@
import com.google.common.collect.ImmutableMap;
import com.launchdarkly.sdk.server.Components;
+import com.launchdarkly.sdk.server.interfaces.ApplicationInfo;
import com.launchdarkly.sdk.server.interfaces.BasicConfiguration;
import com.launchdarkly.sdk.server.interfaces.HttpConfiguration;
@@ -32,7 +33,7 @@
@SuppressWarnings("javadoc")
public class HttpConfigurationBuilderTest {
private static final String SDK_KEY = "sdk-key";
- private static final BasicConfiguration BASIC_CONFIG = new BasicConfiguration(SDK_KEY, false, 0);
+ private static final BasicConfiguration BASIC_CONFIG = new BasicConfiguration(SDK_KEY, false, 0, null);
private static ImmutableMap.Builder
+ *
+ * LDConfig config = new LDConfig.Builder()
+ * .applicationInfo(
+ * Components.applicationInfo()
+ * .applicationId("authentication-service")
+ * .applicationVersion("1.0.0")
+ * )
+ * .build();
+ *