diff --git a/.craft.yml b/.craft.yml index 41874418817..cd403ade4c2 100644 --- a/.craft.yml +++ b/.craft.yml @@ -41,6 +41,7 @@ targets: maven:io.sentry:sentry-android-fragment: maven:io.sentry:sentry-bom: maven:io.sentry:sentry-openfeign: + #maven:io.sentry:sentry-openfeature: maven:io.sentry:sentry-opentelemetry-agent: maven:io.sentry:sentry-opentelemetry-agentcustomization: maven:io.sentry:sentry-opentelemetry-agentless: diff --git a/.github/ISSUE_TEMPLATE/bug_report_java.yml b/.github/ISSUE_TEMPLATE/bug_report_java.yml index a7ca3cbb770..9059373b8f3 100644 --- a/.github/ISSUE_TEMPLATE/bug_report_java.yml +++ b/.github/ISSUE_TEMPLATE/bug_report_java.yml @@ -35,6 +35,7 @@ body: - sentry-graphql-22 - sentry-quartz - sentry-openfeign + - sentry-openfeature - sentry-apache-http-client-5 - sentry-okhttp - sentry-reactor diff --git a/CHANGELOG.md b/CHANGELOG.md index 48cd0a9cc56..7af3c34a3a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ ### Features +- Implement OpenFeature Integration that tracks Feature Flag evaluations ([#4910](https://github.com/getsentry/sentry-java/pull/4910)) + - To make use of it, add the `sentry-openfeature` dependency and register the the hook using: `openFeatureApiInstance.addHooks(new SentryOpenFeatureHook());` - Detect oversized events and reduce their size ([#4903](https://github.com/getsentry/sentry-java/pull/4903)) - You can opt into this new behaviour by setting `enableEventSizeLimiting` to `true` (`sentry.enable-event-size-limiting=true` for Spring Boot `application.properties`) - You may optionally register an `onOversizedEvent` callback to implement custom logic that is executed in case an oversized event is detected diff --git a/README.md b/README.md index 3838a90b54a..8bd1d8fadd0 100644 --- a/README.md +++ b/README.md @@ -17,53 +17,54 @@ Sentry SDK for Java and Android [![X Follow](https://img.shields.io/twitter/follow/sentry?label=sentry&style=social)](https://x.com/intent/follow?screen_name=sentry) [![Discord Chat](https://img.shields.io/discord/621778831602221064?logo=discord&logoColor=ffffff&color=7389D8)](https://discord.gg/PXa5Apfe7K) -| Packages | Maven Central | Minimum Android API Version | -|-----------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| ------- | -| sentry-android | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-android?style=for-the-badge&logo=sentry&color=green) | 21 | -| sentry-android-core | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-android-core?style=for-the-badge&logo=sentry&color=green) | 21 | -| sentry-android-distribution | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-android-distribution?style=for-the-badge&logo=sentry&color=green) | 21 | -| sentry-android-ndk | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-android-ndk?style=for-the-badge&logo=sentry&color=green) | 21 | -| sentry-android-timber | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-android-timber?style=for-the-badge&logo=sentry&color=green) | 21 | -| sentry-android-fragment | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-android-fragment?style=for-the-badge&logo=sentry&color=green) | 21 | -| sentry-android-navigation | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-android-navigation?style=for-the-badge&logo=sentry&color=green) | 21 | -| sentry-android-sqlite | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-android-sqlite?style=for-the-badge&logo=sentry&color=green) | 21 | -| sentry-android-replay | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-android-replay?style=for-the-badge&logo=sentry&color=green) | 26 | -| sentry-compose-android | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-compose-android?style=for-the-badge&logo=sentry&color=green) | 21 | -| sentry-compose-desktop | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-compose-desktop?style=for-the-badge&logo=sentry&color=green) | -| sentry-compose | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-compose?style=for-the-badge&logo=sentry&color=green) | -| sentry-apache-http-client-5 | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-apache-http-client-5?style=for-the-badge&logo=sentry&color=green) | -| sentry | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry?style=for-the-badge&logo=sentry&color=green) | 21 | -| sentry-jul | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-jul?style=for-the-badge&logo=sentry&color=green) | -| sentry-jdbc | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-jdbc?style=for-the-badge&logo=sentry&color=green) | -| sentry-apollo | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-apollo?style=for-the-badge&logo=sentry&color=green) | 21 | -| sentry-apollo-3 | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-apollo-3?style=for-the-badge&logo=sentry&color=green) | 21 | -| sentry-apollo-4 | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-apollo-4?style=for-the-badge&logo=sentry&color=green) | 21 | -| sentry-kotlin-extensions | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-kotlin-extensions?style=for-the-badge&logo=sentry&color=green) | 21 | -| sentry-ktor-client | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-ktor-client?style=for-the-badge&logo=sentry&color=green) | 21 | -| sentry-servlet | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-servlet?style=for-the-badge&logo=sentry&color=green) | | -| sentry-servlet-jakarta | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-servlet-jakarta?style=for-the-badge&logo=sentry&color=green) | | -| sentry-spring-boot | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-spring-boot?style=for-the-badge&logo=sentry&color=green) | -| sentry-spring-boot-jakarta | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-spring-boot-jakarta?style=for-the-badge&logo=sentry&color=green) | -| sentry-spring-boot-4 | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-spring-boot-4?style=for-the-badge&logo=sentry&color=green) | -| sentry-spring-boot-4-starter | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-spring-boot-4-starter?style=for-the-badge&logo=sentry&color=green) | -| sentry-spring-boot-starter | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-spring-boot-starter?style=for-the-badge&logo=sentry&color=green) | -| sentry-spring-boot-starter-jakarta | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-spring-boot-starter-jakarta?style=for-the-badge&logo=sentry&color=green) | -| sentry-spring | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-spring?style=for-the-badge&logo=sentry&color=green) | -| sentry-spring-jakarta | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-spring-jakarta?style=for-the-badge&logo=sentry&color=green) | -| sentry-spring-7 | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-spring-7?style=for-the-badge&logo=sentry&color=green) | -| sentry-logback | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-logback?style=for-the-badge&logo=sentry&color=green) | -| sentry-log4j2 | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-log4j2?style=for-the-badge&logo=sentry&color=green) | -| sentry-bom | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-bom?style=for-the-badge&logo=sentry&color=green) | -| sentry-graphql | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-graphql?style=for-the-badge&logo=sentry&color=green) | -| sentry-graphql-core | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-graphql-core?style=for-the-badge&logo=sentry&color=green) | -| sentry-graphql-22 | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-graphql-22?style=for-the-badge&logo=sentry&color=green) | -| sentry-quartz | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-quartz?style=for-the-badge&logo=sentry&color=green) | -| sentry-openfeign | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-openfeign?style=for-the-badge&logo=sentry&color=green) | -| sentry-opentelemetry-agent | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-opentelemetry-agent?style=for-the-badge&logo=sentry&color=green) | +| Packages | Maven Central | Minimum Android API Version | +|-----------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------| ------- | +| sentry-android | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-android?style=for-the-badge&logo=sentry&color=green) | 21 | +| sentry-android-core | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-android-core?style=for-the-badge&logo=sentry&color=green) | 21 | +| sentry-android-distribution | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-android-distribution?style=for-the-badge&logo=sentry&color=green) | 21 | +| sentry-android-ndk | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-android-ndk?style=for-the-badge&logo=sentry&color=green) | 21 | +| sentry-android-timber | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-android-timber?style=for-the-badge&logo=sentry&color=green) | 21 | +| sentry-android-fragment | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-android-fragment?style=for-the-badge&logo=sentry&color=green) | 21 | +| sentry-android-navigation | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-android-navigation?style=for-the-badge&logo=sentry&color=green) | 21 | +| sentry-android-sqlite | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-android-sqlite?style=for-the-badge&logo=sentry&color=green) | 21 | +| sentry-android-replay | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-android-replay?style=for-the-badge&logo=sentry&color=green) | 26 | +| sentry-compose-android | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-compose-android?style=for-the-badge&logo=sentry&color=green) | 21 | +| sentry-compose-desktop | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-compose-desktop?style=for-the-badge&logo=sentry&color=green) | +| sentry-compose | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-compose?style=for-the-badge&logo=sentry&color=green) | +| sentry-apache-http-client-5 | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-apache-http-client-5?style=for-the-badge&logo=sentry&color=green) | +| sentry | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry?style=for-the-badge&logo=sentry&color=green) | 21 | +| sentry-jul | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-jul?style=for-the-badge&logo=sentry&color=green) | +| sentry-jdbc | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-jdbc?style=for-the-badge&logo=sentry&color=green) | +| sentry-apollo | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-apollo?style=for-the-badge&logo=sentry&color=green) | 21 | +| sentry-apollo-3 | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-apollo-3?style=for-the-badge&logo=sentry&color=green) | 21 | +| sentry-apollo-4 | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-apollo-4?style=for-the-badge&logo=sentry&color=green) | 21 | +| sentry-kotlin-extensions | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-kotlin-extensions?style=for-the-badge&logo=sentry&color=green) | 21 | +| sentry-ktor-client | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-ktor-client?style=for-the-badge&logo=sentry&color=green) | 21 | +| sentry-servlet | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-servlet?style=for-the-badge&logo=sentry&color=green) | | +| sentry-servlet-jakarta | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-servlet-jakarta?style=for-the-badge&logo=sentry&color=green) | | +| sentry-spring-boot | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-spring-boot?style=for-the-badge&logo=sentry&color=green) | +| sentry-spring-boot-jakarta | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-spring-boot-jakarta?style=for-the-badge&logo=sentry&color=green) | +| sentry-spring-boot-4 | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-spring-boot-4?style=for-the-badge&logo=sentry&color=green) | +| sentry-spring-boot-4-starter | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-spring-boot-4-starter?style=for-the-badge&logo=sentry&color=green) | +| sentry-spring-boot-starter | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-spring-boot-starter?style=for-the-badge&logo=sentry&color=green) | +| sentry-spring-boot-starter-jakarta | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-spring-boot-starter-jakarta?style=for-the-badge&logo=sentry&color=green) | +| sentry-spring | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-spring?style=for-the-badge&logo=sentry&color=green) | +| sentry-spring-jakarta | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-spring-jakarta?style=for-the-badge&logo=sentry&color=green) | +| sentry-spring-7 | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-spring-7?style=for-the-badge&logo=sentry&color=green) | +| sentry-logback | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-logback?style=for-the-badge&logo=sentry&color=green) | +| sentry-log4j2 | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-log4j2?style=for-the-badge&logo=sentry&color=green) | +| sentry-bom | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-bom?style=for-the-badge&logo=sentry&color=green) | +| sentry-graphql | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-graphql?style=for-the-badge&logo=sentry&color=green) | +| sentry-graphql-core | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-graphql-core?style=for-the-badge&logo=sentry&color=green) | +| sentry-graphql-22 | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-graphql-22?style=for-the-badge&logo=sentry&color=green) | +| sentry-quartz | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-quartz?style=for-the-badge&logo=sentry&color=green) | +| sentry-openfeign | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-openfeign?style=for-the-badge&logo=sentry&color=green) | +| sentry-openfeature | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-openfeature?style=for-the-badge&logo=sentry&color=green) | +| sentry-opentelemetry-agent | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-opentelemetry-agent?style=for-the-badge&logo=sentry&color=green) | | sentry-opentelemetry-agentcustomization | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-opentelemetry-agentcustomization?style=for-the-badge&logo=sentry&color=green) | -| sentry-opentelemetry-core | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-opentelemetry-core?style=for-the-badge&logo=sentry&color=green) | -| sentry-okhttp | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-okhttp?style=for-the-badge&logo=sentry&color=green) | -| sentry-reactor | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-reactor?style=for-the-badge&logo=sentry&color=green) | +| sentry-opentelemetry-core | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-opentelemetry-core?style=for-the-badge&logo=sentry&color=green) | +| sentry-okhttp | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-okhttp?style=for-the-badge&logo=sentry&color=green) | +| sentry-reactor | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-reactor?style=for-the-badge&logo=sentry&color=green) | # Releases diff --git a/buildSrc/src/main/java/Config.kt b/buildSrc/src/main/java/Config.kt index 9ffe5ac3117..887bf1415b2 100644 --- a/buildSrc/src/main/java/Config.kt +++ b/buildSrc/src/main/java/Config.kt @@ -77,6 +77,7 @@ object Config { val SENTRY_GRAPHQL22_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.graphql22" val SENTRY_QUARTZ_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.quartz" val SENTRY_JDBC_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.jdbc" + val SENTRY_OPENFEATURE_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.openfeature" val SENTRY_SERVLET_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.servlet" val SENTRY_SERVLET_JAKARTA_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.servlet.jakarta" val SENTRY_COMPOSE_HELPER_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.compose.helper" diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 154504b7b75..3432bad6eec 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -40,6 +40,7 @@ minSdk = "21" spotless = "7.0.4" gummyBears = "0.12.0" camerax = "1.3.0" +openfeature = "1.18.2" [plugins] kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } @@ -124,6 +125,7 @@ nopen-annotations = { module = "com.jakewharton.nopen:nopen-annotations", versio nopen-checker = { module = "com.jakewharton.nopen:nopen-checker", version.ref = "nopen" } nullaway = { module = "com.uber.nullaway:nullaway", version = "0.9.5" } okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" } +openfeature = { module = "dev.openfeature:sdk", version.ref = "openfeature" } otel = { module = "io.opentelemetry:opentelemetry-sdk", version.ref = "otel" } otel-extension-autoconfigure = { module = "io.opentelemetry:opentelemetry-sdk-extension-autoconfigure", version.ref = "otel" } otel-extension-autoconfigure-spi = { module = "io.opentelemetry:opentelemetry-sdk-extension-autoconfigure-spi", version.ref = "otel" } diff --git a/sentry-openfeature/api/sentry-openfeature.api b/sentry-openfeature/api/sentry-openfeature.api new file mode 100644 index 00000000000..5fe3402b593 --- /dev/null +++ b/sentry-openfeature/api/sentry-openfeature.api @@ -0,0 +1,10 @@ +public final class io/sentry/openfeature/BuildConfig { + public static final field SENTRY_OPENFEATURE_SDK_NAME Ljava/lang/String; + public static final field VERSION_NAME Ljava/lang/String; +} + +public final class io/sentry/openfeature/SentryOpenFeatureHook : dev/openfeature/sdk/BooleanHook { + public fun ()V + public fun after (Ldev/openfeature/sdk/HookContext;Ldev/openfeature/sdk/FlagEvaluationDetails;Ljava/util/Map;)V +} + diff --git a/sentry-openfeature/build.gradle.kts b/sentry-openfeature/build.gradle.kts new file mode 100644 index 00000000000..632d16b55cf --- /dev/null +++ b/sentry-openfeature/build.gradle.kts @@ -0,0 +1,91 @@ +import net.ltgt.gradle.errorprone.errorprone +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + `java-library` + id("io.sentry.javadoc") + alias(libs.plugins.kotlin.jvm) + jacoco + alias(libs.plugins.errorprone) + alias(libs.plugins.gradle.versions) + alias(libs.plugins.buildconfig) +} + +tasks.withType().configureEach { + compilerOptions.jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_1_8 + compilerOptions.languageVersion = org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_9 + compilerOptions.apiVersion = org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_9 +} + +dependencies { + api(projects.sentry) + + compileOnly(libs.openfeature) + + compileOnly(libs.jetbrains.annotations) + compileOnly(libs.nopen.annotations) + errorprone(libs.errorprone.core) + errorprone(libs.nopen.checker) + errorprone(libs.nullaway) + + // tests + testImplementation(projects.sentry) + testImplementation(projects.sentryTestSupport) + testImplementation(kotlin(Config.kotlinStdLib)) + testImplementation(libs.kotlin.test.junit) + testImplementation(libs.mockito.kotlin) + testImplementation(libs.mockito.inline) + testImplementation(libs.openfeature) +} + +configure { test { java.srcDir("src/test/java") } } + +jacoco { toolVersion = libs.versions.jacoco.get() } + +tasks.jacocoTestReport { + reports { + xml.required.set(true) + html.required.set(false) + } +} + +tasks { + jacocoTestCoverageVerification { + violationRules { rule { limit { minimum = Config.QualityPlugins.Jacoco.minimumCoverage } } } + } + check { + dependsOn(jacocoTestCoverageVerification) + dependsOn(jacocoTestReport) + } +} + +tasks.withType().configureEach { + options.errorprone { + check("NullAway", net.ltgt.gradle.errorprone.CheckSeverity.ERROR) + option("NullAway:AnnotatedPackages", "io.sentry") + } +} + +buildConfig { + useJavaOutput() + packageName("io.sentry.openfeature") + buildConfigField( + "String", + "SENTRY_OPENFEATURE_SDK_NAME", + "\"${Config.Sentry.SENTRY_OPENFEATURE_SDK_NAME}\"", + ) + buildConfigField("String", "VERSION_NAME", "\"${project.version}\"") +} + +tasks.jar { + manifest { + attributes( + "Sentry-Version-Name" to project.version, + "Sentry-SDK-Name" to Config.Sentry.SENTRY_OPENFEATURE_SDK_NAME, + "Sentry-SDK-Package-Name" to "maven:io.sentry:sentry-openfeature", + "Implementation-Vendor" to "Sentry", + "Implementation-Title" to project.name, + "Implementation-Version" to project.version, + ) + } +} diff --git a/sentry-openfeature/src/main/java/io/sentry/openfeature/SentryOpenFeatureHook.java b/sentry-openfeature/src/main/java/io/sentry/openfeature/SentryOpenFeatureHook.java new file mode 100644 index 00000000000..683417c33c1 --- /dev/null +++ b/sentry-openfeature/src/main/java/io/sentry/openfeature/SentryOpenFeatureHook.java @@ -0,0 +1,75 @@ +package io.sentry.openfeature; + +import static io.sentry.util.IntegrationUtils.addIntegrationToSdkVersion; + +import dev.openfeature.sdk.BooleanHook; +import dev.openfeature.sdk.FlagEvaluationDetails; +import dev.openfeature.sdk.FlagValueType; +import dev.openfeature.sdk.HookContext; +import io.sentry.IScopes; +import io.sentry.ScopesAdapter; +import io.sentry.SentryIntegrationPackageStorage; +import io.sentry.SentryLevel; +import java.util.Map; +import java.util.Objects; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.VisibleForTesting; + +public final class SentryOpenFeatureHook implements BooleanHook { + private final IScopes scopes; + + static { + SentryIntegrationPackageStorage.getInstance() + .addPackage("maven:io.sentry:sentry-openfeature", BuildConfig.VERSION_NAME); + } + + public SentryOpenFeatureHook() { + this(ScopesAdapter.getInstance()); + addPackageAndIntegrationInfo(); + } + + private void addPackageAndIntegrationInfo() { + addIntegrationToSdkVersion("OpenFeature"); + } + + @VisibleForTesting + SentryOpenFeatureHook(@NotNull IScopes scopes) { + this.scopes = Objects.requireNonNull(scopes, "Scopes are required"); + } + + @Override + public void after( + final @Nullable HookContext context, + final @Nullable FlagEvaluationDetails details, + final @Nullable Map hints) { + if (context == null || details == null) { + return; + } + try { + final @Nullable String flagKey = details.getFlagKey(); + final @Nullable FlagValueType type = context.getType(); + final @Nullable Object value = details.getValue(); + + if (flagKey == null || type == null || value == null) { + return; + } + + if (!FlagValueType.BOOLEAN.equals(type)) { + return; + } + + if (!(value instanceof Boolean)) { + return; + } + final @NotNull Boolean flagValue = (Boolean) value; + + scopes.addFeatureFlag(flagKey, flagValue); + } catch (Exception e) { + scopes + .getOptions() + .getLogger() + .log(SentryLevel.ERROR, "Failed to capture feature flag evaluation", e); + } + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-jakarta/build.gradle.kts b/sentry-samples/sentry-samples-spring-boot-jakarta/build.gradle.kts index cde2de9c0f4..570d35b727b 100644 --- a/sentry-samples/sentry-samples-spring-boot-jakarta/build.gradle.kts +++ b/sentry-samples/sentry-samples-spring-boot-jakarta/build.gradle.kts @@ -53,6 +53,10 @@ dependencies { implementation(projects.sentryGraphql22) implementation(projects.sentryQuartz) implementation(projects.sentryAsyncProfiler) + implementation(projects.sentryOpenfeature) + + // OpenFeature SDK + implementation(libs.openfeature) // database query tracing implementation(projects.sentryJdbc) diff --git a/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/FeatureFlagController.java b/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/FeatureFlagController.java new file mode 100644 index 00000000000..2393c06158b --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/FeatureFlagController.java @@ -0,0 +1,68 @@ +package io.sentry.samples.spring.boot.jakarta; + +import dev.openfeature.sdk.Client; +import dev.openfeature.sdk.ImmutableContext; +import dev.openfeature.sdk.OpenFeatureAPI; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/feature-flag/") +public class FeatureFlagController { + private static final Logger LOGGER = LoggerFactory.getLogger(FeatureFlagController.class); + private final OpenFeatureAPI openFeatureAPI; + + public FeatureFlagController(OpenFeatureAPI openFeatureAPI) { + this.openFeatureAPI = openFeatureAPI; + } + + @GetMapping("check/{flagKey}") + public FeatureFlagResponse checkFlag(@PathVariable String flagKey) { + Client client = openFeatureAPI.getClient(); + + // Evaluate boolean feature flag + // This will trigger the SentryOpenFeatureHook which tracks the evaluation + boolean flagValue = + client.getBooleanValue(flagKey, false, new ImmutableContext("example-context-key")); + + LOGGER.info("Feature flag '{}' evaluated to: {}", flagKey, flagValue); + + return new FeatureFlagResponse(flagKey, flagValue); + } + + @GetMapping("error/{flagKey}") + public String errorWithFeatureFlag(@PathVariable String flagKey) { + Client client = openFeatureAPI.getClient(); + + // Evaluate feature flag before throwing error + // The feature flag will be included in the Sentry event + boolean flagValue = + client.getBooleanValue(flagKey, false, new ImmutableContext("example-context-key")); + + LOGGER.info("Feature flag '{}' evaluated to: {} before error", flagKey, flagValue); + + throw new RuntimeException("Error occurred with feature flag: " + flagKey + " = " + flagValue); + } + + public static class FeatureFlagResponse { + private final String flagKey; + private final boolean value; + + public FeatureFlagResponse(String flagKey, boolean value) { + this.flagKey = flagKey; + this.value = value; + } + + public String getFlagKey() { + return flagKey; + } + + public boolean isValue() { + return value; + } + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/OpenFeatureConfig.java b/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/OpenFeatureConfig.java new file mode 100644 index 00000000000..2ada5e18b52 --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/OpenFeatureConfig.java @@ -0,0 +1,226 @@ +package io.sentry.samples.spring.boot.jakarta; + +import dev.openfeature.sdk.EvaluationContext; +import dev.openfeature.sdk.FeatureProvider; +import dev.openfeature.sdk.Metadata; +import dev.openfeature.sdk.OpenFeatureAPI; +import dev.openfeature.sdk.ProviderEvaluation; +import dev.openfeature.sdk.Reason; +import dev.openfeature.sdk.Value; +import io.sentry.openfeature.SentryOpenFeatureHook; +import java.util.HashMap; +import java.util.Map; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class OpenFeatureConfig { + + @Bean + public OpenFeatureAPI openFeatureAPI() { + OpenFeatureAPI api = OpenFeatureAPI.getInstance(); + + // Use simple in-memory provider for demo purposes + // In production, you would use a real provider like LaunchDarkly, Flagsmith, etc. + FeatureProvider provider = new InMemoryProvider(createFeatureFlags()); + api.setProvider(provider); + + // Register Sentry hook to track feature flag evaluations + api.addHooks(new SentryOpenFeatureHook()); + + return api; + } + + private Map createFeatureFlags() { + Map flags = new HashMap<>(); + + // Boolean flags + flags.put("new-checkout-flow", true); + flags.put("experimental-feature", false); + flags.put("enable-caching", true); + flags.put("dark-mode", false); + flags.put("beta-features", true); + + // String flags + flags.put("theme-color", "blue"); + flags.put("api-version", "v2"); + flags.put("default-language", "en"); + flags.put("welcome-message", "Welcome to Sentry!"); + + // Integer flags + flags.put("max-retries", 3); + flags.put("page-size", 20); + flags.put("timeout-seconds", 30); + flags.put("max-connections", 100); + + // Double flags + flags.put("discount-percentage", 0.15); + flags.put("tax-rate", 0.08); + flags.put("conversion-rate", 0.025); + + // Object/Structure flags (stored as Map) + Map configMap = new HashMap<>(); + configMap.put("enabled", true); + configMap.put("threshold", 100); + configMap.put("mode", "production"); + flags.put("advanced-config", configMap); + + Map uiConfig = new HashMap<>(); + uiConfig.put("layout", "grid"); + uiConfig.put("itemsPerPage", 12); + flags.put("ui-settings", uiConfig); + + return flags; + } + + /** + * Simple in-memory provider for demo purposes. In production, use a real provider like + * LaunchDarkly, Flagsmith, etc. + */ + private static class InMemoryProvider implements FeatureProvider { + private final Map flags; + + public InMemoryProvider(Map flags) { + this.flags = flags; + } + + @Override + public Metadata getMetadata() { + return new Metadata() { + @Override + public String getName() { + return "InMemoryProvider"; + } + }; + } + + @Override + public void initialize(EvaluationContext evaluationContext) { + // No initialization needed for in-memory provider + } + + @Override + public void shutdown() { + // No cleanup needed for in-memory provider + } + + @Override + public ProviderEvaluation getBooleanEvaluation( + String key, Boolean defaultValue, EvaluationContext ctx) { + Object value = flags.get(key); + if (value == null) { + return ProviderEvaluation.builder() + .value(defaultValue) + .reason(Reason.DEFAULT.name()) + .build(); + } + if (value instanceof Boolean) { + return ProviderEvaluation.builder() + .value((Boolean) value) + .reason(Reason.STATIC.name()) + .build(); + } + // Type mismatch - return default + return ProviderEvaluation.builder() + .value(defaultValue) + .reason(Reason.DEFAULT.name()) + .build(); + } + + @Override + public ProviderEvaluation getStringEvaluation( + String key, String defaultValue, EvaluationContext ctx) { + Object value = flags.get(key); + if (value == null) { + return ProviderEvaluation.builder() + .value(defaultValue) + .reason(Reason.DEFAULT.name()) + .build(); + } + if (value instanceof String) { + return ProviderEvaluation.builder() + .value((String) value) + .reason(Reason.STATIC.name()) + .build(); + } + // Type mismatch - return default + return ProviderEvaluation.builder() + .value(defaultValue) + .reason(Reason.DEFAULT.name()) + .build(); + } + + @Override + public ProviderEvaluation getIntegerEvaluation( + String key, Integer defaultValue, EvaluationContext ctx) { + Object value = flags.get(key); + if (value == null) { + return ProviderEvaluation.builder() + .value(defaultValue) + .reason(Reason.DEFAULT.name()) + .build(); + } + if (value instanceof Integer) { + return ProviderEvaluation.builder() + .value((Integer) value) + .reason(Reason.STATIC.name()) + .build(); + } + // Type mismatch - return default + return ProviderEvaluation.builder() + .value(defaultValue) + .reason(Reason.DEFAULT.name()) + .build(); + } + + @Override + public ProviderEvaluation getDoubleEvaluation( + String key, Double defaultValue, EvaluationContext ctx) { + Object value = flags.get(key); + if (value == null) { + return ProviderEvaluation.builder() + .value(defaultValue) + .reason(Reason.DEFAULT.name()) + .build(); + } + if (value instanceof Double) { + return ProviderEvaluation.builder() + .value((Double) value) + .reason(Reason.STATIC.name()) + .build(); + } + // Type mismatch - return default + return ProviderEvaluation.builder() + .value(defaultValue) + .reason(Reason.DEFAULT.name()) + .build(); + } + + @Override + public ProviderEvaluation getObjectEvaluation( + String key, Value defaultValue, EvaluationContext ctx) { + Object value = flags.get(key); + if (value == null) { + return ProviderEvaluation.builder() + .value(defaultValue) + .reason(Reason.DEFAULT.name()) + .build(); + } + if (value instanceof Map) { + // Convert Map to Value object + @SuppressWarnings("unchecked") + Map mapValue = (Map) value; + Value valueObj = Value.objectToValue(mapValue); + return ProviderEvaluation.builder() + .value(valueObj) + .reason(Reason.STATIC.name()) + .build(); + } + // Type mismatch - return default + return ProviderEvaluation.builder() + .value(defaultValue) + .reason(Reason.DEFAULT.name()) + .build(); + } + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-jakarta/src/test/kotlin/io/sentry/systemtest/FeatureFlagSystemTest.kt b/sentry-samples/sentry-samples-spring-boot-jakarta/src/test/kotlin/io/sentry/systemtest/FeatureFlagSystemTest.kt new file mode 100644 index 00000000000..10233e579e5 --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-jakarta/src/test/kotlin/io/sentry/systemtest/FeatureFlagSystemTest.kt @@ -0,0 +1,102 @@ +package io.sentry.systemtest + +import io.sentry.protocol.FeatureFlag +import io.sentry.systemtest.util.FeatureFlagResponse +import io.sentry.systemtest.util.TestHelper +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import org.junit.Before + +class FeatureFlagSystemTest { + lateinit var testHelper: TestHelper + + @Before + fun setup() { + testHelper = TestHelper("http://localhost:8080") + testHelper.reset() + } + + @Test + fun `check feature flag includes flag in transaction`() { + val restClient = testHelper.restClient + val response: FeatureFlagResponse? = restClient.checkFeatureFlag("new-checkout-flow") + + assertEquals(200, restClient.lastKnownStatusCode) + assertNotNull(response) + assertEquals("new-checkout-flow", response!!.flagKey) + assertEquals(true, response.value) + + // Verify feature flag is included in the transaction + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> + transaction.transaction == "GET /feature-flag/check/{flagKey}" && + testHelper.doesTransactionHave( + transaction, + op = "http.server", + featureFlag = FeatureFlag("flag.evaluation.new-checkout-flow", true), + ) + } + } + + @Test + fun `error with feature flag includes flag in error event and transaction`() { + val restClient = testHelper.restClient + restClient.errorWithFeatureFlag("beta-features") + assertEquals(500, restClient.lastKnownStatusCode) + + // Verify feature flag is included in the error event + testHelper.ensureErrorReceived { event -> + testHelper.doesEventHaveExceptionMessage( + event, + "Error occurred with feature flag: beta-features = true", + ) && testHelper.doesEventHaveFlag(event, "beta-features", true) + } + + // Verify feature flag is also included in the transaction + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> + transaction.transaction == "GET /feature-flag/error/{flagKey}" && + testHelper.doesTransactionHave( + transaction, + op = "http.server", + featureFlag = FeatureFlag("flag.evaluation.beta-features", true), + ) + } + } + + @Test + fun `check non-existent feature flag returns default value`() { + val restClient = testHelper.restClient + val response: FeatureFlagResponse? = restClient.checkFeatureFlag("non-existent-flag") + + assertEquals(200, restClient.lastKnownStatusCode) + assertNotNull(response) + assertEquals("non-existent-flag", response!!.flagKey) + assertEquals(false, response.value) // Default value + + // Verify transaction is still created + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> + transaction.transaction == "GET /feature-flag/check/{flagKey}" + } + } + + @Test + fun `check feature flag with false value`() { + val restClient = testHelper.restClient + val response: FeatureFlagResponse? = restClient.checkFeatureFlag("experimental-feature") + + assertEquals(200, restClient.lastKnownStatusCode) + assertNotNull(response) + assertEquals("experimental-feature", response!!.flagKey) + assertEquals(false, response.value) + + // Verify feature flag with false value is included in transaction + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> + transaction.transaction == "GET /feature-flag/check/{flagKey}" && + testHelper.doesTransactionHave( + transaction, + op = "http.server", + featureFlag = FeatureFlag("flag.evaluation.experimental-feature", false), + ) + } + } +} diff --git a/sentry-system-test-support/api/sentry-system-test-support.api b/sentry-system-test-support/api/sentry-system-test-support.api index d8fa7efe13f..1f5d1382a93 100644 --- a/sentry-system-test-support/api/sentry-system-test-support.api +++ b/sentry-system-test-support/api/sentry-system-test-support.api @@ -516,6 +516,19 @@ public final class io/sentry/systemtest/util/EnvelopesReceived { public fun toString ()Ljava/lang/String; } +public final class io/sentry/systemtest/util/FeatureFlagResponse { + public fun (Ljava/lang/String;Z)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Z + public final fun copy (Ljava/lang/String;Z)Lio/sentry/systemtest/util/FeatureFlagResponse; + public static synthetic fun copy$default (Lio/sentry/systemtest/util/FeatureFlagResponse;Ljava/lang/String;ZILjava/lang/Object;)Lio/sentry/systemtest/util/FeatureFlagResponse; + public fun equals (Ljava/lang/Object;)Z + public final fun getFlagKey ()Ljava/lang/String; + public final fun getValue ()Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + public class io/sentry/systemtest/util/LoggingInsecureRestClient { public fun ()V protected final fun call (Lokhttp3/Request$Builder;ZLjava/util/Map;)Lokhttp3/Response; @@ -530,10 +543,12 @@ public class io/sentry/systemtest/util/LoggingInsecureRestClient { public final class io/sentry/systemtest/util/RestTestClient : io/sentry/systemtest/util/LoggingInsecureRestClient { public fun (Ljava/lang/String;)V + public final fun checkFeatureFlag (Ljava/lang/String;)Lio/sentry/systemtest/util/FeatureFlagResponse; public final fun createPerson (Lio/sentry/systemtest/Person;Ljava/util/Map;)Lio/sentry/systemtest/Person; public static synthetic fun createPerson$default (Lio/sentry/systemtest/util/RestTestClient;Lio/sentry/systemtest/Person;Ljava/util/Map;ILjava/lang/Object;)Lio/sentry/systemtest/Person; public final fun createPersonDistributedTracing (Lio/sentry/systemtest/Person;Ljava/util/Map;)Lio/sentry/systemtest/Person; public static synthetic fun createPersonDistributedTracing$default (Lio/sentry/systemtest/util/RestTestClient;Lio/sentry/systemtest/Person;Ljava/util/Map;ILjava/lang/Object;)Lio/sentry/systemtest/Person; + public final fun errorWithFeatureFlag (Ljava/lang/String;)Ljava/lang/String; public final fun getPerson (J)Lio/sentry/systemtest/Person; public final fun getPersonDistributedTracing (JLjava/util/Map;)Lio/sentry/systemtest/Person; public static synthetic fun getPersonDistributedTracing$default (Lio/sentry/systemtest/util/RestTestClient;JLjava/util/Map;ILjava/lang/Object;)Lio/sentry/systemtest/Person; diff --git a/sentry-system-test-support/src/main/kotlin/io/sentry/systemtest/util/RestTestClient.kt b/sentry-system-test-support/src/main/kotlin/io/sentry/systemtest/util/RestTestClient.kt index 6a7fdbd4b59..cb097d4483f 100644 --- a/sentry-system-test-support/src/main/kotlin/io/sentry/systemtest/util/RestTestClient.kt +++ b/sentry-system-test-support/src/main/kotlin/io/sentry/systemtest/util/RestTestClient.kt @@ -49,4 +49,19 @@ class RestTestClient(private val backendBaseUrl: String) : LoggingInsecureRestCl return callTyped(request, true) } + + fun checkFeatureFlag(flagKey: String): FeatureFlagResponse? { + val request = Request.Builder().url("$backendBaseUrl/feature-flag/check/$flagKey") + + return callTyped(request, true) + } + + fun errorWithFeatureFlag(flagKey: String): String? { + val request = Request.Builder().url("$backendBaseUrl/feature-flag/error/$flagKey") + + val response = call(request, true) + return response?.body?.string() + } } + +data class FeatureFlagResponse(val flagKey: String, val value: Boolean) diff --git a/settings.gradle.kts b/settings.gradle.kts index c19f1e1499f..3b2c7a3d5a5 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -65,6 +65,7 @@ include( "sentry-opentelemetry:sentry-opentelemetry-agentless-spring", "sentry-quartz", "sentry-okhttp", + "sentry-openfeature", "sentry-reactor", "sentry-async-profiler", "sentry-ktor-client",