From 5b475d1cf20f4d0d6030b68a87e0a1383391af00 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Thu, 16 Oct 2025 12:58:08 +0200 Subject: [PATCH 01/15] Add scope based feature flags --- .../java/io/sentry/samples/console/Main.java | 2 + .../ConsoleApplicationSystemTest.kt | 2 +- .../java/io/sentry/samples/console/Main.java | 2 + .../ConsoleApplicationSystemTest.kt | 2 +- .../main/java/io/sentry/samples/jul/Main.java | 5 + .../ConsoleApplicationSystemTest.kt | 5 + .../java/io/sentry/samples/log4j2/Main.java | 3 + .../ConsoleApplicationSystemTest.kt | 5 + .../java/io/sentry/samples/logback/Main.java | 4 + .../ConsoleApplicationSystemTest.kt | 5 + .../samples/spring7/web/PersonController.java | 1 + .../io/sentry/systemtest/PersonSystemTest.kt | 5 + .../spring/boot4/PersonController.java | 1 + .../io/sentry/systemtest/PersonSystemTest.kt | 10 + .../spring/boot4/PersonController.java | 1 + .../io/sentry/systemtest/PersonSystemTest.kt | 10 + .../spring/boot4/PersonController.java | 1 + .../io/sentry/systemtest/PersonSystemTest.kt | 5 + .../spring/boot4/PersonController.java | 1 + .../io/sentry/systemtest/PersonSystemTest.kt | 10 + .../spring/boot/jakarta/PersonController.java | 1 + .../io/sentry/systemtest/PersonSystemTest.kt | 10 + .../spring/boot/jakarta/PersonController.java | 1 + .../io/sentry/systemtest/PersonSystemTest.kt | 10 + .../spring/boot/jakarta/PersonController.java | 1 + .../io/sentry/systemtest/PersonSystemTest.kt | 10 + .../samples/spring/boot/PersonController.java | 1 + .../io/sentry/systemtest/PersonSystemTest.kt | 10 + .../samples/spring/boot/PersonController.java | 1 + .../io/sentry/systemtest/PersonSystemTest.kt | 10 + .../spring/boot/jakarta/PersonController.java | 1 + .../io/sentry/systemtest/PersonSystemTest.kt | 5 + .../samples/spring/boot/PersonController.java | 1 + .../io/sentry/systemtest/PersonSystemTest.kt | 5 + .../samples/spring/boot/PersonController.java | 1 + .../io/sentry/systemtest/PersonSystemTest.kt | 5 + .../spring/jakarta/web/PersonController.java | 1 + .../io/sentry/systemtest/PersonSystemTest.kt | 5 + .../samples/spring/web/PersonController.java | 1 + .../io/sentry/systemtest/PersonSystemTest.kt | 5 + .../api/sentry-system-test-support.api | 2 + .../io/sentry/systemtest/util/TestHelper.kt | 25 +++ sentry/api/sentry.api | 99 +++++++++ .../java/io/sentry/CombinedContextsView.java | 23 ++ .../java/io/sentry/CombinedScopeView.java | 22 ++ .../src/main/java/io/sentry/HubAdapter.java | 5 + .../main/java/io/sentry/HubScopesWrapper.java | 5 + sentry/src/main/java/io/sentry/IScope.java | 12 + sentry/src/main/java/io/sentry/IScopes.java | 2 + sentry/src/main/java/io/sentry/NoOpHub.java | 3 + sentry/src/main/java/io/sentry/NoOpScope.java | 16 ++ .../src/main/java/io/sentry/NoOpScopes.java | 3 + sentry/src/main/java/io/sentry/Scope.java | 23 ++ sentry/src/main/java/io/sentry/Scopes.java | 5 + .../main/java/io/sentry/ScopesAdapter.java | 5 + sentry/src/main/java/io/sentry/Sentry.java | 4 + .../src/main/java/io/sentry/SentryClient.java | 8 + .../main/java/io/sentry/SentryOptions.java | 23 ++ .../featureflags/FeatureFlagBuffer.java | 207 ++++++++++++++++++ .../featureflags/IFeatureFlagBuffer.java | 17 ++ .../featureflags/NoOpFeatureFlagBuffer.java | 28 +++ .../java/io/sentry/protocol/Contexts.java | 11 + .../java/io/sentry/protocol/FeatureFlag.java | 142 ++++++++++++ .../java/io/sentry/protocol/FeatureFlags.java | 127 +++++++++++ sentry/src/test/java/io/sentry/ScopeTest.kt | 16 ++ sentry/src/test/java/io/sentry/ScopesTest.kt | 38 ++++ .../test/java/io/sentry/SentryOptionsTest.kt | 13 ++ .../featureflags/FeatureFlagBufferTest.kt | 182 +++++++++++++++ 68 files changed, 1229 insertions(+), 2 deletions(-) create mode 100644 sentry/src/main/java/io/sentry/featureflags/FeatureFlagBuffer.java create mode 100644 sentry/src/main/java/io/sentry/featureflags/IFeatureFlagBuffer.java create mode 100644 sentry/src/main/java/io/sentry/featureflags/NoOpFeatureFlagBuffer.java create mode 100644 sentry/src/main/java/io/sentry/protocol/FeatureFlag.java create mode 100644 sentry/src/main/java/io/sentry/protocol/FeatureFlags.java create mode 100644 sentry/src/test/java/io/sentry/featureflags/FeatureFlagBufferTest.kt diff --git a/sentry-samples/sentry-samples-console-opentelemetry-noagent/src/main/java/io/sentry/samples/console/Main.java b/sentry-samples/sentry-samples-console-opentelemetry-noagent/src/main/java/io/sentry/samples/console/Main.java index 8af939c32b..a41e853e15 100644 --- a/sentry-samples/sentry-samples-console-opentelemetry-noagent/src/main/java/io/sentry/samples/console/Main.java +++ b/sentry-samples/sentry-samples-console-opentelemetry-noagent/src/main/java/io/sentry/samples/console/Main.java @@ -61,6 +61,8 @@ public static void main(String[] args) throws InterruptedException { // Only data added to the scope on `configureScope` above is included. Sentry.captureMessage("Some warning!", SentryLevel.WARNING); + Sentry.addFeatureFlag("my-feature-flag", true); + // Sending exception: Exception exception = new RuntimeException("Some error!"); Sentry.captureException(exception); diff --git a/sentry-samples/sentry-samples-console-opentelemetry-noagent/src/test/kotlin/sentry/systemtest/ConsoleApplicationSystemTest.kt b/sentry-samples/sentry-samples-console-opentelemetry-noagent/src/test/kotlin/sentry/systemtest/ConsoleApplicationSystemTest.kt index 427c930653..29d144355a 100644 --- a/sentry-samples/sentry-samples-console-opentelemetry-noagent/src/test/kotlin/sentry/systemtest/ConsoleApplicationSystemTest.kt +++ b/sentry-samples/sentry-samples-console-opentelemetry-noagent/src/test/kotlin/sentry/systemtest/ConsoleApplicationSystemTest.kt @@ -55,7 +55,7 @@ class ConsoleApplicationSystemTest { // Verify we received the RuntimeException testHelper.ensureErrorReceived { event -> event.exceptions?.any { ex -> ex.type == "RuntimeException" && ex.value == "Some error!" } == - true + true && testHelper.doesEventHaveFlag(event, "my-feature-flag", true) } // Verify we received the detailed event with fingerprint diff --git a/sentry-samples/sentry-samples-console/src/main/java/io/sentry/samples/console/Main.java b/sentry-samples/sentry-samples-console/src/main/java/io/sentry/samples/console/Main.java index 9d6a8fd7a9..5f557b7ad2 100644 --- a/sentry-samples/sentry-samples-console/src/main/java/io/sentry/samples/console/Main.java +++ b/sentry-samples/sentry-samples-console/src/main/java/io/sentry/samples/console/Main.java @@ -126,6 +126,8 @@ public static void main(String[] args) throws InterruptedException { // Only data added to the scope on `configureScope` above is included. Sentry.captureMessage("Some warning!", SentryLevel.WARNING); + Sentry.addFeatureFlag("my-feature-flag", true); + // Sending exception: Exception exception = new RuntimeException("Some error!"); Sentry.captureException(exception); diff --git a/sentry-samples/sentry-samples-console/src/test/kotlin/io/sentry/systemtest/ConsoleApplicationSystemTest.kt b/sentry-samples/sentry-samples-console/src/test/kotlin/io/sentry/systemtest/ConsoleApplicationSystemTest.kt index 18409c5b4e..cf09728047 100644 --- a/sentry-samples/sentry-samples-console/src/test/kotlin/io/sentry/systemtest/ConsoleApplicationSystemTest.kt +++ b/sentry-samples/sentry-samples-console/src/test/kotlin/io/sentry/systemtest/ConsoleApplicationSystemTest.kt @@ -51,7 +51,7 @@ class ConsoleApplicationSystemTest { // Verify we received the RuntimeException testHelper.ensureErrorReceived { event -> event.exceptions?.any { ex -> ex.type == "RuntimeException" && ex.value == "Some error!" } == - true + true && testHelper.doesEventHaveFlag(event, "my-feature-flag", true) } // Verify we received the detailed event with fingerprint diff --git a/sentry-samples/sentry-samples-jul/src/main/java/io/sentry/samples/jul/Main.java b/sentry-samples/sentry-samples-jul/src/main/java/io/sentry/samples/jul/Main.java index 86030003f6..9f245470af 100644 --- a/sentry-samples/sentry-samples-jul/src/main/java/io/sentry/samples/jul/Main.java +++ b/sentry-samples/sentry-samples-jul/src/main/java/io/sentry/samples/jul/Main.java @@ -1,5 +1,6 @@ package io.sentry.samples.jul; +import io.sentry.Sentry; import java.util.UUID; import java.util.logging.Level; import java.util.logging.LogManager; @@ -22,6 +23,10 @@ public static void main(String[] args) throws Exception { MDC.put("userId", UUID.randomUUID().toString()); MDC.put("requestId", UUID.randomUUID().toString()); + Sentry.addFeatureFlag("my-feature-flag", true); + + LOGGER.warning("important warning"); + // logging arguments are converted to Sentry Event parameters LOGGER.log(Level.INFO, "User has made a purchase of product: %d", 445); diff --git a/sentry-samples/sentry-samples-jul/src/test/kotlin/io/sentry/systemtest/ConsoleApplicationSystemTest.kt b/sentry-samples/sentry-samples-jul/src/test/kotlin/io/sentry/systemtest/ConsoleApplicationSystemTest.kt index 3c89c5a7e2..d23428da94 100644 --- a/sentry-samples/sentry-samples-jul/src/test/kotlin/io/sentry/systemtest/ConsoleApplicationSystemTest.kt +++ b/sentry-samples/sentry-samples-jul/src/test/kotlin/io/sentry/systemtest/ConsoleApplicationSystemTest.kt @@ -57,6 +57,11 @@ class ConsoleApplicationSystemTest { } != null } + testHelper.ensureErrorReceived { event -> + event.message?.message == "important warning" && + testHelper.doesEventHaveFlag(event, "my-feature-flag", true) + } + testHelper.ensureLogsReceived { logs, _ -> testHelper.doesContainLogWithBody(logs, "User has made a purchase of product: 445") && testHelper.doesContainLogWithBody(logs, "Something went wrong") diff --git a/sentry-samples/sentry-samples-log4j2/src/main/java/io/sentry/samples/log4j2/Main.java b/sentry-samples/sentry-samples-log4j2/src/main/java/io/sentry/samples/log4j2/Main.java index 9a7612354a..5703fff5d4 100644 --- a/sentry-samples/sentry-samples-log4j2/src/main/java/io/sentry/samples/log4j2/Main.java +++ b/sentry-samples/sentry-samples-log4j2/src/main/java/io/sentry/samples/log4j2/Main.java @@ -1,5 +1,6 @@ package io.sentry.samples.log4j2; +import io.sentry.Sentry; import java.util.UUID; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -19,6 +20,8 @@ public static void main(String[] args) { // ThreadContext tag not listed in log4j2.xml ThreadContext.put("context-tag", "context-tag-value"); + Sentry.addFeatureFlag("my-feature-flag", true); + // logging arguments are converted to Sentry Event parameters LOGGER.info("User has made a purchase of product: {}", 445); // because minimumEventLevel is set to WARN this raises an event diff --git a/sentry-samples/sentry-samples-log4j2/src/test/kotlin/io/sentry/systemtest/ConsoleApplicationSystemTest.kt b/sentry-samples/sentry-samples-log4j2/src/test/kotlin/io/sentry/systemtest/ConsoleApplicationSystemTest.kt index eed0354863..5d3266c6ff 100644 --- a/sentry-samples/sentry-samples-log4j2/src/test/kotlin/io/sentry/systemtest/ConsoleApplicationSystemTest.kt +++ b/sentry-samples/sentry-samples-log4j2/src/test/kotlin/io/sentry/systemtest/ConsoleApplicationSystemTest.kt @@ -47,6 +47,11 @@ class ConsoleApplicationSystemTest { event.level?.name == "ERROR" } + testHelper.ensureErrorReceived { event -> + event.message?.message == "Important warning" && + testHelper.doesEventHaveFlag(event, "my-feature-flag", true) + } + testHelper.ensureErrorReceived { event -> event.breadcrumbs?.firstOrNull { it.message == "Hello Sentry!" && it.level == SentryLevel.DEBUG diff --git a/sentry-samples/sentry-samples-logback/src/main/java/io/sentry/samples/logback/Main.java b/sentry-samples/sentry-samples-logback/src/main/java/io/sentry/samples/logback/Main.java index 4aeae0038f..ec3928998a 100644 --- a/sentry-samples/sentry-samples-logback/src/main/java/io/sentry/samples/logback/Main.java +++ b/sentry-samples/sentry-samples-logback/src/main/java/io/sentry/samples/logback/Main.java @@ -1,5 +1,6 @@ package io.sentry.samples.logback; +import io.sentry.Sentry; import java.util.UUID; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -17,6 +18,9 @@ public static void main(String[] args) { // MDC tag not listed in logback.xml MDC.put("context-tag", "context-tag-value"); + Sentry.addFeatureFlag("my-feature-flag", true); + LOGGER.warn("important warning"); + // logging arguments are converted to Sentry Event parameters LOGGER.info("User has made a purchase of product: {}", 445); diff --git a/sentry-samples/sentry-samples-logback/src/test/kotlin/io/sentry/systemtest/ConsoleApplicationSystemTest.kt b/sentry-samples/sentry-samples-logback/src/test/kotlin/io/sentry/systemtest/ConsoleApplicationSystemTest.kt index 72744c0ec3..4016988222 100644 --- a/sentry-samples/sentry-samples-logback/src/test/kotlin/io/sentry/systemtest/ConsoleApplicationSystemTest.kt +++ b/sentry-samples/sentry-samples-logback/src/test/kotlin/io/sentry/systemtest/ConsoleApplicationSystemTest.kt @@ -53,6 +53,11 @@ class ConsoleApplicationSystemTest { } != null } + testHelper.ensureErrorReceived { event -> + event.message?.message == "important warning" && + testHelper.doesEventHaveFlag(event, "my-feature-flag", true) + } + testHelper.ensureErrorReceived { event -> event.breadcrumbs?.firstOrNull { it.message == "User has made a purchase of product: 445" && it.level == SentryLevel.INFO diff --git a/sentry-samples/sentry-samples-spring-7/src/main/java/io/sentry/samples/spring7/web/PersonController.java b/sentry-samples/sentry-samples-spring-7/src/main/java/io/sentry/samples/spring7/web/PersonController.java index a9a413fd5f..d66cf747c1 100644 --- a/sentry-samples/sentry-samples-spring-7/src/main/java/io/sentry/samples/spring7/web/PersonController.java +++ b/sentry-samples/sentry-samples-spring-7/src/main/java/io/sentry/samples/spring7/web/PersonController.java @@ -26,6 +26,7 @@ Person person(@PathVariable("id") Long id) { Sentry.logger().warn("warn Sentry logging"); Sentry.logger().error("error Sentry logging"); Sentry.logger().info("hello %s %s", "there", "world!"); + Sentry.addFeatureFlag("my-feature-flag", true); LOGGER.info("Loading person with id={}", id); if (id > 10L) { throw new IllegalArgumentException("Something went wrong [id=" + id + "]"); diff --git a/sentry-samples/sentry-samples-spring-7/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt b/sentry-samples/sentry-samples-spring-7/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt index 628c27f4c6..3ad118d52f 100644 --- a/sentry-samples/sentry-samples-spring-7/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt +++ b/sentry-samples/sentry-samples-spring-7/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt @@ -20,6 +20,11 @@ class PersonSystemTest { restClient.getPerson(11L) assertEquals(500, restClient.lastKnownStatusCode) + testHelper.ensureErrorReceived { event -> + testHelper.doesEventHaveExceptionMessage(event, "Something went wrong [id=11]") && + testHelper.doesEventHaveFlag(event, "my-feature-flag", true) + } + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> testHelper.doesTransactionHaveOp(transaction, "http.server") } diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot4/PersonController.java b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot4/PersonController.java index f3f03b39e1..70159b8aef 100644 --- a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot4/PersonController.java +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot4/PersonController.java @@ -36,6 +36,7 @@ Person person(@PathVariable Long id) { Sentry.logger().warn("warn Sentry logging"); Sentry.logger().error("error Sentry logging"); Sentry.logger().info("hello %s %s", "there", "world!"); + Sentry.addFeatureFlag("my-feature-flag", true); ISpan currentSpan = Sentry.getSpan(); ISpan sentrySpan = currentSpan.startChild("spanCreatedThroughSentryApi"); try { diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt index 707b5025dc..2488cc87d1 100644 --- a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt @@ -20,6 +20,16 @@ class PersonSystemTest { restClient.getPerson(1L) assertEquals(500, restClient.lastKnownStatusCode) + testHelper.ensureErrorReceived { event -> + event.message?.formatted == "Trying person with id=1" && + testHelper.doesEventHaveFlag(event, "my-feature-flag", true) + } + + testHelper.ensureErrorReceived { event -> + testHelper.doesEventHaveExceptionMessage(event, "Something went wrong [id=1]") && + testHelper.doesEventHaveFlag(event, "my-feature-flag", true) + } + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> testHelper.doesTransactionContainSpanWithOp(transaction, "spanCreatedThroughOtelApi") && testHelper.doesTransactionContainSpanWithOp(transaction, "spanCreatedThroughSentryApi") diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot4/PersonController.java b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot4/PersonController.java index 9b727447ff..2861168fc7 100644 --- a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot4/PersonController.java +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot4/PersonController.java @@ -34,6 +34,7 @@ Person person(@PathVariable Long id) { Sentry.logger().warn("warn Sentry logging"); Sentry.logger().error("error Sentry logging"); Sentry.logger().info("hello %s %s", "there", "world!"); + Sentry.addFeatureFlag("my-feature-flag", true); ISpan currentSpan = Sentry.getSpan(); ISpan sentrySpan = currentSpan.startChild("spanCreatedThroughSentryApi"); try { diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt index 707b5025dc..2488cc87d1 100644 --- a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt @@ -20,6 +20,16 @@ class PersonSystemTest { restClient.getPerson(1L) assertEquals(500, restClient.lastKnownStatusCode) + testHelper.ensureErrorReceived { event -> + event.message?.formatted == "Trying person with id=1" && + testHelper.doesEventHaveFlag(event, "my-feature-flag", true) + } + + testHelper.ensureErrorReceived { event -> + testHelper.doesEventHaveExceptionMessage(event, "Something went wrong [id=1]") && + testHelper.doesEventHaveFlag(event, "my-feature-flag", true) + } + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> testHelper.doesTransactionContainSpanWithOp(transaction, "spanCreatedThroughOtelApi") && testHelper.doesTransactionContainSpanWithOp(transaction, "spanCreatedThroughSentryApi") diff --git a/sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot4/PersonController.java b/sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot4/PersonController.java index b2563200c8..0db43f5ab7 100644 --- a/sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot4/PersonController.java +++ b/sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot4/PersonController.java @@ -26,6 +26,7 @@ Person person(@PathVariable Long id) { Sentry.logger().warn("warn Sentry logging"); Sentry.logger().error("error Sentry logging"); Sentry.logger().info("hello %s %s", "there", "world!"); + Sentry.addFeatureFlag("my-feature-flag", true); LOGGER.info("Loading person with id={}", id); throw new IllegalArgumentException("Something went wrong [id=" + id + "]"); } diff --git a/sentry-samples/sentry-samples-spring-boot-4-webflux/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt b/sentry-samples/sentry-samples-spring-boot-4-webflux/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt index 26c3282e7a..ac74d5e495 100644 --- a/sentry-samples/sentry-samples-spring-boot-4-webflux/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt +++ b/sentry-samples/sentry-samples-spring-boot-4-webflux/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt @@ -20,6 +20,11 @@ class PersonSystemTest { restClient.getPerson(1L) assertEquals(500, restClient.lastKnownStatusCode) + testHelper.ensureErrorReceived { event -> + testHelper.doesEventHaveExceptionMessage(event, "Something went wrong [id=1]") && + testHelper.doesEventHaveFlag(event, "my-feature-flag", true) + } + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> testHelper.doesTransactionHaveOp(transaction, "http.server") } diff --git a/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot4/PersonController.java b/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot4/PersonController.java index 305850ec18..ff54534959 100644 --- a/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot4/PersonController.java +++ b/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot4/PersonController.java @@ -29,6 +29,7 @@ Person person(@PathVariable Long id) { Sentry.logger().warn("warn Sentry logging"); Sentry.logger().error("error Sentry logging"); Sentry.logger().info("hello %s %s", "there", "world!"); + Sentry.addFeatureFlag("my-feature-flag", true); LOGGER.error("Trying person with id={}", id, new RuntimeException("error while loading")); throw new IllegalArgumentException("Something went wrong [id=" + id + "]"); } finally { diff --git a/sentry-samples/sentry-samples-spring-boot-4/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt b/sentry-samples/sentry-samples-spring-boot-4/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt index 7d6e018253..03a0abbdf2 100644 --- a/sentry-samples/sentry-samples-spring-boot-4/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt +++ b/sentry-samples/sentry-samples-spring-boot-4/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt @@ -20,6 +20,16 @@ class PersonSystemTest { restClient.getPerson(1L) assertEquals(500, restClient.lastKnownStatusCode) + testHelper.ensureErrorReceived { event -> + event.message?.formatted == "Trying person with id=1" && + testHelper.doesEventHaveFlag(event, "my-feature-flag", true) + } + + testHelper.ensureErrorReceived { event -> + testHelper.doesEventHaveExceptionMessage(event, "Something went wrong [id=1]") && + testHelper.doesEventHaveFlag(event, "my-feature-flag", true) + } + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> testHelper.doesTransactionHaveOp(transaction, "http.server") } diff --git a/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/PersonController.java b/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/PersonController.java index b3f22fd7fd..26784880a7 100644 --- a/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/PersonController.java +++ b/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/PersonController.java @@ -36,6 +36,7 @@ Person person(@PathVariable Long id) { Sentry.logger().warn("warn Sentry logging"); Sentry.logger().error("error Sentry logging"); Sentry.logger().info("hello %s %s", "there", "world!"); + Sentry.addFeatureFlag("my-feature-flag", true); ISpan currentSpan = Sentry.getSpan(); ISpan sentrySpan = currentSpan.startChild("spanCreatedThroughSentryApi"); try { diff --git a/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry-noagent/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt b/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry-noagent/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt index 707b5025dc..2488cc87d1 100644 --- a/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry-noagent/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt +++ b/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry-noagent/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt @@ -20,6 +20,16 @@ class PersonSystemTest { restClient.getPerson(1L) assertEquals(500, restClient.lastKnownStatusCode) + testHelper.ensureErrorReceived { event -> + event.message?.formatted == "Trying person with id=1" && + testHelper.doesEventHaveFlag(event, "my-feature-flag", true) + } + + testHelper.ensureErrorReceived { event -> + testHelper.doesEventHaveExceptionMessage(event, "Something went wrong [id=1]") && + testHelper.doesEventHaveFlag(event, "my-feature-flag", true) + } + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> testHelper.doesTransactionContainSpanWithOp(transaction, "spanCreatedThroughOtelApi") && testHelper.doesTransactionContainSpanWithOp(transaction, "spanCreatedThroughSentryApi") diff --git a/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/PersonController.java b/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/PersonController.java index 1584a9e823..1880799c28 100644 --- a/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/PersonController.java +++ b/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/PersonController.java @@ -34,6 +34,7 @@ Person person(@PathVariable Long id) { Sentry.logger().warn("warn Sentry logging"); Sentry.logger().error("error Sentry logging"); Sentry.logger().info("hello %s %s", "there", "world!"); + Sentry.addFeatureFlag("my-feature-flag", true); ISpan currentSpan = Sentry.getSpan(); ISpan sentrySpan = currentSpan.startChild("spanCreatedThroughSentryApi"); try { diff --git a/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt b/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt index 707b5025dc..2488cc87d1 100644 --- a/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt +++ b/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt @@ -20,6 +20,16 @@ class PersonSystemTest { restClient.getPerson(1L) assertEquals(500, restClient.lastKnownStatusCode) + testHelper.ensureErrorReceived { event -> + event.message?.formatted == "Trying person with id=1" && + testHelper.doesEventHaveFlag(event, "my-feature-flag", true) + } + + testHelper.ensureErrorReceived { event -> + testHelper.doesEventHaveExceptionMessage(event, "Something went wrong [id=1]") && + testHelper.doesEventHaveFlag(event, "my-feature-flag", true) + } + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> testHelper.doesTransactionContainSpanWithOp(transaction, "spanCreatedThroughOtelApi") && testHelper.doesTransactionContainSpanWithOp(transaction, "spanCreatedThroughSentryApi") diff --git a/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/PersonController.java b/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/PersonController.java index 94a4b9b852..2e24833b80 100644 --- a/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/PersonController.java +++ b/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/PersonController.java @@ -29,6 +29,7 @@ Person person(@PathVariable Long id) { Sentry.logger().warn("warn Sentry logging"); Sentry.logger().error("error Sentry logging"); Sentry.logger().info("hello %s %s", "there", "world!"); + Sentry.addFeatureFlag("my-feature-flag", true); LOGGER.error("Trying person with id={}", id, new RuntimeException("error while loading")); throw new IllegalArgumentException("Something went wrong [id=" + id + "]"); } finally { diff --git a/sentry-samples/sentry-samples-spring-boot-jakarta/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt b/sentry-samples/sentry-samples-spring-boot-jakarta/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt index 7d6e018253..03a0abbdf2 100644 --- a/sentry-samples/sentry-samples-spring-boot-jakarta/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt +++ b/sentry-samples/sentry-samples-spring-boot-jakarta/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt @@ -20,6 +20,16 @@ class PersonSystemTest { restClient.getPerson(1L) assertEquals(500, restClient.lastKnownStatusCode) + testHelper.ensureErrorReceived { event -> + event.message?.formatted == "Trying person with id=1" && + testHelper.doesEventHaveFlag(event, "my-feature-flag", true) + } + + testHelper.ensureErrorReceived { event -> + testHelper.doesEventHaveExceptionMessage(event, "Something went wrong [id=1]") && + testHelper.doesEventHaveFlag(event, "my-feature-flag", true) + } + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> testHelper.doesTransactionHaveOp(transaction, "http.server") } diff --git a/sentry-samples/sentry-samples-spring-boot-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/PersonController.java b/sentry-samples/sentry-samples-spring-boot-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/PersonController.java index 97c5aa2e6f..e2574e788c 100644 --- a/sentry-samples/sentry-samples-spring-boot-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/PersonController.java +++ b/sentry-samples/sentry-samples-spring-boot-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/PersonController.java @@ -36,6 +36,7 @@ Person person(@PathVariable Long id) { Sentry.logger().warn("warn Sentry logging"); Sentry.logger().error("error Sentry logging"); Sentry.logger().info("hello %s %s", "there", "world!"); + Sentry.addFeatureFlag("my-feature-flag", true); ISpan currentSpan = Sentry.getSpan(); ISpan sentrySpan = currentSpan.startChild("spanCreatedThroughSentryApi"); try { diff --git a/sentry-samples/sentry-samples-spring-boot-opentelemetry-noagent/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt b/sentry-samples/sentry-samples-spring-boot-opentelemetry-noagent/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt index 707b5025dc..2488cc87d1 100644 --- a/sentry-samples/sentry-samples-spring-boot-opentelemetry-noagent/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt +++ b/sentry-samples/sentry-samples-spring-boot-opentelemetry-noagent/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt @@ -20,6 +20,16 @@ class PersonSystemTest { restClient.getPerson(1L) assertEquals(500, restClient.lastKnownStatusCode) + testHelper.ensureErrorReceived { event -> + event.message?.formatted == "Trying person with id=1" && + testHelper.doesEventHaveFlag(event, "my-feature-flag", true) + } + + testHelper.ensureErrorReceived { event -> + testHelper.doesEventHaveExceptionMessage(event, "Something went wrong [id=1]") && + testHelper.doesEventHaveFlag(event, "my-feature-flag", true) + } + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> testHelper.doesTransactionContainSpanWithOp(transaction, "spanCreatedThroughOtelApi") && testHelper.doesTransactionContainSpanWithOp(transaction, "spanCreatedThroughSentryApi") diff --git a/sentry-samples/sentry-samples-spring-boot-opentelemetry/src/main/java/io/sentry/samples/spring/boot/PersonController.java b/sentry-samples/sentry-samples-spring-boot-opentelemetry/src/main/java/io/sentry/samples/spring/boot/PersonController.java index 04816e3463..6c6209403b 100644 --- a/sentry-samples/sentry-samples-spring-boot-opentelemetry/src/main/java/io/sentry/samples/spring/boot/PersonController.java +++ b/sentry-samples/sentry-samples-spring-boot-opentelemetry/src/main/java/io/sentry/samples/spring/boot/PersonController.java @@ -34,6 +34,7 @@ Person person(@PathVariable Long id) { Sentry.logger().warn("warn Sentry logging"); Sentry.logger().error("error Sentry logging"); Sentry.logger().info("hello %s %s", "there", "world!"); + Sentry.addFeatureFlag("my-feature-flag", true); ISpan currentSpan = Sentry.getSpan(); ISpan sentrySpan = currentSpan.startChild("spanCreatedThroughSentryApi"); try { diff --git a/sentry-samples/sentry-samples-spring-boot-opentelemetry/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt b/sentry-samples/sentry-samples-spring-boot-opentelemetry/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt index 707b5025dc..2488cc87d1 100644 --- a/sentry-samples/sentry-samples-spring-boot-opentelemetry/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt +++ b/sentry-samples/sentry-samples-spring-boot-opentelemetry/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt @@ -20,6 +20,16 @@ class PersonSystemTest { restClient.getPerson(1L) assertEquals(500, restClient.lastKnownStatusCode) + testHelper.ensureErrorReceived { event -> + event.message?.formatted == "Trying person with id=1" && + testHelper.doesEventHaveFlag(event, "my-feature-flag", true) + } + + testHelper.ensureErrorReceived { event -> + testHelper.doesEventHaveExceptionMessage(event, "Something went wrong [id=1]") && + testHelper.doesEventHaveFlag(event, "my-feature-flag", true) + } + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> testHelper.doesTransactionContainSpanWithOp(transaction, "spanCreatedThroughOtelApi") && testHelper.doesTransactionContainSpanWithOp(transaction, "spanCreatedThroughSentryApi") diff --git a/sentry-samples/sentry-samples-spring-boot-webflux-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/PersonController.java b/sentry-samples/sentry-samples-spring-boot-webflux-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/PersonController.java index a7b7752806..d1a505d0e5 100644 --- a/sentry-samples/sentry-samples-spring-boot-webflux-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/PersonController.java +++ b/sentry-samples/sentry-samples-spring-boot-webflux-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/PersonController.java @@ -26,6 +26,7 @@ Person person(@PathVariable Long id) { Sentry.logger().warn("warn Sentry logging"); Sentry.logger().error("error Sentry logging"); Sentry.logger().info("hello %s %s", "there", "world!"); + Sentry.addFeatureFlag("my-feature-flag", true); LOGGER.info("Loading person with id={}", id); throw new IllegalArgumentException("Something went wrong [id=" + id + "]"); } diff --git a/sentry-samples/sentry-samples-spring-boot-webflux-jakarta/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt b/sentry-samples/sentry-samples-spring-boot-webflux-jakarta/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt index 26c3282e7a..ac74d5e495 100644 --- a/sentry-samples/sentry-samples-spring-boot-webflux-jakarta/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt +++ b/sentry-samples/sentry-samples-spring-boot-webflux-jakarta/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt @@ -20,6 +20,11 @@ class PersonSystemTest { restClient.getPerson(1L) assertEquals(500, restClient.lastKnownStatusCode) + testHelper.ensureErrorReceived { event -> + testHelper.doesEventHaveExceptionMessage(event, "Something went wrong [id=1]") && + testHelper.doesEventHaveFlag(event, "my-feature-flag", true) + } + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> testHelper.doesTransactionHaveOp(transaction, "http.server") } diff --git a/sentry-samples/sentry-samples-spring-boot-webflux/src/main/java/io/sentry/samples/spring/boot/PersonController.java b/sentry-samples/sentry-samples-spring-boot-webflux/src/main/java/io/sentry/samples/spring/boot/PersonController.java index 010e42f302..d0b5435efc 100644 --- a/sentry-samples/sentry-samples-spring-boot-webflux/src/main/java/io/sentry/samples/spring/boot/PersonController.java +++ b/sentry-samples/sentry-samples-spring-boot-webflux/src/main/java/io/sentry/samples/spring/boot/PersonController.java @@ -26,6 +26,7 @@ Person person(@PathVariable Long id) { Sentry.logger().warn("warn Sentry logging"); Sentry.logger().error("error Sentry logging"); Sentry.logger().info("hello %s %s", "there", "world!"); + Sentry.addFeatureFlag("my-feature-flag", true); LOGGER.info("Loading person with id={}", id); throw new IllegalArgumentException("Something went wrong [id=" + id + "]"); } diff --git a/sentry-samples/sentry-samples-spring-boot-webflux/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt b/sentry-samples/sentry-samples-spring-boot-webflux/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt index 26c3282e7a..ac74d5e495 100644 --- a/sentry-samples/sentry-samples-spring-boot-webflux/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt +++ b/sentry-samples/sentry-samples-spring-boot-webflux/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt @@ -20,6 +20,11 @@ class PersonSystemTest { restClient.getPerson(1L) assertEquals(500, restClient.lastKnownStatusCode) + testHelper.ensureErrorReceived { event -> + testHelper.doesEventHaveExceptionMessage(event, "Something went wrong [id=1]") && + testHelper.doesEventHaveFlag(event, "my-feature-flag", true) + } + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> testHelper.doesTransactionHaveOp(transaction, "http.server") } diff --git a/sentry-samples/sentry-samples-spring-boot/src/main/java/io/sentry/samples/spring/boot/PersonController.java b/sentry-samples/sentry-samples-spring-boot/src/main/java/io/sentry/samples/spring/boot/PersonController.java index 45e3b96f88..3bf03cb785 100644 --- a/sentry-samples/sentry-samples-spring-boot/src/main/java/io/sentry/samples/spring/boot/PersonController.java +++ b/sentry-samples/sentry-samples-spring-boot/src/main/java/io/sentry/samples/spring/boot/PersonController.java @@ -25,6 +25,7 @@ Person person(@PathVariable Long id) { Sentry.logger().warn("warn Sentry logging"); Sentry.logger().error("error Sentry logging"); Sentry.logger().info("hello %s %s", "there", "world!"); + Sentry.addFeatureFlag("my-feature-flag", true); LOGGER.info("Loading person with id={}", id); throw new IllegalArgumentException("Something went wrong [id=" + id + "]"); } diff --git a/sentry-samples/sentry-samples-spring-boot/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt b/sentry-samples/sentry-samples-spring-boot/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt index 7d6e018253..8119e5ab4e 100644 --- a/sentry-samples/sentry-samples-spring-boot/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt +++ b/sentry-samples/sentry-samples-spring-boot/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt @@ -20,6 +20,11 @@ class PersonSystemTest { restClient.getPerson(1L) assertEquals(500, restClient.lastKnownStatusCode) + testHelper.ensureErrorReceived { event -> + testHelper.doesEventHaveExceptionMessage(event, "Something went wrong [id=1]") && + testHelper.doesEventHaveFlag(event, "my-feature-flag", true) + } + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> testHelper.doesTransactionHaveOp(transaction, "http.server") } diff --git a/sentry-samples/sentry-samples-spring-jakarta/src/main/java/io/sentry/samples/spring/jakarta/web/PersonController.java b/sentry-samples/sentry-samples-spring-jakarta/src/main/java/io/sentry/samples/spring/jakarta/web/PersonController.java index dab805281e..ec33f36096 100644 --- a/sentry-samples/sentry-samples-spring-jakarta/src/main/java/io/sentry/samples/spring/jakarta/web/PersonController.java +++ b/sentry-samples/sentry-samples-spring-jakarta/src/main/java/io/sentry/samples/spring/jakarta/web/PersonController.java @@ -26,6 +26,7 @@ Person person(@PathVariable("id") Long id) { Sentry.logger().warn("warn Sentry logging"); Sentry.logger().error("error Sentry logging"); Sentry.logger().info("hello %s %s", "there", "world!"); + Sentry.addFeatureFlag("my-feature-flag", true); LOGGER.info("Loading person with id={}", id); if (id > 10L) { throw new IllegalArgumentException("Something went wrong [id=" + id + "]"); diff --git a/sentry-samples/sentry-samples-spring-jakarta/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt b/sentry-samples/sentry-samples-spring-jakarta/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt index 80d21b1d93..1215b81912 100644 --- a/sentry-samples/sentry-samples-spring-jakarta/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt +++ b/sentry-samples/sentry-samples-spring-jakarta/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt @@ -20,6 +20,11 @@ class PersonSystemTest { restClient.getPerson(11L) assertEquals(500, restClient.lastKnownStatusCode) + testHelper.ensureErrorReceived { event -> + testHelper.doesEventHaveExceptionMessage(event, "Something went wrong [id=11]") && + testHelper.doesEventHaveFlag(event, "my-feature-flag", true) + } + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> testHelper.doesTransactionHaveOp(transaction, "http.server") } diff --git a/sentry-samples/sentry-samples-spring/src/main/java/io/sentry/samples/spring/web/PersonController.java b/sentry-samples/sentry-samples-spring/src/main/java/io/sentry/samples/spring/web/PersonController.java index 37da24d581..ee4020e032 100644 --- a/sentry-samples/sentry-samples-spring/src/main/java/io/sentry/samples/spring/web/PersonController.java +++ b/sentry-samples/sentry-samples-spring/src/main/java/io/sentry/samples/spring/web/PersonController.java @@ -26,6 +26,7 @@ Person person(@PathVariable("id") Long id) { Sentry.logger().warn("warn Sentry logging"); Sentry.logger().error("error Sentry logging"); Sentry.logger().info("hello %s %s", "there", "world!"); + Sentry.addFeatureFlag("my-feature-flag", true); LOGGER.info("Loading person with id={}", id); if (id > 10L) { throw new IllegalArgumentException("Something went wrong [id=" + id + "]"); diff --git a/sentry-samples/sentry-samples-spring/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt b/sentry-samples/sentry-samples-spring/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt index 97b826e10d..aecc6ce24e 100644 --- a/sentry-samples/sentry-samples-spring/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt +++ b/sentry-samples/sentry-samples-spring/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt @@ -20,6 +20,11 @@ class PersonSystemTest { restClient.getPerson(11L) assertEquals(500, restClient.lastKnownStatusCode) + testHelper.ensureErrorReceived { event -> + testHelper.doesEventHaveExceptionMessage(event, "Something went wrong [id=11]") && + testHelper.doesEventHaveFlag(event, "my-feature-flag", true) + } + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> testHelper.doesTransactionHaveOp(transaction, "http.server") } 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 b32583074d..b2a159f92e 100644 --- a/sentry-system-test-support/api/sentry-system-test-support.api +++ b/sentry-system-test-support/api/sentry-system-test-support.api @@ -552,6 +552,8 @@ public final class io/sentry/systemtest/util/SentryMockServerClient : io/sentry/ public final class io/sentry/systemtest/util/TestHelper { public fun (Ljava/lang/String;)V public final fun doesContainLogWithBody (Lio/sentry/SentryLogEvents;Ljava/lang/String;)Z + public final fun doesEventHaveExceptionMessage (Lio/sentry/SentryEvent;Ljava/lang/String;)Z + public final fun doesEventHaveFlag (Lio/sentry/SentryEvent;Ljava/lang/String;Z)Z public final fun doesTransactionContainSpanWithDescription (Lio/sentry/protocol/SentryTransaction;Ljava/lang/String;)Z public final fun doesTransactionContainSpanWithOp (Lio/sentry/protocol/SentryTransaction;Ljava/lang/String;)Z public final fun doesTransactionContainSpanWithOpAndDescription (Lio/sentry/protocol/SentryTransaction;Ljava/lang/String;Ljava/lang/String;)Z diff --git a/sentry-system-test-support/src/main/kotlin/io/sentry/systemtest/util/TestHelper.kt b/sentry-system-test-support/src/main/kotlin/io/sentry/systemtest/util/TestHelper.kt index 73b2acd2ae..00bfa743a3 100644 --- a/sentry-system-test-support/src/main/kotlin/io/sentry/systemtest/util/TestHelper.kt +++ b/sentry-system-test-support/src/main/kotlin/io/sentry/systemtest/util/TestHelper.kt @@ -275,6 +275,31 @@ class TestHelper(backendUrl: String) { return true } + fun doesEventHaveExceptionMessage(event: SentryEvent, expectedMessage: String): Boolean { + val exceptions = event.exceptions + if (exceptions == null) { + println("Unable to find exceptions in event") + return false + } + + val foundException = exceptions.firstOrNull { expectedMessage == it.value } + return foundException != null + } + + fun doesEventHaveFlag(event: SentryEvent, flag: String, result: Boolean): Boolean { + val featureFlags = event.contexts.featureFlags + if (featureFlags == null) { + println("Unable to find feature flags in event:") + return false + } + val foundFlag = + featureFlags.values.firstOrNull { featureFlag -> + println("checking flag ${featureFlag.flag}:${featureFlag.result}") + featureFlag.flag == flag && featureFlag.result == result + } + return foundFlag != null + } + fun findJar(prefix: String, inDir: String = "build/libs"): File { val buildDir = File(inDir) val jarFiles = diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 4bfa96f120..88de3232de 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -228,6 +228,7 @@ public final class io/sentry/CombinedContextsView : io/sentry/protocol/Contexts public fun getApp ()Lio/sentry/protocol/App; public fun getBrowser ()Lio/sentry/protocol/Browser; public fun getDevice ()Lio/sentry/protocol/Device; + public fun getFeatureFlags ()Lio/sentry/protocol/FeatureFlags; public fun getGpu ()Lio/sentry/protocol/Gpu; public fun getOperatingSystem ()Lio/sentry/protocol/OperatingSystem; public fun getResponse ()Lio/sentry/protocol/Response; @@ -246,6 +247,7 @@ public final class io/sentry/CombinedContextsView : io/sentry/protocol/Contexts public fun setApp (Lio/sentry/protocol/App;)V public fun setBrowser (Lio/sentry/protocol/Browser;)V public fun setDevice (Lio/sentry/protocol/Device;)V + public fun setFeatureFlags (Lio/sentry/protocol/FeatureFlags;)V public fun setGpu (Lio/sentry/protocol/Gpu;)V public fun setOperatingSystem (Lio/sentry/protocol/OperatingSystem;)V public fun setResponse (Lio/sentry/protocol/Response;)V @@ -262,6 +264,7 @@ public final class io/sentry/CombinedScopeView : io/sentry/IScope { public fun addBreadcrumb (Lio/sentry/Breadcrumb;)V public fun addBreadcrumb (Lio/sentry/Breadcrumb;Lio/sentry/Hint;)V public fun addEventProcessor (Lio/sentry/EventProcessor;)V + public fun addFeatureFlag (Ljava/lang/String;Z)V public fun assignTraceContext (Lio/sentry/SentryEvent;)V public fun bindClient (Lio/sentry/ISentryClient;)V public fun clear ()V @@ -279,6 +282,8 @@ public final class io/sentry/CombinedScopeView : io/sentry/IScope { public fun getEventProcessors ()Ljava/util/List; public fun getEventProcessorsWithOrder ()Ljava/util/List; public fun getExtras ()Ljava/util/Map; + public fun getFeatureFlagBuffer ()Lio/sentry/featureflags/IFeatureFlagBuffer; + public fun getFeatureFlags ()Lio/sentry/protocol/FeatureFlags; public fun getFingerprint ()Ljava/util/List; public fun getLastEventId ()Lio/sentry/protocol/SentryId; public fun getLevel ()Lio/sentry/SentryLevel; @@ -607,6 +612,7 @@ public final class io/sentry/HttpStatusCodeRange { public final class io/sentry/HubAdapter : io/sentry/IHub { public fun addBreadcrumb (Lio/sentry/Breadcrumb;)V public fun addBreadcrumb (Lio/sentry/Breadcrumb;Lio/sentry/Hint;)V + public fun addFeatureFlag (Ljava/lang/String;Z)V public fun bindClient (Lio/sentry/ISentryClient;)V public fun captureCheckIn (Lio/sentry/CheckIn;)Lio/sentry/protocol/SentryId; public fun captureEnvelope (Lio/sentry/SentryEnvelope;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; @@ -679,6 +685,7 @@ public final class io/sentry/HubScopesWrapper : io/sentry/IHub { public fun (Lio/sentry/IScopes;)V public fun addBreadcrumb (Lio/sentry/Breadcrumb;)V public fun addBreadcrumb (Lio/sentry/Breadcrumb;Lio/sentry/Hint;)V + public fun addFeatureFlag (Ljava/lang/String;Z)V public fun bindClient (Lio/sentry/ISentryClient;)V public fun captureCheckIn (Lio/sentry/CheckIn;)Lio/sentry/protocol/SentryId; public fun captureEnvelope (Lio/sentry/SentryEnvelope;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; @@ -841,6 +848,7 @@ public abstract interface class io/sentry/IScope { public abstract fun addBreadcrumb (Lio/sentry/Breadcrumb;)V public abstract fun addBreadcrumb (Lio/sentry/Breadcrumb;Lio/sentry/Hint;)V public abstract fun addEventProcessor (Lio/sentry/EventProcessor;)V + public abstract fun addFeatureFlag (Ljava/lang/String;Z)V public abstract fun assignTraceContext (Lio/sentry/SentryEvent;)V public abstract fun bindClient (Lio/sentry/ISentryClient;)V public abstract fun clear ()V @@ -857,6 +865,8 @@ public abstract interface class io/sentry/IScope { public abstract fun getEventProcessors ()Ljava/util/List; public abstract fun getEventProcessorsWithOrder ()Ljava/util/List; public abstract fun getExtras ()Ljava/util/Map; + public abstract fun getFeatureFlagBuffer ()Lio/sentry/featureflags/IFeatureFlagBuffer; + public abstract fun getFeatureFlags ()Lio/sentry/protocol/FeatureFlags; public abstract fun getFingerprint ()Ljava/util/List; public abstract fun getLastEventId ()Lio/sentry/protocol/SentryId; public abstract fun getLevel ()Lio/sentry/SentryLevel; @@ -926,6 +936,7 @@ public abstract interface class io/sentry/IScopes { public abstract fun addBreadcrumb (Lio/sentry/Breadcrumb;Lio/sentry/Hint;)V public fun addBreadcrumb (Ljava/lang/String;)V public fun addBreadcrumb (Ljava/lang/String;Ljava/lang/String;)V + public abstract fun addFeatureFlag (Ljava/lang/String;Z)V public abstract fun bindClient (Lio/sentry/ISentryClient;)V public abstract fun captureCheckIn (Lio/sentry/CheckIn;)Lio/sentry/protocol/SentryId; public fun captureEnvelope (Lio/sentry/SentryEnvelope;)Lio/sentry/protocol/SentryId; @@ -1507,6 +1518,7 @@ public final class io/sentry/NoOpEnvelopeReader : io/sentry/IEnvelopeReader { public final class io/sentry/NoOpHub : io/sentry/IHub { public fun addBreadcrumb (Lio/sentry/Breadcrumb;)V public fun addBreadcrumb (Lio/sentry/Breadcrumb;Lio/sentry/Hint;)V + public fun addFeatureFlag (Ljava/lang/String;Z)V public fun bindClient (Lio/sentry/ISentryClient;)V public fun captureCheckIn (Lio/sentry/CheckIn;)Lio/sentry/protocol/SentryId; public fun captureEnvelope (Lio/sentry/SentryEnvelope;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; @@ -1608,6 +1620,7 @@ public final class io/sentry/NoOpScope : io/sentry/IScope { public fun addBreadcrumb (Lio/sentry/Breadcrumb;)V public fun addBreadcrumb (Lio/sentry/Breadcrumb;Lio/sentry/Hint;)V public fun addEventProcessor (Lio/sentry/EventProcessor;)V + public fun addFeatureFlag (Ljava/lang/String;Z)V public fun assignTraceContext (Lio/sentry/SentryEvent;)V public fun bindClient (Lio/sentry/ISentryClient;)V public fun clear ()V @@ -1625,6 +1638,8 @@ public final class io/sentry/NoOpScope : io/sentry/IScope { public fun getEventProcessors ()Ljava/util/List; public fun getEventProcessorsWithOrder ()Ljava/util/List; public fun getExtras ()Ljava/util/Map; + public fun getFeatureFlagBuffer ()Lio/sentry/featureflags/IFeatureFlagBuffer; + public fun getFeatureFlags ()Lio/sentry/protocol/FeatureFlags; public fun getFingerprint ()Ljava/util/List; public static fun getInstance ()Lio/sentry/NoOpScope; public fun getLastEventId ()Lio/sentry/protocol/SentryId; @@ -1674,6 +1689,7 @@ public final class io/sentry/NoOpScope : io/sentry/IScope { public final class io/sentry/NoOpScopes : io/sentry/IScopes { public fun addBreadcrumb (Lio/sentry/Breadcrumb;)V public fun addBreadcrumb (Lio/sentry/Breadcrumb;Lio/sentry/Hint;)V + public fun addFeatureFlag (Ljava/lang/String;Z)V public fun bindClient (Lio/sentry/ISentryClient;)V public fun captureCheckIn (Lio/sentry/CheckIn;)Lio/sentry/protocol/SentryId; public fun captureEnvelope (Lio/sentry/SentryEnvelope;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; @@ -2267,6 +2283,7 @@ public final class io/sentry/Scope : io/sentry/IScope { public fun addBreadcrumb (Lio/sentry/Breadcrumb;)V public fun addBreadcrumb (Lio/sentry/Breadcrumb;Lio/sentry/Hint;)V public fun addEventProcessor (Lio/sentry/EventProcessor;)V + public fun addFeatureFlag (Ljava/lang/String;Z)V public fun assignTraceContext (Lio/sentry/SentryEvent;)V public fun bindClient (Lio/sentry/ISentryClient;)V public fun clear ()V @@ -2284,6 +2301,8 @@ public final class io/sentry/Scope : io/sentry/IScope { public fun getEventProcessors ()Ljava/util/List; public fun getEventProcessorsWithOrder ()Ljava/util/List; public fun getExtras ()Ljava/util/Map; + public fun getFeatureFlagBuffer ()Lio/sentry/featureflags/IFeatureFlagBuffer; + public fun getFeatureFlags ()Lio/sentry/protocol/FeatureFlags; public fun getFingerprint ()Ljava/util/List; public fun getLastEventId ()Lio/sentry/protocol/SentryId; public fun getLevel ()Lio/sentry/SentryLevel; @@ -2382,6 +2401,7 @@ public final class io/sentry/Scopes : io/sentry/IScopes { public fun (Lio/sentry/IScope;Lio/sentry/IScope;Lio/sentry/IScope;Ljava/lang/String;)V public fun addBreadcrumb (Lio/sentry/Breadcrumb;)V public fun addBreadcrumb (Lio/sentry/Breadcrumb;Lio/sentry/Hint;)V + public fun addFeatureFlag (Ljava/lang/String;Z)V public fun bindClient (Lio/sentry/ISentryClient;)V public fun captureCheckIn (Lio/sentry/CheckIn;)Lio/sentry/protocol/SentryId; public fun captureEnvelope (Lio/sentry/SentryEnvelope;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; @@ -2453,6 +2473,7 @@ public final class io/sentry/Scopes : io/sentry/IScopes { public final class io/sentry/ScopesAdapter : io/sentry/IScopes { public fun addBreadcrumb (Lio/sentry/Breadcrumb;)V public fun addBreadcrumb (Lio/sentry/Breadcrumb;Lio/sentry/Hint;)V + public fun addFeatureFlag (Ljava/lang/String;Z)V public fun bindClient (Lio/sentry/ISentryClient;)V public fun captureCheckIn (Lio/sentry/CheckIn;)Lio/sentry/protocol/SentryId; public fun captureEnvelope (Lio/sentry/SentryEnvelope;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; @@ -2563,6 +2584,7 @@ public final class io/sentry/Sentry { public static fun addBreadcrumb (Lio/sentry/Breadcrumb;Lio/sentry/Hint;)V public static fun addBreadcrumb (Ljava/lang/String;)V public static fun addBreadcrumb (Ljava/lang/String;Ljava/lang/String;)V + public static fun addFeatureFlag (Ljava/lang/String;Z)V public static fun bindClient (Lio/sentry/ISentryClient;)V public static fun captureCheckIn (Lio/sentry/CheckIn;)Lio/sentry/protocol/SentryId; public static fun captureEvent (Lio/sentry/SentryEvent;)Lio/sentry/protocol/SentryId; @@ -3373,6 +3395,7 @@ public class io/sentry/SentryOptions { public fun getMaxBreadcrumbs ()I public fun getMaxCacheItems ()I public fun getMaxDepth ()I + public fun getMaxFeatureFlags ()I public fun getMaxQueueSize ()I public fun getMaxRequestBodySize ()Lio/sentry/SentryOptions$RequestSize; public fun getMaxSpans ()I @@ -3519,6 +3542,7 @@ public class io/sentry/SentryOptions { public fun setMaxBreadcrumbs (I)V public fun setMaxCacheItems (I)V public fun setMaxDepth (I)V + public fun setMaxFeatureFlags (I)V public fun setMaxQueueSize (I)V public fun setMaxRequestBodySize (Lio/sentry/SentryOptions$RequestSize;)V public fun setMaxSpans (I)V @@ -4693,6 +4717,30 @@ public final class io/sentry/exception/SentryHttpClientException : java/lang/Exc public fun (Ljava/lang/String;)V } +public final class io/sentry/featureflags/FeatureFlagBuffer : io/sentry/featureflags/IFeatureFlagBuffer { + public fun add (Ljava/lang/String;Z)V + public fun clone ()Lio/sentry/featureflags/IFeatureFlagBuffer; + public synthetic fun clone ()Ljava/lang/Object; + public static fun create (Lio/sentry/SentryOptions;)Lio/sentry/featureflags/IFeatureFlagBuffer; + public fun getFeatureFlags ()Lio/sentry/protocol/FeatureFlags; + public static fun merged (Lio/sentry/SentryOptions;Lio/sentry/featureflags/IFeatureFlagBuffer;Lio/sentry/featureflags/IFeatureFlagBuffer;Lio/sentry/featureflags/IFeatureFlagBuffer;)Lio/sentry/featureflags/IFeatureFlagBuffer; +} + +public abstract interface class io/sentry/featureflags/IFeatureFlagBuffer { + public abstract fun add (Ljava/lang/String;Z)V + public abstract fun clone ()Lio/sentry/featureflags/IFeatureFlagBuffer; + public abstract fun getFeatureFlags ()Lio/sentry/protocol/FeatureFlags; +} + +public final class io/sentry/featureflags/NoOpFeatureFlagBuffer : io/sentry/featureflags/IFeatureFlagBuffer { + public fun ()V + public fun add (Ljava/lang/String;Z)V + public fun clone ()Lio/sentry/featureflags/IFeatureFlagBuffer; + public synthetic fun clone ()Ljava/lang/Object; + public fun getFeatureFlags ()Lio/sentry/protocol/FeatureFlags; + public static fun getInstance ()Lio/sentry/featureflags/NoOpFeatureFlagBuffer; +} + public abstract interface class io/sentry/hints/AbnormalExit { public abstract fun ignoreCurrentThread ()Z public abstract fun mechanism ()Ljava/lang/String; @@ -5164,6 +5212,7 @@ public class io/sentry/protocol/Contexts : io/sentry/JsonSerializable { public fun getApp ()Lio/sentry/protocol/App; public fun getBrowser ()Lio/sentry/protocol/Browser; public fun getDevice ()Lio/sentry/protocol/Device; + public fun getFeatureFlags ()Lio/sentry/protocol/FeatureFlags; public fun getFeedback ()Lio/sentry/protocol/Feedback; public fun getGpu ()Lio/sentry/protocol/Gpu; public fun getOperatingSystem ()Lio/sentry/protocol/OperatingSystem; @@ -5185,6 +5234,7 @@ public class io/sentry/protocol/Contexts : io/sentry/JsonSerializable { public fun setApp (Lio/sentry/protocol/App;)V public fun setBrowser (Lio/sentry/protocol/Browser;)V public fun setDevice (Lio/sentry/protocol/Device;)V + public fun setFeatureFlags (Lio/sentry/protocol/FeatureFlags;)V public fun setFeedback (Lio/sentry/protocol/Feedback;)V public fun setGpu (Lio/sentry/protocol/Gpu;)V public fun setOperatingSystem (Lio/sentry/protocol/OperatingSystem;)V @@ -5410,6 +5460,55 @@ public final class io/sentry/protocol/Device$JsonKeys { public fun ()V } +public final class io/sentry/protocol/FeatureFlag : io/sentry/JsonSerializable, io/sentry/JsonUnknown { + public fun (Ljava/lang/String;Z)V + public fun equals (Ljava/lang/Object;)Z + public fun getFlag ()Ljava/lang/String; + public fun getResult ()Ljava/lang/Boolean; + public fun getUnknown ()Ljava/util/Map; + public fun hashCode ()I + public fun serialize (Lio/sentry/ObjectWriter;Lio/sentry/ILogger;)V + public fun setFlag (Ljava/lang/String;)V + public fun setResult (Ljava/lang/Boolean;)V + public fun setUnknown (Ljava/util/Map;)V +} + +public final class io/sentry/protocol/FeatureFlag$Deserializer : io/sentry/JsonDeserializer { + public fun ()V + public fun deserialize (Lio/sentry/ObjectReader;Lio/sentry/ILogger;)Lio/sentry/protocol/FeatureFlag; + public synthetic fun deserialize (Lio/sentry/ObjectReader;Lio/sentry/ILogger;)Ljava/lang/Object; +} + +public final class io/sentry/protocol/FeatureFlag$JsonKeys { + public static final field FLAG Ljava/lang/String; + public static final field RESULT Ljava/lang/String; + public fun ()V +} + +public final class io/sentry/protocol/FeatureFlags : io/sentry/JsonSerializable, io/sentry/JsonUnknown { + public static final field TYPE Ljava/lang/String; + public fun ()V + public fun (Ljava/util/List;)V + public fun equals (Ljava/lang/Object;)Z + public fun getUnknown ()Ljava/util/Map; + public fun getValues ()Ljava/util/List; + public fun hashCode ()I + public fun serialize (Lio/sentry/ObjectWriter;Lio/sentry/ILogger;)V + public fun setUnknown (Ljava/util/Map;)V + public fun setValues (Ljava/util/List;)V +} + +public final class io/sentry/protocol/FeatureFlags$Deserializer : io/sentry/JsonDeserializer { + public fun ()V + public fun deserialize (Lio/sentry/ObjectReader;Lio/sentry/ILogger;)Lio/sentry/protocol/FeatureFlags; + public synthetic fun deserialize (Lio/sentry/ObjectReader;Lio/sentry/ILogger;)Ljava/lang/Object; +} + +public final class io/sentry/protocol/FeatureFlags$JsonKeys { + public static final field VALUES Ljava/lang/String; + public fun ()V +} + public final class io/sentry/protocol/Feedback : io/sentry/JsonSerializable, io/sentry/JsonUnknown { public static final field TYPE Ljava/lang/String; public fun (Lio/sentry/protocol/Feedback;)V diff --git a/sentry/src/main/java/io/sentry/CombinedContextsView.java b/sentry/src/main/java/io/sentry/CombinedContextsView.java index 31b5c06062..3cb10e88b8 100644 --- a/sentry/src/main/java/io/sentry/CombinedContextsView.java +++ b/sentry/src/main/java/io/sentry/CombinedContextsView.java @@ -4,6 +4,7 @@ import io.sentry.protocol.Browser; import io.sentry.protocol.Contexts; import io.sentry.protocol.Device; +import io.sentry.protocol.FeatureFlags; import io.sentry.protocol.Gpu; import io.sentry.protocol.OperatingSystem; import io.sentry.protocol.Response; @@ -14,6 +15,7 @@ import java.util.Enumeration; import java.util.Map; import java.util.Set; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -225,6 +227,27 @@ public void setSpring(@NotNull Spring spring) { getDefaultContexts().setSpring(spring); } + @Override + public @Nullable FeatureFlags getFeatureFlags() { + // these are not intended to be set on a scopes Context directly + final @Nullable FeatureFlags current = currentContexts.getFeatureFlags(); + if (current != null) { + return current; + } + final @Nullable FeatureFlags isolation = isolationContexts.getFeatureFlags(); + if (isolation != null) { + return isolation; + } + return globalContexts.getFeatureFlags(); + } + + @ApiStatus.Internal + @Override + /** Not intended to be set on a scopes Context directly */ + public void setFeatureFlags(@NotNull FeatureFlags spring) { + getDefaultContexts().setFeatureFlags(spring); + } + @Override public int size() { return mergeContexts().size(); diff --git a/sentry/src/main/java/io/sentry/CombinedScopeView.java b/sentry/src/main/java/io/sentry/CombinedScopeView.java index d6ac5b824a..4b1faddb56 100644 --- a/sentry/src/main/java/io/sentry/CombinedScopeView.java +++ b/sentry/src/main/java/io/sentry/CombinedScopeView.java @@ -2,8 +2,11 @@ import static io.sentry.Scope.createBreadcrumbsList; +import io.sentry.featureflags.FeatureFlagBuffer; +import io.sentry.featureflags.IFeatureFlagBuffer; import io.sentry.internal.eventprocessor.EventProcessorAndOrder; import io.sentry.protocol.Contexts; +import io.sentry.protocol.FeatureFlags; import io.sentry.protocol.Request; import io.sentry.protocol.SentryId; import io.sentry.protocol.User; @@ -507,4 +510,23 @@ public void replaceOptions(@NotNull SentryOptions options) { public void setReplayId(@NotNull SentryId replayId) { getDefaultWriteScope().setReplayId(replayId); } + + @Override + public void addFeatureFlag(final @NotNull String flag, final boolean result) { + getDefaultWriteScope().addFeatureFlag(flag, result); + } + + @Override + public @Nullable FeatureFlags getFeatureFlags() { + return getFeatureFlagBuffer().getFeatureFlags(); + } + + @Override + public @NotNull IFeatureFlagBuffer getFeatureFlagBuffer() { + return FeatureFlagBuffer.merged( + getOptions(), + globalScope.getFeatureFlagBuffer(), + isolationScope.getFeatureFlagBuffer(), + scope.getFeatureFlagBuffer()); + } } diff --git a/sentry/src/main/java/io/sentry/HubAdapter.java b/sentry/src/main/java/io/sentry/HubAdapter.java index f9065dd64c..f92b477cbe 100644 --- a/sentry/src/main/java/io/sentry/HubAdapter.java +++ b/sentry/src/main/java/io/sentry/HubAdapter.java @@ -388,4 +388,9 @@ public void reportFullyDisplayed() { public @NotNull ILoggerApi logger() { return Sentry.getCurrentScopes().logger(); } + + @Override + public void addFeatureFlag(final @NotNull String flag, final boolean result) { + Sentry.addFeatureFlag(flag, result); + } } diff --git a/sentry/src/main/java/io/sentry/HubScopesWrapper.java b/sentry/src/main/java/io/sentry/HubScopesWrapper.java index 4430402af0..818d62f6b9 100644 --- a/sentry/src/main/java/io/sentry/HubScopesWrapper.java +++ b/sentry/src/main/java/io/sentry/HubScopesWrapper.java @@ -373,4 +373,9 @@ public void reportFullyDisplayed() { public @NotNull ILoggerApi logger() { return scopes.logger(); } + + @Override + public void addFeatureFlag(final @NotNull String flag, final boolean result) { + scopes.addFeatureFlag(flag, result); + } } diff --git a/sentry/src/main/java/io/sentry/IScope.java b/sentry/src/main/java/io/sentry/IScope.java index ddabd00569..4617b4cf31 100644 --- a/sentry/src/main/java/io/sentry/IScope.java +++ b/sentry/src/main/java/io/sentry/IScope.java @@ -1,7 +1,9 @@ package io.sentry; +import io.sentry.featureflags.IFeatureFlagBuffer; import io.sentry.internal.eventprocessor.EventProcessorAndOrder; import io.sentry.protocol.Contexts; +import io.sentry.protocol.FeatureFlags; import io.sentry.protocol.Request; import io.sentry.protocol.SentryId; import io.sentry.protocol.User; @@ -422,4 +424,14 @@ void setSpanContext( @ApiStatus.Internal void replaceOptions(final @NotNull SentryOptions options); + + void addFeatureFlag(final @NotNull String flag, final boolean result); + + @ApiStatus.Internal + @Nullable + FeatureFlags getFeatureFlags(); + + @ApiStatus.Internal + @NotNull + IFeatureFlagBuffer getFeatureFlagBuffer(); } diff --git a/sentry/src/main/java/io/sentry/IScopes.java b/sentry/src/main/java/io/sentry/IScopes.java index 0fbc100859..465fbc5eb3 100644 --- a/sentry/src/main/java/io/sentry/IScopes.java +++ b/sentry/src/main/java/io/sentry/IScopes.java @@ -743,4 +743,6 @@ default boolean isNoOp() { @NotNull ILoggerApi logger(); + + void addFeatureFlag(final @NotNull String flag, final boolean result); } diff --git a/sentry/src/main/java/io/sentry/NoOpHub.java b/sentry/src/main/java/io/sentry/NoOpHub.java index c6b31c1a5c..0292d1a82d 100644 --- a/sentry/src/main/java/io/sentry/NoOpHub.java +++ b/sentry/src/main/java/io/sentry/NoOpHub.java @@ -330,4 +330,7 @@ public boolean isNoOp() { public @NotNull ILoggerApi logger() { return NoOpLoggerApi.getInstance(); } + + @Override + public void addFeatureFlag(final @NotNull String flag, final boolean result) {} } diff --git a/sentry/src/main/java/io/sentry/NoOpScope.java b/sentry/src/main/java/io/sentry/NoOpScope.java index dd1a202b54..617269d3a2 100644 --- a/sentry/src/main/java/io/sentry/NoOpScope.java +++ b/sentry/src/main/java/io/sentry/NoOpScope.java @@ -1,7 +1,10 @@ package io.sentry; +import io.sentry.featureflags.IFeatureFlagBuffer; +import io.sentry.featureflags.NoOpFeatureFlagBuffer; import io.sentry.internal.eventprocessor.EventProcessorAndOrder; import io.sentry.protocol.Contexts; +import io.sentry.protocol.FeatureFlags; import io.sentry.protocol.Request; import io.sentry.protocol.SentryId; import io.sentry.protocol.User; @@ -296,4 +299,17 @@ public void setSpanContext( @Override public void replaceOptions(@NotNull SentryOptions options) {} + + @Override + public void addFeatureFlag(final @NotNull String flag, final boolean result) {} + + @Override + public @Nullable FeatureFlags getFeatureFlags() { + return null; + } + + @Override + public @NotNull IFeatureFlagBuffer getFeatureFlagBuffer() { + return NoOpFeatureFlagBuffer.getInstance(); + } } diff --git a/sentry/src/main/java/io/sentry/NoOpScopes.java b/sentry/src/main/java/io/sentry/NoOpScopes.java index 4e039a7b50..5d3447dd7c 100644 --- a/sentry/src/main/java/io/sentry/NoOpScopes.java +++ b/sentry/src/main/java/io/sentry/NoOpScopes.java @@ -328,4 +328,7 @@ public boolean isNoOp() { public @NotNull ILoggerApi logger() { return NoOpLoggerApi.getInstance(); } + + @Override + public void addFeatureFlag(final @NotNull String flag, final boolean result) {} } diff --git a/sentry/src/main/java/io/sentry/Scope.java b/sentry/src/main/java/io/sentry/Scope.java index 7a54c4c755..6f3f28399a 100644 --- a/sentry/src/main/java/io/sentry/Scope.java +++ b/sentry/src/main/java/io/sentry/Scope.java @@ -1,8 +1,11 @@ package io.sentry; +import io.sentry.featureflags.FeatureFlagBuffer; +import io.sentry.featureflags.IFeatureFlagBuffer; import io.sentry.internal.eventprocessor.EventProcessorAndOrder; import io.sentry.protocol.App; import io.sentry.protocol.Contexts; +import io.sentry.protocol.FeatureFlags; import io.sentry.protocol.Request; import io.sentry.protocol.SentryId; import io.sentry.protocol.TransactionNameSource; @@ -103,6 +106,8 @@ public final class Scope implements IScope { private final @NotNull Map, String>> throwableToSpan = Collections.synchronizedMap(new WeakHashMap<>()); + private final @NotNull IFeatureFlagBuffer featureFlags; + /** * Scope's ctor * @@ -111,6 +116,7 @@ public final class Scope implements IScope { public Scope(final @NotNull SentryOptions options) { this.options = Objects.requireNonNull(options, "SentryOptions is required."); this.breadcrumbs = createBreadcrumbsList(this.options.getMaxBreadcrumbs()); + this.featureFlags = FeatureFlagBuffer.create(options); this.propagationContext = new PropagationContext(); this.lastEventId = SentryId.EMPTY_ID; } @@ -173,6 +179,8 @@ private Scope(final @NotNull Scope scope) { this.attachments = new CopyOnWriteArrayList<>(scope.attachments); + this.featureFlags = scope.featureFlags.clone(); + this.propagationContext = new PropagationContext(scope.propagationContext); } @@ -1119,6 +1127,21 @@ public void bindClient(@NotNull ISentryClient client) { return client; } + @Override + public void addFeatureFlag(final @NotNull String flag, final boolean result) { + featureFlags.add(flag, result); + } + + @Override + public @Nullable FeatureFlags getFeatureFlags() { + return featureFlags.getFeatureFlags(); + } + + @Override + public @NotNull IFeatureFlagBuffer getFeatureFlagBuffer() { + return featureFlags; + } + @Override @ApiStatus.Internal public void assignTraceContext(final @NotNull SentryEvent event) { diff --git a/sentry/src/main/java/io/sentry/Scopes.java b/sentry/src/main/java/io/sentry/Scopes.java index 12309e355c..7bf3bb0ca2 100644 --- a/sentry/src/main/java/io/sentry/Scopes.java +++ b/sentry/src/main/java/io/sentry/Scopes.java @@ -1220,6 +1220,11 @@ public void reportFullyDisplayed() { return logger; } + @Override + public void addFeatureFlag(final @NotNull String flag, final boolean result) { + combinedScope.addFeatureFlag(flag, result); + } + private static void validateOptions(final @NotNull SentryOptions options) { Objects.requireNonNull(options, "SentryOptions is required."); if (options.getDsn() == null || options.getDsn().isEmpty()) { diff --git a/sentry/src/main/java/io/sentry/ScopesAdapter.java b/sentry/src/main/java/io/sentry/ScopesAdapter.java index 86d316b967..8e6c8d6581 100644 --- a/sentry/src/main/java/io/sentry/ScopesAdapter.java +++ b/sentry/src/main/java/io/sentry/ScopesAdapter.java @@ -385,4 +385,9 @@ public void reportFullyDisplayed() { public @NotNull ILoggerApi logger() { return Sentry.getCurrentScopes().logger(); } + + @Override + public void addFeatureFlag(final @NotNull String flag, final boolean result) { + Sentry.addFeatureFlag(flag, result); + } } diff --git a/sentry/src/main/java/io/sentry/Sentry.java b/sentry/src/main/java/io/sentry/Sentry.java index 4aee71715d..5bf2063d73 100644 --- a/sentry/src/main/java/io/sentry/Sentry.java +++ b/sentry/src/main/java/io/sentry/Sentry.java @@ -1372,4 +1372,8 @@ public static void showUserFeedbackDialog( final @NotNull SentryOptions options = getCurrentScopes().getOptions(); options.getFeedbackOptions().getDialogHandler().showDialog(associatedEventId, configurator); } + + public static void addFeatureFlag(final @NotNull String flag, final boolean result) { + getCurrentScopes().addFeatureFlag(flag, result); + } } diff --git a/sentry/src/main/java/io/sentry/SentryClient.java b/sentry/src/main/java/io/sentry/SentryClient.java index bfcf4e780b..19529c550f 100644 --- a/sentry/src/main/java/io/sentry/SentryClient.java +++ b/sentry/src/main/java/io/sentry/SentryClient.java @@ -13,6 +13,7 @@ import io.sentry.logger.NoOpLoggerBatchProcessor; import io.sentry.protocol.Contexts; import io.sentry.protocol.DebugMeta; +import io.sentry.protocol.FeatureFlags; import io.sentry.protocol.Feedback; import io.sentry.protocol.SentryId; import io.sentry.protocol.SentryTransaction; @@ -1250,6 +1251,13 @@ public void captureBatchedLogEvents(final @NotNull SentryLogEvents logEvents) { } } + if (event.getContexts().getFeatureFlags() == null) { + final @Nullable FeatureFlags featureFlags = scope.getFeatureFlags(); + if (featureFlags != null) { + event.getContexts().setFeatureFlags(featureFlags); + } + } + event = processEvent(event, hint, scope.getEventProcessors()); } return event; diff --git a/sentry/src/main/java/io/sentry/SentryOptions.java b/sentry/src/main/java/io/sentry/SentryOptions.java index f868ecacad..6a488473b0 100644 --- a/sentry/src/main/java/io/sentry/SentryOptions.java +++ b/sentry/src/main/java/io/sentry/SentryOptions.java @@ -196,6 +196,11 @@ public class SentryOptions { */ private int maxBreadcrumbs = 100; + /** + * This variable controls the total amount of feature flags that should be captured Default is 100 + */ + private int maxFeatureFlags = 100; + /** Sets the release. SDK will try to automatically configure a release out of the box */ private @Nullable String release; @@ -1025,6 +1030,24 @@ public void setMaxBreadcrumbs(int maxBreadcrumbs) { this.maxBreadcrumbs = maxBreadcrumbs; } + /** + * Returns the max feature flags Default is 100 + * + * @return the max feature flags + */ + public int getMaxFeatureFlags() { + return maxFeatureFlags; + } + + /** + * Sets the max feature flags Default is 100 + * + * @param maxFeatureFlags the max feature flags + */ + public void setMaxFeatureFlags(int maxFeatureFlags) { + this.maxFeatureFlags = maxFeatureFlags; + } + /** * Returns the release * diff --git a/sentry/src/main/java/io/sentry/featureflags/FeatureFlagBuffer.java b/sentry/src/main/java/io/sentry/featureflags/FeatureFlagBuffer.java new file mode 100644 index 0000000000..338e6c0e8a --- /dev/null +++ b/sentry/src/main/java/io/sentry/featureflags/FeatureFlagBuffer.java @@ -0,0 +1,207 @@ +package io.sentry.featureflags; + +import io.sentry.ISentryLifecycleToken; +import io.sentry.ScopeType; +import io.sentry.SentryOptions; +import io.sentry.protocol.FeatureFlag; +import io.sentry.protocol.FeatureFlags; +import io.sentry.util.AutoClosableReentrantLock; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +@ApiStatus.Internal +public final class FeatureFlagBuffer implements IFeatureFlagBuffer { + + private volatile @NotNull CopyOnWriteArrayList flags; + private final @NotNull AutoClosableReentrantLock lock = new AutoClosableReentrantLock(); + private int maxSize; + + private FeatureFlagBuffer(int maxSize) { + this.maxSize = maxSize; + this.flags = new CopyOnWriteArrayList<>(); + } + + private FeatureFlagBuffer( + int maxSize, final @NotNull CopyOnWriteArrayList flags) { + this.maxSize = maxSize; + this.flags = flags; + } + + private FeatureFlagBuffer(@NotNull FeatureFlagBuffer other) { + this.maxSize = other.maxSize; + this.flags = other.flags; + } + + @Override + public void add(@NotNull String flag, boolean result) { + try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { + final int size = flags.size(); + final @NotNull ArrayList tmpList = new ArrayList<>(size + 1); + for (FeatureFlagEntry entry : flags) { + if (!entry.flag.equals(flag)) { + tmpList.add(entry); + } + } + tmpList.add(new FeatureFlagEntry(flag, result, System.nanoTime())); + + if (tmpList.size() > maxSize) { + tmpList.remove(0); + } + + flags = new CopyOnWriteArrayList<>(tmpList); + } + } + + @Override + public @NotNull FeatureFlags getFeatureFlags() { + List featureFlags = new ArrayList<>(); + for (FeatureFlagEntry entry : flags) { + featureFlags.add(entry.toFeatureFlag()); + } + return new FeatureFlags(featureFlags); + } + + @Override + public IFeatureFlagBuffer clone() { + return new FeatureFlagBuffer(this); + } + + public static @NotNull IFeatureFlagBuffer create(final @NotNull SentryOptions options) { + final int maxFeatureFlags = options.getMaxFeatureFlags(); + if (maxFeatureFlags > 0) { + return new FeatureFlagBuffer(maxFeatureFlags); + } else { + return NoOpFeatureFlagBuffer.getInstance(); + } + } + + public static @NotNull IFeatureFlagBuffer merged( + final @NotNull SentryOptions options, + final @Nullable IFeatureFlagBuffer globalBuffer, + final @Nullable IFeatureFlagBuffer isolationBuffer, + final @Nullable IFeatureFlagBuffer currentBuffer) { + final int maxSize = options.getMaxFeatureFlags(); + if (maxSize <= 0) { + return NoOpFeatureFlagBuffer.getInstance(); + } + + return merged( + maxSize, + globalBuffer instanceof FeatureFlagBuffer ? (FeatureFlagBuffer) globalBuffer : null, + isolationBuffer instanceof FeatureFlagBuffer ? (FeatureFlagBuffer) isolationBuffer : null, + currentBuffer instanceof FeatureFlagBuffer ? (FeatureFlagBuffer) currentBuffer : null); + } + + private static @NotNull IFeatureFlagBuffer merged( + final int maxSize, + final @Nullable FeatureFlagBuffer globalBuffer, + final @Nullable FeatureFlagBuffer isolationBuffer, + final @Nullable FeatureFlagBuffer currentBuffer) { + + // Capture references to avoid inconsistencies from concurrent modifications + final @Nullable CopyOnWriteArrayList globalFlags = + globalBuffer == null ? null : globalBuffer.flags; + final @Nullable CopyOnWriteArrayList isolationFlags = + isolationBuffer == null ? null : isolationBuffer.flags; + final @Nullable CopyOnWriteArrayList currentFlags = + currentBuffer == null ? null : currentBuffer.flags; + + final int globalSize = globalFlags == null ? 0 : globalFlags.size(); + final int isolationSize = isolationFlags == null ? 0 : isolationFlags.size(); + final int currentSize = currentFlags == null ? 0 : currentFlags.size(); + + // Early exit if all buffers are empty + if (globalSize == 0 && isolationSize == 0 && currentSize == 0) { + return NoOpFeatureFlagBuffer.getInstance(); + } + + int globalIndex = globalSize - 1; + int isolationIndex = isolationSize - 1; + int currentIndex = currentSize - 1; + + final @NotNull java.util.Map uniqueFlags = + new java.util.LinkedHashMap<>(maxSize); + + // check if there is still room and remaining items to check + while (uniqueFlags.size() < maxSize + && (globalIndex >= 0 || isolationIndex >= 0 || currentIndex >= 0)) { + final FeatureFlagEntry globalEntry = + (globalFlags != null && globalIndex >= 0) ? globalFlags.get(globalIndex) : null; + final FeatureFlagEntry isolationEntry = + (isolationFlags != null && isolationIndex >= 0) + ? isolationFlags.get(isolationIndex) + : null; + final FeatureFlagEntry currentEntry = + (currentFlags != null && currentIndex >= 0) ? currentFlags.get(currentIndex) : null; + + @Nullable FeatureFlagEntry entryToAdd = null; + @Nullable ScopeType selectedBuffer = null; + + // choose newest entry across all buffers + if (globalEntry != null && (entryToAdd == null || globalEntry.nanos > entryToAdd.nanos)) { + entryToAdd = globalEntry; + selectedBuffer = ScopeType.GLOBAL; + } + if (isolationEntry != null + && (entryToAdd == null || isolationEntry.nanos > entryToAdd.nanos)) { + entryToAdd = isolationEntry; + selectedBuffer = ScopeType.ISOLATION; + } + if (currentEntry != null && (entryToAdd == null || currentEntry.nanos > entryToAdd.nanos)) { + entryToAdd = currentEntry; + selectedBuffer = ScopeType.CURRENT; + } + + if (entryToAdd != null) { + // no need to update existing entries since we already have the latest + if (!uniqueFlags.containsKey(entryToAdd.flag)) { + uniqueFlags.put(entryToAdd.flag, entryToAdd); + } + + // decrement only index of buffer that was selected + if (ScopeType.CURRENT.equals(selectedBuffer)) { + currentIndex--; + } else if (ScopeType.ISOLATION.equals(selectedBuffer)) { + isolationIndex--; + } else if (ScopeType.GLOBAL.equals(selectedBuffer)) { + globalIndex--; + } + } else { + // no need to look any further since lists are sorted and we could not find any newer + // entries anymore + break; + } + } + + // Convert to list in reverse order (oldest first, newest last) + final @NotNull List resultList = new ArrayList<>(uniqueFlags.values()); + Collections.reverse(resultList); + return new FeatureFlagBuffer(maxSize, new CopyOnWriteArrayList<>(resultList)); + } + + private static class FeatureFlagEntry { + + private final @NotNull String flag; + private final boolean result; + + @SuppressWarnings("UnusedVariable") + @NotNull + private final Long nanos; + + public FeatureFlagEntry( + final @NotNull String flag, final boolean result, final @NotNull Long nanos) { + this.flag = flag; + this.result = result; + this.nanos = nanos; + } + + public @NotNull FeatureFlag toFeatureFlag() { + return new FeatureFlag(flag, result); + } + } +} diff --git a/sentry/src/main/java/io/sentry/featureflags/IFeatureFlagBuffer.java b/sentry/src/main/java/io/sentry/featureflags/IFeatureFlagBuffer.java new file mode 100644 index 0000000000..39878759e5 --- /dev/null +++ b/sentry/src/main/java/io/sentry/featureflags/IFeatureFlagBuffer.java @@ -0,0 +1,17 @@ +package io.sentry.featureflags; + +import io.sentry.protocol.FeatureFlags; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +@ApiStatus.Internal +public interface IFeatureFlagBuffer { + void add(@NotNull String flag, boolean result); + + @Nullable + FeatureFlags getFeatureFlags(); + + @NotNull + IFeatureFlagBuffer clone(); +} diff --git a/sentry/src/main/java/io/sentry/featureflags/NoOpFeatureFlagBuffer.java b/sentry/src/main/java/io/sentry/featureflags/NoOpFeatureFlagBuffer.java new file mode 100644 index 0000000000..82f5709c0b --- /dev/null +++ b/sentry/src/main/java/io/sentry/featureflags/NoOpFeatureFlagBuffer.java @@ -0,0 +1,28 @@ +package io.sentry.featureflags; + +import io.sentry.protocol.FeatureFlags; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +@ApiStatus.Internal +public final class NoOpFeatureFlagBuffer implements IFeatureFlagBuffer { + private static final NoOpFeatureFlagBuffer instance = new NoOpFeatureFlagBuffer(); + + public static NoOpFeatureFlagBuffer getInstance() { + return instance; + } + + @Override + public void add(@NotNull String flag, boolean result) {} + + @Override + public @Nullable FeatureFlags getFeatureFlags() { + return null; + } + + @Override + public @NotNull IFeatureFlagBuffer clone() { + return instance; + } +} diff --git a/sentry/src/main/java/io/sentry/protocol/Contexts.java b/sentry/src/main/java/io/sentry/protocol/Contexts.java index e97431db4d..553f4ddbd3 100644 --- a/sentry/src/main/java/io/sentry/protocol/Contexts.java +++ b/sentry/src/main/java/io/sentry/protocol/Contexts.java @@ -181,6 +181,14 @@ public void setSpring(final @NotNull Spring spring) { this.put(Spring.TYPE, spring); } + public @Nullable FeatureFlags getFeatureFlags() { + return toContextType(FeatureFlags.TYPE, FeatureFlags.class); + } + + public void setFeatureFlags(final @NotNull FeatureFlags featureFlags) { + this.put(FeatureFlags.TYPE, featureFlags); + } + public int size() { // since this used to extend map return internalStorage.size(); @@ -339,6 +347,9 @@ public static final class Deserializer implements JsonDeserializer { case Spring.TYPE: contexts.setSpring(new Spring.Deserializer().deserialize(reader, logger)); break; + case FeatureFlags.TYPE: + contexts.setFeatureFlags(new FeatureFlags.Deserializer().deserialize(reader, logger)); + break; default: Object object = reader.nextObjectOrNull(); if (object != null) { diff --git a/sentry/src/main/java/io/sentry/protocol/FeatureFlag.java b/sentry/src/main/java/io/sentry/protocol/FeatureFlag.java new file mode 100644 index 0000000000..ee849b2fa8 --- /dev/null +++ b/sentry/src/main/java/io/sentry/protocol/FeatureFlag.java @@ -0,0 +1,142 @@ +package io.sentry.protocol; + +import io.sentry.ILogger; +import io.sentry.JsonDeserializer; +import io.sentry.JsonSerializable; +import io.sentry.JsonUnknown; +import io.sentry.ObjectReader; +import io.sentry.ObjectWriter; +import io.sentry.SentryLevel; +import io.sentry.util.Objects; +import io.sentry.vendor.gson.stream.JsonToken; +import java.io.IOException; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public final class FeatureFlag implements JsonUnknown, JsonSerializable { + + /** Name of the feature flag. */ + private @NotNull String flag; + + /** Evaluation result of the feature flag. */ + private boolean result; + + @SuppressWarnings("unused") + private @Nullable Map unknown; + + public FeatureFlag(@NotNull String flag, boolean result) { + this.flag = flag; + this.result = result; + } + + public @NotNull String getFlag() { + return flag; + } + + public void setFlag(final @NotNull String flag) { + this.flag = flag; + } + + @NotNull + public Boolean getResult() { + return result; + } + + public void setResult(final @NotNull Boolean result) { + this.result = result; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + FeatureFlag flag = (FeatureFlag) o; + return Objects.equals(flag, flag.flag) && Objects.equals(result, flag.result); + } + + @Override + public int hashCode() { + return Objects.hash(flag, result); + } + + // region json + + @Nullable + @Override + public Map getUnknown() { + return unknown; + } + + @Override + public void setUnknown(@Nullable Map unknown) { + this.unknown = unknown; + } + + public static final class JsonKeys { + public static final String FLAG = "flag"; + public static final String RESULT = "result"; + } + + @Override + public void serialize(final @NotNull ObjectWriter writer, final @NotNull ILogger logger) + throws IOException { + writer.beginObject(); + + writer.name(JsonKeys.FLAG).value(flag); + writer.name(JsonKeys.RESULT).value(result); + if (unknown != null) { + for (String key : unknown.keySet()) { + Object value = unknown.get(key); + writer.name(key).value(logger, value); + } + } + writer.endObject(); + } + + public static final class Deserializer implements JsonDeserializer { + @SuppressWarnings("unchecked") + @Override + public @NotNull FeatureFlag deserialize(@NotNull ObjectReader reader, @NotNull ILogger logger) + throws Exception { + reader.beginObject(); + @Nullable String flag = null; + @Nullable Boolean result = null; + Map unknown = null; + while (reader.peek() == JsonToken.NAME) { + final String nextName = reader.nextName(); + switch (nextName) { + case JsonKeys.FLAG: + flag = reader.nextStringOrNull(); + break; + case JsonKeys.RESULT: + result = reader.nextBooleanOrNull(); + break; + default: + if (unknown == null) { + unknown = new ConcurrentHashMap<>(); + } + reader.nextUnknown(logger, unknown, nextName); + break; + } + } + if (flag == null) { + String message = "Missing required field \"" + JsonKeys.FLAG + "\""; + Exception exception = new IllegalStateException(message); + logger.log(SentryLevel.ERROR, message, exception); + throw exception; + } + if (result == null) { + String message = "Missing required field \"" + JsonKeys.RESULT + "\""; + Exception exception = new IllegalStateException(message); + logger.log(SentryLevel.ERROR, message, exception); + throw exception; + } + FeatureFlag app = new FeatureFlag(flag, result); + app.setUnknown(unknown); + reader.endObject(); + return app; + } + } +} diff --git a/sentry/src/main/java/io/sentry/protocol/FeatureFlags.java b/sentry/src/main/java/io/sentry/protocol/FeatureFlags.java new file mode 100644 index 0000000000..9e84a21de9 --- /dev/null +++ b/sentry/src/main/java/io/sentry/protocol/FeatureFlags.java @@ -0,0 +1,127 @@ +package io.sentry.protocol; + +import io.sentry.ILogger; +import io.sentry.JsonDeserializer; +import io.sentry.JsonSerializable; +import io.sentry.JsonUnknown; +import io.sentry.ObjectReader; +import io.sentry.ObjectWriter; +import io.sentry.util.CollectionUtils; +import io.sentry.util.Objects; +import io.sentry.vendor.gson.stream.JsonToken; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public final class FeatureFlags implements JsonUnknown, JsonSerializable { + public static final String TYPE = "flags"; + + private @NotNull List values; + + public FeatureFlags() { + this.values = new ArrayList<>(); + } + + FeatureFlags(final @NotNull FeatureFlags featureFlags) { + this.values = featureFlags.values; + this.unknown = CollectionUtils.newConcurrentHashMap(featureFlags.unknown); + } + + public FeatureFlags(final @NotNull List values) { + this.values = values; + } + + @SuppressWarnings("unused") + private @Nullable Map unknown; + + @NotNull + public List getValues() { + return values; + } + + public void setValues(final @NotNull List values) { + this.values = values; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + FeatureFlags flags = (FeatureFlags) o; + return Objects.equals(values, flags.values); + } + + @Override + public int hashCode() { + return Objects.hash(values); + } + + // region json + + @Nullable + @Override + public Map getUnknown() { + return unknown; + } + + @Override + public void setUnknown(@Nullable Map unknown) { + this.unknown = unknown; + } + + public static final class JsonKeys { + public static final String VALUES = "values"; + } + + @Override + public void serialize(final @NotNull ObjectWriter writer, final @NotNull ILogger logger) + throws IOException { + writer.beginObject(); + + writer.name(JsonKeys.VALUES).value(logger, values); + + if (unknown != null) { + for (String key : unknown.keySet()) { + Object value = unknown.get(key); + writer.name(key).value(logger, value); + } + } + writer.endObject(); + } + + public static final class Deserializer implements JsonDeserializer { + @SuppressWarnings("unchecked") + @Override + public @NotNull FeatureFlags deserialize(@NotNull ObjectReader reader, @NotNull ILogger logger) + throws Exception { + reader.beginObject(); + @Nullable List values = null; + Map unknown = null; + while (reader.peek() == JsonToken.NAME) { + final String nextName = reader.nextName(); + switch (nextName) { + case JsonKeys.VALUES: + values = reader.nextListOrNull(logger, new FeatureFlag.Deserializer()); + break; + default: + if (unknown == null) { + unknown = new ConcurrentHashMap<>(); + } + reader.nextUnknown(logger, unknown, nextName); + break; + } + } + if (values == null) { + values = new ArrayList<>(); + } + FeatureFlags flags = new FeatureFlags(values); + flags.setUnknown(unknown); + reader.endObject(); + return flags; + } + } +} diff --git a/sentry/src/test/java/io/sentry/ScopeTest.kt b/sentry/src/test/java/io/sentry/ScopeTest.kt index 8671017eae..e80793f54c 100644 --- a/sentry/src/test/java/io/sentry/ScopeTest.kt +++ b/sentry/src/test/java/io/sentry/ScopeTest.kt @@ -1107,6 +1107,22 @@ class ScopeTest { assertTrue(scope.contexts.isEmpty) } + @Test + fun `feature flags can be added and are deduplicated`() { + val scope = Scope(SentryOptions.empty()) + + scope.addFeatureFlag("flag1", true) + scope.addFeatureFlag("flag1", false) + + val flags = scope.featureFlags + assertNotNull(flags) + assertEquals(1, flags.values.size) + + val flag0 = flags.values.first() + assertEquals("flag1", flag0.flag) + assertFalse(flag0.result) + } + private fun eventProcessor(): EventProcessor = object : EventProcessor { override fun process(event: SentryEvent, hint: Hint): SentryEvent? = event diff --git a/sentry/src/test/java/io/sentry/ScopesTest.kt b/sentry/src/test/java/io/sentry/ScopesTest.kt index 4f0ee526dc..6b1ac51c7e 100644 --- a/sentry/src/test/java/io/sentry/ScopesTest.kt +++ b/sentry/src/test/java/io/sentry/ScopesTest.kt @@ -3083,6 +3083,44 @@ class ScopesTest { assertTrue(scopes.globalScope.extras.isEmpty()) } + @Test + fun `feature flags can be added to scopes`() { + val (sut, mockClient) = getEnabledScopes() + + sut.addFeatureFlag("test-feature-flag", true) + sut.scope.addFeatureFlag("current-feature-flag", true) + sut.isolationScope.addFeatureFlag("isolation-feature-flag", false) + sut.globalScope.addFeatureFlag("global-feature-flag", true) + + sut.captureException(RuntimeException("test exception")) + + verify(mockClient) + .captureEvent( + any(), + check { + val featureFlags = it.featureFlags + assertNotNull(featureFlags) + + val flag0 = featureFlags.values[0] + assertEquals("test-feature-flag", flag0.flag) + assertTrue(flag0.result) + + val flag1 = featureFlags.values[1] + assertEquals("current-feature-flag", flag1.flag) + assertTrue(flag1.result) + + val flag2 = featureFlags.values[2] + assertEquals("isolation-feature-flag", flag2.flag) + assertFalse(flag2.result) + + val flag3 = featureFlags.values[3] + assertEquals("global-feature-flag", flag3.flag) + assertTrue(flag3.result) + }, + anyOrNull(), + ) + } + private val dsnTest = "https://key@sentry.io/proj" private fun generateScopes( diff --git a/sentry/src/test/java/io/sentry/SentryOptionsTest.kt b/sentry/src/test/java/io/sentry/SentryOptionsTest.kt index 6d0bcd5790..bf54db9228 100644 --- a/sentry/src/test/java/io/sentry/SentryOptionsTest.kt +++ b/sentry/src/test/java/io/sentry/SentryOptionsTest.kt @@ -895,4 +895,17 @@ class SentryOptionsTest { options.isPropagateTraceparent = true assertTrue(options.isPropagateTraceparent) } + + @Test + fun `maxFeatureFlags defaults to 100`() { + val options = SentryOptions() + assertEquals(100, options.maxFeatureFlags) + } + + @Test + fun `maxFeatureFlags can be changed`() { + val options = SentryOptions() + options.maxFeatureFlags = 50 + assertEquals(50, options.maxFeatureFlags) + } } diff --git a/sentry/src/test/java/io/sentry/featureflags/FeatureFlagBufferTest.kt b/sentry/src/test/java/io/sentry/featureflags/FeatureFlagBufferTest.kt new file mode 100644 index 0000000000..ca996ef91a --- /dev/null +++ b/sentry/src/test/java/io/sentry/featureflags/FeatureFlagBufferTest.kt @@ -0,0 +1,182 @@ +package io.sentry.featureflags + +import io.sentry.SentryOptions +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +class FeatureFlagBufferTest { + @Test + fun `creates noop if limit is 0`() { + val buffer = FeatureFlagBuffer.create(SentryOptions().also { it.maxFeatureFlags = 0 }) + assertTrue(buffer is NoOpFeatureFlagBuffer) + } + + @Test + fun `stores value`() { + val buffer = FeatureFlagBuffer.create(SentryOptions().also { it.maxFeatureFlags = 2 }) + buffer.add("a", true) + buffer.add("b", false) + + val featureFlags = buffer.featureFlags + assertNotNull(featureFlags) + + val featureFlagValues = featureFlags.values + assertEquals(2, featureFlagValues.size) + + assertEquals("a", featureFlagValues[0]!!.flag) + assertTrue(featureFlagValues[0]!!.result) + + assertEquals("b", featureFlagValues[1]!!.flag) + assertFalse(featureFlagValues[1]!!.result) + } + + @Test + fun `drops oldest entry when limit is reached`() { + val buffer = FeatureFlagBuffer.create(SentryOptions().also { it.maxFeatureFlags = 2 }) + buffer.add("a", true) + buffer.add("b", true) + buffer.add("c", true) + + val featureFlags = buffer.featureFlags + assertNotNull(featureFlags) + val featureFlagValues = featureFlags.values + assertEquals(2, featureFlagValues.size) + + assertEquals("b", featureFlagValues[0]!!.flag) + assertEquals("c", featureFlagValues[1]!!.flag) + } + + @Test + fun `drops oldest entries when merging multiple buffers`() { + val options = SentryOptions().also { it.maxFeatureFlags = 2 } + val globalBuffer = FeatureFlagBuffer.create(options) + val isolationBuffer = FeatureFlagBuffer.create(options) + val currentBuffer = FeatureFlagBuffer.create(options) + globalBuffer.add("globalA", true) + isolationBuffer.add("isolationA", true) + currentBuffer.add("currentA", true) + globalBuffer.add("globalB", true) + isolationBuffer.add("isolationB", true) + currentBuffer.add("currentB", true) + globalBuffer.add("globalC", true) + isolationBuffer.add("isolationC", true) + currentBuffer.add("currentC", true) + + val buffer = FeatureFlagBuffer.merged(options, globalBuffer, isolationBuffer, currentBuffer) + + val featureFlags = buffer.featureFlags + assertNotNull(featureFlags) + val featureFlagValues = featureFlags.values + assertEquals(2, featureFlagValues.size) + + assertEquals("isolationC", featureFlagValues[0]!!.flag) + assertEquals("currentC", featureFlagValues[1]!!.flag) + } + + @Test + fun `drops oldest entries when merging multiple buffers even if assymetrically sized`() { + val options = SentryOptions().also { it.maxFeatureFlags = 2 } + val globalBuffer = FeatureFlagBuffer.create(options) + val isolationBuffer = FeatureFlagBuffer.create(options) + val currentBuffer = FeatureFlagBuffer.create(options) + globalBuffer.add("globalA", true) + isolationBuffer.add("isolationA", true) + currentBuffer.add("currentA", true) + globalBuffer.add("globalB", true) + isolationBuffer.add("isolationB", true) + currentBuffer.add("currentB", true) + globalBuffer.add("globalC", true) + + val buffer = FeatureFlagBuffer.merged(options, globalBuffer, isolationBuffer, currentBuffer) + + val featureFlags = buffer.featureFlags + assertNotNull(featureFlags) + val featureFlagValues = featureFlags.values + assertEquals(2, featureFlagValues.size) + + assertEquals("currentB", featureFlagValues[0]!!.flag) + assertEquals("globalC", featureFlagValues[1]!!.flag) + } + + @Test + fun `drops oldest entries when merging multiple buffers all from same source`() { + val options = SentryOptions().also { it.maxFeatureFlags = 2 } + val globalBuffer = FeatureFlagBuffer.create(options) + val isolationBuffer = FeatureFlagBuffer.create(options) + val currentBuffer = FeatureFlagBuffer.create(options) + globalBuffer.add("globalA", true) + globalBuffer.add("globalB", true) + globalBuffer.add("globalC", true) + + isolationBuffer.add("isolationA", true) + isolationBuffer.add("isolationB", true) + isolationBuffer.add("isolationC", true) + + currentBuffer.add("currentA", true) + currentBuffer.add("currentB", true) + currentBuffer.add("currentC", true) + + val buffer = FeatureFlagBuffer.merged(options, globalBuffer, isolationBuffer, currentBuffer) + + val featureFlags = buffer.featureFlags + assertNotNull(featureFlags) + val featureFlagValues = featureFlags.values + assertEquals(2, featureFlagValues.size) + + assertEquals("currentB", featureFlagValues[0]!!.flag) + assertEquals("currentC", featureFlagValues[1]!!.flag) + } + + @Test + fun `updates same flags value`() { + val options = SentryOptions().also { it.maxFeatureFlags = 3 } + val globalBuffer = FeatureFlagBuffer.create(options) + val isolationBuffer = FeatureFlagBuffer.create(options) + val currentBuffer = FeatureFlagBuffer.create(options) + globalBuffer.add("a", true) + globalBuffer.add("b", false) + + isolationBuffer.add("a", true) + isolationBuffer.add("b", false) + + currentBuffer.add("a", false) + currentBuffer.add("b", true) + + val buffer = FeatureFlagBuffer.merged(options, globalBuffer, isolationBuffer, currentBuffer) + + val featureFlags = buffer.featureFlags + assertNotNull(featureFlags) + val featureFlagValues = featureFlags.values + assertEquals(2, featureFlagValues.size) + + assertEquals("a", featureFlagValues[0]!!.flag) + assertFalse(featureFlagValues[0]!!.result) + assertEquals("b", featureFlagValues[1]!!.flag) + assertTrue(featureFlagValues[1]!!.result) + } + + @Test + fun `merges empty buffers`() { + val options = SentryOptions().also { it.maxFeatureFlags = 2 } + val globalBuffer = FeatureFlagBuffer.create(options) + val isolationBuffer = FeatureFlagBuffer.create(options) + val currentBuffer = FeatureFlagBuffer.create(options) + val buffer = FeatureFlagBuffer.merged(options, globalBuffer, isolationBuffer, currentBuffer) + + assertTrue(buffer is NoOpFeatureFlagBuffer) + } + + @Test + fun `merges noop buffers`() { + val options = SentryOptions().also { it.maxFeatureFlags = 0 } + val globalBuffer = NoOpFeatureFlagBuffer.getInstance() + val isolationBuffer = NoOpFeatureFlagBuffer.getInstance() + val currentBuffer = NoOpFeatureFlagBuffer.getInstance() + val buffer = FeatureFlagBuffer.merged(options, globalBuffer, isolationBuffer, currentBuffer) + + assertTrue(buffer is NoOpFeatureFlagBuffer) + } +} From 74cc427dfc099d77a122cd6c80935e28d45d0a20 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Thu, 16 Oct 2025 13:10:00 +0200 Subject: [PATCH 02/15] fix equals --- sentry/src/main/java/io/sentry/protocol/FeatureFlag.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sentry/src/main/java/io/sentry/protocol/FeatureFlag.java b/sentry/src/main/java/io/sentry/protocol/FeatureFlag.java index ee849b2fa8..ebb2e58cd3 100644 --- a/sentry/src/main/java/io/sentry/protocol/FeatureFlag.java +++ b/sentry/src/main/java/io/sentry/protocol/FeatureFlag.java @@ -52,8 +52,8 @@ public void setResult(final @NotNull Boolean result) { public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; - FeatureFlag flag = (FeatureFlag) o; - return Objects.equals(flag, flag.flag) && Objects.equals(result, flag.result); + final @NotNull FeatureFlag otherFlag = (FeatureFlag) o; + return Objects.equals(flag, otherFlag.flag) && Objects.equals(result, otherFlag.result); } @Override From fb4214087b912cf6d5cd45723723fd7be101daaa Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Fri, 17 Oct 2025 13:51:19 +0200 Subject: [PATCH 03/15] add serialization tests --- .../CombinedContextsViewSerializationTest.kt | 1 + .../protocol/ContextsSerializationTest.kt | 1 + .../protocol/FeatureFlagsSerializationTest.kt | 38 +++++++++++++++++++ .../protocol/SentryEventSerializationTest.kt | 5 ++- sentry/src/test/resources/json/contexts.json | 12 ++++++ .../test/resources/json/feature_flags.json | 12 ++++++ .../src/test/resources/json/sentry_event.json | 12 ++++++ 7 files changed, 80 insertions(+), 1 deletion(-) create mode 100644 sentry/src/test/java/io/sentry/protocol/FeatureFlagsSerializationTest.kt create mode 100644 sentry/src/test/resources/json/feature_flags.json diff --git a/sentry/src/test/java/io/sentry/protocol/CombinedContextsViewSerializationTest.kt b/sentry/src/test/java/io/sentry/protocol/CombinedContextsViewSerializationTest.kt index fd7fae9108..d7fd3cf9f7 100644 --- a/sentry/src/test/java/io/sentry/protocol/CombinedContextsViewSerializationTest.kt +++ b/sentry/src/test/java/io/sentry/protocol/CombinedContextsViewSerializationTest.kt @@ -30,6 +30,7 @@ class CombinedContextsViewSerializationTest { isolation.setOperatingSystem(OperatingSystemSerializationTest.Fixture().getSut()) isolation.setResponse(ResponseSerializationTest.Fixture().getSut()) isolation.setSpring(SpringSerializationTest.Fixture().getSut()) + isolation.setFeatureFlags(FeatureFlagsSerializationTest.Fixture().getSut()) global.setRuntime(SentryRuntimeSerializationTest.Fixture().getSut()) global.setGpu(GpuSerializationTest.Fixture().getSut()) diff --git a/sentry/src/test/java/io/sentry/protocol/ContextsSerializationTest.kt b/sentry/src/test/java/io/sentry/protocol/ContextsSerializationTest.kt index c02bb4240b..1a5e252a76 100644 --- a/sentry/src/test/java/io/sentry/protocol/ContextsSerializationTest.kt +++ b/sentry/src/test/java/io/sentry/protocol/ContextsSerializationTest.kt @@ -25,6 +25,7 @@ class ContextsSerializationTest { setResponse(ResponseSerializationTest.Fixture().getSut()) setTrace(SpanContextSerializationTest.Fixture().getSut()) setSpring(SpringSerializationTest.Fixture().getSut()) + setFeatureFlags(FeatureFlagsSerializationTest.Fixture().getSut()) } } diff --git a/sentry/src/test/java/io/sentry/protocol/FeatureFlagsSerializationTest.kt b/sentry/src/test/java/io/sentry/protocol/FeatureFlagsSerializationTest.kt new file mode 100644 index 0000000000..8c401ddcc6 --- /dev/null +++ b/sentry/src/test/java/io/sentry/protocol/FeatureFlagsSerializationTest.kt @@ -0,0 +1,38 @@ +package io.sentry.protocol + +import io.sentry.ILogger +import kotlin.test.assertEquals +import org.junit.Test +import org.mockito.kotlin.mock + +class FeatureFlagsSerializationTest { + class Fixture { + val logger = mock() + + fun getSut() = FeatureFlags(listOf(FeatureFlag("flag-1", true), FeatureFlag("flag-2", false))) + } + + private val fixture = Fixture() + + @Test + fun serialize() { + val expected = SerializationUtils.sanitizedFile("json/feature_flags.json") + val actual = SerializationUtils.serializeToString(fixture.getSut(), fixture.logger) + + assertEquals(expected, actual) + } + + @Test + fun deserialize() { + val expectedJson = SerializationUtils.sanitizedFile("json/feature_flags.json") + val actual = + SerializationUtils.deserializeJson( + expectedJson, + FeatureFlags.Deserializer(), + fixture.logger, + ) + val actualJson = SerializationUtils.serializeToString(actual, fixture.logger) + + assertEquals(expectedJson, actualJson) + } +} diff --git a/sentry/src/test/java/io/sentry/protocol/SentryEventSerializationTest.kt b/sentry/src/test/java/io/sentry/protocol/SentryEventSerializationTest.kt index 389cd121c2..f2cfd1ce07 100644 --- a/sentry/src/test/java/io/sentry/protocol/SentryEventSerializationTest.kt +++ b/sentry/src/test/java/io/sentry/protocol/SentryEventSerializationTest.kt @@ -32,7 +32,10 @@ class SentryEventSerializationTest { level = SentryLevel.ERROR transaction = "e7aea178-e3a6-46bc-be17-38a3ea8920b6" setModule("01c8a4f6-8861-4575-a10e-5ed3fba7c794", "b4083431-47e9-433a-b58f-58796f63e27c") - contexts.apply { setSpring(SpringSerializationTest.Fixture().getSut()) } + contexts.apply { + setSpring(SpringSerializationTest.Fixture().getSut()) + setFeatureFlags(FeatureFlagsSerializationTest.Fixture().getSut()) + } SentryBaseEventSerializationTest.Fixture().update(this) } } diff --git a/sentry/src/test/resources/json/contexts.json b/sentry/src/test/resources/json/contexts.json index 894d73e2dc..7f4c0c16bc 100644 --- a/sentry/src/test/resources/json/contexts.json +++ b/sentry/src/test/resources/json/contexts.json @@ -76,6 +76,18 @@ "url": "url", "unknown": "unknown" }, + "flags": { + "values": [ + { + "flag": "flag-1", + "result": true + }, + { + "flag": "flag-2", + "result": false + } + ] + }, "gpu": { "name": "d623a6b5-e1ab-4402-931b-c06f5a43a5ae", diff --git a/sentry/src/test/resources/json/feature_flags.json b/sentry/src/test/resources/json/feature_flags.json new file mode 100644 index 0000000000..ff4569fb57 --- /dev/null +++ b/sentry/src/test/resources/json/feature_flags.json @@ -0,0 +1,12 @@ +{ + "values": [ + { + "flag": "flag-1", + "result": true + }, + { + "flag": "flag-2", + "result": false + } + ] +} diff --git a/sentry/src/test/resources/json/sentry_event.json b/sentry/src/test/resources/json/sentry_event.json index c96d3bed45..817ae1ff7b 100644 --- a/sentry/src/test/resources/json/sentry_event.json +++ b/sentry/src/test/resources/json/sentry_event.json @@ -204,6 +204,18 @@ "cpu_description": "cpu0", "chipset": "unisoc" }, + "flags": { + "values": [ + { + "flag": "flag-1", + "result": true + }, + { + "flag": "flag-2", + "result": false + } + ] + }, "gpu": { "name": "d623a6b5-e1ab-4402-931b-c06f5a43a5ae", From 045337ab168570a602e6d69a2d85b17a9dca4d97 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Fri, 17 Oct 2025 13:51:37 +0200 Subject: [PATCH 04/15] Create new reference on buffer clone --- .../src/main/java/io/sentry/featureflags/FeatureFlagBuffer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry/src/main/java/io/sentry/featureflags/FeatureFlagBuffer.java b/sentry/src/main/java/io/sentry/featureflags/FeatureFlagBuffer.java index 338e6c0e8a..205143f293 100644 --- a/sentry/src/main/java/io/sentry/featureflags/FeatureFlagBuffer.java +++ b/sentry/src/main/java/io/sentry/featureflags/FeatureFlagBuffer.java @@ -34,7 +34,7 @@ private FeatureFlagBuffer( private FeatureFlagBuffer(@NotNull FeatureFlagBuffer other) { this.maxSize = other.maxSize; - this.flags = other.flags; + this.flags = new CopyOnWriteArrayList<>(other.flags); } @Override From c1061ca5b1d04fb09ee8d60123cf918b3e3ce6e4 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Fri, 17 Oct 2025 13:52:12 +0200 Subject: [PATCH 05/15] add comment explaining the merge method --- .../io/sentry/featureflags/FeatureFlagBuffer.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/sentry/src/main/java/io/sentry/featureflags/FeatureFlagBuffer.java b/sentry/src/main/java/io/sentry/featureflags/FeatureFlagBuffer.java index 205143f293..a63db864c8 100644 --- a/sentry/src/main/java/io/sentry/featureflags/FeatureFlagBuffer.java +++ b/sentry/src/main/java/io/sentry/featureflags/FeatureFlagBuffer.java @@ -97,6 +97,19 @@ public IFeatureFlagBuffer clone() { currentBuffer instanceof FeatureFlagBuffer ? (FeatureFlagBuffer) currentBuffer : null); } + /** + * Iterates all incoming buffers from the end, always taking the latest item across all buffers, + * until maxSize has been reached or no more items are available. + * + *

If a duplicate is found we skip it since we're iterating in reverse order and we already + * have the latest entry. + * + * @param maxSize max number of feature flags + * @param globalBuffer buffer from global scope + * @param isolationBuffer buffer from isolation scope + * @param currentBuffer buffer from current scope + * @return merged buffer containing at most maxSize latest items from incoming buffers + */ private static @NotNull IFeatureFlagBuffer merged( final int maxSize, final @Nullable FeatureFlagBuffer globalBuffer, From 74acb1049138c97eaefba88aa6253f14eb9e4e61 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Fri, 17 Oct 2025 13:52:26 +0200 Subject: [PATCH 06/15] optimize merge method --- .../featureflags/FeatureFlagBuffer.java | 26 ++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/sentry/src/main/java/io/sentry/featureflags/FeatureFlagBuffer.java b/sentry/src/main/java/io/sentry/featureflags/FeatureFlagBuffer.java index a63db864c8..53789edd80 100644 --- a/sentry/src/main/java/io/sentry/featureflags/FeatureFlagBuffer.java +++ b/sentry/src/main/java/io/sentry/featureflags/FeatureFlagBuffer.java @@ -137,20 +137,20 @@ public IFeatureFlagBuffer clone() { int isolationIndex = isolationSize - 1; int currentIndex = currentSize - 1; + @Nullable + FeatureFlagEntry globalEntry = globalFlags == null ? null : globalFlags.get(globalIndex); + @Nullable + FeatureFlagEntry isolationEntry = + isolationFlags == null ? null : isolationFlags.get(isolationIndex); + @Nullable + FeatureFlagEntry currentEntry = currentFlags == null ? null : currentFlags.get(currentIndex); + final @NotNull java.util.Map uniqueFlags = new java.util.LinkedHashMap<>(maxSize); // check if there is still room and remaining items to check while (uniqueFlags.size() < maxSize - && (globalIndex >= 0 || isolationIndex >= 0 || currentIndex >= 0)) { - final FeatureFlagEntry globalEntry = - (globalFlags != null && globalIndex >= 0) ? globalFlags.get(globalIndex) : null; - final FeatureFlagEntry isolationEntry = - (isolationFlags != null && isolationIndex >= 0) - ? isolationFlags.get(isolationIndex) - : null; - final FeatureFlagEntry currentEntry = - (currentFlags != null && currentIndex >= 0) ? currentFlags.get(currentIndex) : null; + && (globalEntry != null || isolationEntry != null || currentEntry != null)) { @Nullable FeatureFlagEntry entryToAdd = null; @Nullable ScopeType selectedBuffer = null; @@ -179,10 +179,18 @@ public IFeatureFlagBuffer clone() { // decrement only index of buffer that was selected if (ScopeType.CURRENT.equals(selectedBuffer)) { currentIndex--; + currentEntry = + currentFlags != null && currentIndex >= 0 ? currentFlags.get(currentIndex) : null; } else if (ScopeType.ISOLATION.equals(selectedBuffer)) { isolationIndex--; + isolationEntry = + isolationFlags != null && isolationIndex >= 0 + ? isolationFlags.get(isolationIndex) + : null; } else if (ScopeType.GLOBAL.equals(selectedBuffer)) { globalIndex--; + globalEntry = + globalFlags != null && globalIndex >= 0 ? globalFlags.get(globalIndex) : null; } } else { // no need to look any further since lists are sorted and we could not find any newer From eeae5c654086acff0d5ff7d60bb1c0b7a8d59fad Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Fri, 17 Oct 2025 14:18:41 +0200 Subject: [PATCH 07/15] make flag and result nullable params --- sentry/api/sentry.api | 30 ++++++++-------- .../java/io/sentry/CombinedScopeView.java | 2 +- .../src/main/java/io/sentry/HubAdapter.java | 2 +- .../main/java/io/sentry/HubScopesWrapper.java | 2 +- sentry/src/main/java/io/sentry/IScope.java | 2 +- sentry/src/main/java/io/sentry/IScopes.java | 2 +- sentry/src/main/java/io/sentry/NoOpHub.java | 2 +- sentry/src/main/java/io/sentry/NoOpScope.java | 2 +- .../src/main/java/io/sentry/NoOpScopes.java | 2 +- sentry/src/main/java/io/sentry/Scope.java | 2 +- sentry/src/main/java/io/sentry/Scopes.java | 2 +- .../main/java/io/sentry/ScopesAdapter.java | 2 +- sentry/src/main/java/io/sentry/Sentry.java | 2 +- .../featureflags/FeatureFlagBuffer.java | 5 ++- .../featureflags/IFeatureFlagBuffer.java | 2 +- .../featureflags/NoOpFeatureFlagBuffer.java | 2 +- sentry/src/test/java/io/sentry/ScopeTest.kt | 14 ++++++++ sentry/src/test/java/io/sentry/ScopesTest.kt | 35 +++++++++++++++++++ 18 files changed, 82 insertions(+), 30 deletions(-) diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 88de3232de..07cf57b4ff 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -264,7 +264,7 @@ public final class io/sentry/CombinedScopeView : io/sentry/IScope { public fun addBreadcrumb (Lio/sentry/Breadcrumb;)V public fun addBreadcrumb (Lio/sentry/Breadcrumb;Lio/sentry/Hint;)V public fun addEventProcessor (Lio/sentry/EventProcessor;)V - public fun addFeatureFlag (Ljava/lang/String;Z)V + public fun addFeatureFlag (Ljava/lang/String;Ljava/lang/Boolean;)V public fun assignTraceContext (Lio/sentry/SentryEvent;)V public fun bindClient (Lio/sentry/ISentryClient;)V public fun clear ()V @@ -612,7 +612,7 @@ public final class io/sentry/HttpStatusCodeRange { public final class io/sentry/HubAdapter : io/sentry/IHub { public fun addBreadcrumb (Lio/sentry/Breadcrumb;)V public fun addBreadcrumb (Lio/sentry/Breadcrumb;Lio/sentry/Hint;)V - public fun addFeatureFlag (Ljava/lang/String;Z)V + public fun addFeatureFlag (Ljava/lang/String;Ljava/lang/Boolean;)V public fun bindClient (Lio/sentry/ISentryClient;)V public fun captureCheckIn (Lio/sentry/CheckIn;)Lio/sentry/protocol/SentryId; public fun captureEnvelope (Lio/sentry/SentryEnvelope;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; @@ -685,7 +685,7 @@ public final class io/sentry/HubScopesWrapper : io/sentry/IHub { public fun (Lio/sentry/IScopes;)V public fun addBreadcrumb (Lio/sentry/Breadcrumb;)V public fun addBreadcrumb (Lio/sentry/Breadcrumb;Lio/sentry/Hint;)V - public fun addFeatureFlag (Ljava/lang/String;Z)V + public fun addFeatureFlag (Ljava/lang/String;Ljava/lang/Boolean;)V public fun bindClient (Lio/sentry/ISentryClient;)V public fun captureCheckIn (Lio/sentry/CheckIn;)Lio/sentry/protocol/SentryId; public fun captureEnvelope (Lio/sentry/SentryEnvelope;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; @@ -848,7 +848,7 @@ public abstract interface class io/sentry/IScope { public abstract fun addBreadcrumb (Lio/sentry/Breadcrumb;)V public abstract fun addBreadcrumb (Lio/sentry/Breadcrumb;Lio/sentry/Hint;)V public abstract fun addEventProcessor (Lio/sentry/EventProcessor;)V - public abstract fun addFeatureFlag (Ljava/lang/String;Z)V + public abstract fun addFeatureFlag (Ljava/lang/String;Ljava/lang/Boolean;)V public abstract fun assignTraceContext (Lio/sentry/SentryEvent;)V public abstract fun bindClient (Lio/sentry/ISentryClient;)V public abstract fun clear ()V @@ -936,7 +936,7 @@ public abstract interface class io/sentry/IScopes { public abstract fun addBreadcrumb (Lio/sentry/Breadcrumb;Lio/sentry/Hint;)V public fun addBreadcrumb (Ljava/lang/String;)V public fun addBreadcrumb (Ljava/lang/String;Ljava/lang/String;)V - public abstract fun addFeatureFlag (Ljava/lang/String;Z)V + public abstract fun addFeatureFlag (Ljava/lang/String;Ljava/lang/Boolean;)V public abstract fun bindClient (Lio/sentry/ISentryClient;)V public abstract fun captureCheckIn (Lio/sentry/CheckIn;)Lio/sentry/protocol/SentryId; public fun captureEnvelope (Lio/sentry/SentryEnvelope;)Lio/sentry/protocol/SentryId; @@ -1518,7 +1518,7 @@ public final class io/sentry/NoOpEnvelopeReader : io/sentry/IEnvelopeReader { public final class io/sentry/NoOpHub : io/sentry/IHub { public fun addBreadcrumb (Lio/sentry/Breadcrumb;)V public fun addBreadcrumb (Lio/sentry/Breadcrumb;Lio/sentry/Hint;)V - public fun addFeatureFlag (Ljava/lang/String;Z)V + public fun addFeatureFlag (Ljava/lang/String;Ljava/lang/Boolean;)V public fun bindClient (Lio/sentry/ISentryClient;)V public fun captureCheckIn (Lio/sentry/CheckIn;)Lio/sentry/protocol/SentryId; public fun captureEnvelope (Lio/sentry/SentryEnvelope;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; @@ -1620,7 +1620,7 @@ public final class io/sentry/NoOpScope : io/sentry/IScope { public fun addBreadcrumb (Lio/sentry/Breadcrumb;)V public fun addBreadcrumb (Lio/sentry/Breadcrumb;Lio/sentry/Hint;)V public fun addEventProcessor (Lio/sentry/EventProcessor;)V - public fun addFeatureFlag (Ljava/lang/String;Z)V + public fun addFeatureFlag (Ljava/lang/String;Ljava/lang/Boolean;)V public fun assignTraceContext (Lio/sentry/SentryEvent;)V public fun bindClient (Lio/sentry/ISentryClient;)V public fun clear ()V @@ -1689,7 +1689,7 @@ public final class io/sentry/NoOpScope : io/sentry/IScope { public final class io/sentry/NoOpScopes : io/sentry/IScopes { public fun addBreadcrumb (Lio/sentry/Breadcrumb;)V public fun addBreadcrumb (Lio/sentry/Breadcrumb;Lio/sentry/Hint;)V - public fun addFeatureFlag (Ljava/lang/String;Z)V + public fun addFeatureFlag (Ljava/lang/String;Ljava/lang/Boolean;)V public fun bindClient (Lio/sentry/ISentryClient;)V public fun captureCheckIn (Lio/sentry/CheckIn;)Lio/sentry/protocol/SentryId; public fun captureEnvelope (Lio/sentry/SentryEnvelope;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; @@ -2283,7 +2283,7 @@ public final class io/sentry/Scope : io/sentry/IScope { public fun addBreadcrumb (Lio/sentry/Breadcrumb;)V public fun addBreadcrumb (Lio/sentry/Breadcrumb;Lio/sentry/Hint;)V public fun addEventProcessor (Lio/sentry/EventProcessor;)V - public fun addFeatureFlag (Ljava/lang/String;Z)V + public fun addFeatureFlag (Ljava/lang/String;Ljava/lang/Boolean;)V public fun assignTraceContext (Lio/sentry/SentryEvent;)V public fun bindClient (Lio/sentry/ISentryClient;)V public fun clear ()V @@ -2401,7 +2401,7 @@ public final class io/sentry/Scopes : io/sentry/IScopes { public fun (Lio/sentry/IScope;Lio/sentry/IScope;Lio/sentry/IScope;Ljava/lang/String;)V public fun addBreadcrumb (Lio/sentry/Breadcrumb;)V public fun addBreadcrumb (Lio/sentry/Breadcrumb;Lio/sentry/Hint;)V - public fun addFeatureFlag (Ljava/lang/String;Z)V + public fun addFeatureFlag (Ljava/lang/String;Ljava/lang/Boolean;)V public fun bindClient (Lio/sentry/ISentryClient;)V public fun captureCheckIn (Lio/sentry/CheckIn;)Lio/sentry/protocol/SentryId; public fun captureEnvelope (Lio/sentry/SentryEnvelope;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; @@ -2473,7 +2473,7 @@ public final class io/sentry/Scopes : io/sentry/IScopes { public final class io/sentry/ScopesAdapter : io/sentry/IScopes { public fun addBreadcrumb (Lio/sentry/Breadcrumb;)V public fun addBreadcrumb (Lio/sentry/Breadcrumb;Lio/sentry/Hint;)V - public fun addFeatureFlag (Ljava/lang/String;Z)V + public fun addFeatureFlag (Ljava/lang/String;Ljava/lang/Boolean;)V public fun bindClient (Lio/sentry/ISentryClient;)V public fun captureCheckIn (Lio/sentry/CheckIn;)Lio/sentry/protocol/SentryId; public fun captureEnvelope (Lio/sentry/SentryEnvelope;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; @@ -2584,7 +2584,7 @@ public final class io/sentry/Sentry { public static fun addBreadcrumb (Lio/sentry/Breadcrumb;Lio/sentry/Hint;)V public static fun addBreadcrumb (Ljava/lang/String;)V public static fun addBreadcrumb (Ljava/lang/String;Ljava/lang/String;)V - public static fun addFeatureFlag (Ljava/lang/String;Z)V + public static fun addFeatureFlag (Ljava/lang/String;Ljava/lang/Boolean;)V public static fun bindClient (Lio/sentry/ISentryClient;)V public static fun captureCheckIn (Lio/sentry/CheckIn;)Lio/sentry/protocol/SentryId; public static fun captureEvent (Lio/sentry/SentryEvent;)Lio/sentry/protocol/SentryId; @@ -4718,7 +4718,7 @@ public final class io/sentry/exception/SentryHttpClientException : java/lang/Exc } public final class io/sentry/featureflags/FeatureFlagBuffer : io/sentry/featureflags/IFeatureFlagBuffer { - public fun add (Ljava/lang/String;Z)V + public fun add (Ljava/lang/String;Ljava/lang/Boolean;)V public fun clone ()Lio/sentry/featureflags/IFeatureFlagBuffer; public synthetic fun clone ()Ljava/lang/Object; public static fun create (Lio/sentry/SentryOptions;)Lio/sentry/featureflags/IFeatureFlagBuffer; @@ -4727,14 +4727,14 @@ public final class io/sentry/featureflags/FeatureFlagBuffer : io/sentry/featuref } public abstract interface class io/sentry/featureflags/IFeatureFlagBuffer { - public abstract fun add (Ljava/lang/String;Z)V + public abstract fun add (Ljava/lang/String;Ljava/lang/Boolean;)V public abstract fun clone ()Lio/sentry/featureflags/IFeatureFlagBuffer; public abstract fun getFeatureFlags ()Lio/sentry/protocol/FeatureFlags; } public final class io/sentry/featureflags/NoOpFeatureFlagBuffer : io/sentry/featureflags/IFeatureFlagBuffer { public fun ()V - public fun add (Ljava/lang/String;Z)V + public fun add (Ljava/lang/String;Ljava/lang/Boolean;)V public fun clone ()Lio/sentry/featureflags/IFeatureFlagBuffer; public synthetic fun clone ()Ljava/lang/Object; public fun getFeatureFlags ()Lio/sentry/protocol/FeatureFlags; diff --git a/sentry/src/main/java/io/sentry/CombinedScopeView.java b/sentry/src/main/java/io/sentry/CombinedScopeView.java index 4b1faddb56..0d7c8460e9 100644 --- a/sentry/src/main/java/io/sentry/CombinedScopeView.java +++ b/sentry/src/main/java/io/sentry/CombinedScopeView.java @@ -512,7 +512,7 @@ public void setReplayId(@NotNull SentryId replayId) { } @Override - public void addFeatureFlag(final @NotNull String flag, final boolean result) { + public void addFeatureFlag(final @Nullable String flag, final @Nullable Boolean result) { getDefaultWriteScope().addFeatureFlag(flag, result); } diff --git a/sentry/src/main/java/io/sentry/HubAdapter.java b/sentry/src/main/java/io/sentry/HubAdapter.java index f92b477cbe..31a2e219cd 100644 --- a/sentry/src/main/java/io/sentry/HubAdapter.java +++ b/sentry/src/main/java/io/sentry/HubAdapter.java @@ -390,7 +390,7 @@ public void reportFullyDisplayed() { } @Override - public void addFeatureFlag(final @NotNull String flag, final boolean result) { + public void addFeatureFlag(final @Nullable String flag, final @Nullable Boolean result) { Sentry.addFeatureFlag(flag, result); } } diff --git a/sentry/src/main/java/io/sentry/HubScopesWrapper.java b/sentry/src/main/java/io/sentry/HubScopesWrapper.java index 818d62f6b9..d15ed72ee4 100644 --- a/sentry/src/main/java/io/sentry/HubScopesWrapper.java +++ b/sentry/src/main/java/io/sentry/HubScopesWrapper.java @@ -375,7 +375,7 @@ public void reportFullyDisplayed() { } @Override - public void addFeatureFlag(final @NotNull String flag, final boolean result) { + public void addFeatureFlag(final @Nullable String flag, final @Nullable Boolean result) { scopes.addFeatureFlag(flag, result); } } diff --git a/sentry/src/main/java/io/sentry/IScope.java b/sentry/src/main/java/io/sentry/IScope.java index 4617b4cf31..f41ea1cbbe 100644 --- a/sentry/src/main/java/io/sentry/IScope.java +++ b/sentry/src/main/java/io/sentry/IScope.java @@ -425,7 +425,7 @@ void setSpanContext( @ApiStatus.Internal void replaceOptions(final @NotNull SentryOptions options); - void addFeatureFlag(final @NotNull String flag, final boolean result); + void addFeatureFlag(final @Nullable String flag, final @Nullable Boolean result); @ApiStatus.Internal @Nullable diff --git a/sentry/src/main/java/io/sentry/IScopes.java b/sentry/src/main/java/io/sentry/IScopes.java index 465fbc5eb3..bf78d28ecd 100644 --- a/sentry/src/main/java/io/sentry/IScopes.java +++ b/sentry/src/main/java/io/sentry/IScopes.java @@ -744,5 +744,5 @@ default boolean isNoOp() { @NotNull ILoggerApi logger(); - void addFeatureFlag(final @NotNull String flag, final boolean result); + void addFeatureFlag(final @Nullable String flag, final @Nullable Boolean result); } diff --git a/sentry/src/main/java/io/sentry/NoOpHub.java b/sentry/src/main/java/io/sentry/NoOpHub.java index 0292d1a82d..811e1d297a 100644 --- a/sentry/src/main/java/io/sentry/NoOpHub.java +++ b/sentry/src/main/java/io/sentry/NoOpHub.java @@ -332,5 +332,5 @@ public boolean isNoOp() { } @Override - public void addFeatureFlag(final @NotNull String flag, final boolean result) {} + public void addFeatureFlag(final @Nullable String flag, final @Nullable Boolean result) {} } diff --git a/sentry/src/main/java/io/sentry/NoOpScope.java b/sentry/src/main/java/io/sentry/NoOpScope.java index 617269d3a2..c04c5af87b 100644 --- a/sentry/src/main/java/io/sentry/NoOpScope.java +++ b/sentry/src/main/java/io/sentry/NoOpScope.java @@ -301,7 +301,7 @@ public void setSpanContext( public void replaceOptions(@NotNull SentryOptions options) {} @Override - public void addFeatureFlag(final @NotNull String flag, final boolean result) {} + public void addFeatureFlag(final @Nullable String flag, final @Nullable Boolean result) {} @Override public @Nullable FeatureFlags getFeatureFlags() { diff --git a/sentry/src/main/java/io/sentry/NoOpScopes.java b/sentry/src/main/java/io/sentry/NoOpScopes.java index 5d3447dd7c..40777da892 100644 --- a/sentry/src/main/java/io/sentry/NoOpScopes.java +++ b/sentry/src/main/java/io/sentry/NoOpScopes.java @@ -330,5 +330,5 @@ public boolean isNoOp() { } @Override - public void addFeatureFlag(final @NotNull String flag, final boolean result) {} + public void addFeatureFlag(final @Nullable String flag, final @Nullable Boolean result) {} } diff --git a/sentry/src/main/java/io/sentry/Scope.java b/sentry/src/main/java/io/sentry/Scope.java index 6f3f28399a..eb17420dd2 100644 --- a/sentry/src/main/java/io/sentry/Scope.java +++ b/sentry/src/main/java/io/sentry/Scope.java @@ -1128,7 +1128,7 @@ public void bindClient(@NotNull ISentryClient client) { } @Override - public void addFeatureFlag(final @NotNull String flag, final boolean result) { + public void addFeatureFlag(final @Nullable String flag, final @Nullable Boolean result) { featureFlags.add(flag, result); } diff --git a/sentry/src/main/java/io/sentry/Scopes.java b/sentry/src/main/java/io/sentry/Scopes.java index 7bf3bb0ca2..c8afde59cc 100644 --- a/sentry/src/main/java/io/sentry/Scopes.java +++ b/sentry/src/main/java/io/sentry/Scopes.java @@ -1221,7 +1221,7 @@ public void reportFullyDisplayed() { } @Override - public void addFeatureFlag(final @NotNull String flag, final boolean result) { + public void addFeatureFlag(final @Nullable String flag, final @Nullable Boolean result) { combinedScope.addFeatureFlag(flag, result); } diff --git a/sentry/src/main/java/io/sentry/ScopesAdapter.java b/sentry/src/main/java/io/sentry/ScopesAdapter.java index 8e6c8d6581..99e70694ee 100644 --- a/sentry/src/main/java/io/sentry/ScopesAdapter.java +++ b/sentry/src/main/java/io/sentry/ScopesAdapter.java @@ -387,7 +387,7 @@ public void reportFullyDisplayed() { } @Override - public void addFeatureFlag(final @NotNull String flag, final boolean result) { + public void addFeatureFlag(final @Nullable String flag, final @Nullable Boolean result) { Sentry.addFeatureFlag(flag, result); } } diff --git a/sentry/src/main/java/io/sentry/Sentry.java b/sentry/src/main/java/io/sentry/Sentry.java index 5bf2063d73..dbc7e5b100 100644 --- a/sentry/src/main/java/io/sentry/Sentry.java +++ b/sentry/src/main/java/io/sentry/Sentry.java @@ -1373,7 +1373,7 @@ public static void showUserFeedbackDialog( options.getFeedbackOptions().getDialogHandler().showDialog(associatedEventId, configurator); } - public static void addFeatureFlag(final @NotNull String flag, final boolean result) { + public static void addFeatureFlag(final @Nullable String flag, final @Nullable Boolean result) { getCurrentScopes().addFeatureFlag(flag, result); } } diff --git a/sentry/src/main/java/io/sentry/featureflags/FeatureFlagBuffer.java b/sentry/src/main/java/io/sentry/featureflags/FeatureFlagBuffer.java index 53789edd80..363524f3a3 100644 --- a/sentry/src/main/java/io/sentry/featureflags/FeatureFlagBuffer.java +++ b/sentry/src/main/java/io/sentry/featureflags/FeatureFlagBuffer.java @@ -38,7 +38,10 @@ private FeatureFlagBuffer(@NotNull FeatureFlagBuffer other) { } @Override - public void add(@NotNull String flag, boolean result) { + public void add(final @Nullable String flag, final @Nullable Boolean result) { + if (flag == null || result == null) { + return; + } try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { final int size = flags.size(); final @NotNull ArrayList tmpList = new ArrayList<>(size + 1); diff --git a/sentry/src/main/java/io/sentry/featureflags/IFeatureFlagBuffer.java b/sentry/src/main/java/io/sentry/featureflags/IFeatureFlagBuffer.java index 39878759e5..7f12026a59 100644 --- a/sentry/src/main/java/io/sentry/featureflags/IFeatureFlagBuffer.java +++ b/sentry/src/main/java/io/sentry/featureflags/IFeatureFlagBuffer.java @@ -7,7 +7,7 @@ @ApiStatus.Internal public interface IFeatureFlagBuffer { - void add(@NotNull String flag, boolean result); + void add(final @Nullable String flag, final @Nullable Boolean result); @Nullable FeatureFlags getFeatureFlags(); diff --git a/sentry/src/main/java/io/sentry/featureflags/NoOpFeatureFlagBuffer.java b/sentry/src/main/java/io/sentry/featureflags/NoOpFeatureFlagBuffer.java index 82f5709c0b..3bfc8f8fd2 100644 --- a/sentry/src/main/java/io/sentry/featureflags/NoOpFeatureFlagBuffer.java +++ b/sentry/src/main/java/io/sentry/featureflags/NoOpFeatureFlagBuffer.java @@ -14,7 +14,7 @@ public static NoOpFeatureFlagBuffer getInstance() { } @Override - public void add(@NotNull String flag, boolean result) {} + public void add(final @Nullable String flag, final @Nullable Boolean result) {} @Override public @Nullable FeatureFlags getFeatureFlags() { diff --git a/sentry/src/test/java/io/sentry/ScopeTest.kt b/sentry/src/test/java/io/sentry/ScopeTest.kt index e80793f54c..42c18049e0 100644 --- a/sentry/src/test/java/io/sentry/ScopeTest.kt +++ b/sentry/src/test/java/io/sentry/ScopeTest.kt @@ -1123,6 +1123,20 @@ class ScopeTest { assertFalse(flag0.result) } + @Test + fun `null feature flags are ignored`() { + val scope = Scope(SentryOptions.empty()) + + scope.addFeatureFlag(null, true) + scope.addFeatureFlag("flag1", null) + scope.addFeatureFlag(null, null) + + val flags = scope.featureFlags + assertNotNull(flags) + + assertEquals(0, flags.values.size) + } + private fun eventProcessor(): EventProcessor = object : EventProcessor { override fun process(event: SentryEvent, hint: Hint): SentryEvent? = event diff --git a/sentry/src/test/java/io/sentry/ScopesTest.kt b/sentry/src/test/java/io/sentry/ScopesTest.kt index 6b1ac51c7e..93368601d9 100644 --- a/sentry/src/test/java/io/sentry/ScopesTest.kt +++ b/sentry/src/test/java/io/sentry/ScopesTest.kt @@ -3121,6 +3121,41 @@ class ScopesTest { ) } + @Test + fun `null feature flags are ignored`() { + val (sut, mockClient) = getEnabledScopes() + + sut.addFeatureFlag(null, true) + sut.addFeatureFlag("flag-1", true) + sut.addFeatureFlag(null, null) + + sut.scope.addFeatureFlag(null, true) + sut.scope.addFeatureFlag("current-feature-flag", null) + sut.scope.addFeatureFlag(null, null) + + sut.isolationScope.addFeatureFlag(null, false) + sut.isolationScope.addFeatureFlag("isolation-feature-flag", null) + sut.isolationScope.addFeatureFlag(null, null) + + sut.globalScope.addFeatureFlag(null, true) + sut.globalScope.addFeatureFlag("global-feature-flag", null) + sut.globalScope.addFeatureFlag(null, null) + + sut.captureException(RuntimeException("test exception")) + + verify(mockClient) + .captureEvent( + any(), + check { + val featureFlags = it.featureFlags + assertNotNull(featureFlags) + + assertEquals(0, featureFlags.values.size) + }, + anyOrNull(), + ) + } + private val dsnTest = "https://key@sentry.io/proj" private fun generateScopes( From 21806f2bee8115beeadc761e2d36aab80e92e15d Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Fri, 17 Oct 2025 14:19:03 +0200 Subject: [PATCH 08/15] handle empty/noop buffer on merge and add tests for it --- .../featureflags/FeatureFlagBuffer.java | 6 +- .../featureflags/FeatureFlagBufferTest.kt | 155 ++++++++++++++++++ 2 files changed, 158 insertions(+), 3 deletions(-) diff --git a/sentry/src/main/java/io/sentry/featureflags/FeatureFlagBuffer.java b/sentry/src/main/java/io/sentry/featureflags/FeatureFlagBuffer.java index 363524f3a3..d17bcbbb80 100644 --- a/sentry/src/main/java/io/sentry/featureflags/FeatureFlagBuffer.java +++ b/sentry/src/main/java/io/sentry/featureflags/FeatureFlagBuffer.java @@ -141,12 +141,12 @@ public IFeatureFlagBuffer clone() { int currentIndex = currentSize - 1; @Nullable - FeatureFlagEntry globalEntry = globalFlags == null ? null : globalFlags.get(globalIndex); + FeatureFlagEntry globalEntry = globalFlags == null || globalIndex < 0 ? null : globalFlags.get(globalIndex); @Nullable FeatureFlagEntry isolationEntry = - isolationFlags == null ? null : isolationFlags.get(isolationIndex); + isolationFlags == null || isolationIndex < 0 ? null : isolationFlags.get(isolationIndex); @Nullable - FeatureFlagEntry currentEntry = currentFlags == null ? null : currentFlags.get(currentIndex); + FeatureFlagEntry currentEntry = currentFlags == null || currentIndex < 0 ? null : currentFlags.get(currentIndex); final @NotNull java.util.Map uniqueFlags = new java.util.LinkedHashMap<>(maxSize); diff --git a/sentry/src/test/java/io/sentry/featureflags/FeatureFlagBufferTest.kt b/sentry/src/test/java/io/sentry/featureflags/FeatureFlagBufferTest.kt index ca996ef91a..471ba880eb 100644 --- a/sentry/src/test/java/io/sentry/featureflags/FeatureFlagBufferTest.kt +++ b/sentry/src/test/java/io/sentry/featureflags/FeatureFlagBufferTest.kt @@ -101,6 +101,150 @@ class FeatureFlagBufferTest { assertEquals("globalC", featureFlagValues[1]!!.flag) } + @Test + fun `when merging global buffer can be empty`() { + val options = SentryOptions().also { it.maxFeatureFlags = 2 } + val globalBuffer = FeatureFlagBuffer.create(options) + val isolationBuffer = FeatureFlagBuffer.create(options) + val currentBuffer = FeatureFlagBuffer.create(options) + isolationBuffer.add("isolationA", true) + currentBuffer.add("currentA", true) + isolationBuffer.add("isolationB", true) + currentBuffer.add("currentB", true) + isolationBuffer.add("isolationC", true) + currentBuffer.add("currentC", true) + + val buffer = FeatureFlagBuffer.merged(options, globalBuffer, isolationBuffer, currentBuffer) + + val featureFlags = buffer.featureFlags + assertNotNull(featureFlags) + val featureFlagValues = featureFlags.values + assertEquals(2, featureFlagValues.size) + + assertEquals("isolationC", featureFlagValues[0]!!.flag) + assertEquals("currentC", featureFlagValues[1]!!.flag) + } + + @Test + fun `when merging isolation buffer can be empty`() { + val options = SentryOptions().also { it.maxFeatureFlags = 2 } + val globalBuffer = FeatureFlagBuffer.create(options) + val isolationBuffer = FeatureFlagBuffer.create(options) + val currentBuffer = FeatureFlagBuffer.create(options) + globalBuffer.add("globalA", true) + currentBuffer.add("currentA", true) + globalBuffer.add("globalB", true) + currentBuffer.add("currentB", true) + globalBuffer.add("globalC", true) + currentBuffer.add("currentC", true) + + val buffer = FeatureFlagBuffer.merged(options, globalBuffer, isolationBuffer, currentBuffer) + + val featureFlags = buffer.featureFlags + assertNotNull(featureFlags) + val featureFlagValues = featureFlags.values + assertEquals(2, featureFlagValues.size) + + assertEquals("globalC", featureFlagValues[0]!!.flag) + assertEquals("currentC", featureFlagValues[1]!!.flag) + } + + @Test + fun `when merging current buffer can be empty`() { + val options = SentryOptions().also { it.maxFeatureFlags = 2 } + val globalBuffer = FeatureFlagBuffer.create(options) + val isolationBuffer = FeatureFlagBuffer.create(options) + val currentBuffer = FeatureFlagBuffer.create(options) + globalBuffer.add("globalA", true) + isolationBuffer.add("isolationA", true) + globalBuffer.add("globalB", true) + isolationBuffer.add("isolationB", true) + globalBuffer.add("globalC", true) + isolationBuffer.add("isolationC", true) + + val buffer = FeatureFlagBuffer.merged(options, globalBuffer, isolationBuffer, currentBuffer) + + val featureFlags = buffer.featureFlags + assertNotNull(featureFlags) + val featureFlagValues = featureFlags.values + assertEquals(2, featureFlagValues.size) + + assertEquals("globalC", featureFlagValues[0]!!.flag) + assertEquals("isolationC", featureFlagValues[1]!!.flag) + } + + @Test + fun `when merging global buffer can be noop`() { + val options = SentryOptions().also { it.maxFeatureFlags = 2 } + val globalBuffer = NoOpFeatureFlagBuffer.getInstance() + val isolationBuffer = FeatureFlagBuffer.create(options) + val currentBuffer = FeatureFlagBuffer.create(options) + isolationBuffer.add("isolationA", true) + currentBuffer.add("currentA", true) + isolationBuffer.add("isolationB", true) + currentBuffer.add("currentB", true) + isolationBuffer.add("isolationC", true) + currentBuffer.add("currentC", true) + + val buffer = FeatureFlagBuffer.merged(options, globalBuffer, isolationBuffer, currentBuffer) + + val featureFlags = buffer.featureFlags + assertNotNull(featureFlags) + val featureFlagValues = featureFlags.values + assertEquals(2, featureFlagValues.size) + + assertEquals("isolationC", featureFlagValues[0]!!.flag) + assertEquals("currentC", featureFlagValues[1]!!.flag) + } + + @Test + fun `when merging isolation buffer can be noop`() { + val options = SentryOptions().also { it.maxFeatureFlags = 2 } + val globalBuffer = FeatureFlagBuffer.create(options) + val isolationBuffer = NoOpFeatureFlagBuffer.getInstance() + val currentBuffer = FeatureFlagBuffer.create(options) + globalBuffer.add("globalA", true) + currentBuffer.add("currentA", true) + globalBuffer.add("globalB", true) + currentBuffer.add("currentB", true) + globalBuffer.add("globalC", true) + currentBuffer.add("currentC", true) + + val buffer = FeatureFlagBuffer.merged(options, globalBuffer, isolationBuffer, currentBuffer) + + val featureFlags = buffer.featureFlags + assertNotNull(featureFlags) + val featureFlagValues = featureFlags.values + assertEquals(2, featureFlagValues.size) + + assertEquals("globalC", featureFlagValues[0]!!.flag) + assertEquals("currentC", featureFlagValues[1]!!.flag) + } + + @Test + fun `when merging current buffer can be noop`() { + val options = SentryOptions().also { it.maxFeatureFlags = 2 } + val globalBuffer = FeatureFlagBuffer.create(options) + val isolationBuffer = FeatureFlagBuffer.create(options) + val currentBuffer = NoOpFeatureFlagBuffer.getInstance() + globalBuffer.add("globalA", true) + isolationBuffer.add("isolationA", true) + globalBuffer.add("globalB", true) + isolationBuffer.add("isolationB", true) + globalBuffer.add("globalC", true) + isolationBuffer.add("isolationC", true) + + val buffer = FeatureFlagBuffer.merged(options, globalBuffer, isolationBuffer, currentBuffer) + + val featureFlags = buffer.featureFlags + assertNotNull(featureFlags) + val featureFlagValues = featureFlags.values + assertEquals(2, featureFlagValues.size) + + assertEquals("globalC", featureFlagValues[0]!!.flag) + assertEquals("isolationC", featureFlagValues[1]!!.flag) + } + @Test fun `drops oldest entries when merging multiple buffers all from same source`() { val options = SentryOptions().also { it.maxFeatureFlags = 2 } @@ -179,4 +323,15 @@ class FeatureFlagBufferTest { assertTrue(buffer is NoOpFeatureFlagBuffer) } + + @Test + fun `null values are ignored`() { + val buffer = FeatureFlagBuffer.create(SentryOptions().also { it.maxFeatureFlags = 2 }) + buffer.add(null, true) + buffer.add("b", null) + buffer.add(null, null) + + val featureFlags = buffer.featureFlags + assertNotNull(featureFlags) + } } From 95550ecb3c19270b498d664562400d217e0e6a0e Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Tue, 21 Oct 2025 08:10:35 +0200 Subject: [PATCH 09/15] optimize add method --- .../sentry/featureflags/FeatureFlagBuffer.java | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/sentry/src/main/java/io/sentry/featureflags/FeatureFlagBuffer.java b/sentry/src/main/java/io/sentry/featureflags/FeatureFlagBuffer.java index d17bcbbb80..1da7d8e6e3 100644 --- a/sentry/src/main/java/io/sentry/featureflags/FeatureFlagBuffer.java +++ b/sentry/src/main/java/io/sentry/featureflags/FeatureFlagBuffer.java @@ -44,19 +44,17 @@ public void add(final @Nullable String flag, final @Nullable Boolean result) { } try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { final int size = flags.size(); - final @NotNull ArrayList tmpList = new ArrayList<>(size + 1); - for (FeatureFlagEntry entry : flags) { - if (!entry.flag.equals(flag)) { - tmpList.add(entry); + for (int i = 0; i < size; i++) { + if (flags.get(i).equals(flag)) { + flags.remove(i); + break; } } - tmpList.add(new FeatureFlagEntry(flag, result, System.nanoTime())); + flags.add(new FeatureFlagEntry(flag, result, System.nanoTime())); - if (tmpList.size() > maxSize) { - tmpList.remove(0); + if (flags.size() > maxSize) { + flags.remove(0); } - - flags = new CopyOnWriteArrayList<>(tmpList); } } From 865ad5d1c9302d9e8e23e06daf07a7efa91cf68a Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Tue, 21 Oct 2025 08:13:19 +0200 Subject: [PATCH 10/15] format; fix test --- .../io/sentry/featureflags/FeatureFlagBuffer.java | 6 ++++-- sentry/src/test/java/io/sentry/ScopesTest.kt | 14 ++------------ 2 files changed, 6 insertions(+), 14 deletions(-) diff --git a/sentry/src/main/java/io/sentry/featureflags/FeatureFlagBuffer.java b/sentry/src/main/java/io/sentry/featureflags/FeatureFlagBuffer.java index 1da7d8e6e3..f140dd92e4 100644 --- a/sentry/src/main/java/io/sentry/featureflags/FeatureFlagBuffer.java +++ b/sentry/src/main/java/io/sentry/featureflags/FeatureFlagBuffer.java @@ -139,12 +139,14 @@ public IFeatureFlagBuffer clone() { int currentIndex = currentSize - 1; @Nullable - FeatureFlagEntry globalEntry = globalFlags == null || globalIndex < 0 ? null : globalFlags.get(globalIndex); + FeatureFlagEntry globalEntry = + globalFlags == null || globalIndex < 0 ? null : globalFlags.get(globalIndex); @Nullable FeatureFlagEntry isolationEntry = isolationFlags == null || isolationIndex < 0 ? null : isolationFlags.get(isolationIndex); @Nullable - FeatureFlagEntry currentEntry = currentFlags == null || currentIndex < 0 ? null : currentFlags.get(currentIndex); + FeatureFlagEntry currentEntry = + currentFlags == null || currentIndex < 0 ? null : currentFlags.get(currentIndex); final @NotNull java.util.Map uniqueFlags = new java.util.LinkedHashMap<>(maxSize); diff --git a/sentry/src/test/java/io/sentry/ScopesTest.kt b/sentry/src/test/java/io/sentry/ScopesTest.kt index 93368601d9..fa82c53fbe 100644 --- a/sentry/src/test/java/io/sentry/ScopesTest.kt +++ b/sentry/src/test/java/io/sentry/ScopesTest.kt @@ -3126,7 +3126,7 @@ class ScopesTest { val (sut, mockClient) = getEnabledScopes() sut.addFeatureFlag(null, true) - sut.addFeatureFlag("flag-1", true) + sut.addFeatureFlag("flag-1", null) sut.addFeatureFlag(null, null) sut.scope.addFeatureFlag(null, true) @@ -3143,17 +3143,7 @@ class ScopesTest { sut.captureException(RuntimeException("test exception")) - verify(mockClient) - .captureEvent( - any(), - check { - val featureFlags = it.featureFlags - assertNotNull(featureFlags) - - assertEquals(0, featureFlags.values.size) - }, - anyOrNull(), - ) + verify(mockClient).captureEvent(any(), check { assertNull(it.featureFlags) }, anyOrNull()) } private val dsnTest = "https://key@sentry.io/proj" From a5c7aef033fbc478546756f04e9e0aa7acaa1772 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Tue, 21 Oct 2025 09:42:56 +0200 Subject: [PATCH 11/15] Fix duplicate check --- .../main/java/io/sentry/featureflags/FeatureFlagBuffer.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sentry/src/main/java/io/sentry/featureflags/FeatureFlagBuffer.java b/sentry/src/main/java/io/sentry/featureflags/FeatureFlagBuffer.java index f140dd92e4..5baa83fda0 100644 --- a/sentry/src/main/java/io/sentry/featureflags/FeatureFlagBuffer.java +++ b/sentry/src/main/java/io/sentry/featureflags/FeatureFlagBuffer.java @@ -45,7 +45,8 @@ public void add(final @Nullable String flag, final @Nullable Boolean result) { try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { final int size = flags.size(); for (int i = 0; i < size; i++) { - if (flags.get(i).equals(flag)) { + final @NotNull FeatureFlagEntry entry = flags.get(i); + if (entry.flag.equals(flag)) { flags.remove(i); break; } From 0ab241eb2b6db1c5742b93cc234e139094d35020 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Tue, 21 Oct 2025 09:43:14 +0200 Subject: [PATCH 12/15] Update sentry/src/main/java/io/sentry/SentryOptions.java Co-authored-by: Lorenzo Cian <17258265+lcian@users.noreply.github.com> --- sentry/src/main/java/io/sentry/SentryOptions.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/sentry/src/main/java/io/sentry/SentryOptions.java b/sentry/src/main/java/io/sentry/SentryOptions.java index 6a488473b0..b12c0ad050 100644 --- a/sentry/src/main/java/io/sentry/SentryOptions.java +++ b/sentry/src/main/java/io/sentry/SentryOptions.java @@ -197,7 +197,9 @@ public class SentryOptions { private int maxBreadcrumbs = 100; /** - * This variable controls the total amount of feature flags that should be captured Default is 100 + * This variable controls the total amount of feature flag evaluations that should be stored on the scope. + * The most recent `maxFeatureFlags` evaluations are stored on each scope. + * Default is 100 */ private int maxFeatureFlags = 100; From ab1a1469c79a6eb4e546535a045de894e088ef89 Mon Sep 17 00:00:00 2001 From: Sentry Github Bot Date: Tue, 21 Oct 2025 07:46:28 +0000 Subject: [PATCH 13/15] Format code --- sentry/src/main/java/io/sentry/SentryOptions.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sentry/src/main/java/io/sentry/SentryOptions.java b/sentry/src/main/java/io/sentry/SentryOptions.java index 2ad368e345..6fa1c0c94e 100644 --- a/sentry/src/main/java/io/sentry/SentryOptions.java +++ b/sentry/src/main/java/io/sentry/SentryOptions.java @@ -199,9 +199,9 @@ public class SentryOptions { private int maxBreadcrumbs = 100; /** - * This variable controls the total amount of feature flag evaluations that should be stored on the scope. - * The most recent `maxFeatureFlags` evaluations are stored on each scope. - * Default is 100 + * This variable controls the total amount of feature flag evaluations that should be stored on + * the scope. The most recent `maxFeatureFlags` evaluations are stored on each scope. Default is + * 100 */ private int maxFeatureFlags = 100; From 15544cd5d42a0f66c3cc509e1acd5f593e73d69e Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Thu, 6 Nov 2025 09:56:30 +0100 Subject: [PATCH 14/15] changelog --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c55dcc5d7..fd6f350ad0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ ## Unreleased +### Features + +- Add feature flags API ([#4812](https://github.com/getsentry/sentry-java/pull/4812)) + - You may now keep track of your feature flag evaluations and have them show up in Sentry. + - You may use top level API (`Sentry.addFeatureFlag("my-feature-flag", true);`) or `IScope` and `IScopes` API + - Feature flag evaluations tracked on scope(s) will be added to any errors reported to Sentry. + - The SDK keeps the latest 100 evaluations from scope(s), replacing old entries as new evaluations are added. + ### Fixes - Removed SentryExecutorService limit for delayed scheduled tasks ([#4846](https://github.com/getsentry/sentry-java/pull/4846)) From 9ac80b5548afec29c0c13384e4463c8fe9194de0 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Thu, 6 Nov 2025 09:56:37 +0100 Subject: [PATCH 15/15] nullable getFeatureFlags --- .../src/main/java/io/sentry/featureflags/FeatureFlagBuffer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry/src/main/java/io/sentry/featureflags/FeatureFlagBuffer.java b/sentry/src/main/java/io/sentry/featureflags/FeatureFlagBuffer.java index 5baa83fda0..46c2e67470 100644 --- a/sentry/src/main/java/io/sentry/featureflags/FeatureFlagBuffer.java +++ b/sentry/src/main/java/io/sentry/featureflags/FeatureFlagBuffer.java @@ -60,7 +60,7 @@ public void add(final @Nullable String flag, final @Nullable Boolean result) { } @Override - public @NotNull FeatureFlags getFeatureFlags() { + public @Nullable FeatureFlags getFeatureFlags() { List featureFlags = new ArrayList<>(); for (FeatureFlagEntry entry : flags) { featureFlags.add(entry.toFeatureFlag());