From 134996c4715d5828869ece82cfe6727ad1ef1b61 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Wed, 12 Nov 2025 14:14:14 +0100 Subject: [PATCH 01/15] Implement OpenFeature Integration --- buildSrc/src/main/java/Config.kt | 1 + gradle/libs.versions.toml | 2 + sentry-openfeature/build.gradle.kts | 92 +++++++ .../openfeature/SentryOpenFeatureHook.java | 62 +++++ .../build.gradle.kts | 4 + .../boot/jakarta/FeatureFlagController.java | 66 +++++ .../boot/jakarta/OpenFeatureConfig.java | 227 ++++++++++++++++++ .../systemtest/FeatureFlagSystemTest.kt | 104 ++++++++ .../sentry/systemtest/util/RestTestClient.kt | 18 ++ settings.gradle.kts | 1 + 10 files changed, 577 insertions(+) create mode 100644 sentry-openfeature/build.gradle.kts create mode 100644 sentry-openfeature/src/main/java/io/sentry/openfeature/SentryOpenFeatureHook.java create mode 100644 sentry-samples/sentry-samples-spring-boot-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/FeatureFlagController.java create mode 100644 sentry-samples/sentry-samples-spring-boot-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/OpenFeatureConfig.java create mode 100644 sentry-samples/sentry-samples-spring-boot-jakarta/src/test/kotlin/io/sentry/systemtest/FeatureFlagSystemTest.kt diff --git a/buildSrc/src/main/java/Config.kt b/buildSrc/src/main/java/Config.kt index 9ffe5ac3117..887bf1415b2 100644 --- a/buildSrc/src/main/java/Config.kt +++ b/buildSrc/src/main/java/Config.kt @@ -77,6 +77,7 @@ object Config { val SENTRY_GRAPHQL22_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.graphql22" val SENTRY_QUARTZ_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.quartz" val SENTRY_JDBC_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.jdbc" + val SENTRY_OPENFEATURE_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.openfeature" val SENTRY_SERVLET_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.servlet" val SENTRY_SERVLET_JAKARTA_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.servlet.jakarta" val SENTRY_COMPOSE_HELPER_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.compose.helper" diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 748bd427ea3..78185fc1f78 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -39,6 +39,7 @@ minSdk = "21" spotless = "7.0.4" gummyBears = "0.12.0" camerax = "1.3.0" +openfeature = "1.18.2" [plugins] kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } @@ -121,6 +122,7 @@ nopen-annotations = { module = "com.jakewharton.nopen:nopen-annotations", versio nopen-checker = { module = "com.jakewharton.nopen:nopen-checker", version.ref = "nopen" } nullaway = { module = "com.uber.nullaway:nullaway", version = "0.9.5" } okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" } +openfeature = { module = "dev.openfeature:sdk", version.ref = "openfeature" } otel = { module = "io.opentelemetry:opentelemetry-sdk", version.ref = "otel" } otel-extension-autoconfigure = { module = "io.opentelemetry:opentelemetry-sdk-extension-autoconfigure", version.ref = "otel" } otel-extension-autoconfigure-spi = { module = "io.opentelemetry:opentelemetry-sdk-extension-autoconfigure-spi", version.ref = "otel" } diff --git a/sentry-openfeature/build.gradle.kts b/sentry-openfeature/build.gradle.kts new file mode 100644 index 00000000000..8117f97db28 --- /dev/null +++ b/sentry-openfeature/build.gradle.kts @@ -0,0 +1,92 @@ +import net.ltgt.gradle.errorprone.errorprone +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + `java-library` + id("io.sentry.javadoc") + alias(libs.plugins.kotlin.jvm) + jacoco + alias(libs.plugins.errorprone) + alias(libs.plugins.gradle.versions) + alias(libs.plugins.buildconfig) +} + +tasks.withType().configureEach { + compilerOptions.jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_1_8 + compilerOptions.languageVersion = org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_9 + compilerOptions.apiVersion = org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_9 +} + +dependencies { + api(projects.sentry) + + compileOnly(libs.openfeature) + + compileOnly(libs.jetbrains.annotations) + compileOnly(libs.nopen.annotations) + errorprone(libs.errorprone.core) + errorprone(libs.nopen.checker) + errorprone(libs.nullaway) + + // tests + testImplementation(projects.sentry) + testImplementation(projects.sentryTestSupport) + testImplementation(kotlin(Config.kotlinStdLib)) + testImplementation(libs.kotlin.test.junit) + testImplementation(libs.mockito.kotlin) + testImplementation(libs.mockito.inline) + testImplementation("dev.openfeature:sdk:1.15.1") +} + +configure { test { java.srcDir("src/test/java") } } + +jacoco { toolVersion = libs.versions.jacoco.get() } + +tasks.jacocoTestReport { + reports { + xml.required.set(true) + html.required.set(false) + } +} + +tasks { + jacocoTestCoverageVerification { + violationRules { rule { limit { minimum = Config.QualityPlugins.Jacoco.minimumCoverage } } } + } + check { + dependsOn(jacocoTestCoverageVerification) + dependsOn(jacocoTestReport) + } +} + +tasks.withType().configureEach { + options.errorprone { + check("NullAway", net.ltgt.gradle.errorprone.CheckSeverity.ERROR) + option("NullAway:AnnotatedPackages", "io.sentry") + } +} + +buildConfig { + useJavaOutput() + packageName("io.sentry.openfeature") + buildConfigField( + "String", + "SENTRY_OPENFEATURE_SDK_NAME", + "\"${Config.Sentry.SENTRY_OPENFEATURE_SDK_NAME}\"", + ) + buildConfigField("String", "VERSION_NAME", "\"${project.version}\"") +} + +tasks.jar { + manifest { + attributes( + "Sentry-Version-Name" to project.version, + "Sentry-SDK-Name" to Config.Sentry.SENTRY_OPENFEATURE_SDK_NAME, + "Sentry-SDK-Package-Name" to "maven:io.sentry:sentry-openfeature", + "Implementation-Vendor" to "Sentry", + "Implementation-Title" to project.name, + "Implementation-Version" to project.version, + ) + } +} + diff --git a/sentry-openfeature/src/main/java/io/sentry/openfeature/SentryOpenFeatureHook.java b/sentry-openfeature/src/main/java/io/sentry/openfeature/SentryOpenFeatureHook.java new file mode 100644 index 00000000000..3539b2a8376 --- /dev/null +++ b/sentry-openfeature/src/main/java/io/sentry/openfeature/SentryOpenFeatureHook.java @@ -0,0 +1,62 @@ +package io.sentry.openfeature; + +import dev.openfeature.sdk.BooleanHook; +import dev.openfeature.sdk.FlagEvaluationDetails; +import dev.openfeature.sdk.FlagValueType; +import dev.openfeature.sdk.Hook; +import dev.openfeature.sdk.HookContext; +import io.sentry.IScopes; +import io.sentry.ScopesAdapter; +import io.sentry.SentryLevel; +import java.util.Map; +import java.util.Objects; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.VisibleForTesting; + +public final class SentryOpenFeatureHook implements BooleanHook { + private final IScopes scopes; + + public SentryOpenFeatureHook() { + this(ScopesAdapter.getInstance()); + } + + @VisibleForTesting + SentryOpenFeatureHook(@NotNull IScopes scopes) { + this.scopes = Objects.requireNonNull(scopes, "Scopes are required"); + } + + @Override + public void after(final @Nullable HookContext context, final @Nullable FlagEvaluationDetails details, final @Nullable Map hints) { + if (context == null || details == null) { + return; + } + try { + final @Nullable String flagKey = details.getFlagKey(); + final @Nullable FlagValueType type = context.getType(); + final @Nullable Object value = details.getValue(); + + if (flagKey == null || type == null || value == null) { + return; + } + + if (!FlagValueType.BOOLEAN.equals(type)) { + return; + } + + if (!(value instanceof Boolean)) { + return; + } + final @NotNull Boolean flagValue = (Boolean) value; + + scopes.addFeatureFlag(flagKey, flagValue); + } catch (Exception e) { + scopes.getOptions().getLogger().log( + SentryLevel.ERROR, + "Failed to capture feature flag evaluation", + e + ); + } + } +} + diff --git a/sentry-samples/sentry-samples-spring-boot-jakarta/build.gradle.kts b/sentry-samples/sentry-samples-spring-boot-jakarta/build.gradle.kts index cde2de9c0f4..570d35b727b 100644 --- a/sentry-samples/sentry-samples-spring-boot-jakarta/build.gradle.kts +++ b/sentry-samples/sentry-samples-spring-boot-jakarta/build.gradle.kts @@ -53,6 +53,10 @@ dependencies { implementation(projects.sentryGraphql22) implementation(projects.sentryQuartz) implementation(projects.sentryAsyncProfiler) + implementation(projects.sentryOpenfeature) + + // OpenFeature SDK + implementation(libs.openfeature) // database query tracing implementation(projects.sentryJdbc) diff --git a/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/FeatureFlagController.java b/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/FeatureFlagController.java new file mode 100644 index 00000000000..8640f2f12b7 --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/FeatureFlagController.java @@ -0,0 +1,66 @@ +package io.sentry.samples.spring.boot.jakarta; + +import dev.openfeature.sdk.Client; +import dev.openfeature.sdk.OpenFeatureAPI; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/feature-flag/") +public class FeatureFlagController { + private static final Logger LOGGER = LoggerFactory.getLogger(FeatureFlagController.class); + private final OpenFeatureAPI openFeatureAPI; + + public FeatureFlagController(OpenFeatureAPI openFeatureAPI) { + this.openFeatureAPI = openFeatureAPI; + } + + @GetMapping("check/{flagKey}") + public FeatureFlagResponse checkFlag(@PathVariable String flagKey) { + Client client = openFeatureAPI.getClient(); + + // Evaluate boolean feature flag + // This will trigger the SentryOpenFeatureHook which tracks the evaluation + boolean flagValue = client.getBooleanValue(flagKey, false); + + LOGGER.info("Feature flag '{}' evaluated to: {}", flagKey, flagValue); + + return new FeatureFlagResponse(flagKey, flagValue); + } + + @GetMapping("error/{flagKey}") + public String errorWithFeatureFlag(@PathVariable String flagKey) { + Client client = openFeatureAPI.getClient(); + + // Evaluate feature flag before throwing error + // The feature flag will be included in the Sentry event + boolean flagValue = client.getBooleanValue(flagKey, false); + + LOGGER.info("Feature flag '{}' evaluated to: {} before error", flagKey, flagValue); + + throw new RuntimeException("Error occurred with feature flag: " + flagKey + " = " + flagValue); + } + + public static class FeatureFlagResponse { + private final String flagKey; + private final boolean value; + + public FeatureFlagResponse(String flagKey, boolean value) { + this.flagKey = flagKey; + this.value = value; + } + + public String getFlagKey() { + return flagKey; + } + + public boolean isValue() { + return value; + } + } +} + diff --git a/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/OpenFeatureConfig.java b/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/OpenFeatureConfig.java new file mode 100644 index 00000000000..70a0464da6d --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/OpenFeatureConfig.java @@ -0,0 +1,227 @@ +package io.sentry.samples.spring.boot.jakarta; + +import dev.openfeature.sdk.EvaluationContext; +import dev.openfeature.sdk.FeatureProvider; +import dev.openfeature.sdk.Metadata; +import dev.openfeature.sdk.OpenFeatureAPI; +import dev.openfeature.sdk.ProviderEvaluation; +import dev.openfeature.sdk.Reason; +import dev.openfeature.sdk.Value; +import io.sentry.openfeature.SentryOpenFeatureHook; +import java.util.HashMap; +import java.util.Map; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class OpenFeatureConfig { + + @Bean + public OpenFeatureAPI openFeatureAPI() { + OpenFeatureAPI api = OpenFeatureAPI.getInstance(); + + // Use simple in-memory provider for demo purposes + // In production, you would use a real provider like LaunchDarkly, Flagsmith, etc. + FeatureProvider provider = new InMemoryProvider(createFeatureFlags()); + api.setProvider(provider); + + // Register Sentry hook to track feature flag evaluations + api.addHooks(new SentryOpenFeatureHook()); + + return api; + } + + private Map createFeatureFlags() { + Map flags = new HashMap<>(); + + // Boolean flags + flags.put("new-checkout-flow", true); + flags.put("experimental-feature", false); + flags.put("enable-caching", true); + flags.put("dark-mode", false); + flags.put("beta-features", true); + + // String flags + flags.put("theme-color", "blue"); + flags.put("api-version", "v2"); + flags.put("default-language", "en"); + flags.put("welcome-message", "Welcome to Sentry!"); + + // Integer flags + flags.put("max-retries", 3); + flags.put("page-size", 20); + flags.put("timeout-seconds", 30); + flags.put("max-connections", 100); + + // Double flags + flags.put("discount-percentage", 0.15); + flags.put("tax-rate", 0.08); + flags.put("conversion-rate", 0.025); + + // Object/Structure flags (stored as Map) + Map configMap = new HashMap<>(); + configMap.put("enabled", true); + configMap.put("threshold", 100); + configMap.put("mode", "production"); + flags.put("advanced-config", configMap); + + Map uiConfig = new HashMap<>(); + uiConfig.put("layout", "grid"); + uiConfig.put("itemsPerPage", 12); + flags.put("ui-settings", uiConfig); + + return flags; + } + + /** + * Simple in-memory provider for demo purposes. + * In production, use a real provider like LaunchDarkly, Flagsmith, etc. + */ + private static class InMemoryProvider implements FeatureProvider { + private final Map flags; + + public InMemoryProvider(Map flags) { + this.flags = flags; + } + + @Override + public Metadata getMetadata() { + return new Metadata() { + @Override + public String getName() { + return "InMemoryProvider"; + } + }; + } + + @Override + public void initialize(EvaluationContext evaluationContext) { + // No initialization needed for in-memory provider + } + + @Override + public void shutdown() { + // No cleanup needed for in-memory provider + } + + @Override + public ProviderEvaluation getBooleanEvaluation( + String key, Boolean defaultValue, EvaluationContext ctx) { + Object value = flags.get(key); + if (value == null) { + return ProviderEvaluation.builder() + .value(defaultValue) + .reason(Reason.DEFAULT.name()) + .build(); + } + if (value instanceof Boolean) { + return ProviderEvaluation.builder() + .value((Boolean) value) + .reason(Reason.STATIC.name()) + .build(); + } + // Type mismatch - return default + return ProviderEvaluation.builder() + .value(defaultValue) + .reason(Reason.DEFAULT.name()) + .build(); + } + + @Override + public ProviderEvaluation getStringEvaluation( + String key, String defaultValue, EvaluationContext ctx) { + Object value = flags.get(key); + if (value == null) { + return ProviderEvaluation.builder() + .value(defaultValue) + .reason(Reason.DEFAULT.name()) + .build(); + } + if (value instanceof String) { + return ProviderEvaluation.builder() + .value((String) value) + .reason(Reason.STATIC.name()) + .build(); + } + // Type mismatch - return default + return ProviderEvaluation.builder() + .value(defaultValue) + .reason(Reason.DEFAULT.name()) + .build(); + } + + @Override + public ProviderEvaluation getIntegerEvaluation( + String key, Integer defaultValue, EvaluationContext ctx) { + Object value = flags.get(key); + if (value == null) { + return ProviderEvaluation.builder() + .value(defaultValue) + .reason(Reason.DEFAULT.name()) + .build(); + } + if (value instanceof Integer) { + return ProviderEvaluation.builder() + .value((Integer) value) + .reason(Reason.STATIC.name()) + .build(); + } + // Type mismatch - return default + return ProviderEvaluation.builder() + .value(defaultValue) + .reason(Reason.DEFAULT.name()) + .build(); + } + + @Override + public ProviderEvaluation getDoubleEvaluation( + String key, Double defaultValue, EvaluationContext ctx) { + Object value = flags.get(key); + if (value == null) { + return ProviderEvaluation.builder() + .value(defaultValue) + .reason(Reason.DEFAULT.name()) + .build(); + } + if (value instanceof Double) { + return ProviderEvaluation.builder() + .value((Double) value) + .reason(Reason.STATIC.name()) + .build(); + } + // Type mismatch - return default + return ProviderEvaluation.builder() + .value(defaultValue) + .reason(Reason.DEFAULT.name()) + .build(); + } + + @Override + public ProviderEvaluation getObjectEvaluation( + String key, Value defaultValue, EvaluationContext ctx) { + Object value = flags.get(key); + if (value == null) { + return ProviderEvaluation.builder() + .value(defaultValue) + .reason(Reason.DEFAULT.name()) + .build(); + } + if (value instanceof Map) { + // Convert Map to Value object + @SuppressWarnings("unchecked") + Map mapValue = (Map) value; + Value valueObj = Value.objectToValue(mapValue); + return ProviderEvaluation.builder() + .value(valueObj) + .reason(Reason.STATIC.name()) + .build(); + } + // Type mismatch - return default + return ProviderEvaluation.builder() + .value(defaultValue) + .reason(Reason.DEFAULT.name()) + .build(); + } + } +} + diff --git a/sentry-samples/sentry-samples-spring-boot-jakarta/src/test/kotlin/io/sentry/systemtest/FeatureFlagSystemTest.kt b/sentry-samples/sentry-samples-spring-boot-jakarta/src/test/kotlin/io/sentry/systemtest/FeatureFlagSystemTest.kt new file mode 100644 index 00000000000..67a83f6b921 --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-jakarta/src/test/kotlin/io/sentry/systemtest/FeatureFlagSystemTest.kt @@ -0,0 +1,104 @@ +package io.sentry.systemtest + +import io.sentry.protocol.FeatureFlag +import io.sentry.systemtest.util.TestHelper +import io.sentry.systemtest.util.FeatureFlagResponse +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import org.junit.Before + +class FeatureFlagSystemTest { + lateinit var testHelper: TestHelper + + @Before + fun setup() { + testHelper = TestHelper("http://localhost:8080") + testHelper.reset() + } + + @Test + fun `check feature flag includes flag in transaction`() { + val restClient = testHelper.restClient + val response: FeatureFlagResponse? = restClient.checkFeatureFlag("new-checkout-flow") + + assertEquals(200, restClient.lastKnownStatusCode) + assertNotNull(response) + assertEquals("new-checkout-flow", response!!.flagKey) + assertEquals(true, response.value) + + // Verify feature flag is included in the transaction + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> + transaction.transaction == "GET /feature-flag/check/{flagKey}" && + testHelper.doesTransactionHave( + transaction, + op = "http.server", + featureFlag = FeatureFlag("flag.evaluation.new-checkout-flow", true), + ) + } + } + + @Test + fun `error with feature flag includes flag in error event and transaction`() { + val restClient = testHelper.restClient + restClient.errorWithFeatureFlag("beta-features") + assertEquals(500, restClient.lastKnownStatusCode) + + // Verify feature flag is included in the error event + testHelper.ensureErrorReceived { event -> + testHelper.doesEventHaveExceptionMessage( + event, + "Error occurred with feature flag: beta-features = true", + ) && + testHelper.doesEventHaveFlag(event, "beta-features", true) + } + + // Verify feature flag is also included in the transaction + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> + transaction.transaction == "GET /feature-flag/error/{flagKey}" && + testHelper.doesTransactionHave( + transaction, + op = "http.server", + featureFlag = FeatureFlag("flag.evaluation.beta-features", true), + ) + } + } + + @Test + fun `check non-existent feature flag returns default value`() { + val restClient = testHelper.restClient + val response: FeatureFlagResponse? = restClient.checkFeatureFlag("non-existent-flag") + + assertEquals(200, restClient.lastKnownStatusCode) + assertNotNull(response) + assertEquals("non-existent-flag", response!!.flagKey) + assertEquals(false, response.value) // Default value + + // Verify transaction is still created + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> + transaction.transaction == "GET /feature-flag/check/{flagKey}" + } + } + + @Test + fun `check feature flag with false value`() { + val restClient = testHelper.restClient + val response: FeatureFlagResponse? = restClient.checkFeatureFlag("experimental-feature") + + assertEquals(200, restClient.lastKnownStatusCode) + assertNotNull(response) + assertEquals("experimental-feature", response!!.flagKey) + assertEquals(false, response.value) + + // Verify feature flag with false value is included in transaction + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> + transaction.transaction == "GET /feature-flag/check/{flagKey}" && + testHelper.doesTransactionHave( + transaction, + op = "http.server", + featureFlag = FeatureFlag("flag.evaluation.experimental-feature", false), + ) + } + } +} + diff --git a/sentry-system-test-support/src/main/kotlin/io/sentry/systemtest/util/RestTestClient.kt b/sentry-system-test-support/src/main/kotlin/io/sentry/systemtest/util/RestTestClient.kt index 6a7fdbd4b59..e60434680c2 100644 --- a/sentry-system-test-support/src/main/kotlin/io/sentry/systemtest/util/RestTestClient.kt +++ b/sentry-system-test-support/src/main/kotlin/io/sentry/systemtest/util/RestTestClient.kt @@ -49,4 +49,22 @@ class RestTestClient(private val backendBaseUrl: String) : LoggingInsecureRestCl return callTyped(request, true) } + + fun checkFeatureFlag(flagKey: String): FeatureFlagResponse? { + val request = Request.Builder().url("$backendBaseUrl/feature-flag/check/$flagKey") + + return callTyped(request, true) + } + + fun errorWithFeatureFlag(flagKey: String): String? { + val request = Request.Builder().url("$backendBaseUrl/feature-flag/error/$flagKey") + + val response = call(request, true) + return response?.body?.string() + } } + +data class FeatureFlagResponse( + val flagKey: String, + val value: Boolean, +) diff --git a/settings.gradle.kts b/settings.gradle.kts index c19f1e1499f..3b2c7a3d5a5 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -65,6 +65,7 @@ include( "sentry-opentelemetry:sentry-opentelemetry-agentless-spring", "sentry-quartz", "sentry-okhttp", + "sentry-openfeature", "sentry-reactor", "sentry-async-profiler", "sentry-ktor-client", From 92753c7538cb854a61f68755ae5e76f011351d24 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Mon, 17 Nov 2025 12:33:09 +0100 Subject: [PATCH 02/15] use evaluation context --- .../spring/boot/jakarta/FeatureFlagController.java | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/FeatureFlagController.java b/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/FeatureFlagController.java index 8640f2f12b7..78941566c91 100644 --- a/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/FeatureFlagController.java +++ b/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/FeatureFlagController.java @@ -1,7 +1,11 @@ package io.sentry.samples.spring.boot.jakarta; import dev.openfeature.sdk.Client; +import dev.openfeature.sdk.EvaluationContext; +import dev.openfeature.sdk.ImmutableContext; import dev.openfeature.sdk.OpenFeatureAPI; +import dev.openfeature.sdk.Value; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.bind.annotation.GetMapping; @@ -9,6 +13,9 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import java.util.HashMap; +import java.util.Map; + @RestController @RequestMapping("/feature-flag/") public class FeatureFlagController { @@ -25,7 +32,7 @@ public FeatureFlagResponse checkFlag(@PathVariable String flagKey) { // Evaluate boolean feature flag // This will trigger the SentryOpenFeatureHook which tracks the evaluation - boolean flagValue = client.getBooleanValue(flagKey, false); + boolean flagValue = client.getBooleanValue(flagKey, false, new ImmutableContext("example-context-key")); LOGGER.info("Feature flag '{}' evaluated to: {}", flagKey, flagValue); @@ -38,7 +45,7 @@ public String errorWithFeatureFlag(@PathVariable String flagKey) { // Evaluate feature flag before throwing error // The feature flag will be included in the Sentry event - boolean flagValue = client.getBooleanValue(flagKey, false); + boolean flagValue = client.getBooleanValue(flagKey, false, new ImmutableContext("example-context-key")); LOGGER.info("Feature flag '{}' evaluated to: {} before error", flagKey, flagValue); From 1efe8bd5d26cbd04f4c81a15e01dc4c98d879c5e Mon Sep 17 00:00:00 2001 From: Sentry Github Bot Date: Mon, 17 Nov 2025 11:50:23 +0000 Subject: [PATCH 03/15] Format code --- sentry-openfeature/build.gradle.kts | 1 - .../openfeature/SentryOpenFeatureHook.java | 76 +++++++++---------- .../boot/jakarta/FeatureFlagController.java | 15 ++-- .../boot/jakarta/OpenFeatureConfig.java | 19 +++-- .../systemtest/FeatureFlagSystemTest.kt | 6 +- .../sentry/systemtest/util/RestTestClient.kt | 5 +- 6 files changed, 55 insertions(+), 67 deletions(-) diff --git a/sentry-openfeature/build.gradle.kts b/sentry-openfeature/build.gradle.kts index 8117f97db28..cbb2be5088b 100644 --- a/sentry-openfeature/build.gradle.kts +++ b/sentry-openfeature/build.gradle.kts @@ -89,4 +89,3 @@ tasks.jar { ) } } - diff --git a/sentry-openfeature/src/main/java/io/sentry/openfeature/SentryOpenFeatureHook.java b/sentry-openfeature/src/main/java/io/sentry/openfeature/SentryOpenFeatureHook.java index 3539b2a8376..bb7cb7ed245 100644 --- a/sentry-openfeature/src/main/java/io/sentry/openfeature/SentryOpenFeatureHook.java +++ b/sentry-openfeature/src/main/java/io/sentry/openfeature/SentryOpenFeatureHook.java @@ -3,7 +3,6 @@ import dev.openfeature.sdk.BooleanHook; import dev.openfeature.sdk.FlagEvaluationDetails; import dev.openfeature.sdk.FlagValueType; -import dev.openfeature.sdk.Hook; import dev.openfeature.sdk.HookContext; import io.sentry.IScopes; import io.sentry.ScopesAdapter; @@ -15,48 +14,49 @@ import org.jetbrains.annotations.VisibleForTesting; public final class SentryOpenFeatureHook implements BooleanHook { - private final IScopes scopes; + private final IScopes scopes; - public SentryOpenFeatureHook() { - this(ScopesAdapter.getInstance()); - } + public SentryOpenFeatureHook() { + this(ScopesAdapter.getInstance()); + } - @VisibleForTesting - SentryOpenFeatureHook(@NotNull IScopes scopes) { - this.scopes = Objects.requireNonNull(scopes, "Scopes are required"); - } + @VisibleForTesting + SentryOpenFeatureHook(@NotNull IScopes scopes) { + this.scopes = Objects.requireNonNull(scopes, "Scopes are required"); + } @Override - public void after(final @Nullable HookContext context, final @Nullable FlagEvaluationDetails details, final @Nullable Map hints) { - if (context == null || details == null) { + public void after( + final @Nullable HookContext context, + final @Nullable FlagEvaluationDetails details, + final @Nullable Map hints) { + if (context == null || details == null) { + return; + } + try { + final @Nullable String flagKey = details.getFlagKey(); + final @Nullable FlagValueType type = context.getType(); + final @Nullable Object value = details.getValue(); + + if (flagKey == null || type == null || value == null) { return; } - try { - final @Nullable String flagKey = details.getFlagKey(); - final @Nullable FlagValueType type = context.getType(); - final @Nullable Object value = details.getValue(); - - if (flagKey == null || type == null || value == null) { - return; - } - - if (!FlagValueType.BOOLEAN.equals(type)) { - return; - } - - if (!(value instanceof Boolean)) { - return; - } - final @NotNull Boolean flagValue = (Boolean) value; - - scopes.addFeatureFlag(flagKey, flagValue); - } catch (Exception e) { - scopes.getOptions().getLogger().log( - SentryLevel.ERROR, - "Failed to capture feature flag evaluation", - e - ); - } + + if (!FlagValueType.BOOLEAN.equals(type)) { + return; + } + + if (!(value instanceof Boolean)) { + return; + } + final @NotNull Boolean flagValue = (Boolean) value; + + scopes.addFeatureFlag(flagKey, flagValue); + } catch (Exception e) { + scopes + .getOptions() + .getLogger() + .log(SentryLevel.ERROR, "Failed to capture feature flag evaluation", e); } + } } - diff --git a/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/FeatureFlagController.java b/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/FeatureFlagController.java index 78941566c91..2393c06158b 100644 --- a/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/FeatureFlagController.java +++ b/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/FeatureFlagController.java @@ -1,11 +1,8 @@ package io.sentry.samples.spring.boot.jakarta; import dev.openfeature.sdk.Client; -import dev.openfeature.sdk.EvaluationContext; import dev.openfeature.sdk.ImmutableContext; import dev.openfeature.sdk.OpenFeatureAPI; -import dev.openfeature.sdk.Value; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.bind.annotation.GetMapping; @@ -13,12 +10,9 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import java.util.HashMap; -import java.util.Map; - @RestController @RequestMapping("/feature-flag/") -public class FeatureFlagController { +public class FeatureFlagController { private static final Logger LOGGER = LoggerFactory.getLogger(FeatureFlagController.class); private final OpenFeatureAPI openFeatureAPI; @@ -32,7 +26,8 @@ public FeatureFlagResponse checkFlag(@PathVariable String flagKey) { // Evaluate boolean feature flag // This will trigger the SentryOpenFeatureHook which tracks the evaluation - boolean flagValue = client.getBooleanValue(flagKey, false, new ImmutableContext("example-context-key")); + boolean flagValue = + client.getBooleanValue(flagKey, false, new ImmutableContext("example-context-key")); LOGGER.info("Feature flag '{}' evaluated to: {}", flagKey, flagValue); @@ -45,7 +40,8 @@ public String errorWithFeatureFlag(@PathVariable String flagKey) { // Evaluate feature flag before throwing error // The feature flag will be included in the Sentry event - boolean flagValue = client.getBooleanValue(flagKey, false, new ImmutableContext("example-context-key")); + boolean flagValue = + client.getBooleanValue(flagKey, false, new ImmutableContext("example-context-key")); LOGGER.info("Feature flag '{}' evaluated to: {} before error", flagKey, flagValue); @@ -70,4 +66,3 @@ public boolean isValue() { } } } - diff --git a/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/OpenFeatureConfig.java b/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/OpenFeatureConfig.java index 70a0464da6d..2ada5e18b52 100644 --- a/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/OpenFeatureConfig.java +++ b/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/OpenFeatureConfig.java @@ -33,49 +33,49 @@ public OpenFeatureAPI openFeatureAPI() { private Map createFeatureFlags() { Map flags = new HashMap<>(); - + // Boolean flags flags.put("new-checkout-flow", true); flags.put("experimental-feature", false); flags.put("enable-caching", true); flags.put("dark-mode", false); flags.put("beta-features", true); - + // String flags flags.put("theme-color", "blue"); flags.put("api-version", "v2"); flags.put("default-language", "en"); flags.put("welcome-message", "Welcome to Sentry!"); - + // Integer flags flags.put("max-retries", 3); flags.put("page-size", 20); flags.put("timeout-seconds", 30); flags.put("max-connections", 100); - + // Double flags flags.put("discount-percentage", 0.15); flags.put("tax-rate", 0.08); flags.put("conversion-rate", 0.025); - + // Object/Structure flags (stored as Map) Map configMap = new HashMap<>(); configMap.put("enabled", true); configMap.put("threshold", 100); configMap.put("mode", "production"); flags.put("advanced-config", configMap); - + Map uiConfig = new HashMap<>(); uiConfig.put("layout", "grid"); uiConfig.put("itemsPerPage", 12); flags.put("ui-settings", uiConfig); - + return flags; } /** - * Simple in-memory provider for demo purposes. - * In production, use a real provider like LaunchDarkly, Flagsmith, etc. + * Simple in-memory provider for demo purposes. In production, use a real provider like + * LaunchDarkly, Flagsmith, etc. */ private static class InMemoryProvider implements FeatureProvider { private final Map flags; @@ -224,4 +224,3 @@ public ProviderEvaluation getObjectEvaluation( } } } - diff --git a/sentry-samples/sentry-samples-spring-boot-jakarta/src/test/kotlin/io/sentry/systemtest/FeatureFlagSystemTest.kt b/sentry-samples/sentry-samples-spring-boot-jakarta/src/test/kotlin/io/sentry/systemtest/FeatureFlagSystemTest.kt index 67a83f6b921..10233e579e5 100644 --- a/sentry-samples/sentry-samples-spring-boot-jakarta/src/test/kotlin/io/sentry/systemtest/FeatureFlagSystemTest.kt +++ b/sentry-samples/sentry-samples-spring-boot-jakarta/src/test/kotlin/io/sentry/systemtest/FeatureFlagSystemTest.kt @@ -1,8 +1,8 @@ package io.sentry.systemtest import io.sentry.protocol.FeatureFlag -import io.sentry.systemtest.util.TestHelper import io.sentry.systemtest.util.FeatureFlagResponse +import io.sentry.systemtest.util.TestHelper import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertNotNull @@ -49,8 +49,7 @@ class FeatureFlagSystemTest { testHelper.doesEventHaveExceptionMessage( event, "Error occurred with feature flag: beta-features = true", - ) && - testHelper.doesEventHaveFlag(event, "beta-features", true) + ) && testHelper.doesEventHaveFlag(event, "beta-features", true) } // Verify feature flag is also included in the transaction @@ -101,4 +100,3 @@ class FeatureFlagSystemTest { } } } - diff --git a/sentry-system-test-support/src/main/kotlin/io/sentry/systemtest/util/RestTestClient.kt b/sentry-system-test-support/src/main/kotlin/io/sentry/systemtest/util/RestTestClient.kt index e60434680c2..cb097d4483f 100644 --- a/sentry-system-test-support/src/main/kotlin/io/sentry/systemtest/util/RestTestClient.kt +++ b/sentry-system-test-support/src/main/kotlin/io/sentry/systemtest/util/RestTestClient.kt @@ -64,7 +64,4 @@ class RestTestClient(private val backendBaseUrl: String) : LoggingInsecureRestCl } } -data class FeatureFlagResponse( - val flagKey: String, - val value: Boolean, -) +data class FeatureFlagResponse(val flagKey: String, val value: Boolean) From 9b9b02569e29c7262c710a8ea4053010c0caa7fa Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Mon, 17 Nov 2025 15:18:17 +0100 Subject: [PATCH 04/15] add missing mentions --- .craft.yml | 1 + .github/ISSUE_TEMPLATE/bug_report_java.yml | 1 + README.md | 93 +++++++++++----------- 3 files changed, 49 insertions(+), 46 deletions(-) diff --git a/.craft.yml b/.craft.yml index 41874418817..cd403ade4c2 100644 --- a/.craft.yml +++ b/.craft.yml @@ -41,6 +41,7 @@ targets: maven:io.sentry:sentry-android-fragment: maven:io.sentry:sentry-bom: maven:io.sentry:sentry-openfeign: + #maven:io.sentry:sentry-openfeature: maven:io.sentry:sentry-opentelemetry-agent: maven:io.sentry:sentry-opentelemetry-agentcustomization: maven:io.sentry:sentry-opentelemetry-agentless: diff --git a/.github/ISSUE_TEMPLATE/bug_report_java.yml b/.github/ISSUE_TEMPLATE/bug_report_java.yml index a7ca3cbb770..9059373b8f3 100644 --- a/.github/ISSUE_TEMPLATE/bug_report_java.yml +++ b/.github/ISSUE_TEMPLATE/bug_report_java.yml @@ -35,6 +35,7 @@ body: - sentry-graphql-22 - sentry-quartz - sentry-openfeign + - sentry-openfeature - sentry-apache-http-client-5 - sentry-okhttp - sentry-reactor diff --git a/README.md b/README.md index 3838a90b54a..8bd1d8fadd0 100644 --- a/README.md +++ b/README.md @@ -17,53 +17,54 @@ Sentry SDK for Java and Android [![X Follow](https://img.shields.io/twitter/follow/sentry?label=sentry&style=social)](https://x.com/intent/follow?screen_name=sentry) [![Discord Chat](https://img.shields.io/discord/621778831602221064?logo=discord&logoColor=ffffff&color=7389D8)](https://discord.gg/PXa5Apfe7K) -| Packages | Maven Central | Minimum Android API Version | -|-----------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| ------- | -| sentry-android | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-android?style=for-the-badge&logo=sentry&color=green) | 21 | -| sentry-android-core | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-android-core?style=for-the-badge&logo=sentry&color=green) | 21 | -| sentry-android-distribution | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-android-distribution?style=for-the-badge&logo=sentry&color=green) | 21 | -| sentry-android-ndk | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-android-ndk?style=for-the-badge&logo=sentry&color=green) | 21 | -| sentry-android-timber | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-android-timber?style=for-the-badge&logo=sentry&color=green) | 21 | -| sentry-android-fragment | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-android-fragment?style=for-the-badge&logo=sentry&color=green) | 21 | -| sentry-android-navigation | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-android-navigation?style=for-the-badge&logo=sentry&color=green) | 21 | -| sentry-android-sqlite | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-android-sqlite?style=for-the-badge&logo=sentry&color=green) | 21 | -| sentry-android-replay | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-android-replay?style=for-the-badge&logo=sentry&color=green) | 26 | -| sentry-compose-android | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-compose-android?style=for-the-badge&logo=sentry&color=green) | 21 | -| sentry-compose-desktop | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-compose-desktop?style=for-the-badge&logo=sentry&color=green) | -| sentry-compose | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-compose?style=for-the-badge&logo=sentry&color=green) | -| sentry-apache-http-client-5 | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-apache-http-client-5?style=for-the-badge&logo=sentry&color=green) | -| sentry | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry?style=for-the-badge&logo=sentry&color=green) | 21 | -| sentry-jul | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-jul?style=for-the-badge&logo=sentry&color=green) | -| sentry-jdbc | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-jdbc?style=for-the-badge&logo=sentry&color=green) | -| sentry-apollo | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-apollo?style=for-the-badge&logo=sentry&color=green) | 21 | -| sentry-apollo-3 | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-apollo-3?style=for-the-badge&logo=sentry&color=green) | 21 | -| sentry-apollo-4 | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-apollo-4?style=for-the-badge&logo=sentry&color=green) | 21 | -| sentry-kotlin-extensions | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-kotlin-extensions?style=for-the-badge&logo=sentry&color=green) | 21 | -| sentry-ktor-client | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-ktor-client?style=for-the-badge&logo=sentry&color=green) | 21 | -| sentry-servlet | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-servlet?style=for-the-badge&logo=sentry&color=green) | | -| sentry-servlet-jakarta | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-servlet-jakarta?style=for-the-badge&logo=sentry&color=green) | | -| sentry-spring-boot | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-spring-boot?style=for-the-badge&logo=sentry&color=green) | -| sentry-spring-boot-jakarta | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-spring-boot-jakarta?style=for-the-badge&logo=sentry&color=green) | -| sentry-spring-boot-4 | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-spring-boot-4?style=for-the-badge&logo=sentry&color=green) | -| sentry-spring-boot-4-starter | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-spring-boot-4-starter?style=for-the-badge&logo=sentry&color=green) | -| sentry-spring-boot-starter | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-spring-boot-starter?style=for-the-badge&logo=sentry&color=green) | -| sentry-spring-boot-starter-jakarta | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-spring-boot-starter-jakarta?style=for-the-badge&logo=sentry&color=green) | -| sentry-spring | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-spring?style=for-the-badge&logo=sentry&color=green) | -| sentry-spring-jakarta | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-spring-jakarta?style=for-the-badge&logo=sentry&color=green) | -| sentry-spring-7 | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-spring-7?style=for-the-badge&logo=sentry&color=green) | -| sentry-logback | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-logback?style=for-the-badge&logo=sentry&color=green) | -| sentry-log4j2 | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-log4j2?style=for-the-badge&logo=sentry&color=green) | -| sentry-bom | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-bom?style=for-the-badge&logo=sentry&color=green) | -| sentry-graphql | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-graphql?style=for-the-badge&logo=sentry&color=green) | -| sentry-graphql-core | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-graphql-core?style=for-the-badge&logo=sentry&color=green) | -| sentry-graphql-22 | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-graphql-22?style=for-the-badge&logo=sentry&color=green) | -| sentry-quartz | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-quartz?style=for-the-badge&logo=sentry&color=green) | -| sentry-openfeign | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-openfeign?style=for-the-badge&logo=sentry&color=green) | -| sentry-opentelemetry-agent | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-opentelemetry-agent?style=for-the-badge&logo=sentry&color=green) | +| Packages | Maven Central | Minimum Android API Version | +|-----------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------| ------- | +| sentry-android | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-android?style=for-the-badge&logo=sentry&color=green) | 21 | +| sentry-android-core | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-android-core?style=for-the-badge&logo=sentry&color=green) | 21 | +| sentry-android-distribution | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-android-distribution?style=for-the-badge&logo=sentry&color=green) | 21 | +| sentry-android-ndk | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-android-ndk?style=for-the-badge&logo=sentry&color=green) | 21 | +| sentry-android-timber | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-android-timber?style=for-the-badge&logo=sentry&color=green) | 21 | +| sentry-android-fragment | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-android-fragment?style=for-the-badge&logo=sentry&color=green) | 21 | +| sentry-android-navigation | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-android-navigation?style=for-the-badge&logo=sentry&color=green) | 21 | +| sentry-android-sqlite | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-android-sqlite?style=for-the-badge&logo=sentry&color=green) | 21 | +| sentry-android-replay | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-android-replay?style=for-the-badge&logo=sentry&color=green) | 26 | +| sentry-compose-android | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-compose-android?style=for-the-badge&logo=sentry&color=green) | 21 | +| sentry-compose-desktop | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-compose-desktop?style=for-the-badge&logo=sentry&color=green) | +| sentry-compose | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-compose?style=for-the-badge&logo=sentry&color=green) | +| sentry-apache-http-client-5 | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-apache-http-client-5?style=for-the-badge&logo=sentry&color=green) | +| sentry | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry?style=for-the-badge&logo=sentry&color=green) | 21 | +| sentry-jul | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-jul?style=for-the-badge&logo=sentry&color=green) | +| sentry-jdbc | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-jdbc?style=for-the-badge&logo=sentry&color=green) | +| sentry-apollo | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-apollo?style=for-the-badge&logo=sentry&color=green) | 21 | +| sentry-apollo-3 | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-apollo-3?style=for-the-badge&logo=sentry&color=green) | 21 | +| sentry-apollo-4 | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-apollo-4?style=for-the-badge&logo=sentry&color=green) | 21 | +| sentry-kotlin-extensions | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-kotlin-extensions?style=for-the-badge&logo=sentry&color=green) | 21 | +| sentry-ktor-client | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-ktor-client?style=for-the-badge&logo=sentry&color=green) | 21 | +| sentry-servlet | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-servlet?style=for-the-badge&logo=sentry&color=green) | | +| sentry-servlet-jakarta | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-servlet-jakarta?style=for-the-badge&logo=sentry&color=green) | | +| sentry-spring-boot | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-spring-boot?style=for-the-badge&logo=sentry&color=green) | +| sentry-spring-boot-jakarta | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-spring-boot-jakarta?style=for-the-badge&logo=sentry&color=green) | +| sentry-spring-boot-4 | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-spring-boot-4?style=for-the-badge&logo=sentry&color=green) | +| sentry-spring-boot-4-starter | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-spring-boot-4-starter?style=for-the-badge&logo=sentry&color=green) | +| sentry-spring-boot-starter | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-spring-boot-starter?style=for-the-badge&logo=sentry&color=green) | +| sentry-spring-boot-starter-jakarta | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-spring-boot-starter-jakarta?style=for-the-badge&logo=sentry&color=green) | +| sentry-spring | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-spring?style=for-the-badge&logo=sentry&color=green) | +| sentry-spring-jakarta | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-spring-jakarta?style=for-the-badge&logo=sentry&color=green) | +| sentry-spring-7 | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-spring-7?style=for-the-badge&logo=sentry&color=green) | +| sentry-logback | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-logback?style=for-the-badge&logo=sentry&color=green) | +| sentry-log4j2 | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-log4j2?style=for-the-badge&logo=sentry&color=green) | +| sentry-bom | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-bom?style=for-the-badge&logo=sentry&color=green) | +| sentry-graphql | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-graphql?style=for-the-badge&logo=sentry&color=green) | +| sentry-graphql-core | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-graphql-core?style=for-the-badge&logo=sentry&color=green) | +| sentry-graphql-22 | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-graphql-22?style=for-the-badge&logo=sentry&color=green) | +| sentry-quartz | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-quartz?style=for-the-badge&logo=sentry&color=green) | +| sentry-openfeign | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-openfeign?style=for-the-badge&logo=sentry&color=green) | +| sentry-openfeature | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-openfeature?style=for-the-badge&logo=sentry&color=green) | +| sentry-opentelemetry-agent | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-opentelemetry-agent?style=for-the-badge&logo=sentry&color=green) | | sentry-opentelemetry-agentcustomization | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-opentelemetry-agentcustomization?style=for-the-badge&logo=sentry&color=green) | -| sentry-opentelemetry-core | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-opentelemetry-core?style=for-the-badge&logo=sentry&color=green) | -| sentry-okhttp | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-okhttp?style=for-the-badge&logo=sentry&color=green) | -| sentry-reactor | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-reactor?style=for-the-badge&logo=sentry&color=green) | +| sentry-opentelemetry-core | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-opentelemetry-core?style=for-the-badge&logo=sentry&color=green) | +| sentry-okhttp | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-okhttp?style=for-the-badge&logo=sentry&color=green) | +| sentry-reactor | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-reactor?style=for-the-badge&logo=sentry&color=green) | # Releases From 58f42196618d559659d024203fbba2383bcd693f Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Mon, 17 Nov 2025 15:19:55 +0100 Subject: [PATCH 05/15] api; register as integration and package --- sentry-openfeature/api/sentry-openfeature.api | 10 ++++++++++ .../sentry/openfeature/SentryOpenFeatureHook.java | 13 +++++++++++++ .../api/sentry-system-test-support.api | 15 +++++++++++++++ 3 files changed, 38 insertions(+) create mode 100644 sentry-openfeature/api/sentry-openfeature.api diff --git a/sentry-openfeature/api/sentry-openfeature.api b/sentry-openfeature/api/sentry-openfeature.api new file mode 100644 index 00000000000..5fe3402b593 --- /dev/null +++ b/sentry-openfeature/api/sentry-openfeature.api @@ -0,0 +1,10 @@ +public final class io/sentry/openfeature/BuildConfig { + public static final field SENTRY_OPENFEATURE_SDK_NAME Ljava/lang/String; + public static final field VERSION_NAME Ljava/lang/String; +} + +public final class io/sentry/openfeature/SentryOpenFeatureHook : dev/openfeature/sdk/BooleanHook { + public fun ()V + public fun after (Ldev/openfeature/sdk/HookContext;Ldev/openfeature/sdk/FlagEvaluationDetails;Ljava/util/Map;)V +} + diff --git a/sentry-openfeature/src/main/java/io/sentry/openfeature/SentryOpenFeatureHook.java b/sentry-openfeature/src/main/java/io/sentry/openfeature/SentryOpenFeatureHook.java index bb7cb7ed245..683417c33c1 100644 --- a/sentry-openfeature/src/main/java/io/sentry/openfeature/SentryOpenFeatureHook.java +++ b/sentry-openfeature/src/main/java/io/sentry/openfeature/SentryOpenFeatureHook.java @@ -1,11 +1,14 @@ package io.sentry.openfeature; +import static io.sentry.util.IntegrationUtils.addIntegrationToSdkVersion; + import dev.openfeature.sdk.BooleanHook; import dev.openfeature.sdk.FlagEvaluationDetails; import dev.openfeature.sdk.FlagValueType; import dev.openfeature.sdk.HookContext; import io.sentry.IScopes; import io.sentry.ScopesAdapter; +import io.sentry.SentryIntegrationPackageStorage; import io.sentry.SentryLevel; import java.util.Map; import java.util.Objects; @@ -16,8 +19,18 @@ public final class SentryOpenFeatureHook implements BooleanHook { private final IScopes scopes; + static { + SentryIntegrationPackageStorage.getInstance() + .addPackage("maven:io.sentry:sentry-openfeature", BuildConfig.VERSION_NAME); + } + public SentryOpenFeatureHook() { this(ScopesAdapter.getInstance()); + addPackageAndIntegrationInfo(); + } + + private void addPackageAndIntegrationInfo() { + addIntegrationToSdkVersion("OpenFeature"); } @VisibleForTesting diff --git a/sentry-system-test-support/api/sentry-system-test-support.api b/sentry-system-test-support/api/sentry-system-test-support.api index d8fa7efe13f..1f5d1382a93 100644 --- a/sentry-system-test-support/api/sentry-system-test-support.api +++ b/sentry-system-test-support/api/sentry-system-test-support.api @@ -516,6 +516,19 @@ public final class io/sentry/systemtest/util/EnvelopesReceived { public fun toString ()Ljava/lang/String; } +public final class io/sentry/systemtest/util/FeatureFlagResponse { + public fun (Ljava/lang/String;Z)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Z + public final fun copy (Ljava/lang/String;Z)Lio/sentry/systemtest/util/FeatureFlagResponse; + public static synthetic fun copy$default (Lio/sentry/systemtest/util/FeatureFlagResponse;Ljava/lang/String;ZILjava/lang/Object;)Lio/sentry/systemtest/util/FeatureFlagResponse; + public fun equals (Ljava/lang/Object;)Z + public final fun getFlagKey ()Ljava/lang/String; + public final fun getValue ()Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + public class io/sentry/systemtest/util/LoggingInsecureRestClient { public fun ()V protected final fun call (Lokhttp3/Request$Builder;ZLjava/util/Map;)Lokhttp3/Response; @@ -530,10 +543,12 @@ public class io/sentry/systemtest/util/LoggingInsecureRestClient { public final class io/sentry/systemtest/util/RestTestClient : io/sentry/systemtest/util/LoggingInsecureRestClient { public fun (Ljava/lang/String;)V + public final fun checkFeatureFlag (Ljava/lang/String;)Lio/sentry/systemtest/util/FeatureFlagResponse; public final fun createPerson (Lio/sentry/systemtest/Person;Ljava/util/Map;)Lio/sentry/systemtest/Person; public static synthetic fun createPerson$default (Lio/sentry/systemtest/util/RestTestClient;Lio/sentry/systemtest/Person;Ljava/util/Map;ILjava/lang/Object;)Lio/sentry/systemtest/Person; public final fun createPersonDistributedTracing (Lio/sentry/systemtest/Person;Ljava/util/Map;)Lio/sentry/systemtest/Person; public static synthetic fun createPersonDistributedTracing$default (Lio/sentry/systemtest/util/RestTestClient;Lio/sentry/systemtest/Person;Ljava/util/Map;ILjava/lang/Object;)Lio/sentry/systemtest/Person; + public final fun errorWithFeatureFlag (Ljava/lang/String;)Ljava/lang/String; public final fun getPerson (J)Lio/sentry/systemtest/Person; public final fun getPersonDistributedTracing (JLjava/util/Map;)Lio/sentry/systemtest/Person; public static synthetic fun getPersonDistributedTracing$default (Lio/sentry/systemtest/util/RestTestClient;JLjava/util/Map;ILjava/lang/Object;)Lio/sentry/systemtest/Person; From 509c6757d3ab3742c0c337c2b21bcf518181f9c8 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Mon, 17 Nov 2025 15:26:43 +0100 Subject: [PATCH 06/15] changelog --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4551f8ca2ef..311577c1290 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## Unreleased + +### Features + +- Implement OpenFeature Integration that tracks Feature Flag evaluations ([#4910](https://github.com/getsentry/sentry-java/pull/4910)) + - To make use of it, add the `sentry-openfeature` dependency and register the the hook using: `openFeatureApiInstance.addHooks(new SentryOpenFeatureHook());` + ## 8.26.0 ### Features From 1d8442a61c3bf9e617bb75c23204c56d1ff73f50 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Wed, 19 Nov 2025 11:01:53 +0100 Subject: [PATCH 07/15] replace hard coded dependency --- sentry-openfeature/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry-openfeature/build.gradle.kts b/sentry-openfeature/build.gradle.kts index cbb2be5088b..632d16b55cf 100644 --- a/sentry-openfeature/build.gradle.kts +++ b/sentry-openfeature/build.gradle.kts @@ -35,7 +35,7 @@ dependencies { testImplementation(libs.kotlin.test.junit) testImplementation(libs.mockito.kotlin) testImplementation(libs.mockito.inline) - testImplementation("dev.openfeature:sdk:1.15.1") + testImplementation(libs.openfeature) } configure { test { java.srcDir("src/test/java") } } From c6720ef5e21b205cae499eea64045f0109573042 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Wed, 19 Nov 2025 15:51:15 +0100 Subject: [PATCH 08/15] Add launchdarkly integrations --- buildSrc/src/main/java/Config.kt | 2 + gradle/libs.versions.toml | 2 + .../api/sentry-launchdarkly-android.api | 15 +++ sentry-launchdarkly-android/build.gradle.kts | 90 ++++++++++++++++++ .../proguard-rules.pro | 8 ++ .../android/SentryLaunchDarklyAndroidHook.kt | 55 +++++++++++ .../api/sentry-launchdarkly-server.api | 10 ++ sentry-launchdarkly-server/build.gradle.kts | 95 +++++++++++++++++++ .../server/SentryLaunchDarklyServerHook.java | 73 ++++++++++++++ settings.gradle.kts | 2 + 10 files changed, 352 insertions(+) create mode 100644 sentry-launchdarkly-android/api/sentry-launchdarkly-android.api create mode 100644 sentry-launchdarkly-android/build.gradle.kts create mode 100644 sentry-launchdarkly-android/proguard-rules.pro create mode 100644 sentry-launchdarkly-android/src/main/java/io/sentry/launchdarkly/android/SentryLaunchDarklyAndroidHook.kt create mode 100644 sentry-launchdarkly-server/api/sentry-launchdarkly-server.api create mode 100644 sentry-launchdarkly-server/build.gradle.kts create mode 100644 sentry-launchdarkly-server/src/main/java/io/sentry/launchdarkly/server/SentryLaunchDarklyServerHook.java diff --git a/buildSrc/src/main/java/Config.kt b/buildSrc/src/main/java/Config.kt index 887bf1415b2..3b6a08ad26b 100644 --- a/buildSrc/src/main/java/Config.kt +++ b/buildSrc/src/main/java/Config.kt @@ -78,6 +78,8 @@ object Config { val SENTRY_QUARTZ_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.quartz" val SENTRY_JDBC_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.jdbc" val SENTRY_OPENFEATURE_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.openfeature" + val SENTRY_LAUNCHDARKLY_SERVER_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.launchdarkly-server" + val SENTRY_LAUNCHDARKLY_ANDROID_SDK_NAME = "$SENTRY_ANDROID_SDK_NAME.launchdarkly" val SENTRY_SERVLET_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.servlet" val SENTRY_SERVLET_JAKARTA_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.servlet.jakarta" val SENTRY_COMPOSE_HELPER_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.compose.helper" diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 3432bad6eec..16dd955b84b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -117,6 +117,8 @@ kotlinx-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" } ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktorClient" } ktor-client-java = { module = "io.ktor:ktor-client-java", version.ref = "ktorClient" } +launchdarkly-android = { module = "com.launchdarkly:launchdarkly-android-client-sdk", version = "5.9.2" } +launchdarkly-server = { module = "com.launchdarkly:launchdarkly-java-server-sdk", version = "7.10.2" } log4j-api = { module = "org.apache.logging.log4j:log4j-api", version.ref = "log4j2" } log4j-core = { module = "org.apache.logging.log4j:log4j-core", version.ref = "log4j2" } leakcanary = { module = "com.squareup.leakcanary:leakcanary-android", version = "2.14" } diff --git a/sentry-launchdarkly-android/api/sentry-launchdarkly-android.api b/sentry-launchdarkly-android/api/sentry-launchdarkly-android.api new file mode 100644 index 00000000000..4c0f5f262a1 --- /dev/null +++ b/sentry-launchdarkly-android/api/sentry-launchdarkly-android.api @@ -0,0 +1,15 @@ +public final class io/sentry/launchdarkly/android/BuildConfig { + public static final field BUILD_TYPE Ljava/lang/String; + public static final field DEBUG Z + public static final field LIBRARY_PACKAGE_NAME Ljava/lang/String; + public static final field SENTRY_LAUNCHDARKLY_ANDROID_SDK_NAME Ljava/lang/String; + public static final field VERSION_NAME Ljava/lang/String; + public fun ()V +} + +public final class io/sentry/launchdarkly/android/SentryLaunchDarklyAndroidHook : com/launchdarkly/sdk/android/integrations/Hook { + public fun ()V + public fun (Lio/sentry/IScopes;)V + public fun afterEvaluation (Lcom/launchdarkly/sdk/android/integrations/EvaluationSeriesContext;Ljava/util/Map;Lcom/launchdarkly/sdk/EvaluationDetail;)Ljava/util/Map; +} + diff --git a/sentry-launchdarkly-android/build.gradle.kts b/sentry-launchdarkly-android/build.gradle.kts new file mode 100644 index 00000000000..68c97cbb0cb --- /dev/null +++ b/sentry-launchdarkly-android/build.gradle.kts @@ -0,0 +1,90 @@ +import io.gitlab.arturbosch.detekt.Detekt + +plugins { + id("com.android.library") + alias(libs.plugins.kotlin.android) + jacoco + alias(libs.plugins.jacoco.android) + alias(libs.plugins.gradle.versions) + alias(libs.plugins.detekt) +} + +android { + compileSdk = libs.versions.compileSdk.get().toInt() + namespace = "io.sentry.launchdarkly.android" + + defaultConfig { + minSdk = libs.versions.minSdk.get().toInt() + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + + // for AGP 4.1 + buildConfigField("String", "VERSION_NAME", "\"${project.version}\"") + buildConfigField( + "String", + "SENTRY_LAUNCHDARKLY_ANDROID_SDK_NAME", + "\"${Config.Sentry.SENTRY_LAUNCHDARKLY_ANDROID_SDK_NAME}\"", + ) + } + + buildTypes { + getByName("debug") { consumerProguardFiles("proguard-rules.pro") } + getByName("release") { consumerProguardFiles("proguard-rules.pro") } + } + + kotlin { + compilerOptions.jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_1_8 + compilerOptions.languageVersion = org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_9 + compilerOptions.apiVersion = org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_9 + } + + testOptions { + animationsDisabled = true + unitTests.apply { + isReturnDefaultValues = true + isIncludeAndroidResources = true + } + } + + lint { + warningsAsErrors = true + checkDependencies = true + + // We run a full lint analysis as build part in CI, so skip vital checks for assemble tasks. + checkReleaseBuilds = false + } + + buildFeatures { buildConfig = true } + + androidComponents.beforeVariants { + it.enable = !Config.Android.shouldSkipDebugVariant(it.buildType) + } +} + +kotlin { explicitApi() } + +dependencies { + api(projects.sentry) + + // LaunchDarkly Android Client SDK + // Note: This is for Android client-side applications + compileOnly(libs.launchdarkly.android) + + implementation(kotlin(Config.kotlinStdLib, Config.kotlinStdLibVersionAndroid)) + + // tests + testImplementation(projects.sentry) + testImplementation(projects.sentryTestSupport) + testImplementation(kotlin(Config.kotlinStdLib)) + testImplementation(libs.kotlin.test.junit) + testImplementation(libs.androidx.test.ext.junit) + testImplementation(libs.mockito.kotlin) + testImplementation(libs.mockito.inline) + // LaunchDarkly Android Client SDK for tests + testImplementation(libs.launchdarkly.android) +} + +tasks.withType().configureEach { + // Target version of the generated JVM bytecode. It is used for type resolution. + jvmTarget = JavaVersion.VERSION_1_8.toString() +} diff --git a/sentry-launchdarkly-android/proguard-rules.pro b/sentry-launchdarkly-android/proguard-rules.pro new file mode 100644 index 00000000000..1015cf61a02 --- /dev/null +++ b/sentry-launchdarkly-android/proguard-rules.pro @@ -0,0 +1,8 @@ +##---------------Begin: proguard configuration for LaunchDarkly Android ---------- + +# To ensure that stack traces is unambiguous +# https://developer.android.com/studio/build/shrink-code#decode-stack-trace +-keepattributes LineNumberTable,SourceFile + +##---------------End: proguard configuration for LaunchDarkly Android ---------- + diff --git a/sentry-launchdarkly-android/src/main/java/io/sentry/launchdarkly/android/SentryLaunchDarklyAndroidHook.kt b/sentry-launchdarkly-android/src/main/java/io/sentry/launchdarkly/android/SentryLaunchDarklyAndroidHook.kt new file mode 100644 index 00000000000..f6cccb413c4 --- /dev/null +++ b/sentry-launchdarkly-android/src/main/java/io/sentry/launchdarkly/android/SentryLaunchDarklyAndroidHook.kt @@ -0,0 +1,55 @@ +package io.sentry.launchdarkly.android + +import com.launchdarkly.sdk.EvaluationDetail +import com.launchdarkly.sdk.LDValue +import com.launchdarkly.sdk.LDValueType +import com.launchdarkly.sdk.android.integrations.EvaluationSeriesContext +import com.launchdarkly.sdk.android.integrations.Hook +import io.sentry.IScopes +import io.sentry.ScopesAdapter +import io.sentry.SentryIntegrationPackageStorage +import io.sentry.SentryLevel +import io.sentry.util.IntegrationUtils.addIntegrationToSdkVersion + +public class SentryLaunchDarklyAndroidHook : Hook { + private val scopes: IScopes + + private companion object { + init { + SentryIntegrationPackageStorage.getInstance() + .addPackage("maven:io.sentry:sentry-launchdarkly-android", BuildConfig.VERSION_NAME) + } + } + + public constructor() : this(ScopesAdapter.getInstance()) + + public constructor(scopes: IScopes) : super("SentryLaunchDarklyAndroidHook") { + this.scopes = scopes + addPackageAndIntegrationInfo() + } + + private fun addPackageAndIntegrationInfo() { + addIntegrationToSdkVersion("LaunchDarkly-Android") + } + + @Suppress("TooGenericExceptionCaught") + override fun afterEvaluation( + seriesContext: EvaluationSeriesContext?, + seriesData: Map?, + evaluationDetail: EvaluationDetail?, + ): Map? { + try { + val flagKey: String? = seriesContext?.flagKey + val value: LDValue? = evaluationDetail?.value + + if (flagKey != null && value != null && LDValueType.BOOLEAN == value.type) { + val flagValue: Boolean = value.booleanValue() + scopes.addFeatureFlag(flagKey, flagValue) + } + } catch (e: Throwable) { + scopes.options.logger.log(SentryLevel.ERROR, "Failed to capture feature flag evaluation", e) + } + + return seriesData + } +} diff --git a/sentry-launchdarkly-server/api/sentry-launchdarkly-server.api b/sentry-launchdarkly-server/api/sentry-launchdarkly-server.api new file mode 100644 index 00000000000..8a42a12d9e7 --- /dev/null +++ b/sentry-launchdarkly-server/api/sentry-launchdarkly-server.api @@ -0,0 +1,10 @@ +public final class io/sentry/launchdarkly/server/BuildConfig { + public static final field SENTRY_LAUNCHDARKLY_SERVER_SDK_NAME Ljava/lang/String; + public static final field VERSION_NAME Ljava/lang/String; +} + +public final class io/sentry/launchdarkly/server/SentryLaunchDarklyServerHook : com/launchdarkly/sdk/server/integrations/Hook { + public fun ()V + public fun afterEvaluation (Lcom/launchdarkly/sdk/server/integrations/EvaluationSeriesContext;Ljava/util/Map;Lcom/launchdarkly/sdk/EvaluationDetail;)Ljava/util/Map; +} + diff --git a/sentry-launchdarkly-server/build.gradle.kts b/sentry-launchdarkly-server/build.gradle.kts new file mode 100644 index 00000000000..71bd47218bb --- /dev/null +++ b/sentry-launchdarkly-server/build.gradle.kts @@ -0,0 +1,95 @@ +import net.ltgt.gradle.errorprone.errorprone +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + `java-library` + id("io.sentry.javadoc") + alias(libs.plugins.kotlin.jvm) + jacoco + alias(libs.plugins.errorprone) + alias(libs.plugins.gradle.versions) + alias(libs.plugins.buildconfig) +} + +tasks.withType().configureEach { + compilerOptions.jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_1_8 + compilerOptions.languageVersion = org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_9 + compilerOptions.apiVersion = org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_9 +} + +dependencies { + api(projects.sentry) + + // LaunchDarkly Java Server SDK (for JVM/server-side applications) + // Note: For Android applications, use sentry-launchdarkly-android module with + // com.launchdarkly:launchdarkly-android-client-sdk instead + compileOnly(libs.launchdarkly.server) + + compileOnly(libs.jetbrains.annotations) + compileOnly(libs.nopen.annotations) + errorprone(libs.errorprone.core) + errorprone(libs.nopen.checker) + errorprone(libs.nullaway) + + // tests + testImplementation(projects.sentry) + testImplementation(projects.sentryTestSupport) + testImplementation(kotlin(Config.kotlinStdLib)) + testImplementation(libs.kotlin.test.junit) + testImplementation(libs.mockito.kotlin) + testImplementation(libs.mockito.inline) + // LaunchDarkly Java Server SDK for tests + testImplementation(libs.launchdarkly.server) +} + +configure { test { java.srcDir("src/test/java") } } + +jacoco { toolVersion = libs.versions.jacoco.get() } + +tasks.jacocoTestReport { + reports { + xml.required.set(true) + html.required.set(false) + } +} + +tasks { + jacocoTestCoverageVerification { + violationRules { rule { limit { minimum = Config.QualityPlugins.Jacoco.minimumCoverage } } } + } + check { + dependsOn(jacocoTestCoverageVerification) + dependsOn(jacocoTestReport) + } +} + +tasks.withType().configureEach { + options.errorprone { + check("NullAway", net.ltgt.gradle.errorprone.CheckSeverity.ERROR) + option("NullAway:AnnotatedPackages", "io.sentry") + } +} + +buildConfig { + useJavaOutput() + packageName("io.sentry.launchdarkly.server") + buildConfigField( + "String", + "SENTRY_LAUNCHDARKLY_SERVER_SDK_NAME", + "\"${Config.Sentry.SENTRY_LAUNCHDARKLY_SERVER_SDK_NAME}\"", + ) + buildConfigField("String", "VERSION_NAME", "\"${project.version}\"") +} + +tasks.jar { + manifest { + attributes( + "Sentry-Version-Name" to project.version, + "Sentry-SDK-Name" to Config.Sentry.SENTRY_LAUNCHDARKLY_SERVER_SDK_NAME, + "Sentry-SDK-Package-Name" to "maven:io.sentry:sentry-launchdarkly-server", + "Implementation-Vendor" to "Sentry", + "Implementation-Title" to project.name, + "Implementation-Version" to project.version, + ) + } +} diff --git a/sentry-launchdarkly-server/src/main/java/io/sentry/launchdarkly/server/SentryLaunchDarklyServerHook.java b/sentry-launchdarkly-server/src/main/java/io/sentry/launchdarkly/server/SentryLaunchDarklyServerHook.java new file mode 100644 index 00000000000..daa4940fc76 --- /dev/null +++ b/sentry-launchdarkly-server/src/main/java/io/sentry/launchdarkly/server/SentryLaunchDarklyServerHook.java @@ -0,0 +1,73 @@ +package io.sentry.launchdarkly.server; + +import static io.sentry.util.IntegrationUtils.addIntegrationToSdkVersion; + +import com.launchdarkly.sdk.EvaluationDetail; +import com.launchdarkly.sdk.LDValue; +import com.launchdarkly.sdk.LDValueType; +import com.launchdarkly.sdk.server.integrations.EvaluationSeriesContext; +import com.launchdarkly.sdk.server.integrations.Hook; +import io.sentry.IScopes; +import io.sentry.ScopesAdapter; +import io.sentry.SentryIntegrationPackageStorage; +import io.sentry.SentryLevel; +import java.util.Map; +import java.util.Objects; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.VisibleForTesting; + +public final class SentryLaunchDarklyServerHook extends Hook { + private final IScopes scopes; + + static { + SentryIntegrationPackageStorage.getInstance() + .addPackage("maven:io.sentry:sentry-launchdarkly-server", BuildConfig.VERSION_NAME); + } + + public SentryLaunchDarklyServerHook() { + this(ScopesAdapter.getInstance()); + } + + @VisibleForTesting + SentryLaunchDarklyServerHook(@NotNull IScopes scopes) { + super("SentryLaunchDarklyServerHook"); + this.scopes = Objects.requireNonNull(scopes, "Scopes are required"); + addPackageAndIntegrationInfo(); + } + + private void addPackageAndIntegrationInfo() { + addIntegrationToSdkVersion("LaunchDarkly-Server"); + } + + @Override + public Map afterEvaluation( + EvaluationSeriesContext seriesContext, + Map seriesData, + EvaluationDetail evaluationDetail) { + if (evaluationDetail == null || seriesContext == null) { + return seriesData; + } + + try { + final @Nullable String flagKey = seriesContext.flagKey; + final @Nullable LDValue value = evaluationDetail.getValue(); + + if (flagKey == null || value == null) { + return seriesData; + } + + if (LDValueType.BOOLEAN.equals(value.getType())) { + final boolean flagValue = value.booleanValue(); + scopes.addFeatureFlag(flagKey, flagValue); + } + } catch (Exception e) { + scopes + .getOptions() + .getLogger() + .log(SentryLevel.ERROR, "Failed to capture feature flag evaluation", e); + } + + return seriesData; + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 3b2c7a3d5a5..e9297876f24 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -66,6 +66,8 @@ include( "sentry-quartz", "sentry-okhttp", "sentry-openfeature", + "sentry-launchdarkly-server", + "sentry-launchdarkly-android", "sentry-reactor", "sentry-async-profiler", "sentry-ktor-client", From da4f5262fbf9d97435f8fe59f668dd0b2e3efd47 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Wed, 19 Nov 2025 16:02:06 +0100 Subject: [PATCH 09/15] changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7af3c34a3a9..ecacdb1adb9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ - Implement OpenFeature Integration that tracks Feature Flag evaluations ([#4910](https://github.com/getsentry/sentry-java/pull/4910)) - To make use of it, add the `sentry-openfeature` dependency and register the the hook using: `openFeatureApiInstance.addHooks(new SentryOpenFeatureHook());` +- Implement LaunchDarkly Integrations that track Feature Flag evaluations ([#4917](https://github.com/getsentry/sentry-java/pull/4917)) + - For Android, please add `sentry-launchdarkly-android` as a dependency and register the `SentryLaunchDarklyAndroidHook` + - For Server / JVM, please add `sentry-launchdarkly-server` as a dependency and register the `SentryLaunchDarklyServerHook` - Detect oversized events and reduce their size ([#4903](https://github.com/getsentry/sentry-java/pull/4903)) - You can opt into this new behaviour by setting `enableEventSizeLimiting` to `true` (`sentry.enable-event-size-limiting=true` for Spring Boot `application.properties`) - You may optionally register an `onOversizedEvent` callback to implement custom logic that is executed in case an oversized event is detected From 6189f26ef25d9b284d026cd6560e7c95403ae8fc Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Wed, 19 Nov 2025 16:05:56 +0100 Subject: [PATCH 10/15] issue templates and readme --- .github/ISSUE_TEMPLATE/bug_report_android.yml | 1 + .github/ISSUE_TEMPLATE/bug_report_java.yml | 1 + README.md | 2 ++ 3 files changed, 4 insertions(+) diff --git a/.github/ISSUE_TEMPLATE/bug_report_android.yml b/.github/ISSUE_TEMPLATE/bug_report_android.yml index f76c38dbe75..e83e485450d 100644 --- a/.github/ISSUE_TEMPLATE/bug_report_android.yml +++ b/.github/ISSUE_TEMPLATE/bug_report_android.yml @@ -16,6 +16,7 @@ body: - sentry-apollo - sentry-apollo-3 - sentry-compose + - sentry-launchdarkly-android - sentry-okhttp - other validations: diff --git a/.github/ISSUE_TEMPLATE/bug_report_java.yml b/.github/ISSUE_TEMPLATE/bug_report_java.yml index 9059373b8f3..3f2df40888b 100644 --- a/.github/ISSUE_TEMPLATE/bug_report_java.yml +++ b/.github/ISSUE_TEMPLATE/bug_report_java.yml @@ -36,6 +36,7 @@ body: - sentry-quartz - sentry-openfeign - sentry-openfeature + - sentry-launchdarkly-server - sentry-apache-http-client-5 - sentry-okhttp - sentry-reactor diff --git a/README.md b/README.md index 8bd1d8fadd0..3a92a2692d2 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,8 @@ Sentry SDK for Java and Android | sentry-quartz | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-quartz?style=for-the-badge&logo=sentry&color=green) | | sentry-openfeign | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-openfeign?style=for-the-badge&logo=sentry&color=green) | | sentry-openfeature | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-openfeature?style=for-the-badge&logo=sentry&color=green) | +| sentry-launchdarkly-android | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-launchdarkly-android?style=for-the-badge&logo=sentry&color=green) | +| sentry-launchdarkly-server | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-launchdarkly-server?style=for-the-badge&logo=sentry&color=green) | | sentry-opentelemetry-agent | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-opentelemetry-agent?style=for-the-badge&logo=sentry&color=green) | | sentry-opentelemetry-agentcustomization | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-opentelemetry-agentcustomization?style=for-the-badge&logo=sentry&color=green) | | sentry-opentelemetry-core | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-opentelemetry-core?style=for-the-badge&logo=sentry&color=green) | From 290a79daf5fc1117202cc0db2b2504c571e64ef8 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Wed, 19 Nov 2025 16:07:28 +0100 Subject: [PATCH 11/15] remove comments from build gradle files --- sentry-launchdarkly-android/build.gradle.kts | 3 --- sentry-launchdarkly-server/build.gradle.kts | 4 ---- 2 files changed, 7 deletions(-) diff --git a/sentry-launchdarkly-android/build.gradle.kts b/sentry-launchdarkly-android/build.gradle.kts index 68c97cbb0cb..793c24e1fa5 100644 --- a/sentry-launchdarkly-android/build.gradle.kts +++ b/sentry-launchdarkly-android/build.gradle.kts @@ -66,8 +66,6 @@ kotlin { explicitApi() } dependencies { api(projects.sentry) - // LaunchDarkly Android Client SDK - // Note: This is for Android client-side applications compileOnly(libs.launchdarkly.android) implementation(kotlin(Config.kotlinStdLib, Config.kotlinStdLibVersionAndroid)) @@ -80,7 +78,6 @@ dependencies { testImplementation(libs.androidx.test.ext.junit) testImplementation(libs.mockito.kotlin) testImplementation(libs.mockito.inline) - // LaunchDarkly Android Client SDK for tests testImplementation(libs.launchdarkly.android) } diff --git a/sentry-launchdarkly-server/build.gradle.kts b/sentry-launchdarkly-server/build.gradle.kts index 71bd47218bb..ee273fa5a9c 100644 --- a/sentry-launchdarkly-server/build.gradle.kts +++ b/sentry-launchdarkly-server/build.gradle.kts @@ -20,9 +20,6 @@ tasks.withType().configureEach { dependencies { api(projects.sentry) - // LaunchDarkly Java Server SDK (for JVM/server-side applications) - // Note: For Android applications, use sentry-launchdarkly-android module with - // com.launchdarkly:launchdarkly-android-client-sdk instead compileOnly(libs.launchdarkly.server) compileOnly(libs.jetbrains.annotations) @@ -38,7 +35,6 @@ dependencies { testImplementation(libs.kotlin.test.junit) testImplementation(libs.mockito.kotlin) testImplementation(libs.mockito.inline) - // LaunchDarkly Java Server SDK for tests testImplementation(libs.launchdarkly.server) } From 7c626d7c5c650d8ed7ecccb64409fd1afd9f25cf Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Thu, 20 Nov 2025 09:47:36 +0100 Subject: [PATCH 12/15] convert to java --- sentry-launchdarkly-android/build.gradle.kts | 3 +- .../proguard-rules.pro | 1 + .../SentryLaunchDarklyAndroidHook.java | 70 +++++++++++++++++++ .../android/SentryLaunchDarklyAndroidHook.kt | 55 --------------- 4 files changed, 72 insertions(+), 57 deletions(-) create mode 100644 sentry-launchdarkly-android/src/main/java/io/sentry/launchdarkly/android/SentryLaunchDarklyAndroidHook.java delete mode 100644 sentry-launchdarkly-android/src/main/java/io/sentry/launchdarkly/android/SentryLaunchDarklyAndroidHook.kt diff --git a/sentry-launchdarkly-android/build.gradle.kts b/sentry-launchdarkly-android/build.gradle.kts index 793c24e1fa5..cdf3ab0f17e 100644 --- a/sentry-launchdarkly-android/build.gradle.kts +++ b/sentry-launchdarkly-android/build.gradle.kts @@ -67,8 +67,7 @@ dependencies { api(projects.sentry) compileOnly(libs.launchdarkly.android) - - implementation(kotlin(Config.kotlinStdLib, Config.kotlinStdLibVersionAndroid)) + compileOnly(libs.jetbrains.annotations) // tests testImplementation(projects.sentry) diff --git a/sentry-launchdarkly-android/proguard-rules.pro b/sentry-launchdarkly-android/proguard-rules.pro index 1015cf61a02..fbb30d1d080 100644 --- a/sentry-launchdarkly-android/proguard-rules.pro +++ b/sentry-launchdarkly-android/proguard-rules.pro @@ -6,3 +6,4 @@ ##---------------End: proguard configuration for LaunchDarkly Android ---------- + diff --git a/sentry-launchdarkly-android/src/main/java/io/sentry/launchdarkly/android/SentryLaunchDarklyAndroidHook.java b/sentry-launchdarkly-android/src/main/java/io/sentry/launchdarkly/android/SentryLaunchDarklyAndroidHook.java new file mode 100644 index 00000000000..0d4f5192d13 --- /dev/null +++ b/sentry-launchdarkly-android/src/main/java/io/sentry/launchdarkly/android/SentryLaunchDarklyAndroidHook.java @@ -0,0 +1,70 @@ +package io.sentry.launchdarkly.android; + +import com.launchdarkly.sdk.EvaluationDetail; +import com.launchdarkly.sdk.LDValue; +import com.launchdarkly.sdk.LDValueType; +import com.launchdarkly.sdk.android.integrations.EvaluationSeriesContext; +import com.launchdarkly.sdk.android.integrations.Hook; +import io.sentry.IScopes; +import io.sentry.ScopesAdapter; +import io.sentry.SentryIntegrationPackageStorage; +import io.sentry.SentryLevel; +import io.sentry.util.IntegrationUtils; +import java.util.Map; +import java.util.Objects; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public final class SentryLaunchDarklyAndroidHook extends Hook { + private final IScopes scopes; + + static { + SentryIntegrationPackageStorage.getInstance() + .addPackage("maven:io.sentry:sentry-launchdarkly-android", BuildConfig.VERSION_NAME); + } + + public SentryLaunchDarklyAndroidHook() { + this(ScopesAdapter.getInstance()); + } + + public SentryLaunchDarklyAndroidHook(final @NotNull IScopes scopes) { + super("SentryLaunchDarklyAndroidHook"); + this.scopes = Objects.requireNonNull(scopes, "Scopes are required"); + addPackageAndIntegrationInfo(); + } + + private void addPackageAndIntegrationInfo() { + IntegrationUtils.addIntegrationToSdkVersion("LaunchDarkly-Android"); + } + + @Override + public Map afterEvaluation( + final EvaluationSeriesContext seriesContext, + final Map seriesData, + final EvaluationDetail evaluationDetail) { + if (evaluationDetail == null || seriesContext == null) { + return seriesData; + } + + try { + final @Nullable String flagKey = seriesContext.flagKey; + final @Nullable LDValue value = evaluationDetail.getValue(); + + if (flagKey == null || value == null) { + return seriesData; + } + + if (LDValueType.BOOLEAN.equals(value.getType())) { + final boolean flagValue = value.booleanValue(); + scopes.addFeatureFlag(flagKey, flagValue); + } + } catch (final Exception e) { + scopes + .getOptions() + .getLogger() + .log(SentryLevel.ERROR, "Failed to capture feature flag evaluation", e); + } + + return seriesData; + } +} diff --git a/sentry-launchdarkly-android/src/main/java/io/sentry/launchdarkly/android/SentryLaunchDarklyAndroidHook.kt b/sentry-launchdarkly-android/src/main/java/io/sentry/launchdarkly/android/SentryLaunchDarklyAndroidHook.kt deleted file mode 100644 index f6cccb413c4..00000000000 --- a/sentry-launchdarkly-android/src/main/java/io/sentry/launchdarkly/android/SentryLaunchDarklyAndroidHook.kt +++ /dev/null @@ -1,55 +0,0 @@ -package io.sentry.launchdarkly.android - -import com.launchdarkly.sdk.EvaluationDetail -import com.launchdarkly.sdk.LDValue -import com.launchdarkly.sdk.LDValueType -import com.launchdarkly.sdk.android.integrations.EvaluationSeriesContext -import com.launchdarkly.sdk.android.integrations.Hook -import io.sentry.IScopes -import io.sentry.ScopesAdapter -import io.sentry.SentryIntegrationPackageStorage -import io.sentry.SentryLevel -import io.sentry.util.IntegrationUtils.addIntegrationToSdkVersion - -public class SentryLaunchDarklyAndroidHook : Hook { - private val scopes: IScopes - - private companion object { - init { - SentryIntegrationPackageStorage.getInstance() - .addPackage("maven:io.sentry:sentry-launchdarkly-android", BuildConfig.VERSION_NAME) - } - } - - public constructor() : this(ScopesAdapter.getInstance()) - - public constructor(scopes: IScopes) : super("SentryLaunchDarklyAndroidHook") { - this.scopes = scopes - addPackageAndIntegrationInfo() - } - - private fun addPackageAndIntegrationInfo() { - addIntegrationToSdkVersion("LaunchDarkly-Android") - } - - @Suppress("TooGenericExceptionCaught") - override fun afterEvaluation( - seriesContext: EvaluationSeriesContext?, - seriesData: Map?, - evaluationDetail: EvaluationDetail?, - ): Map? { - try { - val flagKey: String? = seriesContext?.flagKey - val value: LDValue? = evaluationDetail?.value - - if (flagKey != null && value != null && LDValueType.BOOLEAN == value.type) { - val flagValue: Boolean = value.booleanValue() - scopes.addFeatureFlag(flagKey, flagValue) - } - } catch (e: Throwable) { - scopes.options.logger.log(SentryLevel.ERROR, "Failed to capture feature flag evaluation", e) - } - - return seriesData - } -} From 6f4d0887e5a2d5ff274231797c818f84198ae95c Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Thu, 20 Nov 2025 10:00:29 +0100 Subject: [PATCH 13/15] remove kotlin from build.gradle --- sentry-launchdarkly-android/build.gradle.kts | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/sentry-launchdarkly-android/build.gradle.kts b/sentry-launchdarkly-android/build.gradle.kts index cdf3ab0f17e..5a5a0673a8d 100644 --- a/sentry-launchdarkly-android/build.gradle.kts +++ b/sentry-launchdarkly-android/build.gradle.kts @@ -1,12 +1,8 @@ -import io.gitlab.arturbosch.detekt.Detekt - plugins { id("com.android.library") - alias(libs.plugins.kotlin.android) jacoco alias(libs.plugins.jacoco.android) alias(libs.plugins.gradle.versions) - alias(libs.plugins.detekt) } android { @@ -32,12 +28,6 @@ android { getByName("release") { consumerProguardFiles("proguard-rules.pro") } } - kotlin { - compilerOptions.jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_1_8 - compilerOptions.languageVersion = org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_9 - compilerOptions.apiVersion = org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_9 - } - testOptions { animationsDisabled = true unitTests.apply { @@ -61,8 +51,6 @@ android { } } -kotlin { explicitApi() } - dependencies { api(projects.sentry) @@ -72,15 +60,7 @@ dependencies { // tests testImplementation(projects.sentry) testImplementation(projects.sentryTestSupport) - testImplementation(kotlin(Config.kotlinStdLib)) - testImplementation(libs.kotlin.test.junit) testImplementation(libs.androidx.test.ext.junit) - testImplementation(libs.mockito.kotlin) testImplementation(libs.mockito.inline) testImplementation(libs.launchdarkly.android) } - -tasks.withType().configureEach { - // Target version of the generated JVM bytecode. It is used for type resolution. - jvmTarget = JavaVersion.VERSION_1_8.toString() -} From 6f2d7b0f909a1ce7fe1a87f5fbae731f582e5622 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Thu, 20 Nov 2025 10:57:22 +0100 Subject: [PATCH 14/15] add tests --- sentry-launchdarkly-android/build.gradle.kts | 8 + .../SentryLaunchDarklyAndroidHookTest.kt | 233 +++++++++++++++++ .../SentryLaunchDarklyServerHookTest.kt | 238 ++++++++++++++++++ 3 files changed, 479 insertions(+) create mode 100644 sentry-launchdarkly-android/src/test/java/io/sentry/launchdarkly/android/SentryLaunchDarklyAndroidHookTest.kt create mode 100644 sentry-launchdarkly-server/src/test/java/io/sentry/launchdarkly/server/SentryLaunchDarklyServerHookTest.kt diff --git a/sentry-launchdarkly-android/build.gradle.kts b/sentry-launchdarkly-android/build.gradle.kts index 5a5a0673a8d..af5e6b27220 100644 --- a/sentry-launchdarkly-android/build.gradle.kts +++ b/sentry-launchdarkly-android/build.gradle.kts @@ -1,5 +1,6 @@ plugins { id("com.android.library") + alias(libs.plugins.kotlin.android) jacoco alias(libs.plugins.jacoco.android) alias(libs.plugins.gradle.versions) @@ -28,6 +29,10 @@ android { getByName("release") { consumerProguardFiles("proguard-rules.pro") } } + kotlin { + compilerOptions.jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_1_8 + } + testOptions { animationsDisabled = true unitTests.apply { @@ -60,7 +65,10 @@ dependencies { // tests testImplementation(projects.sentry) testImplementation(projects.sentryTestSupport) + testImplementation(kotlin(Config.kotlinStdLib, Config.kotlinStdLibVersionAndroid)) testImplementation(libs.androidx.test.ext.junit) + testImplementation(libs.kotlin.test.junit) + testImplementation(libs.mockito.kotlin) testImplementation(libs.mockito.inline) testImplementation(libs.launchdarkly.android) } diff --git a/sentry-launchdarkly-android/src/test/java/io/sentry/launchdarkly/android/SentryLaunchDarklyAndroidHookTest.kt b/sentry-launchdarkly-android/src/test/java/io/sentry/launchdarkly/android/SentryLaunchDarklyAndroidHookTest.kt new file mode 100644 index 00000000000..558ae2a0bb4 --- /dev/null +++ b/sentry-launchdarkly-android/src/test/java/io/sentry/launchdarkly/android/SentryLaunchDarklyAndroidHookTest.kt @@ -0,0 +1,233 @@ +package io.sentry.launchdarkly.android + +import com.launchdarkly.sdk.EvaluationDetail +import com.launchdarkly.sdk.LDValue +import com.launchdarkly.sdk.LDValueType +import com.launchdarkly.sdk.android.integrations.EvaluationSeriesContext +import io.sentry.ILogger +import io.sentry.IScopes +import io.sentry.SentryLevel +import io.sentry.SentryOptions +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals +import org.mockito.kotlin.any +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever + +class SentryLaunchDarklyAndroidHookTest { + + private lateinit var mockScopes: IScopes + private lateinit var mockOptions: SentryOptions + private lateinit var mockLogger: ILogger + private lateinit var hook: SentryLaunchDarklyAndroidHook + + @BeforeTest + fun setUp() { + mockScopes = mock() + mockOptions = mock() + mockLogger = mock() + whenever(mockScopes.options).thenReturn(mockOptions) + whenever(mockOptions.logger).thenReturn(mockLogger) + hook = SentryLaunchDarklyAndroidHook(mockScopes) + } + + @Test + fun `afterEvaluation with boolean value calls addFeatureFlag`() { + val flagKey = "test-flag" + val flagValue = true + + val seriesContext = createSeriesContext(flagKey) + + val ldValue = mock() + whenever(ldValue.getType()).thenReturn(LDValueType.BOOLEAN) + whenever(ldValue.booleanValue()).thenReturn(flagValue) + + val evaluationDetail = mock>() + whenever(evaluationDetail.getValue()).thenReturn(ldValue) + + val seriesData = mutableMapOf() + seriesData["existingKey"] = "existingValue" + + val result = hook.afterEvaluation(seriesContext, seriesData, evaluationDetail) + + verify(mockScopes).addFeatureFlag(eq(flagKey), eq(flagValue)) + assertEquals(seriesData, result) + assertEquals("existingValue", result["existingKey"]) + } + + @Test + fun `afterEvaluation with false boolean value calls addFeatureFlag`() { + val flagKey = "test-flag" + val flagValue = false + + val seriesContext = createSeriesContext(flagKey) + + val ldValue = mock() + whenever(ldValue.getType()).thenReturn(LDValueType.BOOLEAN) + whenever(ldValue.booleanValue()).thenReturn(flagValue) + + val evaluationDetail = mock>() + whenever(evaluationDetail.getValue()).thenReturn(ldValue) + + val seriesData = mutableMapOf() + seriesData["existingKey"] = "existingValue" + + val result = hook.afterEvaluation(seriesContext, seriesData, evaluationDetail) + + verify(mockScopes).addFeatureFlag(eq(flagKey), eq(flagValue)) + assertEquals(seriesData, result) + assertEquals("existingValue", result["existingKey"]) + } + + @Test + fun `afterEvaluation with non-boolean value does not call addFeatureFlag`() { + val flagKey = "test-flag" + + val seriesContext = createSeriesContext(flagKey) + + val ldValue = mock() + whenever(ldValue.getType()).thenReturn(LDValueType.STRING) + + val evaluationDetail = mock>() + whenever(evaluationDetail.getValue()).thenReturn(ldValue) + + val seriesData = mutableMapOf() + seriesData["existingKey"] = "existingValue" + + val result = hook.afterEvaluation(seriesContext, seriesData, evaluationDetail) + + verify(mockScopes, never()).addFeatureFlag(any(), any()) + assertEquals(seriesData, result) + assertEquals("existingValue", result["existingKey"]) + } + + @Test + fun `afterEvaluation with null seriesContext returns seriesData`() { + val evaluationDetail = mock>() + val seriesData = mutableMapOf() + seriesData["existingKey"] = "existingValue" + + val result = hook.afterEvaluation(null, seriesData, evaluationDetail) + + verify(mockScopes, never()).addFeatureFlag(any(), any()) + assertEquals(seriesData, result) + assertEquals("existingValue", result["existingKey"]) + } + + @Test + fun `afterEvaluation with null evaluationDetail returns seriesData`() { + val seriesContext = mock() + val seriesData = mutableMapOf() + seriesData["existingKey"] = "existingValue" + + val result = hook.afterEvaluation(seriesContext, seriesData, null) + + verify(mockScopes, never()).addFeatureFlag(any(), any()) + assertEquals(seriesData, result) + assertEquals("existingValue", result["existingKey"]) + } + + @Test + fun `afterEvaluation with null flagKey returns seriesData`() { + val seriesContext = createSeriesContext(null) + + val ldValue = mock() + whenever(ldValue.getType()).thenReturn(LDValueType.BOOLEAN) + + val evaluationDetail = mock>() + whenever(evaluationDetail.getValue()).thenReturn(ldValue) + + val seriesData = mutableMapOf() + seriesData["existingKey"] = "existingValue" + + val result = hook.afterEvaluation(seriesContext, seriesData, evaluationDetail) + + verify(mockScopes, never()).addFeatureFlag(any(), any()) + assertEquals(seriesData, result) + assertEquals("existingValue", result["existingKey"]) + } + + @Test + fun `afterEvaluation with null value returns seriesData`() { + val flagKey = "test-flag" + + val seriesContext = createSeriesContext(flagKey) + + val evaluationDetail = mock>() + whenever(evaluationDetail.getValue()).thenReturn(null) + + val seriesData = mutableMapOf() + seriesData["existingKey"] = "existingValue" + + val result = hook.afterEvaluation(seriesContext, seriesData, evaluationDetail) + + verify(mockScopes, never()).addFeatureFlag(any(), any()) + assertEquals(seriesData, result) + assertEquals("existingValue", result["existingKey"]) + } + + @Test + fun `afterEvaluation with exception logs error`() { + val flagKey = "test-flag" + + val seriesContext = createSeriesContext(flagKey) + + val ldValue = mock() + whenever(ldValue.getType()).thenThrow(RuntimeException("Test exception")) + + val evaluationDetail = mock>() + whenever(evaluationDetail.getValue()).thenReturn(ldValue) + + val seriesData = mutableMapOf() + seriesData["existingKey"] = "existingValue" + + val result = hook.afterEvaluation(seriesContext, seriesData, evaluationDetail) + + verify(mockLogger).log(eq(SentryLevel.ERROR), eq("Failed to capture feature flag evaluation"), any()) + verify(mockScopes, never()).addFeatureFlag(any(), any()) + assertEquals(seriesData, result) + assertEquals("existingValue", result["existingKey"]) + } + + @Test + fun `afterEvaluation returns original seriesData`() { + val flagKey = "test-flag" + val flagValue = true + + val seriesContext = createSeriesContext(flagKey) + + val ldValue = mock() + whenever(ldValue.getType()).thenReturn(LDValueType.BOOLEAN) + whenever(ldValue.booleanValue()).thenReturn(flagValue) + + val evaluationDetail = mock>() + whenever(evaluationDetail.getValue()).thenReturn(ldValue) + + val seriesData = mutableMapOf() + seriesData["key"] = "value" + + val result = hook.afterEvaluation(seriesContext, seriesData, evaluationDetail) + + verify(mockScopes).addFeatureFlag(eq(flagKey), eq(flagValue)) + assertEquals(seriesData, result) + assertEquals("value", result["key"]) + } + + private fun createSeriesContext(flagKey: String?): EvaluationSeriesContext { + val seriesContext = mock() + try { + val field = EvaluationSeriesContext::class.java.getField("flagKey") + field.isAccessible = true + field.set(seriesContext, flagKey) + } catch (e: Exception) { + throw RuntimeException("Failed to set flagKey field", e) + } + return seriesContext + } +} + diff --git a/sentry-launchdarkly-server/src/test/java/io/sentry/launchdarkly/server/SentryLaunchDarklyServerHookTest.kt b/sentry-launchdarkly-server/src/test/java/io/sentry/launchdarkly/server/SentryLaunchDarklyServerHookTest.kt new file mode 100644 index 00000000000..3e4144745dd --- /dev/null +++ b/sentry-launchdarkly-server/src/test/java/io/sentry/launchdarkly/server/SentryLaunchDarklyServerHookTest.kt @@ -0,0 +1,238 @@ +package io.sentry.launchdarkly.server + +import com.launchdarkly.sdk.EvaluationDetail +import com.launchdarkly.sdk.LDValue +import com.launchdarkly.sdk.LDValueType +import com.launchdarkly.sdk.server.integrations.EvaluationSeriesContext +import io.sentry.ILogger +import io.sentry.IScopes +import io.sentry.SentryLevel +import io.sentry.SentryOptions +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals +import org.mockito.kotlin.any +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever + +class SentryLaunchDarklyServerHookTest { + + private lateinit var mockScopes: IScopes + private lateinit var mockOptions: SentryOptions + private lateinit var mockLogger: ILogger + private lateinit var hook: SentryLaunchDarklyServerHook + + @BeforeTest + fun setUp() { + mockScopes = mock() + mockOptions = mock() + mockLogger = mock() + whenever(mockScopes.options).thenReturn(mockOptions) + whenever(mockOptions.logger).thenReturn(mockLogger) + hook = SentryLaunchDarklyServerHook(mockScopes) + } + + @AfterTest + fun tearDown() { + // Cleanup if needed + } + + private fun createSeriesContext(flagKey: String?): EvaluationSeriesContext { + val seriesContext = mock() + try { + val field = EvaluationSeriesContext::class.java.getField("flagKey") + field.isAccessible = true + field.set(seriesContext, flagKey) + } catch (e: Exception) { + throw RuntimeException("Failed to set flagKey field", e) + } + return seriesContext + } + + @Test + fun `afterEvaluation with boolean value calls addFeatureFlag`() { + val flagKey = "test-flag" + val flagValue = true + + val seriesContext = createSeriesContext(flagKey) + + val ldValue = mock() + whenever(ldValue.getType()).thenReturn(LDValueType.BOOLEAN) + whenever(ldValue.booleanValue()).thenReturn(flagValue) + + val evaluationDetail = mock>() + whenever(evaluationDetail.getValue()).thenReturn(ldValue) + + val seriesData = mutableMapOf() + seriesData["existingKey"] = "existingValue" + + val result = hook.afterEvaluation(seriesContext, seriesData, evaluationDetail) + + verify(mockScopes).addFeatureFlag(eq(flagKey), eq(flagValue)) + assertEquals(seriesData, result) + assertEquals("existingValue", result["existingKey"]) + } + + @Test + fun `afterEvaluation with false boolean value calls addFeatureFlag`() { + val flagKey = "test-flag" + val flagValue = false + + val seriesContext = createSeriesContext(flagKey) + + val ldValue = mock() + whenever(ldValue.getType()).thenReturn(LDValueType.BOOLEAN) + whenever(ldValue.booleanValue()).thenReturn(flagValue) + + val evaluationDetail = mock>() + whenever(evaluationDetail.getValue()).thenReturn(ldValue) + + val seriesData = mutableMapOf() + seriesData["existingKey"] = "existingValue" + + val result = hook.afterEvaluation(seriesContext, seriesData, evaluationDetail) + + verify(mockScopes).addFeatureFlag(eq(flagKey), eq(flagValue)) + assertEquals(seriesData, result) + assertEquals("existingValue", result["existingKey"]) + } + + @Test + fun `afterEvaluation with non-boolean value does not call addFeatureFlag`() { + val flagKey = "test-flag" + + val seriesContext = createSeriesContext(flagKey) + + val ldValue = mock() + whenever(ldValue.getType()).thenReturn(LDValueType.STRING) + + val evaluationDetail = mock>() + whenever(evaluationDetail.getValue()).thenReturn(ldValue) + + val seriesData = mutableMapOf() + seriesData["existingKey"] = "existingValue" + + val result = hook.afterEvaluation(seriesContext, seriesData, evaluationDetail) + + verify(mockScopes, never()).addFeatureFlag(any(), any()) + assertEquals(seriesData, result) + assertEquals("existingValue", result["existingKey"]) + } + + @Test + fun `afterEvaluation with null seriesContext returns seriesData`() { + val evaluationDetail = mock>() + val seriesData = mutableMapOf() + seriesData["existingKey"] = "existingValue" + + val result = hook.afterEvaluation(null, seriesData, evaluationDetail) + + verify(mockScopes, never()).addFeatureFlag(any(), any()) + assertEquals(seriesData, result) + assertEquals("existingValue", result["existingKey"]) + } + + @Test + fun `afterEvaluation with null evaluationDetail returns seriesData`() { + val seriesContext = mock() + val seriesData = mutableMapOf() + seriesData["existingKey"] = "existingValue" + + val result = hook.afterEvaluation(seriesContext, seriesData, null) + + verify(mockScopes, never()).addFeatureFlag(any(), any()) + assertEquals(seriesData, result) + assertEquals("existingValue", result["existingKey"]) + } + + @Test + fun `afterEvaluation with null flagKey returns seriesData`() { + val seriesContext = createSeriesContext(null) + + val ldValue = mock() + whenever(ldValue.getType()).thenReturn(LDValueType.BOOLEAN) + + val evaluationDetail = mock>() + whenever(evaluationDetail.getValue()).thenReturn(ldValue) + + val seriesData = mutableMapOf() + seriesData["existingKey"] = "existingValue" + + val result = hook.afterEvaluation(seriesContext, seriesData, evaluationDetail) + + verify(mockScopes, never()).addFeatureFlag(any(), any()) + assertEquals(seriesData, result) + assertEquals("existingValue", result["existingKey"]) + } + + @Test + fun `afterEvaluation with null value returns seriesData`() { + val flagKey = "test-flag" + + val seriesContext = createSeriesContext(flagKey) + + val evaluationDetail = mock>() + whenever(evaluationDetail.getValue()).thenReturn(null) + + val seriesData = mutableMapOf() + seriesData["existingKey"] = "existingValue" + + val result = hook.afterEvaluation(seriesContext, seriesData, evaluationDetail) + + verify(mockScopes, never()).addFeatureFlag(any(), any()) + assertEquals(seriesData, result) + assertEquals("existingValue", result["existingKey"]) + } + + @Test + fun `afterEvaluation with exception logs error`() { + val flagKey = "test-flag" + + val seriesContext = createSeriesContext(flagKey) + + val ldValue = mock() + whenever(ldValue.getType()).thenThrow(RuntimeException("Test exception")) + + val evaluationDetail = mock>() + whenever(evaluationDetail.getValue()).thenReturn(ldValue) + + val seriesData = mutableMapOf() + seriesData["existingKey"] = "existingValue" + + val result = hook.afterEvaluation(seriesContext, seriesData, evaluationDetail) + + verify(mockLogger).log(eq(SentryLevel.ERROR), eq("Failed to capture feature flag evaluation"), any()) + verify(mockScopes, never()).addFeatureFlag(any(), any()) + assertEquals(seriesData, result) + assertEquals("existingValue", result["existingKey"]) + } + + @Test + fun `afterEvaluation returns original seriesData`() { + val flagKey = "test-flag" + val flagValue = true + + val seriesContext = createSeriesContext(flagKey) + + val ldValue = mock() + whenever(ldValue.getType()).thenReturn(LDValueType.BOOLEAN) + whenever(ldValue.booleanValue()).thenReturn(flagValue) + + val evaluationDetail = mock>() + whenever(evaluationDetail.getValue()).thenReturn(ldValue) + + val seriesData = mutableMapOf() + seriesData["key"] = "value" + + val result = hook.afterEvaluation(seriesContext, seriesData, evaluationDetail) + + verify(mockScopes).addFeatureFlag(eq(flagKey), eq(flagValue)) + assertEquals(seriesData, result) + assertEquals("value", result["key"]) + } +} + From bb5a023af10c1573631c9a0c8f5459547e7c46be Mon Sep 17 00:00:00 2001 From: Sentry Github Bot Date: Thu, 20 Nov 2025 10:01:16 +0000 Subject: [PATCH 15/15] Format code --- sentry-launchdarkly-android/build.gradle.kts | 4 +--- .../android/SentryLaunchDarklyAndroidHookTest.kt | 5 ++--- .../launchdarkly/server/SentryLaunchDarklyServerHookTest.kt | 4 ++-- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/sentry-launchdarkly-android/build.gradle.kts b/sentry-launchdarkly-android/build.gradle.kts index af5e6b27220..bf59c256ed1 100644 --- a/sentry-launchdarkly-android/build.gradle.kts +++ b/sentry-launchdarkly-android/build.gradle.kts @@ -29,9 +29,7 @@ android { getByName("release") { consumerProguardFiles("proguard-rules.pro") } } - kotlin { - compilerOptions.jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_1_8 - } + kotlin { compilerOptions.jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_1_8 } testOptions { animationsDisabled = true diff --git a/sentry-launchdarkly-android/src/test/java/io/sentry/launchdarkly/android/SentryLaunchDarklyAndroidHookTest.kt b/sentry-launchdarkly-android/src/test/java/io/sentry/launchdarkly/android/SentryLaunchDarklyAndroidHookTest.kt index 558ae2a0bb4..3e7174af436 100644 --- a/sentry-launchdarkly-android/src/test/java/io/sentry/launchdarkly/android/SentryLaunchDarklyAndroidHookTest.kt +++ b/sentry-launchdarkly-android/src/test/java/io/sentry/launchdarkly/android/SentryLaunchDarklyAndroidHookTest.kt @@ -8,7 +8,6 @@ import io.sentry.ILogger import io.sentry.IScopes import io.sentry.SentryLevel import io.sentry.SentryOptions -import kotlin.test.AfterTest import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertEquals @@ -188,7 +187,8 @@ class SentryLaunchDarklyAndroidHookTest { val result = hook.afterEvaluation(seriesContext, seriesData, evaluationDetail) - verify(mockLogger).log(eq(SentryLevel.ERROR), eq("Failed to capture feature flag evaluation"), any()) + verify(mockLogger) + .log(eq(SentryLevel.ERROR), eq("Failed to capture feature flag evaluation"), any()) verify(mockScopes, never()).addFeatureFlag(any(), any()) assertEquals(seriesData, result) assertEquals("existingValue", result["existingKey"]) @@ -230,4 +230,3 @@ class SentryLaunchDarklyAndroidHookTest { return seriesContext } } - diff --git a/sentry-launchdarkly-server/src/test/java/io/sentry/launchdarkly/server/SentryLaunchDarklyServerHookTest.kt b/sentry-launchdarkly-server/src/test/java/io/sentry/launchdarkly/server/SentryLaunchDarklyServerHookTest.kt index 3e4144745dd..b82f75c75f0 100644 --- a/sentry-launchdarkly-server/src/test/java/io/sentry/launchdarkly/server/SentryLaunchDarklyServerHookTest.kt +++ b/sentry-launchdarkly-server/src/test/java/io/sentry/launchdarkly/server/SentryLaunchDarklyServerHookTest.kt @@ -205,7 +205,8 @@ class SentryLaunchDarklyServerHookTest { val result = hook.afterEvaluation(seriesContext, seriesData, evaluationDetail) - verify(mockLogger).log(eq(SentryLevel.ERROR), eq("Failed to capture feature flag evaluation"), any()) + verify(mockLogger) + .log(eq(SentryLevel.ERROR), eq("Failed to capture feature flag evaluation"), any()) verify(mockScopes, never()).addFeatureFlag(any(), any()) assertEquals(seriesData, result) assertEquals("existingValue", result["existingKey"]) @@ -235,4 +236,3 @@ class SentryLaunchDarklyServerHookTest { assertEquals("value", result["key"]) } } -