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 8af939c32bf..a41e853e156 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 427c930653b..29d144355a7 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 9d6a8fd7a9e..5f557b7ad2a 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 18409c5b4e6..cf09728047d 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 86030003f62..9f245470af4 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 3c89c5a7e20..d23428da943 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 9a7612354a6..5703fff5d44 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 eed0354863f..5d3266c6ff8 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 4aeae0038f0..ec3928998a4 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 72744c0ec31..40169882224 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 a9a413fd5f7..d66cf747c1f 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 628c27f4c6f..3ad118d52f6 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 f3f03b39e1f..70159b8aef0 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 707b5025dcf..2488cc87d11 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 9b727447ffd..2861168fc79 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 707b5025dcf..2488cc87d11 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 b2563200c83..0db43f5ab71 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 26c3282e7a8..ac74d5e4953 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 305850ec18b..ff545349591 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 7d6e0182530..03a0abbdf2f 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 b3f22fd7fd4..26784880a75 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 707b5025dcf..2488cc87d11 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 1584a9e823b..1880799c28e 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 707b5025dcf..2488cc87d11 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 94a4b9b8520..2e24833b80f 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 7d6e0182530..03a0abbdf2f 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 97c5aa2e6f3..e2574e788c4 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 707b5025dcf..2488cc87d11 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 04816e34630..6c6209403b2 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 707b5025dcf..2488cc87d11 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 a7b7752806d..d1a505d0e59 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 26c3282e7a8..ac74d5e4953 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 010e42f3026..d0b5435efc1 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 26c3282e7a8..ac74d5e4953 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 45e3b96f888..3bf03cb785f 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 7d6e0182530..8119e5ab4e9 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 dab805281e1..ec33f360967 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 80d21b1d934..1215b819127 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 37da24d5812..ee4020e0324 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 97b826e10d0..aecc6ce24e1 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 b32583074d4..b2a159f92eb 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 73b2acd2ae1..00bfa743a39 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 f49e035f5ff..4779d08a055 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;Ljava/lang/Boolean;)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;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; @@ -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;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; @@ -842,6 +849,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;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 @@ -858,6 +866,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; @@ -927,6 +937,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;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; @@ -1509,6 +1520,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;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; @@ -1610,6 +1622,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;Ljava/lang/Boolean;)V public fun assignTraceContext (Lio/sentry/SentryEvent;)V public fun bindClient (Lio/sentry/ISentryClient;)V public fun clear ()V @@ -1627,6 +1640,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; @@ -1676,6 +1691,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;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; @@ -2269,6 +2285,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;Ljava/lang/Boolean;)V public fun assignTraceContext (Lio/sentry/SentryEvent;)V public fun bindClient (Lio/sentry/ISentryClient;)V public fun clear ()V @@ -2286,6 +2303,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; @@ -2384,6 +2403,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;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; @@ -2455,6 +2475,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;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; @@ -2572,6 +2593,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;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; @@ -3382,6 +3404,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 @@ -3529,6 +3552,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 @@ -4711,6 +4735,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;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; + 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;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;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; + 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; @@ -5182,6 +5230,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; @@ -5203,6 +5252,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 @@ -5428,6 +5478,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 31b5c060620..3cb10e88b81 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 d6ac5b824a9..0d7c8460e9d 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 @Nullable String flag, final @Nullable 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 f9065dd64c1..31a2e219cd0 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 @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 4430402af06..d15ed72ee4b 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 @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 ddabd00569e..f41ea1cbbe1 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 @Nullable String flag, final @Nullable 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 0fbc1008596..bf78d28ecd3 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 @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 c6b31c1a5ce..811e1d297a3 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 @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 dd1a202b548..c04c5af87bd 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 @Nullable String flag, final @Nullable 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 4e039a7b508..40777da892f 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 @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 7a54c4c755a..eb17420dd24 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 @Nullable String flag, final @Nullable 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 12309e355c5..c8afde59cc1 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 @Nullable String flag, final @Nullable 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 86d316b967f..99e70694ee0 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 @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 a28ee936e62..140a6393419 100644 --- a/sentry/src/main/java/io/sentry/Sentry.java +++ b/sentry/src/main/java/io/sentry/Sentry.java @@ -1373,4 +1373,8 @@ public static void showUserFeedbackDialog( final @NotNull SentryOptions options = getCurrentScopes().getOptions(); options.getFeedbackOptions().getDialogHandler().showDialog(associatedEventId, configurator); } + + 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/SentryClient.java b/sentry/src/main/java/io/sentry/SentryClient.java index bfcf4e780be..19529c550fa 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 3a81b268b25..6fa1c0c94e5 100644 --- a/sentry/src/main/java/io/sentry/SentryOptions.java +++ b/sentry/src/main/java/io/sentry/SentryOptions.java @@ -198,6 +198,13 @@ 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 + */ + private int maxFeatureFlags = 100; + /** Sets the release. SDK will try to automatically configure a release out of the box */ private @Nullable String release; @@ -1030,6 +1037,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 00000000000..5baa83fda07 --- /dev/null +++ b/sentry/src/main/java/io/sentry/featureflags/FeatureFlagBuffer.java @@ -0,0 +1,232 @@ +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 = new CopyOnWriteArrayList<>(other.flags); + } + + @Override + 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(); + for (int i = 0; i < size; i++) { + final @NotNull FeatureFlagEntry entry = flags.get(i); + if (entry.flag.equals(flag)) { + flags.remove(i); + break; + } + } + flags.add(new FeatureFlagEntry(flag, result, System.nanoTime())); + + if (flags.size() > maxSize) { + flags.remove(0); + } + } + } + + @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); + } + + /** + * 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, + 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; + + @Nullable + 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); + + 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 + && (globalEntry != null || isolationEntry != null || currentEntry != 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--; + 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 + // 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 00000000000..7f12026a590 --- /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(final @Nullable String flag, final @Nullable 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 00000000000..3bfc8f8fd2a --- /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(final @Nullable String flag, final @Nullable 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 e97431db4da..553f4ddbd30 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 00000000000..ebb2e58cd3d --- /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; + final @NotNull FeatureFlag otherFlag = (FeatureFlag) o; + return Objects.equals(flag, otherFlag.flag) && Objects.equals(result, otherFlag.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 00000000000..9e84a21de99 --- /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 8671017eae5..42c18049e04 100644 --- a/sentry/src/test/java/io/sentry/ScopeTest.kt +++ b/sentry/src/test/java/io/sentry/ScopeTest.kt @@ -1107,6 +1107,36 @@ 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) + } + + @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 4f0ee526dc5..fa82c53fbe9 100644 --- a/sentry/src/test/java/io/sentry/ScopesTest.kt +++ b/sentry/src/test/java/io/sentry/ScopesTest.kt @@ -3083,6 +3083,69 @@ 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(), + ) + } + + @Test + fun `null feature flags are ignored`() { + val (sut, mockClient) = getEnabledScopes() + + sut.addFeatureFlag(null, true) + sut.addFeatureFlag("flag-1", null) + 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 { assertNull(it.featureFlags) }, 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 6d0bcd5790b..bf54db9228d 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 00000000000..471ba880eb4 --- /dev/null +++ b/sentry/src/test/java/io/sentry/featureflags/FeatureFlagBufferTest.kt @@ -0,0 +1,337 @@ +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 `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 } + 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) + } + + @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) + } +} diff --git a/sentry/src/test/java/io/sentry/protocol/CombinedContextsViewSerializationTest.kt b/sentry/src/test/java/io/sentry/protocol/CombinedContextsViewSerializationTest.kt index fd7fae9108c..d7fd3cf9f7f 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 c02bb4240b2..1a5e252a76d 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 00000000000..8c401ddcc63 --- /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 389cd121c26..f2cfd1ce077 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 894d73e2dc6..7f4c0c16bc2 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 00000000000..ff4569fb57d --- /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 c96d3bed453..817ae1ff7be 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",