From a4fd36f38a9b4d4e275ba30a8f282b12fd21299f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 4 Feb 2025 11:49:29 +0100 Subject: [PATCH 01/27] chore(deps): update Native SDK to v0.7.20 (#4128) Co-authored-by: GitHub --- CHANGELOG.md | 6 ++++++ buildSrc/src/main/java/Config.kt | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3803bc2a7e..287d56e3f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,12 @@ - Previously `java.lang.ClassNotFoundException: io.sentry.opentelemetry.OtelContextScopesStorage` was shown in the log if the class could not be found. - This is just a lookup the SDK performs to configure itself. The SDK also works without OpenTelemetry. +### Dependencies + +- Bump Native SDK from v0.7.19 to v0.7.20 ([#4128](https://github.com/getsentry/sentry-java/pull/4128)) + - [changelog](https://github.com/getsentry/sentry-native/blob/master/CHANGELOG.md#0720) + - [diff](https://github.com/getsentry/sentry-native/compare/v0.7.19...0.7.20) + ## 8.1.0 ### Features diff --git a/buildSrc/src/main/java/Config.kt b/buildSrc/src/main/java/Config.kt index 59a54600cf..e77417d7bb 100644 --- a/buildSrc/src/main/java/Config.kt +++ b/buildSrc/src/main/java/Config.kt @@ -153,7 +153,7 @@ object Config { val apolloKotlin = "com.apollographql.apollo3:apollo-runtime:3.8.2" - val sentryNativeNdk = "io.sentry:sentry-native-ndk:0.7.19" + val sentryNativeNdk = "io.sentry:sentry-native-ndk:0.7.20" object OpenTelemetry { val otelVersion = "1.44.1" From c2e65f0bf78ca1180bff89e9803853f64f98d0d9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 4 Feb 2025 11:01:34 +0000 Subject: [PATCH 02/27] Bump codecov/codecov-action from 5.1.2 to 5.3.1 (#4108) Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 5.1.2 to 5.3.1. - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/codecov/codecov-action/compare/1e68e06f1dbfde0e4cefc87efeba9e4643565303...13ce06bfc6bbe3ecf90edbbf1bc32fe5978ca1d3) --- updated-dependencies: - dependency-name: codecov/codecov-action dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Roman Zavarnitsyn --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9160d5c8ac..c5f2cf9f19 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -35,7 +35,7 @@ jobs: run: make preMerge - name: Upload coverage to Codecov - uses: codecov/codecov-action@1e68e06f1dbfde0e4cefc87efeba9e4643565303 # pin@v4 + uses: codecov/codecov-action@13ce06bfc6bbe3ecf90edbbf1bc32fe5978ca1d3 # pin@v4 with: name: sentry-java fail_ci_if_error: false From 72cbfafc9e2246f43d3b6e08e6e52fcb35fcd05f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 4 Feb 2025 11:32:05 +0000 Subject: [PATCH 03/27] Bump actions/create-github-app-token from 1.11.1 to 1.11.2 (#4131) Bumps [actions/create-github-app-token](https://github.com/actions/create-github-app-token) from 1.11.1 to 1.11.2. - [Release notes](https://github.com/actions/create-github-app-token/releases) - [Commits](https://github.com/actions/create-github-app-token/compare/c1a285145b9d317df6ced56c09f525b5c2b6f755...136412a57a7081aa63c935a2cc2918f76c34f514) --- updated-dependencies: - dependency-name: actions/create-github-app-token dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c15509c56d..6b473ad5ed 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -19,7 +19,7 @@ jobs: steps: - name: Get auth token id: token - uses: actions/create-github-app-token@c1a285145b9d317df6ced56c09f525b5c2b6f755 # v1.11.1 + uses: actions/create-github-app-token@136412a57a7081aa63c935a2cc2918f76c34f514 # v1.11.2 with: app-id: ${{ vars.SENTRY_RELEASE_BOT_CLIENT_ID }} private-key: ${{ secrets.SENTRY_RELEASE_BOT_PRIVATE_KEY }} From ba516997226ea711e57915713c19823aa836bc6c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 4 Feb 2025 13:26:31 +0100 Subject: [PATCH 04/27] Bump github/codeql-action from 3.28.1 to 3.28.8 (#4129) Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.28.1 to 3.28.8. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/b6a472f63d85b9c78a3ac5e89422239fc15e9b3c...dd746615b3b9d728a6a37ca2045b68ca76d4841a) --- updated-dependencies: - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Roman Zavarnitsyn --- .github/workflows/codeql-analysis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 22feb2486f..6afc875814 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -39,7 +39,7 @@ jobs: gradle-home-cache-cleanup: true - name: Initialize CodeQL - uses: github/codeql-action/init@b6a472f63d85b9c78a3ac5e89422239fc15e9b3c # pin@v2 + uses: github/codeql-action/init@dd746615b3b9d728a6a37ca2045b68ca76d4841a # pin@v2 with: languages: 'java' @@ -48,4 +48,4 @@ jobs: ./gradlew buildForCodeQL - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@b6a472f63d85b9c78a3ac5e89422239fc15e9b3c # pin@v2 + uses: github/codeql-action/analyze@dd746615b3b9d728a6a37ca2045b68ca76d4841a # pin@v2 From 36a7532a55b63d475f15b15290b094b630324632 Mon Sep 17 00:00:00 2001 From: Markus Hintersteiner Date: Wed, 5 Feb 2025 11:34:09 +0100 Subject: [PATCH 05/27] Bump Gradle, AGP and Kotlin versions (#3936) * Bump Gradle, AGP and Kotlin versions * Fix Changelog * Spotless * Add a comment about NDK-AGP compatibility * Remove redundant manifest * Remove redundant extension * Revert toLowerCase -> lowercase * Centralize java-library config and mention javadoc/soruces to module files * Fix publications for new locations * Remove matrix.language from codeql * Try m1 runners * wip * Revert to ubuntu on some workflows * Revert to ubuntu and enable config cache * Changelog and more config cache * Remove gradle-wrapper-validation job as its part of setup-gradle now --------- Co-authored-by: Roman Zavarnitsyn --- .github/workflows/agp-matrix.yml | 2 + .github/workflows/build.yml | 4 + .github/workflows/codeql-analysis.yml | 5 +- .../workflows/gradle-wrapper-validation.yml | 16 --- .../integration-tests-benchmarks.yml | 4 + .../integration-tests-ui-critical.yml | 5 + .github/workflows/integration-tests-ui.yml | 2 + .github/workflows/system-tests-backend.yml | 4 +- CHANGELOG.md | 5 + Makefile | 4 +- build.gradle.kts | 119 +++++++++--------- buildSrc/src/main/java/Config.kt | 36 +++--- buildSrc/src/main/java/Publication.kt | 29 +++-- gradle.properties | 5 +- gradle/wrapper/gradle-wrapper.properties | 2 +- sentry-android-core/build.gradle.kts | 11 +- .../src/main/AndroidManifest.xml | 1 + .../util/SentryFrameMetricsCollector.java | 3 +- .../sentry/android/core/ContextUtilsTest.kt | 2 + .../SystemEventsBreadcrumbsIntegrationTest.kt | 9 +- .../core/UserInteractionIntegrationTest.kt | 102 +++++---------- .../api/sentry-android-fragment.api | 4 +- sentry-android-fragment/build.gradle.kts | 11 +- .../fragment/FragmentLifecycleIntegration.kt | 6 +- .../fragment/FragmentLifecycleState.kt | 6 +- .../SentryFragmentLifecycleCallbacks.kt | 16 +-- .../benchmark-proguard-rules.pro | 7 +- .../build.gradle.kts | 12 +- .../build.gradle.kts | 10 +- .../sentry-uitest-android/build.gradle.kts | 11 +- .../test-app-plain/build.gradle.kts | 8 +- .../test-app-plain/proguard-rules.pro | 2 + .../test-app-sentry/build.gradle.kts | 8 +- .../test-app-sentry/proguard-rules.pro | 1 + sentry-android-navigation/build.gradle.kts | 11 +- .../navigation/SentryNavigationListener.kt | 6 +- sentry-android-ndk/build.gradle.kts | 15 ++- .../api/sentry-android-replay.api | 117 ----------------- sentry-android-replay/build.gradle.kts | 11 +- .../DefaultReplayBreadcrumbConverter.kt | 4 +- .../android/replay/ModifierExtensions.kt | 2 +- .../java/io/sentry/android/replay/Recorder.kt | 10 +- .../io/sentry/android/replay/ReplayCache.kt | 6 +- .../android/replay/ReplayIntegration.kt | 6 +- .../android/replay/ScreenshotRecorder.kt | 6 +- .../android/replay/SessionReplayOptions.kt | 4 +- .../sentry/android/replay/ViewExtensions.kt | 4 +- .../replay/gestures/GestureRecorder.kt | 4 +- .../replay/gestures/ReplayGestureConverter.kt | 2 +- .../sentry/android/replay/util/Executors.kt | 2 + .../sentry/android/replay/util/TextLayout.kt | 2 +- .../io/sentry/android/replay/util/Views.kt | 2 +- .../android/replay/video/SimpleFrameMuxer.kt | 2 +- .../replay/video/SimpleMp4FrameMuxer.kt | 2 +- .../replay/viewhierarchy/ViewHierarchyNode.kt | 2 +- .../ComposeMaskingOptionsTest.kt | 15 ++- .../viewhierarchy/MaskingOptionsTest.kt | 15 +++ sentry-android-sqlite/build.gradle.kts | 11 +- .../sqlite/SentrySupportSQLiteOpenHelper.kt | 6 +- .../SentrySupportSQLiteOpenHelperTest.kt | 2 +- sentry-android-timber/build.gradle.kts | 11 +- .../android/timber/SentryTimberIntegration.kt | 6 +- .../sentry/android/timber/SentryTimberTree.kt | 2 +- .../android/timber/SentryTimberTreeTest.kt | 5 +- .../java/io/sentry/core/SentryEventKtx.kt | 8 -- sentry-android/build.gradle.kts | 7 +- sentry-apache-http-client-5/build.gradle.kts | 5 - sentry-apollo-3/build.gradle.kts | 5 - .../apollo3/SentryApollo3HttpInterceptor.kt | 3 +- sentry-apollo/build.gradle.kts | 5 - .../sentry/apollo/SentryApolloInterceptor.kt | 3 +- sentry-bom/build.gradle.kts | 8 -- sentry-compose-helper/build.gradle.kts | 5 +- sentry-compose/build.gradle.kts | 11 +- sentry-graphql-22/build.gradle.kts | 5 - sentry-graphql-core/build.gradle.kts | 5 - sentry-graphql/build.gradle.kts | 5 - sentry-jdbc/build.gradle.kts | 5 - sentry-jul/build.gradle.kts | 5 - .../api/sentry-kotlin-extensions.api | 4 - sentry-kotlin-extensions/build.gradle.kts | 5 - sentry-log4j2/build.gradle.kts | 5 - sentry-logback/build.gradle.kts | 5 - sentry-okhttp/build.gradle.kts | 5 - .../io/sentry/okhttp/SentryOkHttpEvent.kt | 3 +- sentry-openfeign/build.gradle.kts | 5 - .../build.gradle.kts | 7 +- .../build.gradle.kts | 5 - .../build.gradle.kts | 5 - .../build.gradle.kts | 5 - .../build.gradle.kts | 5 - .../build.gradle.kts | 5 - sentry-quartz/build.gradle.kts | 5 - .../sentry-samples-android/build.gradle.kts | 21 +++- .../src/main/AndroidManifest.xml | 8 +- sentry-servlet-jakarta/build.gradle.kts | 5 - sentry-servlet/build.gradle.kts | 5 - sentry-spring-boot-starter/build.gradle.kts | 5 - sentry-spring-boot/build.gradle.kts | 5 - sentry-spring/build.gradle.kts | 5 - sentry/build.gradle.kts | 5 - 101 files changed, 367 insertions(+), 611 deletions(-) delete mode 100644 .github/workflows/gradle-wrapper-validation.yml delete mode 100644 sentry-android-timber/src/test/java/io/sentry/core/SentryEventKtx.kt diff --git a/.github/workflows/agp-matrix.yml b/.github/workflows/agp-matrix.yml index f1e375ba65..b5d15c41a6 100644 --- a/.github/workflows/agp-matrix.yml +++ b/.github/workflows/agp-matrix.yml @@ -24,6 +24,7 @@ jobs: env: VERSION_AGP: ${{ matrix.agp }} APPLY_SENTRY_INTEGRATIONS: ${{ matrix.integrations }} + GRADLE_ENCRYPTION_KEY: ${{ secrets.GRADLE_ENCRYPTION_KEY }} steps: - name: Checkout Repo @@ -41,6 +42,7 @@ jobs: uses: gradle/actions/setup-gradle@8790d96bb8fdd8ae7edfb2eada090c650b257f27 # pin@v3 with: gradle-home-cache-cleanup: true + cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }} - name: Setup KVM shell: bash diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c5f2cf9f19..4be6d59d60 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -14,6 +14,9 @@ jobs: name: Build Job ubuntu-latest - Java 17 runs-on: ubuntu-latest + env: + GRADLE_ENCRYPTION_KEY: ${{ secrets.GRADLE_ENCRYPTION_KEY }} + steps: - name: Checkout Repo uses: actions/checkout@v4 @@ -30,6 +33,7 @@ jobs: uses: gradle/actions/setup-gradle@8790d96bb8fdd8ae7edfb2eada090c650b257f27 # pin@v3 with: gradle-home-cache-cleanup: true + cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }} - name: Run Tests with coverage and Lint run: make preMerge diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 6afc875814..c488d2dd39 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -18,8 +18,8 @@ jobs: name: Analyze runs-on: ubuntu-latest - strategy: - fail-fast: false + env: + GRADLE_ENCRYPTION_KEY: ${{ secrets.GRADLE_ENCRYPTION_KEY }} steps: - name: Checkout Repo @@ -37,6 +37,7 @@ jobs: uses: gradle/actions/setup-gradle@8790d96bb8fdd8ae7edfb2eada090c650b257f27 # pin@v3 with: gradle-home-cache-cleanup: true + cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }} - name: Initialize CodeQL uses: github/codeql-action/init@dd746615b3b9d728a6a37ca2045b68ca76d4841a # pin@v2 diff --git a/.github/workflows/gradle-wrapper-validation.yml b/.github/workflows/gradle-wrapper-validation.yml deleted file mode 100644 index 4b2fe0a78a..0000000000 --- a/.github/workflows/gradle-wrapper-validation.yml +++ /dev/null @@ -1,16 +0,0 @@ -name: 'Validate Gradle Wrapper' -on: - push: - branches: - - main - - release/** - pull_request: - -jobs: - validation: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - submodules: 'recursive' - - uses: gradle/wrapper-validation-action@f9c9c575b8b21b6485636a91ffecd10e558c62f6 # pin@v1 diff --git a/.github/workflows/integration-tests-benchmarks.yml b/.github/workflows/integration-tests-benchmarks.yml index c04d2bc624..46dc783d72 100644 --- a/.github/workflows/integration-tests-benchmarks.yml +++ b/.github/workflows/integration-tests-benchmarks.yml @@ -23,6 +23,7 @@ jobs: # we copy the secret to the env variable in order to access it in the workflow env: SAUCE_USERNAME: ${{ secrets.SAUCE_USERNAME }} + GRADLE_ENCRYPTION_KEY: ${{ secrets.GRADLE_ENCRYPTION_KEY }} steps: - name: Git checkout @@ -40,6 +41,7 @@ jobs: uses: gradle/actions/setup-gradle@8790d96bb8fdd8ae7edfb2eada090c650b257f27 # pin@v3 with: gradle-home-cache-cleanup: true + cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }} # Clean, build and release a test apk, but only if we will run the benchmark - name: Make assembleBenchmarks @@ -72,6 +74,7 @@ jobs: # we copy the secret to the env variable in order to access it in the workflow env: SAUCE_USERNAME: ${{ secrets.SAUCE_USERNAME }} + GRADLE_ENCRYPTION_KEY: ${{ secrets.GRADLE_ENCRYPTION_KEY }} steps: - name: Git checkout @@ -89,6 +92,7 @@ jobs: uses: gradle/actions/setup-gradle@8790d96bb8fdd8ae7edfb2eada090c650b257f27 # pin@v3 with: gradle-home-cache-cleanup: true + cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }} - uses: actions/cache@v4 id: app-plain-cache diff --git a/.github/workflows/integration-tests-ui-critical.yml b/.github/workflows/integration-tests-ui-critical.yml index 112dc8ad25..15b2129602 100644 --- a/.github/workflows/integration-tests-ui-critical.yml +++ b/.github/workflows/integration-tests-ui-critical.yml @@ -21,6 +21,10 @@ jobs: build: name: Build runs-on: ubuntu-latest + + env: + GRADLE_ENCRYPTION_KEY: ${{ secrets.GRADLE_ENCRYPTION_KEY }} + steps: - name: Checkout code uses: actions/checkout@v4 @@ -35,6 +39,7 @@ jobs: uses: gradle/actions/setup-gradle@8790d96bb8fdd8ae7edfb2eada090c650b257f27 # pin@v3 with: gradle-home-cache-cleanup: true + cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }} - name: Build debug APK run: make assembleUiTestCriticalRelease diff --git a/.github/workflows/integration-tests-ui.yml b/.github/workflows/integration-tests-ui.yml index 0bf4b12dd6..d79753e83d 100644 --- a/.github/workflows/integration-tests-ui.yml +++ b/.github/workflows/integration-tests-ui.yml @@ -18,6 +18,7 @@ jobs: env: SAUCE_USERNAME: ${{ secrets.SAUCE_USERNAME }} SAUCE_ACCESS_KEY: ${{ secrets.SAUCE_ACCESS_KEY }} + GRADLE_ENCRYPTION_KEY: ${{ secrets.GRADLE_ENCRYPTION_KEY }} steps: - name: Git checkout @@ -35,6 +36,7 @@ jobs: uses: gradle/actions/setup-gradle@8790d96bb8fdd8ae7edfb2eada090c650b257f27 # pin@v3 with: gradle-home-cache-cleanup: true + cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }} # Clean, build and release a test apk, but only if we will run the benchmark - name: Make assembleUiTests diff --git a/.github/workflows/system-tests-backend.yml b/.github/workflows/system-tests-backend.yml index b4d4233b08..e7c04a14d3 100644 --- a/.github/workflows/system-tests-backend.yml +++ b/.github/workflows/system-tests-backend.yml @@ -91,11 +91,11 @@ jobs: - name: Build server jar run: | - ./gradlew :sentry-samples:${{ matrix.sample }}:bootJar + ./gradlew :sentry-samples:${{ matrix.sample }}:bootJar --no-configuration-cache - name: Build agent jar run: | - ./gradlew :sentry-opentelemetry:sentry-opentelemetry-agent:assemble + ./gradlew :sentry-opentelemetry:sentry-opentelemetry-agent:assemble --no-configuration-cache - name: Start server and run integration test for sentry-cli commands run: | diff --git a/CHANGELOG.md b/CHANGELOG.md index 287d56e3f0..d778d82cd5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,11 +2,16 @@ ## Unreleased +### Breaking Changes + +- The Kotlin Language version is now set to 1.6 ([#3936](https://github.com/getsentry/sentry-java/pull/3936)) + ### Fixes - Do not log if `OtelContextScopesStorage` cannot be found ([#4127](https://github.com/getsentry/sentry-java/pull/4127)) - Previously `java.lang.ClassNotFoundException: io.sentry.opentelemetry.OtelContextScopesStorage` was shown in the log if the class could not be found. - This is just a lookup the SDK performs to configure itself. The SDK also works without OpenTelemetry. +- Mention javadoc and sources for published artifacts in Gradle `.module` metadata ([#3936](https://github.com/getsentry/sentry-java/pull/3936)) ### Dependencies diff --git a/Makefile b/Makefile index 62e6e258f3..3fff2c01ff 100644 --- a/Makefile +++ b/Makefile @@ -8,7 +8,7 @@ publish: clean dryRelease # deep clean clean: - ./gradlew clean + ./gradlew clean --no-configuration-cache rm -rf distributions # build and run tests @@ -20,7 +20,7 @@ javadocs: # do a dry release (like a local deploy) dryRelease: - ./gradlew aggregateJavadocs distZip --no-build-cache + ./gradlew aggregateJavadocs distZip --no-build-cache --no-configuration-cache # check for dependencies update update: diff --git a/build.gradle.kts b/build.gradle.kts index e7d828e61f..bb60529161 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,7 +1,7 @@ import com.diffplug.spotless.LineEnding +import com.vanniktech.maven.publish.JavaLibrary +import com.vanniktech.maven.publish.JavadocJar import com.vanniktech.maven.publish.MavenPublishBaseExtension -import com.vanniktech.maven.publish.MavenPublishPlugin -import com.vanniktech.maven.publish.MavenPublishPluginExtension import groovy.util.Node import io.gitlab.arturbosch.detekt.extensions.DetektExtension import kotlinx.kover.gradle.plugin.dsl.KoverReportExtension @@ -17,6 +17,7 @@ plugins { id(Config.QualityPlugins.binaryCompatibilityValidator) version Config.QualityPlugins.binaryCompatibilityValidatorVersion id(Config.QualityPlugins.jacocoAndroid) version Config.QualityPlugins.jacocoAndroidVersion apply false id(Config.QualityPlugins.kover) version Config.QualityPlugins.koverVersion apply false + id(Config.BuildPlugins.gradleMavenPublishPlugin) version Config.BuildPlugins.gradleMavenPublishPluginVersion apply false } buildscript { @@ -26,7 +27,6 @@ buildscript { dependencies { classpath(Config.BuildPlugins.androidGradle) classpath(kotlin(Config.BuildPlugins.kotlinGradlePlugin, version = Config.kotlinVersion)) - classpath(Config.BuildPlugins.gradleMavenPublishPlugin) // dokka is required by gradle-maven-publish-plugin. classpath(Config.BuildPlugins.dokkaPlugin) classpath(Config.QualityPlugins.errorpronePlugin) @@ -95,7 +95,7 @@ allprojects { TestLogEvent.PASSED, TestLogEvent.FAILED ) - maxParallelForks = Runtime.getRuntime().availableProcessors() / 4 + maxParallelForks = 1 // Cap JVM args per test minHeapSize = "256m" @@ -140,7 +140,7 @@ subprojects { androidReports("release") { xml { // Change the report file name so the Codecov Github action can find it - setReportFile(file("$buildDir/reports/kover/report.xml")) + setReportFile(project.layout.buildDirectory.file("reports/kover/report.xml").get().asFile) } } } @@ -157,6 +157,7 @@ subprojects { if (!this.name.contains("sample") && !this.name.contains("integration-tests") && this.name != "sentry-test-support" && this.name != "sentry-compose-helper") { apply() + apply() val sep = File.separator @@ -179,22 +180,30 @@ subprojects { tasks.named("distZip").configure { this.dependsOn("publishToMavenLocal") this.doLast { - val distributionFilePath = - "${this.project.buildDir}${sep}distributions${sep}${this.project.name}-${this.project.version}.zip" - val file = File(distributionFilePath) - if (!file.exists()) throw IllegalStateException("Distribution file: $distributionFilePath does not exist") - if (file.length() == 0L) throw IllegalStateException("Distribution file: $distributionFilePath is empty") + val file = this.project.layout.buildDirectory.file("distributions${sep}${this.project.name}-${this.project.version}.zip").get().asFile + if (!file.exists()) throw IllegalStateException("Distribution file: ${file.absolutePath} does not exist") + if (file.length() == 0L) throw IllegalStateException("Distribution file: ${file.absolutePath} is empty") } } - afterEvaluate { - apply() + plugins.withId("java-library") { + configure { + // we have to disable javadoc publication in maven-publish plugin as it's not + // including it in the .module file https://github.com/vanniktech/gradle-maven-publish-plugin/issues/861 + // and do it ourselves + configure(JavaLibrary(JavadocJar.None(), sourcesJar = true)) + } + + configure { + withJavadocJar() - configure { - // signing is done when uploading files to MC - // via gpg:sign-and-deploy-file (release.kts) - releaseSigningEnabled = false + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 } + } + + afterEvaluate { + apply() @Suppress("UnstableApiUsage") configure { @@ -206,7 +215,7 @@ subprojects { repositories { maven { name = "unityMaven" - url = file("${rootProject.buildDir}/unityMaven").toURI() + url = rootProject.layout.buildDirectory.file("unityMaven").get().asFile.toURI() } } } @@ -241,48 +250,46 @@ spotless { } } -gradle.projectsEvaluated { - tasks.create("aggregateJavadocs", Javadoc::class.java) { - setDestinationDir(file("$buildDir/docs/javadoc")) - title = "${project.name} $version API" - val opts = options as StandardJavadocDocletOptions - opts.quiet() - opts.encoding = "UTF-8" - opts.memberLevel = JavadocMemberLevel.PROTECTED - opts.stylesheetFile(file("$projectDir/docs/stylesheet.css")) - opts.links = listOf( - "https://docs.oracle.com/javase/8/docs/api/", - "https://docs.spring.io/spring-framework/docs/current/javadoc-api/", - "https://docs.spring.io/spring-boot/docs/current/api/" - ) - subprojects - .filter { !it.name.contains("sample") && !it.name.contains("integration-tests") } - .forEach { proj -> - proj.tasks.withType().forEach { javadocTask -> - source += javadocTask.source - classpath += javadocTask.classpath - excludes += javadocTask.excludes - includes += javadocTask.includes - } +tasks.register("aggregateJavadocs", Javadoc::class.java) { + setDestinationDir(project.layout.buildDirectory.file("docs/javadoc").get().asFile) + title = "${project.name} $version API" + val opts = options as StandardJavadocDocletOptions + opts.quiet() + opts.encoding = "UTF-8" + opts.memberLevel = JavadocMemberLevel.PROTECTED + opts.stylesheetFile(file("$projectDir/docs/stylesheet.css")) + opts.links = listOf( + "https://docs.oracle.com/javase/8/docs/api/", + "https://docs.spring.io/spring-framework/docs/current/javadoc-api/", + "https://docs.spring.io/spring-boot/docs/current/api/" + ) + subprojects + .filter { !it.name.contains("sample") && !it.name.contains("integration-tests") } + .forEach { proj -> + proj.tasks.withType().forEach { javadocTask -> + source += javadocTask.source + classpath += javadocTask.classpath + excludes += javadocTask.excludes + includes += javadocTask.includes } - } + } +} - tasks.create("buildForCodeQL") { - subprojects - .filter { - !it.displayName.contains("sample") && - !it.displayName.contains("integration-tests") && - !it.displayName.contains("bom") && - it.name != "sentry-opentelemetry" - } - .forEach { proj -> - if (proj.plugins.hasPlugin("com.android.library")) { - this.dependsOn(proj.tasks.findByName("compileReleaseUnitTestSources")) - } else { - this.dependsOn(proj.tasks.findByName("testClasses")) - } +tasks.register("buildForCodeQL") { + subprojects + .filter { + !it.displayName.contains("sample") && + !it.displayName.contains("integration-tests") && + !it.displayName.contains("bom") && + it.name != "sentry-opentelemetry" + } + .forEach { proj -> + if (proj.plugins.hasPlugin("com.android.library")) { + this.dependsOn(proj.tasks.findByName("compileReleaseUnitTestSources")) + } else { + this.dependsOn(proj.tasks.findByName("testClasses")) } - } + } } // Workaround for https://youtrack.jetbrains.com/issue/IDEA-316081/Gradle-8-toolchain-error-Toolchain-from-executable-property-does-not-match-toolchain-from-javaLauncher-property-when-different diff --git a/buildSrc/src/main/java/Config.kt b/buildSrc/src/main/java/Config.kt index e77417d7bb..f508b21b1d 100644 --- a/buildSrc/src/main/java/Config.kt +++ b/buildSrc/src/main/java/Config.kt @@ -2,16 +2,18 @@ import java.math.BigDecimal object Config { - val AGP = System.getenv("VERSION_AGP") ?: "7.4.2" - val kotlinVersion = "1.8.0" + val AGP = System.getenv("VERSION_AGP") ?: "8.6.0" + val kotlinVersion = "1.9.24" val kotlinStdLib = "stdlib-jdk8" val springBootVersion = "2.7.5" val springBoot3Version = "3.4.2" - val kotlinCompatibleLanguageVersion = "1.4" + val kotlinCompatibleLanguageVersion = "1.6" - val composeVersion = "1.5.3" - val androidComposeCompilerVersion = "1.4.0" + // see https://www.jetbrains.com/help/kotlin-multiplatform-dev/compose-compatibility-and-versioning.html#kotlin-compatibility + // see https://developer.android.com/jetpack/androidx/releases/compose-kotlin + val composeVersion = "1.6.11" + val androidComposeCompilerVersion = "1.5.14" object BuildPlugins { val androidGradle = "com.android.tools.build:gradle:$AGP" @@ -23,8 +25,9 @@ object Config { val springDependencyManagementVersion = "1.0.11.RELEASE" val gretty = "org.gretty" val grettyVersion = "4.0.0" - val gradleMavenPublishPlugin = "com.vanniktech:gradle-maven-publish-plugin:0.18.0" - val dokkaPlugin = "org.jetbrains.dokka:dokka-gradle-plugin:1.7.10" + val gradleMavenPublishPlugin = "com.vanniktech.maven.publish" + val gradleMavenPublishPluginVersion = "0.30.0" + val dokkaPlugin = "org.jetbrains.dokka:dokka-gradle-plugin:1.9.20" val dokkaPluginAlias = "org.jetbrains.dokka" val composeGradlePlugin = "org.jetbrains.compose:compose-gradle-plugin:$composeVersion" val commonsCompressOverride = "org.apache.commons:commons-compress:1.25.0" @@ -39,7 +42,7 @@ object Config { val abiFilters = listOf("x86", "armeabi-v7a", "x86_64", "arm64-v8a") - fun shouldSkipDebugVariant(name: String): Boolean { + fun shouldSkipDebugVariant(name: String?): Boolean { return System.getenv("CI")?.toBoolean() ?: false && name == "debug" } } @@ -58,6 +61,7 @@ object Config { val androidxCore = "androidx.core:core:1.3.2" val androidxSqlite = "androidx.sqlite:sqlite:2.3.1" val androidxRecylerView = "androidx.recyclerview:recyclerview:1.2.1" + val androidxAnnotation = "androidx.annotation:annotation:1.9.1" val slf4jApi = "org.slf4j:slf4j-api:1.7.30" val slf4jApi2 = "org.slf4j:slf4j-api:2.0.5" @@ -142,14 +146,14 @@ object Config { // compose deps val composeNavigation = "androidx.navigation:navigation-compose:$navigationVersion" - val composeActivity = "androidx.activity:activity-compose:1.4.0" - val composeFoundation = "androidx.compose.foundation:foundation:$composeVersion" - val composeUi = "androidx.compose.ui:ui:$composeVersion" + val composeActivity = "androidx.activity:activity-compose:1.8.2" + val composeFoundation = "androidx.compose.foundation:foundation:1.6.3" + val composeUi = "androidx.compose.ui:ui:1.6.3" + val composeFoundationLayout = "androidx.compose.foundation:foundation-layout:1.6.3" + val composeMaterial = "androidx.compose.material3:material3:1.2.1" val composeUiReplay = "androidx.compose.ui:ui:1.5.0" // Note: don't change without testing forwards compatibility - val composeFoundationLayout = "androidx.compose.foundation:foundation-layout:$composeVersion" - val composeMaterial = "androidx.compose.material3:material3:1.0.0-alpha13" - val composeCoil = "io.coil-kt:coil-compose:2.0.0" + val composeCoil = "io.coil-kt:coil-compose:2.6.0" val apolloKotlin = "com.apollographql.apollo3:apollo-runtime:3.8.2" @@ -193,7 +197,7 @@ object Config { val androidxTestOrchestrator = "androidx.test:orchestrator:1.5.0" val androidxJunit = "androidx.test.ext:junit:1.1.5" val androidxCoreKtx = "androidx.core:core-ktx:1.7.0" - val robolectric = "org.robolectric:robolectric:4.10.3" + val robolectric = "org.robolectric:robolectric:4.14" val mockitoKotlin = "org.mockito.kotlin:mockito-kotlin:4.1.0" val mockitoInline = "org.mockito:mockito-inline:4.8.0" val awaitility = "org.awaitility:awaitility-kotlin:4.1.1" @@ -220,7 +224,7 @@ object Config { val gradleVersionsPlugin = "com.github.ben-manes:gradle-versions-plugin:0.42.0" val gradleVersions = "com.github.ben-manes.versions" val detekt = "io.gitlab.arturbosch.detekt" - val detektVersion = "1.19.0" + val detektVersion = "1.23.5" val detektPlugin = "io.gitlab.arturbosch.detekt" val binaryCompatibilityValidatorVersion = "0.13.0" val binaryCompatibilityValidatorPlugin = "org.jetbrains.kotlinx:binary-compatibility-validator:$binaryCompatibilityValidatorVersion" diff --git a/buildSrc/src/main/java/Publication.kt b/buildSrc/src/main/java/Publication.kt index 08a81c703f..bae398e27c 100644 --- a/buildSrc/src/main/java/Publication.kt +++ b/buildSrc/src/main/java/Publication.kt @@ -24,7 +24,10 @@ fun DistributionContainer.configureForMultiplatform(project: Project) { } from("build${sep}libs") { include("*android*") - withJavadoc(renameTo = "compose-android") + include("*androidRelease-javadoc*") + rename { + it.replace("androidRelease-javadoc", "android") + } } } this.getByName("main").contents { @@ -38,8 +41,8 @@ fun DistributionContainer.configureForMultiplatform(project: Project) { rename { it.replace("-kotlin", "") .replace("-metadata", "") + .replace("Multiplatform-javadoc", "") } - withJavadoc() } } this.maybeCreate("desktop").contents { @@ -49,7 +52,10 @@ fun DistributionContainer.configureForMultiplatform(project: Project) { } from("build${sep}libs") { include("*desktop*") - withJavadoc(renameTo = "compose-desktop") + include("*desktop-javadoc*") + rename { + it.replace("desktop-javadoc", "desktop") + } } } @@ -77,16 +83,13 @@ fun DistributionContainer.configureForJvm(project: Project) { from("build${sep}publications${sep}release") { renameModule(project.name, version = version) } - } -} - -private fun CopySpec.withJavadoc(renameTo: String = "compose") { - include("*javadoc*") - rename { - if (it.contains("javadoc")) { - it.replace("compose", renameTo) - } else { - it + from("build${sep}intermediates${sep}java_doc_jar${sep}release") { + include("*javadoc*") + rename { it.replace("release", "${project.name}-$version") } + } + from("build${sep}intermediates${sep}source_jar${sep}release") { + include("*sources*") + rename { it.replace("release", "${project.name}-$version") } } } } diff --git a/gradle.properties b/gradle.properties index cdaf2c4f44..39c9cb9985 100644 --- a/gradle.properties +++ b/gradle.properties @@ -2,6 +2,8 @@ org.gradle.jvmargs=-Xmx12g -XX:MaxMetaspaceSize=4g -XX:+CrashOnOutOfMemoryError -Dfile.encoding=UTF-8 -XX:+UseParallelGC org.gradle.caching=true org.gradle.parallel=true +org.gradle.configureondemand=true +org.gradle.configuration-cache=true # Daemons workers org.gradle.workers.max=2 @@ -9,9 +11,6 @@ org.gradle.workers.max=2 # AndroidX required by AGP >= 3.6.x android.useAndroidX=true -# Required by AGP >= 8.0.x -android.defaults.buildfeatures.buildconfig=true - # Release information versionName=8.1.0 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 1af9e0930b..09523c0e54 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/sentry-android-core/build.gradle.kts b/sentry-android-core/build.gradle.kts index 12e6e6ad4f..15692681cd 100644 --- a/sentry-android-core/build.gradle.kts +++ b/sentry-android-core/build.gradle.kts @@ -15,7 +15,6 @@ android { namespace = "io.sentry.android.core" defaultConfig { - targetSdk = Config.Android.targetSdkVersion minSdk = Config.Android.minSdkVersion testInstrumentationRunner = Config.TestLibs.androidJUnitRunner @@ -53,15 +52,17 @@ android { checkReleaseBuilds = false } + buildFeatures { + buildConfig = true + } + // needed because of Kotlin 1.4.x configurations.all { resolutionStrategy.force(Config.CompileOnly.jetbrainsAnnotations) } - variantFilter { - if (Config.Android.shouldSkipDebugVariant(buildType.name)) { - ignore = true - } + androidComponents.beforeVariants { + it.enable = !Config.Android.shouldSkipDebugVariant(it.buildType) } } diff --git a/sentry-android-core/src/main/AndroidManifest.xml b/sentry-android-core/src/main/AndroidManifest.xml index dba3e7df8e..a304ee075b 100644 --- a/sentry-android-core/src/main/AndroidManifest.xml +++ b/sentry-android-core/src/main/AndroidManifest.xml @@ -1,6 +1,7 @@ + () diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/SystemEventsBreadcrumbsIntegrationTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/SystemEventsBreadcrumbsIntegrationTest.kt index 45e247a5cb..e0d053066b 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/SystemEventsBreadcrumbsIntegrationTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/SystemEventsBreadcrumbsIntegrationTest.kt @@ -3,6 +3,7 @@ package io.sentry.android.core import android.content.Context import android.content.Intent import android.os.BatteryManager +import android.os.Build import androidx.test.ext.junit.runners.AndroidJUnit4 import io.sentry.Breadcrumb import io.sentry.IScopes @@ -19,6 +20,7 @@ import org.mockito.kotlin.never import org.mockito.kotlin.verify import org.mockito.kotlin.verifyNoMoreInteractions import org.mockito.kotlin.whenever +import org.robolectric.annotation.Config import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFalse @@ -26,6 +28,7 @@ import kotlin.test.assertNotNull import kotlin.test.assertNull @RunWith(AndroidJUnit4::class) +@Config(sdk = [Build.VERSION_CODES.TIRAMISU]) class SystemEventsBreadcrumbsIntegrationTest { private class Fixture { @@ -50,7 +53,7 @@ class SystemEventsBreadcrumbsIntegrationTest { sut.register(fixture.scopes, fixture.options) - verify(fixture.context).registerReceiver(any(), any()) + verify(fixture.context).registerReceiver(any(), any(), any()) assertNotNull(sut.receiver) } @@ -69,7 +72,7 @@ class SystemEventsBreadcrumbsIntegrationTest { sut.register(fixture.scopes, fixture.options) - verify(fixture.context, never()).registerReceiver(any(), any()) + verify(fixture.context, never()).registerReceiver(any(), any(), any()) assertNull(sut.receiver) } @@ -174,7 +177,7 @@ class SystemEventsBreadcrumbsIntegrationTest { @Test fun `Do not crash if registerReceiver throws exception`() { val sut = fixture.getSut() - whenever(fixture.context.registerReceiver(any(), any())).thenThrow(SecurityException()) + whenever(fixture.context.registerReceiver(any(), any(), any())).thenThrow(SecurityException()) sut.register(fixture.scopes, fixture.options) diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/UserInteractionIntegrationTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/UserInteractionIntegrationTest.kt index 97e4d46845..239aa85dbe 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/UserInteractionIntegrationTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/UserInteractionIntegrationTest.kt @@ -2,24 +2,24 @@ package io.sentry.android.core import android.app.Activity import android.app.Application -import android.content.Context -import android.content.res.Resources -import android.util.DisplayMetrics import android.view.Window import androidx.test.ext.junit.runners.AndroidJUnit4 import io.sentry.Scopes import io.sentry.android.core.internal.gestures.NoOpWindowCallback import io.sentry.android.core.internal.gestures.SentryWindowCallback +import junit.framework.TestCase.assertNull import org.junit.runner.RunWith import org.mockito.kotlin.any import org.mockito.kotlin.anyOrNull -import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.mock import org.mockito.kotlin.never import org.mockito.kotlin.verify import org.mockito.kotlin.whenever +import org.robolectric.Robolectric.buildActivity import kotlin.test.Test -import kotlin.test.assertTrue +import kotlin.test.assertIs +import kotlin.test.assertNotEquals +import kotlin.test.assertSame @RunWith(AndroidJUnit4::class) class UserInteractionIntegrationTest { @@ -30,8 +30,8 @@ class UserInteractionIntegrationTest { val options = SentryAndroidOptions().apply { dsn = "https://key@sentry.io/proj" } - val activity = mock() - val window = mock() + val activity: Activity = buildActivity(EmptyActivity::class.java).setup().get() + val window: Window = activity.window val loadClass = mock() fun getSut( @@ -40,23 +40,10 @@ class UserInteractionIntegrationTest { ): UserInteractionIntegration { whenever(loadClass.isClassAvailable(any(), anyOrNull())).thenReturn(isAndroidXAvailable) whenever(scopes.options).thenReturn(options) - whenever(window.callback).thenReturn(callback) - whenever(activity.window).thenReturn(window) - - val resources = mockResources() - whenever(activity.resources).thenReturn(resources) - return UserInteractionIntegration(application, loadClass) - } - - companion object { - fun mockResources(): Resources { - val displayMetrics = mock() - displayMetrics.density = 1.0f - - val resources = mock() - whenever(resources.displayMetrics).thenReturn(displayMetrics) - return resources + if (callback != null) { + window.callback = callback } + return UserInteractionIntegration(application, loadClass) } } @@ -105,74 +92,50 @@ class UserInteractionIntegrationTest { sut.register(fixture.scopes, fixture.options) sut.onActivityResumed(fixture.activity) - - val argumentCaptor = argumentCaptor() - verify(fixture.window).callback = argumentCaptor.capture() - assertTrue { argumentCaptor.firstValue is SentryWindowCallback } + assertIs(fixture.activity.window.callback) } @Test fun `when no original callback delegates to NoOpWindowCallback`() { val sut = fixture.getSut() sut.register(fixture.scopes, fixture.options) + fixture.window.callback = null sut.onActivityResumed(fixture.activity) - - val argumentCaptor = argumentCaptor() - verify(fixture.window).callback = argumentCaptor.capture() - assertTrue { - argumentCaptor.firstValue is SentryWindowCallback && - (argumentCaptor.firstValue as SentryWindowCallback).delegate is NoOpWindowCallback - } + assertIs(fixture.activity.window.callback) + assertIs((fixture.activity.window.callback as SentryWindowCallback).delegate) } @Test fun `unregisters window callback on activity paused`() { - val context = mock() - val resources = Fixture.mockResources() - whenever(context.resources).thenReturn(resources) - val sut = fixture.getSut( - SentryWindowCallback( - NoOpWindowCallback(), - context, - mock(), - mock() - ) - ) + val sut = fixture.getSut() + fixture.activity.window.callback = null - sut.register(fixture.scopes, fixture.options) + sut.onActivityResumed(fixture.activity) sut.onActivityPaused(fixture.activity) - verify(fixture.window).callback = null + assertNull(fixture.activity.window.callback) } @Test fun `preserves original callback on activity paused`() { - val delegate = mock() - val context = mock() - val resources = Fixture.mockResources() - whenever(context.resources).thenReturn(resources) - val sut = fixture.getSut( - SentryWindowCallback( - delegate, - context, - mock(), - mock() - ) - ) + val sut = fixture.getSut() + val mockCallback = mock() - sut.register(fixture.scopes, fixture.options) + fixture.window.callback = mockCallback + + sut.onActivityResumed(fixture.activity) sut.onActivityPaused(fixture.activity) - verify(fixture.window).callback = delegate + assertSame(mockCallback, fixture.activity.window.callback) } @Test fun `stops tracing on activity paused`() { val callback = mock() - val sut = fixture.getSut(callback) + val sut = fixture.getSut() + fixture.activity.window.callback = callback - sut.register(fixture.scopes, fixture.options) sut.onActivityPaused(fixture.activity) verify(callback).stopTracking() @@ -180,13 +143,9 @@ class UserInteractionIntegrationTest { @Test fun `does not instrument if the callback is already ours`() { - val delegate = mock() - val context = mock() - val resources = Fixture.mockResources() - whenever(context.resources).thenReturn(resources) val existingCallback = SentryWindowCallback( - delegate, - context, + NoOpWindowCallback(), + fixture.activity, mock(), mock() ) @@ -195,7 +154,8 @@ class UserInteractionIntegrationTest { sut.register(fixture.scopes, fixture.options) sut.onActivityResumed(fixture.activity) - val argumentCaptor = argumentCaptor() - verify(fixture.window, never()).callback = argumentCaptor.capture() + assertNotEquals(existingCallback, (fixture.window.callback as SentryWindowCallback).delegate) } } + +private class EmptyActivity : Activity() diff --git a/sentry-android-fragment/api/sentry-android-fragment.api b/sentry-android-fragment/api/sentry-android-fragment.api index 044d9cfbf5..ddd7d0f669 100644 --- a/sentry-android-fragment/api/sentry-android-fragment.api +++ b/sentry-android-fragment/api/sentry-android-fragment.api @@ -39,7 +39,7 @@ public final class io/sentry/android/fragment/FragmentLifecycleState : java/lang } public final class io/sentry/android/fragment/FragmentLifecycleState$Companion { - public final fun getStates ()Ljava/util/HashSet; + public final fun getStates ()Ljava/util/Set; } public final class io/sentry/android/fragment/SentryFragmentLifecycleCallbacks : androidx/fragment/app/FragmentManager$FragmentLifecycleCallbacks { @@ -50,9 +50,7 @@ public final class io/sentry/android/fragment/SentryFragmentLifecycleCallbacks : public fun (Lio/sentry/IScopes;ZZ)V public fun (ZZ)V public synthetic fun (ZZILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun getEnableAutoFragmentLifecycleTracing ()Z public final fun getEnableFragmentLifecycleBreadcrumbs ()Z - public final fun getFilterFragmentLifecycleBreadcrumbs ()Ljava/util/Set; public fun onFragmentAttached (Landroidx/fragment/app/FragmentManager;Landroidx/fragment/app/Fragment;Landroid/content/Context;)V public fun onFragmentCreated (Landroidx/fragment/app/FragmentManager;Landroidx/fragment/app/Fragment;Landroid/os/Bundle;)V public fun onFragmentDestroyed (Landroidx/fragment/app/FragmentManager;Landroidx/fragment/app/Fragment;)V diff --git a/sentry-android-fragment/build.gradle.kts b/sentry-android-fragment/build.gradle.kts index 14fb3ff9c1..37bba3e7ca 100644 --- a/sentry-android-fragment/build.gradle.kts +++ b/sentry-android-fragment/build.gradle.kts @@ -14,7 +14,6 @@ android { namespace = "io.sentry.android.fragment" defaultConfig { - targetSdk = Config.Android.targetSdkVersion minSdk = Config.Android.minSdkVersion // for AGP 4.1 @@ -48,10 +47,12 @@ android { checkReleaseBuilds = false } - variantFilter { - if (Config.Android.shouldSkipDebugVariant(buildType.name)) { - ignore = true - } + buildFeatures { + buildConfig = true + } + + androidComponents.beforeVariants { + it.enable = !Config.Android.shouldSkipDebugVariant(it.buildType) } } diff --git a/sentry-android-fragment/src/main/java/io/sentry/android/fragment/FragmentLifecycleIntegration.kt b/sentry-android-fragment/src/main/java/io/sentry/android/fragment/FragmentLifecycleIntegration.kt index f1c44422a5..0577944be2 100644 --- a/sentry-android-fragment/src/main/java/io/sentry/android/fragment/FragmentLifecycleIntegration.kt +++ b/sentry-android-fragment/src/main/java/io/sentry/android/fragment/FragmentLifecycleIntegration.kt @@ -13,7 +13,7 @@ import io.sentry.SentryOptions import io.sentry.util.IntegrationUtils.addIntegrationToSdkVersion import java.io.Closeable -class FragmentLifecycleIntegration( +public class FragmentLifecycleIntegration( private val application: Application, private val filterFragmentLifecycleBreadcrumbs: Set, private val enableAutoFragmentLifecycleTracing: Boolean @@ -22,13 +22,13 @@ class FragmentLifecycleIntegration( Integration, Closeable { - constructor(application: Application) : this( + public constructor(application: Application) : this( application = application, filterFragmentLifecycleBreadcrumbs = FragmentLifecycleState.states, enableAutoFragmentLifecycleTracing = false ) - constructor( + public constructor( application: Application, enableFragmentLifecycleBreadcrumbs: Boolean, enableAutoFragmentLifecycleTracing: Boolean diff --git a/sentry-android-fragment/src/main/java/io/sentry/android/fragment/FragmentLifecycleState.kt b/sentry-android-fragment/src/main/java/io/sentry/android/fragment/FragmentLifecycleState.kt index fd52437d60..3a45dd60d0 100644 --- a/sentry-android-fragment/src/main/java/io/sentry/android/fragment/FragmentLifecycleState.kt +++ b/sentry-android-fragment/src/main/java/io/sentry/android/fragment/FragmentLifecycleState.kt @@ -1,6 +1,6 @@ package io.sentry.android.fragment -enum class FragmentLifecycleState(internal val breadcrumbName: String) { +public enum class FragmentLifecycleState(internal val breadcrumbName: String) { ATTACHED("attached"), SAVE_INSTANCE_STATE("save instance state"), CREATED("created"), @@ -13,8 +13,8 @@ enum class FragmentLifecycleState(internal val breadcrumbName: String) { DESTROYED("destroyed"), DETACHED("detached"); - companion object { - val states = HashSet().apply { + public companion object { + public val states: Set = HashSet().apply { add(ATTACHED) add(SAVE_INSTANCE_STATE) add(CREATED) diff --git a/sentry-android-fragment/src/main/java/io/sentry/android/fragment/SentryFragmentLifecycleCallbacks.kt b/sentry-android-fragment/src/main/java/io/sentry/android/fragment/SentryFragmentLifecycleCallbacks.kt index cf5b14b43c..c7877180cb 100644 --- a/sentry-android-fragment/src/main/java/io/sentry/android/fragment/SentryFragmentLifecycleCallbacks.kt +++ b/sentry-android-fragment/src/main/java/io/sentry/android/fragment/SentryFragmentLifecycleCallbacks.kt @@ -19,13 +19,13 @@ import java.util.WeakHashMap private const val TRACE_ORIGIN = "auto.ui.fragment" @Suppress("TooManyFunctions") -class SentryFragmentLifecycleCallbacks( +public class SentryFragmentLifecycleCallbacks( private val scopes: IScopes = ScopesAdapter.getInstance(), - val filterFragmentLifecycleBreadcrumbs: Set, - val enableAutoFragmentLifecycleTracing: Boolean + internal val filterFragmentLifecycleBreadcrumbs: Set, + internal val enableAutoFragmentLifecycleTracing: Boolean ) : FragmentLifecycleCallbacks() { - constructor( + public constructor( scopes: IScopes, enableFragmentLifecycleBreadcrumbs: Boolean, enableAutoFragmentLifecycleTracing: Boolean @@ -37,7 +37,7 @@ class SentryFragmentLifecycleCallbacks( enableAutoFragmentLifecycleTracing = enableAutoFragmentLifecycleTracing ) - constructor( + public constructor( enableFragmentLifecycleBreadcrumbs: Boolean = true, enableAutoFragmentLifecycleTracing: Boolean = false ) : this( @@ -52,7 +52,7 @@ class SentryFragmentLifecycleCallbacks( private val fragmentsWithOngoingTransactions = WeakHashMap() - val enableFragmentLifecycleBreadcrumbs: Boolean + public val enableFragmentLifecycleBreadcrumbs: Boolean get() = filterFragmentLifecycleBreadcrumbs.isNotEmpty() override fun onFragmentAttached( @@ -190,7 +190,7 @@ class SentryFragmentLifecycleCallbacks( } } - companion object { - const val FRAGMENT_LOAD_OP = "ui.load" + public companion object { + public const val FRAGMENT_LOAD_OP: String = "ui.load" } } diff --git a/sentry-android-integration-tests/sentry-uitest-android-benchmark/benchmark-proguard-rules.pro b/sentry-android-integration-tests/sentry-uitest-android-benchmark/benchmark-proguard-rules.pro index 8f5e14b25e..b2db365bad 100644 --- a/sentry-android-integration-tests/sentry-uitest-android-benchmark/benchmark-proguard-rules.pro +++ b/sentry-android-integration-tests/sentry-uitest-android-benchmark/benchmark-proguard-rules.pro @@ -24,8 +24,6 @@ #Shrinking removes annotations and "unused classes" from test apk, so we don't shrink -dontshrink --ignorewarnings - -keepattributes *Annotation* -dontnote junit.framework.** @@ -33,3 +31,8 @@ -dontwarn androidx.test.** -dontwarn org.junit.** + +-dontwarn androidx.annotation.** +-dontwarn com.google.errorprone.** + +-ignorewarnings diff --git a/sentry-android-integration-tests/sentry-uitest-android-benchmark/build.gradle.kts b/sentry-android-integration-tests/sentry-uitest-android-benchmark/build.gradle.kts index b15e7626dc..66fe84199b 100644 --- a/sentry-android-integration-tests/sentry-uitest-android-benchmark/build.gradle.kts +++ b/sentry-android-integration-tests/sentry-uitest-android-benchmark/build.gradle.kts @@ -55,12 +55,14 @@ android { isMinifyEnabled = true signingConfig = signingConfigs.getByName("debug") proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "benchmark-proguard-rules.pro") + testProguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "benchmark-proguard-rules.pro") } getByName("release") { isMinifyEnabled = true isShrinkResources = true signingConfig = signingConfigs.getByName("debug") // to be able to run release mode proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "benchmark-proguard-rules.pro") + testProguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "benchmark-proguard-rules.pro") } } @@ -76,10 +78,8 @@ android { checkReleaseBuilds = false } - variantFilter { - if (Config.Android.shouldSkipDebugVariant(buildType.name)) { - ignore = true - } + androidComponents.beforeVariants { + it.enable = !Config.Android.shouldSkipDebugVariant(it.buildType) } } @@ -121,7 +121,3 @@ tasks.withType { // Target version of the generated JVM bytecode. It is used for type resolution. jvmTarget = JavaVersion.VERSION_1_8.toString() } - -kotlin { - explicitApi() -} diff --git a/sentry-android-integration-tests/sentry-uitest-android-critical/build.gradle.kts b/sentry-android-integration-tests/sentry-uitest-android-critical/build.gradle.kts index da7add25cc..dcd52443ca 100644 --- a/sentry-android-integration-tests/sentry-uitest-android-critical/build.gradle.kts +++ b/sentry-android-integration-tests/sentry-uitest-android-critical/build.gradle.kts @@ -42,10 +42,8 @@ android { composeOptions { kotlinCompilerExtensionVersion = Config.androidComposeCompilerVersion } - variantFilter { - if (Config.Android.shouldSkipDebugVariant(buildType.name)) { - ignore = true - } + androidComponents.beforeVariants { + it.enable = !Config.Android.shouldSkipDebugVariant(it.buildType) } } @@ -63,7 +61,3 @@ tasks.withType { // Target version of the generated JVM bytecode. It is used for type resolution. jvmTarget = JavaVersion.VERSION_1_8.toString() } - -kotlin { - explicitApi() -} diff --git a/sentry-android-integration-tests/sentry-uitest-android/build.gradle.kts b/sentry-android-integration-tests/sentry-uitest-android/build.gradle.kts index e3afc9823f..c817467a06 100644 --- a/sentry-android-integration-tests/sentry-uitest-android/build.gradle.kts +++ b/sentry-android-integration-tests/sentry-uitest-android/build.gradle.kts @@ -38,6 +38,7 @@ android { // Note that the viewBinding.enabled property is now deprecated. viewBinding = true compose = true + buildConfig = true } composeOptions { @@ -82,10 +83,8 @@ android { checkReleaseBuilds = false } - variantFilter { - if (Config.Android.shouldSkipDebugVariant(buildType.name)) { - ignore = true - } + androidComponents.beforeVariants { + it.enable = !Config.Android.shouldSkipDebugVariant(it.buildType) } } @@ -142,7 +141,3 @@ tasks.withType { // Target version of the generated JVM bytecode. It is used for type resolution. jvmTarget = JavaVersion.VERSION_1_8.toString() } - -kotlin { - explicitApi() -} diff --git a/sentry-android-integration-tests/test-app-plain/build.gradle.kts b/sentry-android-integration-tests/test-app-plain/build.gradle.kts index 7a094fe283..e2592ca31f 100644 --- a/sentry-android-integration-tests/test-app-plain/build.gradle.kts +++ b/sentry-android-integration-tests/test-app-plain/build.gradle.kts @@ -18,7 +18,7 @@ android { getByName("release") { isMinifyEnabled = true signingConfig = signingConfigs.getByName("debug") // to be able to run release mode - proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "benchmark-proguard-rules.pro") + proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") ndk { abiFilters.clear() abiFilters.add("arm64-v8a") @@ -41,10 +41,8 @@ android { } } - variantFilter { - if (Config.Android.shouldSkipDebugVariant(buildType.name)) { - ignore = true - } + androidComponents.beforeVariants { + it.enable = !Config.Android.shouldSkipDebugVariant(it.buildType) } } diff --git a/sentry-android-integration-tests/test-app-plain/proguard-rules.pro b/sentry-android-integration-tests/test-app-plain/proguard-rules.pro index 2f9dc5a47e..d5b66a5b79 100644 --- a/sentry-android-integration-tests/test-app-plain/proguard-rules.pro +++ b/sentry-android-integration-tests/test-app-plain/proguard-rules.pro @@ -19,3 +19,5 @@ # If you keep the line number information, uncomment this to # hide the original source file name. #-renamesourcefileattribute SourceFile + +-keep,allowobfuscation,allowshrinking class * extends androidx.navigation.Navigator diff --git a/sentry-android-integration-tests/test-app-sentry/build.gradle.kts b/sentry-android-integration-tests/test-app-sentry/build.gradle.kts index 6464ca9d57..34973ff38a 100644 --- a/sentry-android-integration-tests/test-app-sentry/build.gradle.kts +++ b/sentry-android-integration-tests/test-app-sentry/build.gradle.kts @@ -18,7 +18,7 @@ android { getByName("release") { isMinifyEnabled = true signingConfig = signingConfigs.getByName("debug") // to be able to run release mode - proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "benchmark-proguard-rules.pro") + proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") ndk { abiFilters.clear() abiFilters.add("arm64-v8a") @@ -41,10 +41,8 @@ android { } } - variantFilter { - if (Config.Android.shouldSkipDebugVariant(buildType.name)) { - ignore = true - } + androidComponents.beforeVariants { + it.enable = !Config.Android.shouldSkipDebugVariant(it.buildType) } } diff --git a/sentry-android-integration-tests/test-app-sentry/proguard-rules.pro b/sentry-android-integration-tests/test-app-sentry/proguard-rules.pro index 2f9dc5a47e..e6edffeb9e 100644 --- a/sentry-android-integration-tests/test-app-sentry/proguard-rules.pro +++ b/sentry-android-integration-tests/test-app-sentry/proguard-rules.pro @@ -19,3 +19,4 @@ # If you keep the line number information, uncomment this to # hide the original source file name. #-renamesourcefileattribute SourceFile +-keep,allowobfuscation,allowshrinking class * extends androidx.navigation.Navigator diff --git a/sentry-android-navigation/build.gradle.kts b/sentry-android-navigation/build.gradle.kts index d46c7e2b84..ddf8e1f6c0 100644 --- a/sentry-android-navigation/build.gradle.kts +++ b/sentry-android-navigation/build.gradle.kts @@ -14,7 +14,6 @@ android { namespace = "io.sentry.android.navigation" defaultConfig { - targetSdk = Config.Android.targetSdkVersion minSdk = Config.Android.minSdkVersion // for AGP 4.1 @@ -49,10 +48,12 @@ android { checkReleaseBuilds = false } - variantFilter { - if (Config.Android.shouldSkipDebugVariant(buildType.name)) { - ignore = true - } + buildFeatures { + buildConfig = true + } + + androidComponents.beforeVariants { + it.enable = !Config.Android.shouldSkipDebugVariant(it.buildType) } } diff --git a/sentry-android-navigation/src/main/java/io/sentry/android/navigation/SentryNavigationListener.kt b/sentry-android-navigation/src/main/java/io/sentry/android/navigation/SentryNavigationListener.kt index 1ea9e42c3c..4f546851a6 100644 --- a/sentry-android-navigation/src/main/java/io/sentry/android/navigation/SentryNavigationListener.kt +++ b/sentry-android-navigation/src/main/java/io/sentry/android/navigation/SentryNavigationListener.kt @@ -34,7 +34,7 @@ private const val TRACE_ORIGIN = "auto.navigation" * @param enableNavigationTracing Whether the integration should start a new idle [ITransaction] * with [SentryOptions.idleTimeout] for navigation events. */ -class SentryNavigationListener @JvmOverloads constructor( +public class SentryNavigationListener @JvmOverloads constructor( private val scopes: IScopes = ScopesAdapter.getInstance(), private val enableNavigationBreadcrumbs: Boolean = true, private val enableNavigationTracing: Boolean = true, @@ -193,7 +193,7 @@ class SentryNavigationListener @JvmOverloads constructor( return "/" + name.substringBefore('/') // strip out arguments from the tx name } - companion object { - const val NAVIGATION_OP = "navigation" + public companion object { + public const val NAVIGATION_OP: String = "navigation" } } diff --git a/sentry-android-ndk/build.gradle.kts b/sentry-android-ndk/build.gradle.kts index 339c168214..0d75024ddc 100644 --- a/sentry-android-ndk/build.gradle.kts +++ b/sentry-android-ndk/build.gradle.kts @@ -13,7 +13,6 @@ android { namespace = "io.sentry.android.ndk" defaultConfig { - targetSdk = Config.Android.targetSdkVersion minSdk = Config.Android.minSdkVersion testInstrumentationRunner = Config.TestLibs.androidJUnitRunner @@ -58,12 +57,18 @@ android { resolutionStrategy.force(Config.CompileOnly.jetbrainsAnnotations) } - variantFilter { - if (Config.Android.shouldSkipDebugVariant(buildType.name)) { - ignore = true - } + buildFeatures { + buildConfig = true + } + + androidComponents.beforeVariants { + it.enable = !Config.Android.shouldSkipDebugVariant(it.buildType) } + // the default AGP version is too high, we keep this low for compatibility reasons + // see https://developer.android.com/build/releases/past-releases/ to find the AGP-NDK mapping + ndkVersion = "23.1.7779620" + @Suppress("UnstableApiUsage") packagingOptions { jniLibs { diff --git a/sentry-android-replay/api/sentry-android-replay.api b/sentry-android-replay/api/sentry-android-replay.api index a7f7931326..d4e20da038 100644 --- a/sentry-android-replay/api/sentry-android-replay.api +++ b/sentry-android-replay/api/sentry-android-replay.api @@ -42,19 +42,12 @@ public abstract interface class io/sentry/android/replay/Recorder : java/io/Clos public final class io/sentry/android/replay/ReplayCache : java/io/Closeable { public static final field $stable I - public static final field Companion Lio/sentry/android/replay/ReplayCache$Companion; public fun (Lio/sentry/SentryOptions;Lio/sentry/protocol/SentryId;)V public final fun addFrame (Ljava/io/File;JLjava/lang/String;)V public static synthetic fun addFrame$default (Lio/sentry/android/replay/ReplayCache;Ljava/io/File;JLjava/lang/String;ILjava/lang/Object;)V public fun close ()V public final fun createVideoOf (JJIIIIILjava/io/File;)Lio/sentry/android/replay/GeneratedVideo; public static synthetic fun createVideoOf$default (Lio/sentry/android/replay/ReplayCache;JJIIIIILjava/io/File;ILjava/lang/Object;)Lio/sentry/android/replay/GeneratedVideo; - public final fun persistSegmentValues (Ljava/lang/String;Ljava/lang/String;)V - public final fun rotate (J)Ljava/lang/String; -} - -public final class io/sentry/android/replay/ReplayCache$Companion { - public final fun makeReplayCacheDir (Lio/sentry/SentryOptions;Lio/sentry/protocol/SentryId;)Ljava/io/File; } public final class io/sentry/android/replay/ReplayIntegration : android/content/ComponentCallbacks, io/sentry/IConnectionStatusProvider$IConnectionStatusObserver, io/sentry/Integration, io/sentry/ReplayController, io/sentry/android/replay/ScreenshotRecorderCallback, io/sentry/android/replay/gestures/TouchRecorderCallback, io/sentry/transport/RateLimiter$IRateLimitObserver, java/io/Closeable { @@ -90,7 +83,6 @@ public abstract interface class io/sentry/android/replay/ScreenshotRecorderCallb public final class io/sentry/android/replay/ScreenshotRecorderConfig { public static final field $stable I - public static final field Companion Lio/sentry/android/replay/ScreenshotRecorderConfig$Companion; public fun (IIFFII)V public final fun component1 ()I public final fun component2 ()I @@ -111,10 +103,6 @@ public final class io/sentry/android/replay/ScreenshotRecorderConfig { public fun toString ()Ljava/lang/String; } -public final class io/sentry/android/replay/ScreenshotRecorderConfig$Companion { - public final fun from (Landroid/content/Context;Lio/sentry/SentryReplayOptions;)Lio/sentry/android/replay/ScreenshotRecorderConfig; -} - public final class io/sentry/android/replay/SentryReplayModifiers { public static final field $stable I public static final field INSTANCE Lio/sentry/android/replay/SentryReplayModifiers; @@ -133,36 +121,10 @@ public final class io/sentry/android/replay/ViewExtensionsKt { public static final fun sentryReplayUnmask (Landroid/view/View;)V } -public final class io/sentry/android/replay/gestures/GestureRecorder : io/sentry/android/replay/OnRootViewsChangedListener { - public static final field $stable I - public fun (Lio/sentry/SentryOptions;Lio/sentry/android/replay/gestures/TouchRecorderCallback;)V - public fun onRootViewsChanged (Landroid/view/View;Z)V - public final fun stop ()V -} - -public final class io/sentry/android/replay/gestures/ReplayGestureConverter { - public static final field $stable I - public fun (Lio/sentry/transport/ICurrentDateProvider;)V - public final fun convert (Landroid/view/MotionEvent;Lio/sentry/android/replay/ScreenshotRecorderConfig;)Ljava/util/List; -} - public abstract interface class io/sentry/android/replay/gestures/TouchRecorderCallback { public abstract fun onTouchEvent (Landroid/view/MotionEvent;)V } -public final class io/sentry/android/replay/util/AndroidTextLayout : io/sentry/android/replay/util/TextLayout { - public static final field $stable I - public fun (Landroid/text/Layout;)V - public fun getDominantTextColor ()Ljava/lang/Integer; - public fun getEllipsisCount (I)I - public fun getLineBottom (I)I - public fun getLineCount ()I - public fun getLineStart (I)I - public fun getLineTop (I)I - public fun getLineVisibleEnd (I)I - public fun getPrimaryHorizontal (II)F -} - public class io/sentry/android/replay/util/FixedWindowCallback : android/view/Window$Callback { public final field delegate Landroid/view/Window$Callback; public fun (Landroid/view/Window$Callback;)V @@ -193,82 +155,3 @@ public class io/sentry/android/replay/util/FixedWindowCallback : android/view/Wi public fun onWindowStartingActionMode (Landroid/view/ActionMode$Callback;I)Landroid/view/ActionMode; } -public abstract interface class io/sentry/android/replay/util/TextLayout { - public abstract fun getDominantTextColor ()Ljava/lang/Integer; - public abstract fun getEllipsisCount (I)I - public abstract fun getLineBottom (I)I - public abstract fun getLineCount ()I - public abstract fun getLineStart (I)I - public abstract fun getLineTop (I)I - public abstract fun getLineVisibleEnd (I)I - public abstract fun getPrimaryHorizontal (II)F -} - -public abstract interface class io/sentry/android/replay/video/SimpleFrameMuxer { - public abstract fun getVideoTime ()J - public abstract fun isStarted ()Z - public abstract fun muxVideoFrame (Ljava/nio/ByteBuffer;Landroid/media/MediaCodec$BufferInfo;)V - public abstract fun release ()V - public abstract fun start (Landroid/media/MediaFormat;)V -} - -public final class io/sentry/android/replay/video/SimpleMp4FrameMuxer : io/sentry/android/replay/video/SimpleFrameMuxer { - public static final field $stable I - public fun (Ljava/lang/String;F)V - public fun getVideoTime ()J - public fun isStarted ()Z - public fun muxVideoFrame (Ljava/nio/ByteBuffer;Landroid/media/MediaCodec$BufferInfo;)V - public fun release ()V - public fun start (Landroid/media/MediaFormat;)V -} - -public abstract class io/sentry/android/replay/viewhierarchy/ViewHierarchyNode { - public static final field $stable I - public static final field Companion Lio/sentry/android/replay/viewhierarchy/ViewHierarchyNode$Companion; - public synthetic fun (FFIIFILio/sentry/android/replay/viewhierarchy/ViewHierarchyNode;ZZZLandroid/graphics/Rect;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public synthetic fun (FFIIFILio/sentry/android/replay/viewhierarchy/ViewHierarchyNode;ZZZLandroid/graphics/Rect;Lkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun getChildren ()Ljava/util/List; - public final fun getDistance ()I - public final fun getElevation ()F - public final fun getHeight ()I - public final fun getParent ()Lio/sentry/android/replay/viewhierarchy/ViewHierarchyNode; - public final fun getShouldMask ()Z - public final fun getVisibleRect ()Landroid/graphics/Rect; - public final fun getWidth ()I - public final fun getX ()F - public final fun getY ()F - public final fun isImportantForContentCapture ()Z - public final fun isObscured (Lio/sentry/android/replay/viewhierarchy/ViewHierarchyNode;)Z - public final fun isVisible ()Z - public final fun setChildren (Ljava/util/List;)V - public final fun setImportantForCaptureToAncestors (Z)V - public final fun setImportantForContentCapture (Z)V - public final fun traverse (Lkotlin/jvm/functions/Function1;)V -} - -public final class io/sentry/android/replay/viewhierarchy/ViewHierarchyNode$Companion { - public final fun fromView (Landroid/view/View;Lio/sentry/android/replay/viewhierarchy/ViewHierarchyNode;ILio/sentry/SentryOptions;)Lio/sentry/android/replay/viewhierarchy/ViewHierarchyNode; -} - -public final class io/sentry/android/replay/viewhierarchy/ViewHierarchyNode$GenericViewHierarchyNode : io/sentry/android/replay/viewhierarchy/ViewHierarchyNode { - public static final field $stable I - public fun (FFIIFILio/sentry/android/replay/viewhierarchy/ViewHierarchyNode;ZZZLandroid/graphics/Rect;)V - public synthetic fun (FFIIFILio/sentry/android/replay/viewhierarchy/ViewHierarchyNode;ZZZLandroid/graphics/Rect;ILkotlin/jvm/internal/DefaultConstructorMarker;)V -} - -public final class io/sentry/android/replay/viewhierarchy/ViewHierarchyNode$ImageViewHierarchyNode : io/sentry/android/replay/viewhierarchy/ViewHierarchyNode { - public static final field $stable I - public fun (FFIIFILio/sentry/android/replay/viewhierarchy/ViewHierarchyNode;ZZZLandroid/graphics/Rect;)V - public synthetic fun (FFIIFILio/sentry/android/replay/viewhierarchy/ViewHierarchyNode;ZZZLandroid/graphics/Rect;ILkotlin/jvm/internal/DefaultConstructorMarker;)V -} - -public final class io/sentry/android/replay/viewhierarchy/ViewHierarchyNode$TextViewHierarchyNode : io/sentry/android/replay/viewhierarchy/ViewHierarchyNode { - public static final field $stable I - public fun (Lio/sentry/android/replay/util/TextLayout;Ljava/lang/Integer;IIFFIIFILio/sentry/android/replay/viewhierarchy/ViewHierarchyNode;ZZZLandroid/graphics/Rect;)V - public synthetic fun (Lio/sentry/android/replay/util/TextLayout;Ljava/lang/Integer;IIFFIIFILio/sentry/android/replay/viewhierarchy/ViewHierarchyNode;ZZZLandroid/graphics/Rect;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun getDominantColor ()Ljava/lang/Integer; - public final fun getLayout ()Lio/sentry/android/replay/util/TextLayout; - public final fun getPaddingLeft ()I - public final fun getPaddingTop ()I -} - diff --git a/sentry-android-replay/build.gradle.kts b/sentry-android-replay/build.gradle.kts index 7f1424096c..45a51906eb 100644 --- a/sentry-android-replay/build.gradle.kts +++ b/sentry-android-replay/build.gradle.kts @@ -17,7 +17,6 @@ android { namespace = "io.sentry.android.replay" defaultConfig { - targetSdk = Config.Android.targetSdkVersion minSdk = Config.Android.minSdkVersion testInstrumentationRunner = Config.TestLibs.androidJUnitRunner @@ -63,10 +62,12 @@ android { checkReleaseBuilds = false } - variantFilter { - if (Config.Android.shouldSkipDebugVariant(buildType.name)) { - ignore = true - } + buildFeatures { + buildConfig = true + } + + androidComponents.beforeVariants { + it.enable = !Config.Android.shouldSkipDebugVariant(it.buildType) } } diff --git a/sentry-android-replay/src/main/java/io/sentry/android/replay/DefaultReplayBreadcrumbConverter.kt b/sentry-android-replay/src/main/java/io/sentry/android/replay/DefaultReplayBreadcrumbConverter.kt index d5c666b5b0..d7b4b7739e 100644 --- a/sentry-android-replay/src/main/java/io/sentry/android/replay/DefaultReplayBreadcrumbConverter.kt +++ b/sentry-android-replay/src/main/java/io/sentry/android/replay/DefaultReplayBreadcrumbConverter.kt @@ -126,7 +126,9 @@ public open class DefaultReplayBreadcrumbConverter : ReplayBreadcrumbConverter { } private fun String.snakeToCamelCase(): String { - return replace(snakecasePattern) { it.value.last().uppercase() } + return replace(snakecasePattern) { + it.value.last().toString().uppercase() + } } private fun Breadcrumb.toRRWebSpanEvent(): RRWebSpanEvent { diff --git a/sentry-android-replay/src/main/java/io/sentry/android/replay/ModifierExtensions.kt b/sentry-android-replay/src/main/java/io/sentry/android/replay/ModifierExtensions.kt index b5d5222388..5d0c27af35 100644 --- a/sentry-android-replay/src/main/java/io/sentry/android/replay/ModifierExtensions.kt +++ b/sentry-android-replay/src/main/java/io/sentry/android/replay/ModifierExtensions.kt @@ -6,7 +6,7 @@ import androidx.compose.ui.semantics.semantics import io.sentry.android.replay.SentryReplayModifiers.SentryPrivacy public object SentryReplayModifiers { - val SentryPrivacy = SemanticsPropertyKey( + public val SentryPrivacy: SemanticsPropertyKey = SemanticsPropertyKey( name = "SentryPrivacy", mergePolicy = { parentValue, _ -> parentValue } ) diff --git a/sentry-android-replay/src/main/java/io/sentry/android/replay/Recorder.kt b/sentry-android-replay/src/main/java/io/sentry/android/replay/Recorder.kt index 6cf86b6a7e..0946018c78 100644 --- a/sentry-android-replay/src/main/java/io/sentry/android/replay/Recorder.kt +++ b/sentry-android-replay/src/main/java/io/sentry/android/replay/Recorder.kt @@ -2,17 +2,17 @@ package io.sentry.android.replay import java.io.Closeable -interface Recorder : Closeable { +public interface Recorder : Closeable { /** * @param recorderConfig a [ScreenshotRecorderConfig] that can be used to determine frame rate * at which the screenshots should be taken, and the screenshots size/resolution, which can * change e.g. in the case of orientation change or window size change */ - fun start(recorderConfig: ScreenshotRecorderConfig) + public fun start(recorderConfig: ScreenshotRecorderConfig) - fun resume() + public fun resume() - fun pause() + public fun pause() - fun stop() + public fun stop() } diff --git a/sentry-android-replay/src/main/java/io/sentry/android/replay/ReplayCache.kt b/sentry-android-replay/src/main/java/io/sentry/android/replay/ReplayCache.kt index d67605505d..d926acb9ae 100644 --- a/sentry-android-replay/src/main/java/io/sentry/android/replay/ReplayCache.kt +++ b/sentry-android-replay/src/main/java/io/sentry/android/replay/ReplayCache.kt @@ -247,7 +247,7 @@ public class ReplayCache( * @param until value until whose the frames should be removed, represented as unix timestamp * @return the first screen in the rotated buffer, if any */ - fun rotate(until: Long): String? { + internal fun rotate(until: Long): String? { var screen: String? = null frames.removeAll { if (it.timestamp < until) { @@ -270,7 +270,7 @@ public class ReplayCache( } // TODO: it's awful, choose a better serialization format - fun persistSegmentValues(key: String, value: String?) { + internal fun persistSegmentValues(key: String, value: String?) { lock.acquire().use { if (isClosed.get()) { return @@ -292,7 +292,7 @@ public class ReplayCache( } } - companion object { + internal companion object { internal const val ONGOING_SEGMENT = ".ongoing_segment" internal const val SEGMENT_KEY_HEIGHT = "config.height" diff --git a/sentry-android-replay/src/main/java/io/sentry/android/replay/ReplayIntegration.kt b/sentry-android-replay/src/main/java/io/sentry/android/replay/ReplayIntegration.kt index 07ecf47756..5f97290694 100644 --- a/sentry-android-replay/src/main/java/io/sentry/android/replay/ReplayIntegration.kt +++ b/sentry-android-replay/src/main/java/io/sentry/android/replay/ReplayIntegration.kt @@ -67,7 +67,7 @@ public class ReplayIntegration( IRateLimitObserver { // needed for the Java's call site - constructor(context: Context, dateProvider: ICurrentDateProvider) : this( + public constructor(context: Context, dateProvider: ICurrentDateProvider) : this( context.appContext(), dateProvider, null, @@ -151,7 +151,7 @@ public class ReplayIntegration( finalizePreviousReplay() } - override fun isRecording() = isRecording.get() + override fun isRecording(): Boolean = isRecording.get() override fun start() { // TODO: add lifecycle state instead and manage it in start/pause/resume/stop @@ -318,7 +318,7 @@ public class ReplayIntegration( } } - override fun onLowMemory() = Unit + override fun onLowMemory(): Unit = Unit override fun onTouchEvent(event: MotionEvent) { captureStrategy?.onTouchEvent(event) diff --git a/sentry-android-replay/src/main/java/io/sentry/android/replay/ScreenshotRecorder.kt b/sentry-android-replay/src/main/java/io/sentry/android/replay/ScreenshotRecorder.kt index 62752c74cc..70be2756e3 100644 --- a/sentry-android-replay/src/main/java/io/sentry/android/replay/ScreenshotRecorder.kt +++ b/sentry-android-replay/src/main/java/io/sentry/android/replay/ScreenshotRecorder.kt @@ -264,7 +264,7 @@ public data class ScreenshotRecorderConfig( bitRate = 0 ) - companion object { + internal companion object { /** * Since codec block size is 16, so we have to adjust the width and height to it, otherwise * the codec might fail to configure on some devices, see https://cs.android.com/android/platform/superproject/+/master:frameworks/base/media/java/android/media/MediaCodecInfo.java;l=1999-2001 @@ -325,7 +325,7 @@ public interface ScreenshotRecorderCallback { * * @param bitmap a screenshot taken in the form of [android.graphics.Bitmap] */ - fun onScreenshotRecorded(bitmap: Bitmap) + public fun onScreenshotRecorded(bitmap: Bitmap) /** * Called whenever a new frame screenshot is available. @@ -333,5 +333,5 @@ public interface ScreenshotRecorderCallback { * @param screenshot file containing the frame screenshot * @param frameTimestamp the timestamp when the frame screenshot was taken */ - fun onScreenshotRecorded(screenshot: File, frameTimestamp: Long) + public fun onScreenshotRecorded(screenshot: File, frameTimestamp: Long) } diff --git a/sentry-android-replay/src/main/java/io/sentry/android/replay/SessionReplayOptions.kt b/sentry-android-replay/src/main/java/io/sentry/android/replay/SessionReplayOptions.kt index fb5105565b..e86b7b03b3 100644 --- a/sentry-android-replay/src/main/java/io/sentry/android/replay/SessionReplayOptions.kt +++ b/sentry-android-replay/src/main/java/io/sentry/android/replay/SessionReplayOptions.kt @@ -12,7 +12,7 @@ import io.sentry.SentryReplayOptions * *

Default is enabled. */ -var SentryReplayOptions.maskAllText: Boolean +public var SentryReplayOptions.maskAllText: Boolean @Deprecated("Getter is unsupported.", level = DeprecationLevel.ERROR) get() = error("Getter not supported") set(value) = setMaskAllText(value) @@ -25,7 +25,7 @@ var SentryReplayOptions.maskAllText: Boolean * *

Default is enabled. */ -var SentryReplayOptions.maskAllImages: Boolean +public var SentryReplayOptions.maskAllImages: Boolean @Deprecated("Getter is unsupported.", level = DeprecationLevel.ERROR) get() = error("Getter not supported") set(value) = setMaskAllImages(value) diff --git a/sentry-android-replay/src/main/java/io/sentry/android/replay/ViewExtensions.kt b/sentry-android-replay/src/main/java/io/sentry/android/replay/ViewExtensions.kt index 2625399c99..b39b96b523 100644 --- a/sentry-android-replay/src/main/java/io/sentry/android/replay/ViewExtensions.kt +++ b/sentry-android-replay/src/main/java/io/sentry/android/replay/ViewExtensions.kt @@ -5,7 +5,7 @@ import android.view.View /** * Marks this view to be masked in session replay. */ -fun View.sentryReplayMask() { +public fun View.sentryReplayMask() { setTag(R.id.sentry_privacy, "mask") } @@ -13,6 +13,6 @@ fun View.sentryReplayMask() { * Marks this view to be unmasked in session replay. * All its content will be visible in the replay, use with caution. */ -fun View.sentryReplayUnmask() { +public fun View.sentryReplayUnmask() { setTag(R.id.sentry_privacy, "unmask") } diff --git a/sentry-android-replay/src/main/java/io/sentry/android/replay/gestures/GestureRecorder.kt b/sentry-android-replay/src/main/java/io/sentry/android/replay/gestures/GestureRecorder.kt index a8da77d851..fbdee803d7 100644 --- a/sentry-android-replay/src/main/java/io/sentry/android/replay/gestures/GestureRecorder.kt +++ b/sentry-android-replay/src/main/java/io/sentry/android/replay/gestures/GestureRecorder.kt @@ -12,7 +12,7 @@ import io.sentry.android.replay.util.FixedWindowCallback import io.sentry.util.AutoClosableReentrantLock import java.lang.ref.WeakReference -class GestureRecorder( +internal class GestureRecorder( private val options: SentryOptions, private val touchRecorderCallback: TouchRecorderCallback ) : OnRootViewsChangedListener { @@ -88,5 +88,5 @@ class GestureRecorder( } public interface TouchRecorderCallback { - fun onTouchEvent(event: MotionEvent) + public fun onTouchEvent(event: MotionEvent) } diff --git a/sentry-android-replay/src/main/java/io/sentry/android/replay/gestures/ReplayGestureConverter.kt b/sentry-android-replay/src/main/java/io/sentry/android/replay/gestures/ReplayGestureConverter.kt index fa215960c4..ca7636268a 100644 --- a/sentry-android-replay/src/main/java/io/sentry/android/replay/gestures/ReplayGestureConverter.kt +++ b/sentry-android-replay/src/main/java/io/sentry/android/replay/gestures/ReplayGestureConverter.kt @@ -9,7 +9,7 @@ import io.sentry.rrweb.RRWebInteractionMoveEvent import io.sentry.rrweb.RRWebInteractionMoveEvent.Position import io.sentry.transport.ICurrentDateProvider -class ReplayGestureConverter( +internal class ReplayGestureConverter( private val dateProvider: ICurrentDateProvider ) { diff --git a/sentry-android-replay/src/main/java/io/sentry/android/replay/util/Executors.kt b/sentry-android-replay/src/main/java/io/sentry/android/replay/util/Executors.kt index 3c07ad7eaa..107cedebea 100644 --- a/sentry-android-replay/src/main/java/io/sentry/android/replay/util/Executors.kt +++ b/sentry-android-replay/src/main/java/io/sentry/android/replay/util/Executors.kt @@ -1,5 +1,6 @@ package io.sentry.android.replay.util +import android.annotation.SuppressLint import io.sentry.ISentryExecutorService import io.sentry.SentryLevel.ERROR import io.sentry.SentryOptions @@ -69,6 +70,7 @@ internal fun ExecutorService.submitSafely( } } +@SuppressLint("DiscouragedApi") internal fun ScheduledExecutorService.scheduleAtFixedRateSafely( options: SentryOptions, taskName: String, diff --git a/sentry-android-replay/src/main/java/io/sentry/android/replay/util/TextLayout.kt b/sentry-android-replay/src/main/java/io/sentry/android/replay/util/TextLayout.kt index cd07c6d170..26e61fb135 100644 --- a/sentry-android-replay/src/main/java/io/sentry/android/replay/util/TextLayout.kt +++ b/sentry-android-replay/src/main/java/io/sentry/android/replay/util/TextLayout.kt @@ -3,7 +3,7 @@ package io.sentry.android.replay.util /** * An abstraction over [android.text.Layout] with different implementations for Views and Compose. */ -interface TextLayout { +internal interface TextLayout { val lineCount: Int /** diff --git a/sentry-android-replay/src/main/java/io/sentry/android/replay/util/Views.kt b/sentry-android-replay/src/main/java/io/sentry/android/replay/util/Views.kt index f3e667dc32..2e53f8fb18 100644 --- a/sentry-android-replay/src/main/java/io/sentry/android/replay/util/Views.kt +++ b/sentry-android-replay/src/main/java/io/sentry/android/replay/util/Views.kt @@ -147,7 +147,7 @@ internal val TextView.totalPaddingTopSafe: Int */ internal fun Int.toOpaque() = this or 0xFF000000.toInt() -class AndroidTextLayout(private val layout: Layout) : TextLayout { +internal class AndroidTextLayout(private val layout: Layout) : TextLayout { override val lineCount: Int get() = layout.lineCount override val dominantTextColor: Int? get() { if (layout.text !is Spanned) return null diff --git a/sentry-android-replay/src/main/java/io/sentry/android/replay/video/SimpleFrameMuxer.kt b/sentry-android-replay/src/main/java/io/sentry/android/replay/video/SimpleFrameMuxer.kt index 17f454967b..298e27db62 100644 --- a/sentry-android-replay/src/main/java/io/sentry/android/replay/video/SimpleFrameMuxer.kt +++ b/sentry-android-replay/src/main/java/io/sentry/android/replay/video/SimpleFrameMuxer.kt @@ -34,7 +34,7 @@ import android.media.MediaCodec import android.media.MediaFormat import java.nio.ByteBuffer -interface SimpleFrameMuxer { +internal interface SimpleFrameMuxer { fun isStarted(): Boolean fun start(videoFormat: MediaFormat) diff --git a/sentry-android-replay/src/main/java/io/sentry/android/replay/video/SimpleMp4FrameMuxer.kt b/sentry-android-replay/src/main/java/io/sentry/android/replay/video/SimpleMp4FrameMuxer.kt index cf30f9e49f..cfd89d1241 100644 --- a/sentry-android-replay/src/main/java/io/sentry/android/replay/video/SimpleMp4FrameMuxer.kt +++ b/sentry-android-replay/src/main/java/io/sentry/android/replay/video/SimpleMp4FrameMuxer.kt @@ -37,7 +37,7 @@ import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit.MICROSECONDS import java.util.concurrent.TimeUnit.MILLISECONDS -class SimpleMp4FrameMuxer(path: String, fps: Float) : SimpleFrameMuxer { +internal class SimpleMp4FrameMuxer(path: String, fps: Float) : SimpleFrameMuxer { private val frameDurationUsec: Long = (TimeUnit.SECONDS.toMicros(1L) / fps).toLong() private val muxer: MediaMuxer = MediaMuxer(path, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4) diff --git a/sentry-android-replay/src/main/java/io/sentry/android/replay/viewhierarchy/ViewHierarchyNode.kt b/sentry-android-replay/src/main/java/io/sentry/android/replay/viewhierarchy/ViewHierarchyNode.kt index 329717d62c..1fba20d6f8 100644 --- a/sentry-android-replay/src/main/java/io/sentry/android/replay/viewhierarchy/ViewHierarchyNode.kt +++ b/sentry-android-replay/src/main/java/io/sentry/android/replay/viewhierarchy/ViewHierarchyNode.kt @@ -16,7 +16,7 @@ import io.sentry.android.replay.util.toOpaque import io.sentry.android.replay.util.totalPaddingTopSafe @TargetApi(26) -sealed class ViewHierarchyNode( +internal sealed class ViewHierarchyNode( val x: Float, val y: Float, val width: Int, diff --git a/sentry-android-replay/src/test/java/io/sentry/android/replay/viewhierarchy/ComposeMaskingOptionsTest.kt b/sentry-android-replay/src/test/java/io/sentry/android/replay/viewhierarchy/ComposeMaskingOptionsTest.kt index 8fa3106058..2a8eee6cba 100644 --- a/sentry-android-replay/src/test/java/io/sentry/android/replay/viewhierarchy/ComposeMaskingOptionsTest.kt +++ b/sentry-android-replay/src/test/java/io/sentry/android/replay/viewhierarchy/ComposeMaskingOptionsTest.kt @@ -3,6 +3,7 @@ package io.sentry.android.replay.viewhierarchy import android.app.Activity import android.net.Uri import android.os.Bundle +import android.os.Looper import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.foundation.layout.Arrangement @@ -34,6 +35,7 @@ import io.sentry.android.replay.viewhierarchy.ViewHierarchyNode.TextViewHierarch import org.junit.Before import org.junit.runner.RunWith import org.robolectric.Robolectric.buildActivity +import org.robolectric.Shadows.shadowOf import org.robolectric.annotation.Config import java.io.File import kotlin.test.Test @@ -48,6 +50,7 @@ class ComposeMaskingOptionsTest { @Before fun setup() { System.setProperty("robolectric.areWindowsMarkedVisible", "true") + System.setProperty("robolectric.pixelCopyRenderMode", "hardware") ComposeMaskingOptionsActivity.textModifierApplier = null ComposeMaskingOptionsActivity.containerModifierApplier = null } @@ -55,6 +58,7 @@ class ComposeMaskingOptionsTest { @Test fun `when maskAllText is set all Text nodes are masked`() { val activity = buildActivity(ComposeMaskingOptionsActivity::class.java).setup() + shadowOf(Looper.getMainLooper()).idle() val options = SentryOptions().apply { sessionReplay.maskAllText = true @@ -70,6 +74,7 @@ class ComposeMaskingOptionsTest { @Test fun `when maskAllText is set to false all Text nodes are unmasked`() { val activity = buildActivity(ComposeMaskingOptionsActivity::class.java).setup() + shadowOf(Looper.getMainLooper()).idle() val options = SentryOptions().apply { sessionReplay.maskAllText = false @@ -83,6 +88,7 @@ class ComposeMaskingOptionsTest { @Test fun `when maskAllImages is set all Image nodes are masked`() { val activity = buildActivity(ComposeMaskingOptionsActivity::class.java).setup() + shadowOf(Looper.getMainLooper()).idle() val options = SentryOptions().apply { sessionReplay.maskAllImages = true @@ -96,6 +102,7 @@ class ComposeMaskingOptionsTest { @Test fun `when maskAllImages is set to false all Image nodes are unmasked`() { val activity = buildActivity(ComposeMaskingOptionsActivity::class.java).setup() + shadowOf(Looper.getMainLooper()).idle() val options = SentryOptions().apply { sessionReplay.maskAllImages = false @@ -110,6 +117,7 @@ class ComposeMaskingOptionsTest { fun `when sentry-mask modifier is set masks the node`() { ComposeMaskingOptionsActivity.textModifierApplier = { Modifier.sentryReplayMask() } val activity = buildActivity(ComposeMaskingOptionsActivity::class.java).setup() + shadowOf(Looper.getMainLooper()).idle() val options = SentryOptions().apply { sessionReplay.maskAllText = false @@ -130,6 +138,7 @@ class ComposeMaskingOptionsTest { fun `when sentry-unmask modifier is set unmasks the node`() { ComposeMaskingOptionsActivity.textModifierApplier = { Modifier.sentryReplayUnmask() } val activity = buildActivity(ComposeMaskingOptionsActivity::class.java).setup() + shadowOf(Looper.getMainLooper()).idle() val options = SentryOptions().apply { sessionReplay.maskAllText = true @@ -139,9 +148,9 @@ class ComposeMaskingOptionsTest { assertEquals(4, textNodes.size) // [TextField, Text, Button, Activity Title] textNodes.forEach { if ((it.layout as? ComposeTextLayout)?.layout?.layoutInput?.text?.text == "Make Request") { - assertFalse(it.shouldMask) + assertFalse(it.shouldMask, "Node with text ${(it.layout as? ComposeTextLayout)?.layout?.layoutInput?.text?.text} should not be masked") } else { - assertTrue(it.shouldMask) + assertTrue(it.shouldMask, "Node with text ${(it.layout as? ComposeTextLayout)?.layout?.layoutInput?.text?.text} should be masked") } } } @@ -150,6 +159,7 @@ class ComposeMaskingOptionsTest { fun `when view is not visible, does not mask the view`() { ComposeMaskingOptionsActivity.textModifierApplier = { Modifier.semantics { invisibleToUser() } } val activity = buildActivity(ComposeMaskingOptionsActivity::class.java).setup() + shadowOf(Looper.getMainLooper()).idle() val options = SentryOptions().apply { sessionReplay.maskAllText = true @@ -169,6 +179,7 @@ class ComposeMaskingOptionsTest { fun `when a container view is unmasked its children are not unmasked`() { ComposeMaskingOptionsActivity.containerModifierApplier = { Modifier.sentryReplayUnmask() } val activity = buildActivity(ComposeMaskingOptionsActivity::class.java).setup() + shadowOf(Looper.getMainLooper()).idle() val options = SentryOptions() diff --git a/sentry-android-replay/src/test/java/io/sentry/android/replay/viewhierarchy/MaskingOptionsTest.kt b/sentry-android-replay/src/test/java/io/sentry/android/replay/viewhierarchy/MaskingOptionsTest.kt index 6620392bde..e03a52b527 100644 --- a/sentry-android-replay/src/test/java/io/sentry/android/replay/viewhierarchy/MaskingOptionsTest.kt +++ b/sentry-android-replay/src/test/java/io/sentry/android/replay/viewhierarchy/MaskingOptionsTest.kt @@ -6,6 +6,7 @@ import android.graphics.Canvas import android.graphics.Color import android.graphics.drawable.Drawable import android.os.Bundle +import android.os.Looper import android.view.View import android.widget.ImageView import android.widget.LinearLayout @@ -22,6 +23,7 @@ import io.sentry.android.replay.viewhierarchy.ViewHierarchyNode.ImageViewHierarc import io.sentry.android.replay.viewhierarchy.ViewHierarchyNode.TextViewHierarchyNode import org.junit.runner.RunWith import org.robolectric.Robolectric.buildActivity +import org.robolectric.Shadows.shadowOf import org.robolectric.annotation.Config import kotlin.test.BeforeTest import kotlin.test.Test @@ -35,11 +37,13 @@ class MaskingOptionsTest { @BeforeTest fun setup() { System.setProperty("robolectric.areWindowsMarkedVisible", "true") + System.setProperty("robolectric.pixelCopyRenderMode", "hardware") } @Test fun `when maskAllText is set all TextView nodes are masked`() { buildActivity(MaskingOptionsActivity::class.java).setup() + shadowOf(Looper.getMainLooper()).idle() val options = SentryOptions().apply { sessionReplay.maskAllText = true @@ -58,6 +62,7 @@ class MaskingOptionsTest { @Test fun `when maskAllText is set to false all TextView nodes are unmasked`() { buildActivity(MaskingOptionsActivity::class.java).setup() + shadowOf(Looper.getMainLooper()).idle() val options = SentryOptions().apply { sessionReplay.maskAllText = false @@ -76,6 +81,7 @@ class MaskingOptionsTest { @Test fun `when maskAllImages is set all ImageView nodes are masked`() { buildActivity(MaskingOptionsActivity::class.java).setup() + shadowOf(Looper.getMainLooper()).idle() val options = SentryOptions().apply { sessionReplay.maskAllImages = true @@ -90,6 +96,7 @@ class MaskingOptionsTest { @Test fun `when maskAllImages is set to false all ImageView nodes are unmasked`() { buildActivity(MaskingOptionsActivity::class.java).setup() + shadowOf(Looper.getMainLooper()).idle() val options = SentryOptions().apply { sessionReplay.maskAllImages = false @@ -104,6 +111,7 @@ class MaskingOptionsTest { @Test fun `when sentry-mask tag is set mask the view`() { buildActivity(MaskingOptionsActivity::class.java).setup() + shadowOf(Looper.getMainLooper()).idle() val options = SentryOptions().apply { sessionReplay.maskAllText = false @@ -118,6 +126,7 @@ class MaskingOptionsTest { @Test fun `when sentry-unmask tag is set unmasks the view`() { buildActivity(MaskingOptionsActivity::class.java).setup() + shadowOf(Looper.getMainLooper()).idle() val options = SentryOptions().apply { sessionReplay.maskAllText = true @@ -132,6 +141,7 @@ class MaskingOptionsTest { @Test fun `when sentry-privacy tag is set to mask masks the view`() { buildActivity(MaskingOptionsActivity::class.java).setup() + shadowOf(Looper.getMainLooper()).idle() val options = SentryOptions().apply { sessionReplay.maskAllText = false @@ -146,6 +156,7 @@ class MaskingOptionsTest { @Test fun `when sentry-privacy tag is set to unmask unmasks the view`() { buildActivity(MaskingOptionsActivity::class.java).setup() + shadowOf(Looper.getMainLooper()).idle() val options = SentryOptions().apply { sessionReplay.maskAllText = true @@ -160,6 +171,7 @@ class MaskingOptionsTest { @Test fun `when view is not visible, does not mask the view`() { buildActivity(MaskingOptionsActivity::class.java).setup() + shadowOf(Looper.getMainLooper()).idle() val options = SentryOptions().apply { sessionReplay.maskAllText = true @@ -174,6 +186,7 @@ class MaskingOptionsTest { @Test fun `when added to mask list masks custom view`() { buildActivity(MaskingOptionsActivity::class.java).setup() + shadowOf(Looper.getMainLooper()).idle() val options = SentryOptions().apply { sessionReplay.maskViewClasses.add(CustomView::class.java.canonicalName) @@ -187,6 +200,7 @@ class MaskingOptionsTest { @Test fun `when subclass is added to ignored classes ignores all instances of that class`() { buildActivity(MaskingOptionsActivity::class.java).setup() + shadowOf(Looper.getMainLooper()).idle() val options = SentryOptions().apply { sessionReplay.maskAllText = true // all TextView subclasses @@ -203,6 +217,7 @@ class MaskingOptionsTest { @Test fun `when a container view is ignored its children are not ignored`() { buildActivity(MaskingOptionsActivity::class.java).setup() + shadowOf(Looper.getMainLooper()).idle() val options = SentryOptions().apply { sessionReplay.unmaskViewClasses.add(LinearLayout::class.java.canonicalName) diff --git a/sentry-android-sqlite/build.gradle.kts b/sentry-android-sqlite/build.gradle.kts index 6de1b1ab30..fe983f1dd8 100644 --- a/sentry-android-sqlite/build.gradle.kts +++ b/sentry-android-sqlite/build.gradle.kts @@ -15,7 +15,6 @@ android { namespace = "io.sentry.android.sqlite" defaultConfig { - targetSdk = Config.Android.targetSdkVersion minSdk = Config.Android.minSdkVersion // for AGP 4.1 @@ -50,10 +49,12 @@ android { checkReleaseBuilds = false } - variantFilter { - if (Config.Android.shouldSkipDebugVariant(buildType.name)) { - ignore = true - } + buildFeatures { + buildConfig = true + } + + androidComponents.beforeVariants { + it.enable = !Config.Android.shouldSkipDebugVariant(it.buildType) } } diff --git a/sentry-android-sqlite/src/main/java/io/sentry/android/sqlite/SentrySupportSQLiteOpenHelper.kt b/sentry-android-sqlite/src/main/java/io/sentry/android/sqlite/SentrySupportSQLiteOpenHelper.kt index 08777b7fb5..f22b90d8df 100644 --- a/sentry-android-sqlite/src/main/java/io/sentry/android/sqlite/SentrySupportSQLiteOpenHelper.kt +++ b/sentry-android-sqlite/src/main/java/io/sentry/android/sqlite/SentrySupportSQLiteOpenHelper.kt @@ -30,7 +30,7 @@ import androidx.sqlite.db.SupportSQLiteOpenHelper * * @param delegate The [SupportSQLiteOpenHelper] instance to delegate calls to. */ -class SentrySupportSQLiteOpenHelper private constructor( +public class SentrySupportSQLiteOpenHelper private constructor( private val delegate: SupportSQLiteOpenHelper ) : SupportSQLiteOpenHelper by delegate { @@ -50,11 +50,11 @@ class SentrySupportSQLiteOpenHelper private constructor( override val readableDatabase: SupportSQLiteDatabase get() = sentryReadableDatabase - companion object { + public companion object { // @JvmStatic is needed to let this method be accessed by our gradle plugin @JvmStatic - fun create(delegate: SupportSQLiteOpenHelper): SupportSQLiteOpenHelper { + public fun create(delegate: SupportSQLiteOpenHelper): SupportSQLiteOpenHelper { return if (delegate is SentrySupportSQLiteOpenHelper) { delegate } else { diff --git a/sentry-android-sqlite/src/test/java/io/sentry/android/sqlite/SentrySupportSQLiteOpenHelperTest.kt b/sentry-android-sqlite/src/test/java/io/sentry/android/sqlite/SentrySupportSQLiteOpenHelperTest.kt index 8160d49fae..6add02165d 100644 --- a/sentry-android-sqlite/src/test/java/io/sentry/android/sqlite/SentrySupportSQLiteOpenHelperTest.kt +++ b/sentry-android-sqlite/src/test/java/io/sentry/android/sqlite/SentrySupportSQLiteOpenHelperTest.kt @@ -53,7 +53,7 @@ class SentrySupportSQLiteOpenHelperTest { @Test fun `create returns a SentrySupportSQLiteOpenHelper wrapper`() { val openHelper: SupportSQLiteOpenHelper = SentrySupportSQLiteOpenHelper.Companion.create(fixture.mockOpenHelper) - assertIs(openHelper) + assertIs(openHelper.writableDatabase) assertNotEquals(fixture.mockOpenHelper, openHelper) } diff --git a/sentry-android-timber/build.gradle.kts b/sentry-android-timber/build.gradle.kts index a2a999da4a..8f8afafd79 100644 --- a/sentry-android-timber/build.gradle.kts +++ b/sentry-android-timber/build.gradle.kts @@ -15,7 +15,6 @@ android { namespace = "io.sentry.android.timber" defaultConfig { - targetSdk = Config.Android.targetSdkVersion minSdk = Config.Android.minSdkVersion testInstrumentationRunner = Config.TestLibs.androidJUnitRunner @@ -53,10 +52,12 @@ android { checkReleaseBuilds = false } - variantFilter { - if (Config.Android.shouldSkipDebugVariant(buildType.name)) { - ignore = true - } + buildFeatures { + buildConfig = true + } + + androidComponents.beforeVariants { + it.enable = !Config.Android.shouldSkipDebugVariant(it.buildType) } } diff --git a/sentry-android-timber/src/main/java/io/sentry/android/timber/SentryTimberIntegration.kt b/sentry-android-timber/src/main/java/io/sentry/android/timber/SentryTimberIntegration.kt index b0b756fa26..a0d9f4dad4 100644 --- a/sentry-android-timber/src/main/java/io/sentry/android/timber/SentryTimberIntegration.kt +++ b/sentry-android-timber/src/main/java/io/sentry/android/timber/SentryTimberIntegration.kt @@ -14,9 +14,9 @@ import java.io.Closeable /** * Sentry integration for Timber. */ -class SentryTimberIntegration( - val minEventLevel: SentryLevel = SentryLevel.ERROR, - val minBreadcrumbLevel: SentryLevel = SentryLevel.INFO +public class SentryTimberIntegration( + public val minEventLevel: SentryLevel = SentryLevel.ERROR, + public val minBreadcrumbLevel: SentryLevel = SentryLevel.INFO ) : Integration, Closeable { private lateinit var tree: SentryTimberTree private lateinit var logger: ILogger diff --git a/sentry-android-timber/src/main/java/io/sentry/android/timber/SentryTimberTree.kt b/sentry-android-timber/src/main/java/io/sentry/android/timber/SentryTimberTree.kt index dddab75133..dda29c61d8 100644 --- a/sentry-android-timber/src/main/java/io/sentry/android/timber/SentryTimberTree.kt +++ b/sentry-android-timber/src/main/java/io/sentry/android/timber/SentryTimberTree.kt @@ -12,7 +12,7 @@ import timber.log.Timber * Sentry Timber tree which is responsible to capture events via Timber */ @Suppress("TooManyFunctions") // we have to override all methods to be able to tweak logging -class SentryTimberTree( +public class SentryTimberTree( private val scopes: IScopes, private val minEventLevel: SentryLevel, private val minBreadcrumbLevel: SentryLevel diff --git a/sentry-android-timber/src/test/java/io/sentry/android/timber/SentryTimberTreeTest.kt b/sentry-android-timber/src/test/java/io/sentry/android/timber/SentryTimberTreeTest.kt index 2ab7ff64db..30afeb928d 100644 --- a/sentry-android-timber/src/test/java/io/sentry/android/timber/SentryTimberTreeTest.kt +++ b/sentry-android-timber/src/test/java/io/sentry/android/timber/SentryTimberTreeTest.kt @@ -3,7 +3,6 @@ package io.sentry.android.timber import io.sentry.Breadcrumb import io.sentry.IScopes import io.sentry.SentryLevel -import io.sentry.getExc import org.mockito.kotlin.any import org.mockito.kotlin.check import org.mockito.kotlin.mock @@ -130,7 +129,7 @@ class SentryTimberTreeTest { sut.e(throwable) verify(fixture.scopes).captureEvent( check { - assertEquals(throwable, it.getExc()) + assertEquals(throwable, it.throwable) } ) } @@ -141,7 +140,7 @@ class SentryTimberTreeTest { sut.e("message") verify(fixture.scopes).captureEvent( check { - assertNull(it.getExc()) + assertNull(it.throwable) } ) } diff --git a/sentry-android-timber/src/test/java/io/sentry/core/SentryEventKtx.kt b/sentry-android-timber/src/test/java/io/sentry/core/SentryEventKtx.kt deleted file mode 100644 index 95d342f3b9..0000000000 --- a/sentry-android-timber/src/test/java/io/sentry/core/SentryEventKtx.kt +++ /dev/null @@ -1,8 +0,0 @@ -package io.sentry - -/** - * package-private hack. - */ -fun SentryEvent.getExc(): Throwable? { - return this.throwable -} diff --git a/sentry-android/build.gradle.kts b/sentry-android/build.gradle.kts index 49f7e75006..43d07bd44d 100644 --- a/sentry-android/build.gradle.kts +++ b/sentry-android/build.gradle.kts @@ -9,7 +9,6 @@ android { namespace = "io.sentry.android" defaultConfig { - targetSdk = Config.Android.targetSdkVersion minSdk = Config.Android.minSdkVersion } @@ -25,10 +24,8 @@ android { } } - variantFilter { - if (Config.Android.shouldSkipDebugVariant(buildType.name)) { - ignore = true - } + androidComponents.beforeVariants { + it.enable = !Config.Android.shouldSkipDebugVariant(it.buildType) } } diff --git a/sentry-apache-http-client-5/build.gradle.kts b/sentry-apache-http-client-5/build.gradle.kts index a5d2dc358d..324569c63d 100644 --- a/sentry-apache-http-client-5/build.gradle.kts +++ b/sentry-apache-http-client-5/build.gradle.kts @@ -10,11 +10,6 @@ plugins { id(Config.QualityPlugins.gradleVersions) } -configure { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 -} - tasks.withType().configureEach { kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString() kotlinOptions.languageVersion = Config.kotlinCompatibleLanguageVersion diff --git a/sentry-apollo-3/build.gradle.kts b/sentry-apollo-3/build.gradle.kts index 22583203ed..b3d2f7289e 100644 --- a/sentry-apollo-3/build.gradle.kts +++ b/sentry-apollo-3/build.gradle.kts @@ -10,11 +10,6 @@ plugins { id(Config.BuildPlugins.buildConfig) version Config.BuildPlugins.buildConfigVersion } -configure { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 -} - tasks.withType().configureEach { kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString() kotlinOptions.languageVersion = Config.kotlinCompatibleLanguageVersion diff --git a/sentry-apollo-3/src/main/java/io/sentry/apollo3/SentryApollo3HttpInterceptor.kt b/sentry-apollo-3/src/main/java/io/sentry/apollo3/SentryApollo3HttpInterceptor.kt index abfa41e5e1..ea10503f2b 100644 --- a/sentry-apollo-3/src/main/java/io/sentry/apollo3/SentryApollo3HttpInterceptor.kt +++ b/sentry-apollo-3/src/main/java/io/sentry/apollo3/SentryApollo3HttpInterceptor.kt @@ -37,7 +37,6 @@ import io.sentry.util.UrlUtils import io.sentry.vendor.Base64 import okio.Buffer import org.jetbrains.annotations.ApiStatus -import java.util.Locale private const val TRACE_ORIGIN = "auto.graphql.apollo3" @@ -177,7 +176,7 @@ class SentryApollo3HttpInterceptor @JvmOverloads constructor( variables?.let { setData("variables", it) } - setData(HTTP_METHOD_KEY, method.toUpperCase(Locale.ROOT)) + setData(HTTP_METHOD_KEY, method.uppercase()) } } diff --git a/sentry-apollo/build.gradle.kts b/sentry-apollo/build.gradle.kts index d05a31856b..3568254e81 100644 --- a/sentry-apollo/build.gradle.kts +++ b/sentry-apollo/build.gradle.kts @@ -10,11 +10,6 @@ plugins { id(Config.BuildPlugins.buildConfig) version Config.BuildPlugins.buildConfigVersion } -configure { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 -} - tasks.withType().configureEach { kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString() kotlinOptions.languageVersion = Config.kotlinCompatibleLanguageVersion diff --git a/sentry-apollo/src/main/java/io/sentry/apollo/SentryApolloInterceptor.kt b/sentry-apollo/src/main/java/io/sentry/apollo/SentryApolloInterceptor.kt index a24507ce51..c65f57bcc8 100644 --- a/sentry-apollo/src/main/java/io/sentry/apollo/SentryApolloInterceptor.kt +++ b/sentry-apollo/src/main/java/io/sentry/apollo/SentryApolloInterceptor.kt @@ -27,7 +27,6 @@ import io.sentry.TypeCheckHint.APOLLO_RESPONSE import io.sentry.util.IntegrationUtils.addIntegrationToSdkVersion import io.sentry.util.SpanUtils import io.sentry.util.TracingUtils -import java.util.Locale import java.util.concurrent.Executor private const val TRACE_ORIGIN = "auto.graphql.apollo" @@ -77,7 +76,7 @@ class SentryApolloInterceptor( response.httpResponse.map { it.request().method() }.orNull()?.let { span.setData( SpanDataConvention.HTTP_METHOD_KEY, - it.toUpperCase(Locale.ROOT) + it.uppercase() ) } diff --git a/sentry-bom/build.gradle.kts b/sentry-bom/build.gradle.kts index 9f0a0ff117..8af147e82d 100644 --- a/sentry-bom/build.gradle.kts +++ b/sentry-bom/build.gradle.kts @@ -27,11 +27,3 @@ dependencies { } } } - -publishing { - publications { - create("maven") { - from(components["javaPlatform"]) - } - } -} diff --git a/sentry-compose-helper/build.gradle.kts b/sentry-compose-helper/build.gradle.kts index 4bdb1b1f99..be5637280e 100644 --- a/sentry-compose-helper/build.gradle.kts +++ b/sentry-compose-helper/build.gradle.kts @@ -20,6 +20,8 @@ kotlin { compileOnly(compose.runtime) compileOnly(compose.ui) + + compileOnly(Config.Libs.androidxAnnotation) } } val jvmTest by getting { @@ -27,6 +29,7 @@ kotlin { implementation(compose.runtime) implementation(compose.ui) + compileOnly(Config.Libs.androidxAnnotation) implementation(Config.TestLibs.kotlinTestJunit) implementation(Config.TestLibs.mockitoKotlin) implementation(Config.TestLibs.mockitoInline) @@ -50,7 +53,7 @@ val embeddedJar by configurations.creating { } artifacts { - add("embeddedJar", File("$buildDir/libs/sentry-compose-helper-jvm-$version.jar")) + add("embeddedJar", project.layout.buildDirectory.file("libs/sentry-compose-helper-jvm-$version.jar").get().asFile) } buildConfig { diff --git a/sentry-compose/build.gradle.kts b/sentry-compose/build.gradle.kts index 0253b97268..597a619154 100644 --- a/sentry-compose/build.gradle.kts +++ b/sentry-compose/build.gradle.kts @@ -70,7 +70,6 @@ android { namespace = "io.sentry.compose" defaultConfig { - targetSdk = Config.Android.targetSdkVersion minSdk = Config.Android.minSdkVersion // for AGP 4.1 @@ -104,10 +103,12 @@ android { checkReleaseBuilds = false } - variantFilter { - if (Config.Android.shouldSkipDebugVariant(buildType.name)) { - ignore = true - } + buildFeatures { + buildConfig = true + } + + androidComponents.beforeVariants { + it.enable = !Config.Android.shouldSkipDebugVariant(it.buildType) } } diff --git a/sentry-graphql-22/build.gradle.kts b/sentry-graphql-22/build.gradle.kts index 5463456f8c..4cae7bc7b6 100644 --- a/sentry-graphql-22/build.gradle.kts +++ b/sentry-graphql-22/build.gradle.kts @@ -10,11 +10,6 @@ plugins { id(Config.BuildPlugins.buildConfig) version Config.BuildPlugins.buildConfigVersion } -configure { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 -} - tasks.withType().configureEach { kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString() kotlinOptions.languageVersion = Config.kotlinCompatibleLanguageVersion diff --git a/sentry-graphql-core/build.gradle.kts b/sentry-graphql-core/build.gradle.kts index ed1c197acd..20c0602489 100644 --- a/sentry-graphql-core/build.gradle.kts +++ b/sentry-graphql-core/build.gradle.kts @@ -10,11 +10,6 @@ plugins { id(Config.BuildPlugins.buildConfig) version Config.BuildPlugins.buildConfigVersion } -configure { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 -} - tasks.withType().configureEach { kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString() kotlinOptions.languageVersion = Config.kotlinCompatibleLanguageVersion diff --git a/sentry-graphql/build.gradle.kts b/sentry-graphql/build.gradle.kts index f0de17f288..2fec033ed7 100644 --- a/sentry-graphql/build.gradle.kts +++ b/sentry-graphql/build.gradle.kts @@ -10,11 +10,6 @@ plugins { id(Config.BuildPlugins.buildConfig) version Config.BuildPlugins.buildConfigVersion } -configure { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 -} - tasks.withType().configureEach { kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString() kotlinOptions.languageVersion = Config.kotlinCompatibleLanguageVersion diff --git a/sentry-jdbc/build.gradle.kts b/sentry-jdbc/build.gradle.kts index 239bd46cab..eb4b53a962 100644 --- a/sentry-jdbc/build.gradle.kts +++ b/sentry-jdbc/build.gradle.kts @@ -10,11 +10,6 @@ plugins { id(Config.BuildPlugins.buildConfig) version Config.BuildPlugins.buildConfigVersion } -configure { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 -} - tasks.withType().configureEach { kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString() } diff --git a/sentry-jul/build.gradle.kts b/sentry-jul/build.gradle.kts index 009561801b..f59f14bbf4 100644 --- a/sentry-jul/build.gradle.kts +++ b/sentry-jul/build.gradle.kts @@ -10,11 +10,6 @@ plugins { id(Config.BuildPlugins.buildConfig) version Config.BuildPlugins.buildConfigVersion } -configure { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 -} - tasks.withType().configureEach { kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString() } diff --git a/sentry-kotlin-extensions/api/sentry-kotlin-extensions.api b/sentry-kotlin-extensions/api/sentry-kotlin-extensions.api index 7e3be67279..0555383c1b 100644 --- a/sentry-kotlin-extensions/api/sentry-kotlin-extensions.api +++ b/sentry-kotlin-extensions/api/sentry-kotlin-extensions.api @@ -3,11 +3,7 @@ public final class io/sentry/kotlin/SentryContext : kotlin/coroutines/AbstractCo public fun (Lio/sentry/IScopes;)V public synthetic fun (Lio/sentry/IScopes;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public fun copyForChild ()Lkotlinx/coroutines/CopyableThreadContextElement; - public fun fold (Ljava/lang/Object;Lkotlin/jvm/functions/Function2;)Ljava/lang/Object; - public fun get (Lkotlin/coroutines/CoroutineContext$Key;)Lkotlin/coroutines/CoroutineContext$Element; public fun mergeForChild (Lkotlin/coroutines/CoroutineContext$Element;)Lkotlin/coroutines/CoroutineContext; - public fun minusKey (Lkotlin/coroutines/CoroutineContext$Key;)Lkotlin/coroutines/CoroutineContext; - public fun plus (Lkotlin/coroutines/CoroutineContext;)Lkotlin/coroutines/CoroutineContext; public fun restoreThreadContext (Lkotlin/coroutines/CoroutineContext;Lio/sentry/IScopes;)V public synthetic fun restoreThreadContext (Lkotlin/coroutines/CoroutineContext;Ljava/lang/Object;)V public fun updateThreadContext (Lkotlin/coroutines/CoroutineContext;)Lio/sentry/IScopes; diff --git a/sentry-kotlin-extensions/build.gradle.kts b/sentry-kotlin-extensions/build.gradle.kts index c920b6cf33..9db72d89ca 100644 --- a/sentry-kotlin-extensions/build.gradle.kts +++ b/sentry-kotlin-extensions/build.gradle.kts @@ -10,11 +10,6 @@ plugins { id(Config.QualityPlugins.detektPlugin) } -configure { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 -} - tasks.withType().configureEach { kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString() kotlinOptions.languageVersion = Config.kotlinCompatibleLanguageVersion diff --git a/sentry-log4j2/build.gradle.kts b/sentry-log4j2/build.gradle.kts index 933b28bfc9..1be24487c3 100644 --- a/sentry-log4j2/build.gradle.kts +++ b/sentry-log4j2/build.gradle.kts @@ -10,11 +10,6 @@ plugins { id(Config.BuildPlugins.buildConfig) version Config.BuildPlugins.buildConfigVersion } -configure { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 -} - tasks.withType().configureEach { kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString() } diff --git a/sentry-logback/build.gradle.kts b/sentry-logback/build.gradle.kts index 255e35022f..48c81a452e 100644 --- a/sentry-logback/build.gradle.kts +++ b/sentry-logback/build.gradle.kts @@ -10,11 +10,6 @@ plugins { id(Config.BuildPlugins.buildConfig) version Config.BuildPlugins.buildConfigVersion } -configure { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 -} - tasks.withType().configureEach { kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString() } diff --git a/sentry-okhttp/build.gradle.kts b/sentry-okhttp/build.gradle.kts index a30e2d0594..71e09d8778 100644 --- a/sentry-okhttp/build.gradle.kts +++ b/sentry-okhttp/build.gradle.kts @@ -11,11 +11,6 @@ plugins { id(Config.BuildPlugins.buildConfig) version Config.BuildPlugins.buildConfigVersion } -configure { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 -} - tasks.withType().configureEach { kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString() } diff --git a/sentry-okhttp/src/main/java/io/sentry/okhttp/SentryOkHttpEvent.kt b/sentry-okhttp/src/main/java/io/sentry/okhttp/SentryOkHttpEvent.kt index 137af27913..5bc488d074 100644 --- a/sentry-okhttp/src/main/java/io/sentry/okhttp/SentryOkHttpEvent.kt +++ b/sentry-okhttp/src/main/java/io/sentry/okhttp/SentryOkHttpEvent.kt @@ -12,7 +12,6 @@ import io.sentry.util.Platform import io.sentry.util.UrlUtils import okhttp3.Request import okhttp3.Response -import java.util.Locale import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicBoolean @@ -56,7 +55,7 @@ internal class SentryOkHttpEvent(private val scopes: IScopes, private val reques callSpan?.setData("url", url) callSpan?.setData("host", host) callSpan?.setData("path", encodedPath) - callSpan?.setData(SpanDataConvention.HTTP_METHOD_KEY, method.uppercase(Locale.ROOT)) + callSpan?.setData(SpanDataConvention.HTTP_METHOD_KEY, method.uppercase()) } /** diff --git a/sentry-openfeign/build.gradle.kts b/sentry-openfeign/build.gradle.kts index 0679c42f41..305b020a3b 100644 --- a/sentry-openfeign/build.gradle.kts +++ b/sentry-openfeign/build.gradle.kts @@ -10,11 +10,6 @@ plugins { id(Config.BuildPlugins.buildConfig) version Config.BuildPlugins.buildConfigVersion } -configure { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 -} - tasks.withType().configureEach { kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString() } diff --git a/sentry-opentelemetry/sentry-opentelemetry-agent/build.gradle.kts b/sentry-opentelemetry/sentry-opentelemetry-agent/build.gradle.kts index 2d4ea259c6..88569f69b6 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-agent/build.gradle.kts +++ b/sentry-opentelemetry/sentry-opentelemetry-agent/build.gradle.kts @@ -5,11 +5,6 @@ plugins { id("com.github.johnrengelman.shadow") version "7.1.2" } -configure { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 -} - fun relocatePackages(shadowJar: ShadowJar) { // rewrite dependencies calling Logger.getLogger shadowJar.relocate("java.util.logging.Logger", "io.opentelemetry.javaagent.bootstrap.PatchLogger") @@ -123,7 +118,7 @@ tasks { dependsOn(findByName("relocateJavaagentLibs")) with(isolateClasses(findByName("relocateJavaagentLibs")!!.outputs.files)) - into("$buildDir/isolated/javaagentLibs") + into(project.layout.buildDirectory.file("isolated/javaagentLibs").get().asFile) } // 3. the relocated and isolated javaagent libs are merged together with the bootstrap libs (which undergo relocation diff --git a/sentry-opentelemetry/sentry-opentelemetry-agentcustomization/build.gradle.kts b/sentry-opentelemetry/sentry-opentelemetry-agentcustomization/build.gradle.kts index 77ef8f56a8..97452cab95 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-agentcustomization/build.gradle.kts +++ b/sentry-opentelemetry/sentry-opentelemetry-agentcustomization/build.gradle.kts @@ -9,11 +9,6 @@ plugins { id(Config.QualityPlugins.gradleVersions) } -configure { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 -} - tasks.withType().configureEach { kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString() } diff --git a/sentry-opentelemetry/sentry-opentelemetry-agentless-spring/build.gradle.kts b/sentry-opentelemetry/sentry-opentelemetry-agentless-spring/build.gradle.kts index a79bd6c94f..1465574c65 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-agentless-spring/build.gradle.kts +++ b/sentry-opentelemetry/sentry-opentelemetry-agentless-spring/build.gradle.kts @@ -2,11 +2,6 @@ plugins { `java-library` } -configure { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 -} - dependencies { api(projects.sentry) implementation(projects.sentryOpentelemetry.sentryOpentelemetryBootstrap) diff --git a/sentry-opentelemetry/sentry-opentelemetry-agentless/build.gradle.kts b/sentry-opentelemetry/sentry-opentelemetry-agentless/build.gradle.kts index 26a404e49c..efad0063a1 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-agentless/build.gradle.kts +++ b/sentry-opentelemetry/sentry-opentelemetry-agentless/build.gradle.kts @@ -2,11 +2,6 @@ plugins { `java-library` } -configure { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 -} - dependencies { api(projects.sentry) implementation(projects.sentryOpentelemetry.sentryOpentelemetryBootstrap) diff --git a/sentry-opentelemetry/sentry-opentelemetry-bootstrap/build.gradle.kts b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/build.gradle.kts index 6eb8e6d6f1..447b0823f5 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-bootstrap/build.gradle.kts +++ b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/build.gradle.kts @@ -9,11 +9,6 @@ plugins { id(Config.QualityPlugins.gradleVersions) } -configure { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 -} - tasks.withType().configureEach { kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString() } diff --git a/sentry-opentelemetry/sentry-opentelemetry-core/build.gradle.kts b/sentry-opentelemetry/sentry-opentelemetry-core/build.gradle.kts index e46ff2783a..de2143f01d 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-core/build.gradle.kts +++ b/sentry-opentelemetry/sentry-opentelemetry-core/build.gradle.kts @@ -9,11 +9,6 @@ plugins { id(Config.QualityPlugins.gradleVersions) } -configure { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 -} - tasks.withType().configureEach { kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString() } diff --git a/sentry-quartz/build.gradle.kts b/sentry-quartz/build.gradle.kts index 8731f6a40b..605ae7a3b7 100644 --- a/sentry-quartz/build.gradle.kts +++ b/sentry-quartz/build.gradle.kts @@ -10,11 +10,6 @@ plugins { id(Config.BuildPlugins.buildConfig) version Config.BuildPlugins.buildConfigVersion } -configure { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 -} - tasks.withType().configureEach { kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString() kotlinOptions.languageVersion = Config.kotlinCompatibleLanguageVersion diff --git a/sentry-samples/sentry-samples-android/build.gradle.kts b/sentry-samples/sentry-samples-android/build.gradle.kts index 88ee38e931..04772af8aa 100644 --- a/sentry-samples/sentry-samples-android/build.gradle.kts +++ b/sentry-samples/sentry-samples-android/build.gradle.kts @@ -28,11 +28,26 @@ android { } } + lint { + disable.addAll( + listOf( + "Typos", + "PluralsCandidate", + "MonochromeLauncherIcon", + "TextFields", + "ContentDescription", + "LabelFor", + "HardcodedText" + ) + ) + } + buildFeatures { // Determines whether to support View Binding. // Note that the viewBinding.enabled property is now deprecated. viewBinding = true compose = true + buildConfig = true prefab = true } @@ -90,10 +105,8 @@ android { jvmTarget = JavaVersion.VERSION_1_8.toString() } - variantFilter { - if (Config.Android.shouldSkipDebugVariant(buildType.name)) { - ignore = true - } + androidComponents.beforeVariants { + it.enable = !Config.Android.shouldSkipDebugVariant(it.buildType) } @Suppress("UnstableApiUsage") diff --git a/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml b/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml index 482787ec5a..7fa16daae5 100644 --- a/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml +++ b/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml @@ -9,7 +9,6 @@ - @@ -17,9 +16,12 @@ - + + { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 -} - tasks.withType().configureEach { kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString() kotlinOptions.languageVersion = Config.kotlinCompatibleLanguageVersion diff --git a/sentry-servlet/build.gradle.kts b/sentry-servlet/build.gradle.kts index 8d03ede152..593f346034 100644 --- a/sentry-servlet/build.gradle.kts +++ b/sentry-servlet/build.gradle.kts @@ -10,11 +10,6 @@ plugins { id(Config.BuildPlugins.buildConfig) version Config.BuildPlugins.buildConfigVersion } -configure { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 -} - tasks.withType().configureEach { kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString() kotlinOptions.languageVersion = Config.kotlinCompatibleLanguageVersion diff --git a/sentry-spring-boot-starter/build.gradle.kts b/sentry-spring-boot-starter/build.gradle.kts index fcbdd0d9a6..d68f2c9332 100644 --- a/sentry-spring-boot-starter/build.gradle.kts +++ b/sentry-spring-boot-starter/build.gradle.kts @@ -11,11 +11,6 @@ plugins { id(Config.BuildPlugins.springBoot) version Config.springBootVersion apply false } -configure { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 -} - tasks.withType().configureEach { kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString() kotlinOptions.languageVersion = Config.kotlinCompatibleLanguageVersion diff --git a/sentry-spring-boot/build.gradle.kts b/sentry-spring-boot/build.gradle.kts index f6a84d47f0..426cea26f6 100644 --- a/sentry-spring-boot/build.gradle.kts +++ b/sentry-spring-boot/build.gradle.kts @@ -12,11 +12,6 @@ plugins { id(Config.BuildPlugins.springBoot) version Config.springBootVersion apply false } -configure { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 -} - tasks.withType().configureEach { kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString() kotlinOptions.languageVersion = Config.kotlinCompatibleLanguageVersion diff --git a/sentry-spring/build.gradle.kts b/sentry-spring/build.gradle.kts index 8f7ed519d2..6873c1cada 100644 --- a/sentry-spring/build.gradle.kts +++ b/sentry-spring/build.gradle.kts @@ -13,11 +13,6 @@ plugins { id(Config.BuildPlugins.springBoot) version Config.springBootVersion apply false } -configure { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 -} - tasks.withType().configureEach { kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString() kotlinOptions.languageVersion = Config.kotlinCompatibleLanguageVersion diff --git a/sentry/build.gradle.kts b/sentry/build.gradle.kts index 08efc550d5..4498e3bc2f 100644 --- a/sentry/build.gradle.kts +++ b/sentry/build.gradle.kts @@ -10,11 +10,6 @@ plugins { id(Config.BuildPlugins.buildConfig) version Config.BuildPlugins.buildConfigVersion } -configure { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 -} - tasks.withType().configureEach { kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString() } From c7cf756426aa2ab447015b10418bb1777b255f6d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 5 Feb 2025 12:09:12 +0100 Subject: [PATCH 06/27] Bump gradle/actions from 8790d96bb8fdd8ae7edfb2eada090c650b257f27 to 6962c6c931ff9effc947259cc1b9c6edba90b9d3 (#4139) Bumps [gradle/actions](https://github.com/gradle/actions) from 8790d96bb8fdd8ae7edfb2eada090c650b257f27 to 6962c6c931ff9effc947259cc1b9c6edba90b9d3. - [Release notes](https://github.com/gradle/actions/releases) - [Commits](https://github.com/gradle/actions/compare/8790d96bb8fdd8ae7edfb2eada090c650b257f27...6962c6c931ff9effc947259cc1b9c6edba90b9d3) --- updated-dependencies: - dependency-name: gradle/actions dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/agp-matrix.yml | 2 +- .github/workflows/build.yml | 2 +- .github/workflows/codeql-analysis.yml | 2 +- .github/workflows/enforce-license-compliance.yml | 2 +- .github/workflows/generate-javadocs.yml | 2 +- .github/workflows/integration-tests-benchmarks.yml | 4 ++-- .github/workflows/integration-tests-ui-critical.yml | 2 +- .github/workflows/integration-tests-ui.yml | 2 +- .github/workflows/release-build.yml | 2 +- .github/workflows/system-tests-backend.yml | 2 +- 10 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/agp-matrix.yml b/.github/workflows/agp-matrix.yml index b5d15c41a6..b407635fdd 100644 --- a/.github/workflows/agp-matrix.yml +++ b/.github/workflows/agp-matrix.yml @@ -39,7 +39,7 @@ jobs: java-version: '17' - name: Setup Gradle - uses: gradle/actions/setup-gradle@8790d96bb8fdd8ae7edfb2eada090c650b257f27 # pin@v3 + uses: gradle/actions/setup-gradle@6962c6c931ff9effc947259cc1b9c6edba90b9d3 # pin@v3 with: gradle-home-cache-cleanup: true cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }} diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4be6d59d60..b83f9de0b1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -30,7 +30,7 @@ jobs: java-version: '17' - name: Setup Gradle - uses: gradle/actions/setup-gradle@8790d96bb8fdd8ae7edfb2eada090c650b257f27 # pin@v3 + uses: gradle/actions/setup-gradle@6962c6c931ff9effc947259cc1b9c6edba90b9d3 # pin@v3 with: gradle-home-cache-cleanup: true cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }} diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index c488d2dd39..0cd93ef5f7 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -34,7 +34,7 @@ jobs: java-version: '17' - name: Setup Gradle - uses: gradle/actions/setup-gradle@8790d96bb8fdd8ae7edfb2eada090c650b257f27 # pin@v3 + uses: gradle/actions/setup-gradle@6962c6c931ff9effc947259cc1b9c6edba90b9d3 # pin@v3 with: gradle-home-cache-cleanup: true cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }} diff --git a/.github/workflows/enforce-license-compliance.yml b/.github/workflows/enforce-license-compliance.yml index 0fe847cd55..f7e1aad059 100644 --- a/.github/workflows/enforce-license-compliance.yml +++ b/.github/workflows/enforce-license-compliance.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Setup Gradle - uses: gradle/actions/setup-gradle@8790d96bb8fdd8ae7edfb2eada090c650b257f27 # pin@v3 + uses: gradle/actions/setup-gradle@6962c6c931ff9effc947259cc1b9c6edba90b9d3 # pin@v3 with: gradle-home-cache-cleanup: true diff --git a/.github/workflows/generate-javadocs.yml b/.github/workflows/generate-javadocs.yml index dc0e2f5da2..286df36dfe 100644 --- a/.github/workflows/generate-javadocs.yml +++ b/.github/workflows/generate-javadocs.yml @@ -20,7 +20,7 @@ jobs: java-version: '17' - name: Setup Gradle - uses: gradle/actions/setup-gradle@8790d96bb8fdd8ae7edfb2eada090c650b257f27 # pin@v3 + uses: gradle/actions/setup-gradle@6962c6c931ff9effc947259cc1b9c6edba90b9d3 # pin@v3 with: gradle-home-cache-cleanup: true diff --git a/.github/workflows/integration-tests-benchmarks.yml b/.github/workflows/integration-tests-benchmarks.yml index 46dc783d72..8d0e0381d6 100644 --- a/.github/workflows/integration-tests-benchmarks.yml +++ b/.github/workflows/integration-tests-benchmarks.yml @@ -38,7 +38,7 @@ jobs: java-version: '17' - name: Setup Gradle - uses: gradle/actions/setup-gradle@8790d96bb8fdd8ae7edfb2eada090c650b257f27 # pin@v3 + uses: gradle/actions/setup-gradle@6962c6c931ff9effc947259cc1b9c6edba90b9d3 # pin@v3 with: gradle-home-cache-cleanup: true cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }} @@ -89,7 +89,7 @@ jobs: java-version: '17' - name: Setup Gradle - uses: gradle/actions/setup-gradle@8790d96bb8fdd8ae7edfb2eada090c650b257f27 # pin@v3 + uses: gradle/actions/setup-gradle@6962c6c931ff9effc947259cc1b9c6edba90b9d3 # pin@v3 with: gradle-home-cache-cleanup: true cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }} diff --git a/.github/workflows/integration-tests-ui-critical.yml b/.github/workflows/integration-tests-ui-critical.yml index 15b2129602..917d34732f 100644 --- a/.github/workflows/integration-tests-ui-critical.yml +++ b/.github/workflows/integration-tests-ui-critical.yml @@ -36,7 +36,7 @@ jobs: java-version: '17' - name: Setup Gradle - uses: gradle/actions/setup-gradle@8790d96bb8fdd8ae7edfb2eada090c650b257f27 # pin@v3 + uses: gradle/actions/setup-gradle@6962c6c931ff9effc947259cc1b9c6edba90b9d3 # pin@v3 with: gradle-home-cache-cleanup: true cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }} diff --git a/.github/workflows/integration-tests-ui.yml b/.github/workflows/integration-tests-ui.yml index d79753e83d..b50420533a 100644 --- a/.github/workflows/integration-tests-ui.yml +++ b/.github/workflows/integration-tests-ui.yml @@ -33,7 +33,7 @@ jobs: java-version: '17' - name: Setup Gradle - uses: gradle/actions/setup-gradle@8790d96bb8fdd8ae7edfb2eada090c650b257f27 # pin@v3 + uses: gradle/actions/setup-gradle@6962c6c931ff9effc947259cc1b9c6edba90b9d3 # pin@v3 with: gradle-home-cache-cleanup: true cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }} diff --git a/.github/workflows/release-build.yml b/.github/workflows/release-build.yml index 4de0e49e1a..cbf6997405 100644 --- a/.github/workflows/release-build.yml +++ b/.github/workflows/release-build.yml @@ -26,7 +26,7 @@ jobs: java-version: '17' - name: Setup Gradle - uses: gradle/actions/setup-gradle@8790d96bb8fdd8ae7edfb2eada090c650b257f27 # pin@v3 + uses: gradle/actions/setup-gradle@6962c6c931ff9effc947259cc1b9c6edba90b9d3 # pin@v3 with: gradle-home-cache-cleanup: true diff --git a/.github/workflows/system-tests-backend.yml b/.github/workflows/system-tests-backend.yml index e7c04a14d3..c5d364375e 100644 --- a/.github/workflows/system-tests-backend.yml +++ b/.github/workflows/system-tests-backend.yml @@ -56,7 +56,7 @@ jobs: java-version: '17' - name: Setup Gradle - uses: gradle/actions/setup-gradle@8790d96bb8fdd8ae7edfb2eada090c650b257f27 # pin@v3 + uses: gradle/actions/setup-gradle@6962c6c931ff9effc947259cc1b9c6edba90b9d3 # pin@v3 with: gradle-home-cache-cleanup: true From 20d8dd633a4ca33cf3e2a7800cffd6569d308c70 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 5 Feb 2025 12:39:10 +0100 Subject: [PATCH 07/27] chore(deps): update Gradle to v8.12.1 (#4106) Co-authored-by: GitHub --- CHANGELOG.md | 3 +++ gradle/wrapper/gradle-wrapper.jar | Bin 63721 -> 43504 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew | 7 +++++-- gradlew.bat | 22 ++++++++++++---------- 5 files changed, 21 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d778d82cd5..7ddeaf4968 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,9 @@ - Bump Native SDK from v0.7.19 to v0.7.20 ([#4128](https://github.com/getsentry/sentry-java/pull/4128)) - [changelog](https://github.com/getsentry/sentry-native/blob/master/CHANGELOG.md#0720) - [diff](https://github.com/getsentry/sentry-native/compare/v0.7.19...0.7.20) +- Bump Gradle from v8.9.0 to v8.12.1 ([#4106](https://github.com/getsentry/sentry-java/pull/4106)) + - [changelog](https://github.com/gradle/gradle/blob/master/CHANGELOG.md#v8121) + - [diff](https://github.com/gradle/gradle/compare/v8.9.0...v8.12.1) ## 8.1.0 diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 7f93135c49b765f8051ef9d0a6055ff8e46073d8..2c3521197d7c4586c843d1d3e9090525f1898cde 100644 GIT binary patch literal 43504 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 yu?e%!_ptzGB|8GrCfu%p?(_ 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 TZiJi*F`$tkWLn) literal 63721 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鯊%1Et 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⁢ zv5o9#E7!G0XmdzfsS{efPNi`1b44~SZ4Z8fuX!I}#8g+(wxzQwUT#Xb2(tbY1+EUhGKoT@KEU9Ktl>_0 z%bjDJg;#*gtJZv!-Zs`?^}v5eKmnbjqlvnSzE@_SP|LG_PJ6CYU+6zY6>92%E+ z=j@TZf-iW4(%U{lnYxQA;7Q!b;^brF8n0D>)`q5>|WDDXLrqYU_tKN2>=#@~OE7grMnNh?UOz-O~6 z6%rHy{#h9K0AT+lDC7q4{hw^|q6*Ry;;L%Q@)Ga}$60_q%D)rv(CtS$CQbpq9|y1e zRSrN4;$Jyl{m5bZw`$8TGvb}(LpY{-cQ)fcyJv7l3S52TLXVDsphtv&aPuDk1OzCA z4A^QtC(!11`IsNx_HnSy?>EKpHJWT^wmS~hc^p^zIIh@9f6U@I2 zC=Mve{j2^)mS#U$e{@Q?SO6%LDsXz@SY+=cK_QMmXBIU)j!$ajc-zLx3V60EXJ!qC zi<%2x8Q24YN+&8U@CIlN zrZkcT9yh%LrlGS9`G)KdP(@9Eo-AQz@8GEFWcb7U=a0H^ZVbLmz{+&M7W(nXJ4sN8 zJLR7eeK(K8`2-}j(T7JsO`L!+CvbueT%izanm-^A1Dn{`1Nw`9P?cq;7no+XfC`K(GO9?O^5zNIt4M+M8LM0=7Gz8UA@Z0N+lg+cX)NfazRu z5D)~HA^(u%w^cz+@2@_#S|u>GpB+j4KzQ^&Wcl9f z&hG#bCA(Yk0D&t&aJE^xME^&E-&xGHhXn%}psEIj641H+Nl-}boj;)Zt*t(4wZ5DN z@GXF$bL=&pBq-#vkTkh>7hl%K5|3 z{`Vn9b$iR-SoGENp}bn4;fR3>9sA%X2@1L3aE9yTra;Wb#_`xWwLSLdfu+PAu+o3| zGVnpzPr=ch{uuoHjtw7+_!L_2;knQ!DuDl0R`|%jr+}jFzXtrHIKc323?JO{l&;VF z*L1+}JU7%QJOg|5|Tc|D8fN zJORAg=_vsy{ak|o);@)Yh8Lkcg@$FG3k@ep36BRa^>~UmnRPziS>Z=`Jb2x*Q#`%A zU*i3&Vg?TluO@X0O;r2Jl6LKLUOVhSqg1*qOt^|8*c7 zo(298@+r$k_wQNGHv{|$tW(T8L+4_`FQ{kEW5Jgg{yf7ey4ss_(SNKfz(N9lx&a;< je(UuV8hP?p&}TPdm1I$XmG#(RzlD&B2izSj9sl%y5~4qc diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 09523c0e54..e18bc253b8 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.12.1-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index 1aa94a4269..f5feea6d6b 100755 --- a/gradlew +++ b/gradlew @@ -15,6 +15,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## # @@ -55,7 +57,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -84,7 +86,8 @@ done # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s +' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum diff --git a/gradlew.bat b/gradlew.bat index 93e3f59f13..9d21a21834 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -13,6 +13,8 @@ @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem @if "%DEBUG%"=="" @echo off @rem ########################################################################## @@ -43,11 +45,11 @@ set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -57,11 +59,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail From cdecb658151c317fe4f4a77e0e10a7bcf1fee383 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos <6349682+vaind@users.noreply.github.com> Date: Wed, 5 Feb 2025 13:04:22 +0100 Subject: [PATCH 08/27] ci: fix sentry-native updater script (#4140) --- scripts/update-sentry-native-ndk.sh | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/scripts/update-sentry-native-ndk.sh b/scripts/update-sentry-native-ndk.sh index 347e5df921..5407406853 100755 --- a/scripts/update-sentry-native-ndk.sh +++ b/scripts/update-sentry-native-ndk.sh @@ -6,9 +6,7 @@ GRADLE_NDK_FILEPATH=buildSrc/src/main/java/Config.kt case $1 in get-version) - version=$(perl -ne 'print "$1\n" if ( m/io\.sentry:sentry-native-ndk:([0-9.]+)+/ )' $GRADLE_NDK_FILEPATH) - - echo "v$version" + perl -ne 'print "$1\n" if ( m/io\.sentry:sentry-native-ndk:([0-9.]+)+/ )' $GRADLE_NDK_FILEPATH ;; get-repo) echo "https://github.com/getsentry/sentry-native.git" @@ -16,11 +14,6 @@ get-repo) set-version) version=$2 - # Remove leading "v" - if [[ "$version" == v* ]]; then - version="${version:1}" - fi - echo "Setting sentry-native-ndk version to '$version'" PATTERN="io\.sentry:sentry-native-ndk:([0-9.]+)+" From 1f149c3b180c9c790e082aaa14e2c056c05383e9 Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Wed, 5 Feb 2025 13:53:15 +0100 Subject: [PATCH 09/27] Bump AGP versions in matrix (#4141) * Bump AGP versions in matrix * try xlarge runner * Use macos-latest for codeql * Use macos-15 --- .github/workflows/agp-matrix.yml | 2 +- .github/workflows/codeql-analysis.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/agp-matrix.yml b/.github/workflows/agp-matrix.yml index b407635fdd..75253e7251 100644 --- a/.github/workflows/agp-matrix.yml +++ b/.github/workflows/agp-matrix.yml @@ -17,7 +17,7 @@ jobs: strategy: fail-fast: false matrix: - agp: [ '8.0.0','8.1.4','8.2.0','8.3.0-beta01' ] + agp: [ '8.7.0','8.8.0','8.9.0-beta01' ] integrations: [ true, false ] name: AGP Matrix Release - AGP ${{ matrix.agp }} - Integrations ${{ matrix.integrations }} diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 0cd93ef5f7..98cc67159a 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -16,7 +16,7 @@ concurrency: jobs: analyze: name: Analyze - runs-on: ubuntu-latest + runs-on: macos-15 env: GRADLE_ENCRYPTION_KEY: ${{ secrets.GRADLE_ENCRYPTION_KEY }} From 5202700f73ecb6f04d5086c1f1587139926dca09 Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Wed, 5 Feb 2025 21:01:02 +0100 Subject: [PATCH 10/27] [SR] Fix crashes and add replay lifecycle management (#4135) * Wrap viewTreeObserver into try-catch * Fix FileNotFoundException when storing segment values * Use software canvas for Motorola devices * Manage ReplayIntegration lifecycle properly * Make ReplayState internal * Changelog * Fix * Remove import * Always resume the replay in lifecycle watcher --- CHANGELOG.md | 4 + .../sentry/android/core/LifecycleWatcher.java | 8 +- .../android/core/LifecycleWatcherTest.kt | 6 +- .../io/sentry/android/replay/ReplayCache.kt | 5 +- .../android/replay/ReplayIntegration.kt | 180 +++++++++++------- .../sentry/android/replay/ReplayLifecycle.kt | 58 ++++++ .../replay/capture/BaseCaptureStrategy.kt | 7 +- .../io/sentry/android/replay/util/Views.kt | 12 +- .../replay/video/SimpleVideoEncoder.kt | 5 +- .../sentry/android/replay/ReplayCacheTest.kt | 15 ++ .../android/replay/ReplayIntegrationTest.kt | 178 ++++++++++++++++- .../ReplayIntegrationWithRecorderTest.kt | 6 +- .../android/replay/ReplayLifecycleTest.kt | 120 ++++++++++++ 13 files changed, 517 insertions(+), 87 deletions(-) create mode 100644 sentry-android-replay/src/main/java/io/sentry/android/replay/ReplayLifecycle.kt create mode 100644 sentry-android-replay/src/test/java/io/sentry/android/replay/ReplayLifecycleTest.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ddeaf4968..ce9260acd7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,10 @@ - Do not log if `OtelContextScopesStorage` cannot be found ([#4127](https://github.com/getsentry/sentry-java/pull/4127)) - Previously `java.lang.ClassNotFoundException: io.sentry.opentelemetry.OtelContextScopesStorage` was shown in the log if the class could not be found. - This is just a lookup the SDK performs to configure itself. The SDK also works without OpenTelemetry. +- Session Replay: Fix various crashes and issues ([#4135](https://github.com/getsentry/sentry-java/pull/4135)) + - Fix `FileNotFoundException` when trying to read/write `.ongoing_segment` file + - Fix `IllegalStateException` when registering `onDrawListener` + - Fix SIGABRT native crashes on Motorola devices when encoding a video - Mention javadoc and sources for published artifacts in Gradle `.module` metadata ([#3936](https://github.com/getsentry/sentry-java/pull/3936)) ### Dependencies diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/LifecycleWatcher.java b/sentry-android-core/src/main/java/io/sentry/android/core/LifecycleWatcher.java index 73c37dfe97..d83ecdd675 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/LifecycleWatcher.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/LifecycleWatcher.java @@ -12,7 +12,6 @@ import io.sentry.util.AutoClosableReentrantLock; import java.util.Timer; import java.util.TimerTask; -import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -21,7 +20,6 @@ final class LifecycleWatcher implements DefaultLifecycleObserver { private final AtomicLong lastUpdatedSession = new AtomicLong(0L); - private final AtomicBoolean isFreshSession = new AtomicBoolean(false); private final long sessionIntervalMillis; @@ -82,7 +80,6 @@ private void startSession() { final @Nullable Session currentSession = scope.getSession(); if (currentSession != null && currentSession.getStarted() != null) { lastUpdatedSession.set(currentSession.getStarted().getTime()); - isFreshSession.set(true); } } }); @@ -94,11 +91,8 @@ private void startSession() { scopes.startSession(); } scopes.getOptions().getReplayController().start(); - } else if (!isFreshSession.get()) { - // only resume if it's not a fresh session, which has been started in SentryAndroid.init - scopes.getOptions().getReplayController().resume(); } - isFreshSession.set(false); + scopes.getOptions().getReplayController().resume(); this.lastUpdatedSession.set(currentTimeMillis); } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/LifecycleWatcherTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/LifecycleWatcherTest.kt index 5613c8eb1f..5f088221b9 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/LifecycleWatcherTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/LifecycleWatcherTest.kt @@ -254,7 +254,7 @@ class LifecycleWatcherTest { } @Test - fun `if the hub has already a fresh session running, doesn't resume replay`() { + fun `if the hub has already a fresh session running, resumes replay to invalidate isManualPause flag`() { val watcher = fixture.getSUT( enableAppLifecycleBreadcrumbs = false, session = Session( @@ -276,7 +276,7 @@ class LifecycleWatcherTest { ) watcher.onStart(fixture.ownerMock) - verify(fixture.replayController, never()).resume() + verify(fixture.replayController).resume() } @Test @@ -293,7 +293,7 @@ class LifecycleWatcherTest { verify(fixture.replayController).pause() watcher.onStart(fixture.ownerMock) - verify(fixture.replayController).resume() + verify(fixture.replayController, times(2)).resume() watcher.onStop(fixture.ownerMock) verify(fixture.replayController, timeout(10000)).stop() diff --git a/sentry-android-replay/src/main/java/io/sentry/android/replay/ReplayCache.kt b/sentry-android-replay/src/main/java/io/sentry/android/replay/ReplayCache.kt index d926acb9ae..2d7506a94a 100644 --- a/sentry-android-replay/src/main/java/io/sentry/android/replay/ReplayCache.kt +++ b/sentry-android-replay/src/main/java/io/sentry/android/replay/ReplayCache.kt @@ -55,7 +55,7 @@ public class ReplayCache( internal val frames = mutableListOf() private val ongoingSegment = LinkedHashMap() - private val ongoingSegmentFile: File? by lazy { + internal val ongoingSegmentFile: File? by lazy { if (replayCacheDir == null) { return@lazy null } @@ -275,6 +275,9 @@ public class ReplayCache( if (isClosed.get()) { return } + if (ongoingSegmentFile?.exists() != true) { + ongoingSegmentFile?.createNewFile() + } if (ongoingSegment.isEmpty()) { ongoingSegmentFile?.useLines { lines -> lines.associateTo(ongoingSegment) { diff --git a/sentry-android-replay/src/main/java/io/sentry/android/replay/ReplayIntegration.kt b/sentry-android-replay/src/main/java/io/sentry/android/replay/ReplayIntegration.kt index 5f97290694..7d0d664e2a 100644 --- a/sentry-android-replay/src/main/java/io/sentry/android/replay/ReplayIntegration.kt +++ b/sentry-android-replay/src/main/java/io/sentry/android/replay/ReplayIntegration.kt @@ -21,6 +21,11 @@ import io.sentry.SentryIntegrationPackageStorage import io.sentry.SentryLevel.DEBUG import io.sentry.SentryLevel.INFO import io.sentry.SentryOptions +import io.sentry.android.replay.ReplayState.CLOSED +import io.sentry.android.replay.ReplayState.PAUSED +import io.sentry.android.replay.ReplayState.RESUMED +import io.sentry.android.replay.ReplayState.STARTED +import io.sentry.android.replay.ReplayState.STOPPED import io.sentry.android.replay.capture.BufferCaptureStrategy import io.sentry.android.replay.capture.CaptureStrategy import io.sentry.android.replay.capture.CaptureStrategy.ReplaySegment @@ -40,6 +45,7 @@ import io.sentry.protocol.SentryId import io.sentry.transport.ICurrentDateProvider import io.sentry.transport.RateLimiter import io.sentry.transport.RateLimiter.IRateLimitObserver +import io.sentry.util.AutoClosableReentrantLock import io.sentry.util.FileUtils import io.sentry.util.HintUtils import io.sentry.util.IntegrationUtils.addIntegrationToSdkVersion @@ -100,15 +106,16 @@ public class ReplayIntegration( Executors.newSingleThreadScheduledExecutor(ReplayExecutorServiceThreadFactory()) } - // TODO: probably not everything has to be thread-safe here internal val isEnabled = AtomicBoolean(false) - private val isRecording = AtomicBoolean(false) + internal val isManualPause = AtomicBoolean(false) private var captureStrategy: CaptureStrategy? = null public val replayCacheDir: File? get() = captureStrategy?.replayCacheDir private var replayBreadcrumbConverter: ReplayBreadcrumbConverter = NoOpReplayBreadcrumbConverter.getInstance() private var replayCaptureStrategyProvider: ((isFullSession: Boolean) -> CaptureStrategy)? = null private var mainLooperHandler: MainLooperHandler = MainLooperHandler() private var gestureRecorderProvider: (() -> GestureRecorder)? = null + private val lifecycleLock = AutoClosableReentrantLock() + private val lifecycle = ReplayLifecycle() override fun register(scopes: IScopes, options: SentryOptions) { this.options = options @@ -151,51 +158,68 @@ public class ReplayIntegration( finalizePreviousReplay() } - override fun isRecording(): Boolean = isRecording.get() + override fun isRecording(): Boolean = lifecycle.currentState >= STARTED && lifecycle.currentState < STOPPED override fun start() { - // TODO: add lifecycle state instead and manage it in start/pause/resume/stop - if (!isEnabled.get()) { - return - } + lifecycleLock.acquire().use { + if (!isEnabled.get()) { + return + } - if (isRecording.getAndSet(true)) { - options.logger.log( - DEBUG, - "Session replay is already being recorded, not starting a new one" - ) - return - } + if (!lifecycle.isAllowed(STARTED)) { + options.logger.log( + DEBUG, + "Session replay is already being recorded, not starting a new one" + ) + return + } - val isFullSession = random.sample(options.sessionReplay.sessionSampleRate) - if (!isFullSession && !options.sessionReplay.isSessionReplayForErrorsEnabled) { - options.logger.log(INFO, "Session replay is not started, full session was not sampled and onErrorSampleRate is not specified") - return - } + val isFullSession = random.sample(options.sessionReplay.sessionSampleRate) + if (!isFullSession && !options.sessionReplay.isSessionReplayForErrorsEnabled) { + options.logger.log(INFO, "Session replay is not started, full session was not sampled and onErrorSampleRate is not specified") + return + } - val recorderConfig = recorderConfigProvider?.invoke(false) ?: ScreenshotRecorderConfig.from(context, options.sessionReplay) - captureStrategy = replayCaptureStrategyProvider?.invoke(isFullSession) ?: if (isFullSession) { - SessionCaptureStrategy(options, scopes, dateProvider, replayExecutor, replayCacheProvider) - } else { - BufferCaptureStrategy(options, scopes, dateProvider, random, replayExecutor, replayCacheProvider) - } + val recorderConfig = recorderConfigProvider?.invoke(false) ?: ScreenshotRecorderConfig.from(context, options.sessionReplay) + captureStrategy = replayCaptureStrategyProvider?.invoke(isFullSession) ?: if (isFullSession) { + SessionCaptureStrategy(options, scopes, dateProvider, replayExecutor, replayCacheProvider) + } else { + BufferCaptureStrategy(options, scopes, dateProvider, random, replayExecutor, replayCacheProvider) + } - captureStrategy?.start(recorderConfig) - recorder?.start(recorderConfig) - registerRootViewListeners() + captureStrategy?.start(recorderConfig) + recorder?.start(recorderConfig) + registerRootViewListeners() + lifecycle.currentState = STARTED + } } override fun resume() { - if (!isEnabled.get() || !isRecording.get()) { - return - } + isManualPause.set(false) + resumeInternal() + } - captureStrategy?.resume() - recorder?.resume() + private fun resumeInternal() { + lifecycleLock.acquire().use { + if (!isEnabled.get() || !lifecycle.isAllowed(RESUMED)) { + return + } + + if (isManualPause.get() || options.connectionStatusProvider.connectionStatus == DISCONNECTED || + scopes?.rateLimiter?.isActiveForCategory(All) == true || + scopes?.rateLimiter?.isActiveForCategory(Replay) == true + ) { + return + } + + captureStrategy?.resume() + recorder?.resume() + lifecycle.currentState = RESUMED + } } override fun captureReplay(isTerminating: Boolean?) { - if (!isEnabled.get() || !isRecording.get()) { + if (!isEnabled.get() || !isRecording()) { return } @@ -220,25 +244,35 @@ public class ReplayIntegration( override fun getBreadcrumbConverter(): ReplayBreadcrumbConverter = replayBreadcrumbConverter override fun pause() { - if (!isEnabled.get() || !isRecording.get()) { - return - } + isManualPause.set(true) + pauseInternal() + } - recorder?.pause() - captureStrategy?.pause() + private fun pauseInternal() { + lifecycleLock.acquire().use { + if (!isEnabled.get() || !lifecycle.isAllowed(PAUSED)) { + return + } + + recorder?.pause() + captureStrategy?.pause() + lifecycle.currentState = PAUSED + } } override fun stop() { - if (!isEnabled.get() || !isRecording.get()) { - return - } + lifecycleLock.acquire().use { + if (!isEnabled.get() || !lifecycle.isAllowed(STOPPED)) { + return + } - unregisterRootViewListeners() - recorder?.stop() - gestureRecorder?.stop() - captureStrategy?.stop() - isRecording.set(false) - captureStrategy = null + unregisterRootViewListeners() + recorder?.stop() + gestureRecorder?.stop() + captureStrategy?.stop() + captureStrategy = null + lifecycle.currentState = STOPPED + } } override fun onScreenshotRecorded(bitmap: Bitmap) { @@ -258,27 +292,30 @@ public class ReplayIntegration( } override fun close() { - if (!isEnabled.get()) { - return - } + lifecycleLock.acquire().use { + if (!isEnabled.get() || !lifecycle.isAllowed(CLOSED)) { + return + } - options.connectionStatusProvider.removeConnectionStatusObserver(this) - scopes?.rateLimiter?.removeRateLimitObserver(this) - if (options.sessionReplay.isTrackOrientationChange) { - try { - context.unregisterComponentCallbacks(this) - } catch (ignored: Throwable) { + options.connectionStatusProvider.removeConnectionStatusObserver(this) + scopes?.rateLimiter?.removeRateLimitObserver(this) + if (options.sessionReplay.isTrackOrientationChange) { + try { + context.unregisterComponentCallbacks(this) + } catch (ignored: Throwable) { + } } + stop() + recorder?.close() + recorder = null + rootViewsSpy.close() + replayExecutor.gracefullyShutdown(options) + lifecycle.currentState = CLOSED } - stop() - recorder?.close() - recorder = null - rootViewsSpy.close() - replayExecutor.gracefullyShutdown(options) } override fun onConfigurationChanged(newConfig: Configuration) { - if (!isEnabled.get() || !isRecording.get()) { + if (!isEnabled.get() || !isRecording()) { return } @@ -289,6 +326,10 @@ public class ReplayIntegration( captureStrategy?.onConfigurationChanged(recorderConfig) recorder?.start(recorderConfig) + // we have to restart recorder with a new config and pause immediately if the replay is paused + if (lifecycle.currentState == PAUSED) { + recorder?.pause() + } } override fun onConnectionStatusChanged(status: ConnectionStatus) { @@ -298,10 +339,10 @@ public class ReplayIntegration( } if (status == DISCONNECTED) { - pause() + pauseInternal() } else { // being positive for other states, even if it's NO_PERMISSION - resume() + resumeInternal() } } @@ -312,15 +353,18 @@ public class ReplayIntegration( } if (rateLimiter.isActiveForCategory(All) || rateLimiter.isActiveForCategory(Replay)) { - pause() + pauseInternal() } else { - resume() + resumeInternal() } } override fun onLowMemory(): Unit = Unit override fun onTouchEvent(event: MotionEvent) { + if (!isEnabled.get() || !lifecycle.isTouchRecordingAllowed()) { + return + } captureStrategy?.onTouchEvent(event) } @@ -336,7 +380,7 @@ public class ReplayIntegration( scopes?.rateLimiter?.isActiveForCategory(Replay) == true ) ) { - pause() + pauseInternal() } } diff --git a/sentry-android-replay/src/main/java/io/sentry/android/replay/ReplayLifecycle.kt b/sentry-android-replay/src/main/java/io/sentry/android/replay/ReplayLifecycle.kt new file mode 100644 index 0000000000..fba95fcb41 --- /dev/null +++ b/sentry-android-replay/src/main/java/io/sentry/android/replay/ReplayLifecycle.kt @@ -0,0 +1,58 @@ +package io.sentry.android.replay + +internal enum class ReplayState { + /** + * Initial state of a Replay session. This is the state when ReplayIntegration is constructed + * but has not been started yet. + */ + INITIAL, + + /** + * Started state for a Replay session. This state is reached after the start() method is called + * and the recording is initialized successfully. + */ + STARTED, + + /** + * Resumed state for a Replay session. This state is reached after resume() is called on an + * already started recording. + */ + RESUMED, + + /** + * Paused state for a Replay session. This state is reached after pause() is called on a + * resumed recording. + */ + PAUSED, + + /** + * Stopped state for a Replay session. This state is reached after stop() is called. + * The recording can be started again from this state. + */ + STOPPED, + + /** + * Closed state for a Replay session. This is the terminal state reached after close() is called. + * No further state transitions are possible after this. + */ + CLOSED; +} + +/** + * Class to manage state transitions for ReplayIntegration + */ +internal class ReplayLifecycle { + @field:Volatile + internal var currentState = ReplayState.INITIAL + + fun isAllowed(newState: ReplayState): Boolean = when (currentState) { + ReplayState.INITIAL -> newState == ReplayState.STARTED || newState == ReplayState.CLOSED + ReplayState.STARTED -> newState == ReplayState.PAUSED || newState == ReplayState.STOPPED || newState == ReplayState.CLOSED + ReplayState.RESUMED -> newState == ReplayState.PAUSED || newState == ReplayState.STOPPED || newState == ReplayState.CLOSED + ReplayState.PAUSED -> newState == ReplayState.RESUMED || newState == ReplayState.STOPPED || newState == ReplayState.CLOSED + ReplayState.STOPPED -> newState == ReplayState.STARTED || newState == ReplayState.CLOSED + ReplayState.CLOSED -> false + } + + fun isTouchRecordingAllowed(): Boolean = currentState == ReplayState.STARTED || currentState == ReplayState.RESUMED +} diff --git a/sentry-android-replay/src/main/java/io/sentry/android/replay/capture/BaseCaptureStrategy.kt b/sentry-android-replay/src/main/java/io/sentry/android/replay/capture/BaseCaptureStrategy.kt index 776348ed98..c170dcfbcd 100644 --- a/sentry-android-replay/src/main/java/io/sentry/android/replay/capture/BaseCaptureStrategy.kt +++ b/sentry-android-replay/src/main/java/io/sentry/android/replay/capture/BaseCaptureStrategy.kt @@ -5,6 +5,7 @@ import android.view.MotionEvent import io.sentry.Breadcrumb import io.sentry.DateUtils import io.sentry.IScopes +import io.sentry.SentryLevel.ERROR import io.sentry.SentryOptions import io.sentry.SentryReplayEvent.ReplayType import io.sentry.SentryReplayEvent.ReplayType.BUFFER @@ -183,7 +184,11 @@ internal abstract class BaseCaptureStrategy( task() } } else { - task() + try { + task() + } catch (e: Throwable) { + options.logger.log(ERROR, "Failed to execute task $TAG.runInBackground", e) + } } } diff --git a/sentry-android-replay/src/main/java/io/sentry/android/replay/util/Views.kt b/sentry-android-replay/src/main/java/io/sentry/android/replay/util/Views.kt index 2e53f8fb18..c36c7e9932 100644 --- a/sentry-android-replay/src/main/java/io/sentry/android/replay/util/Views.kt +++ b/sentry-android-replay/src/main/java/io/sentry/android/replay/util/Views.kt @@ -184,12 +184,20 @@ internal fun View?.addOnDrawListenerSafe(listener: ViewTreeObserver.OnDrawListen if (this == null || viewTreeObserver == null || !viewTreeObserver.isAlive) { return } - viewTreeObserver.addOnDrawListener(listener) + try { + viewTreeObserver.addOnDrawListener(listener) + } catch (e: IllegalStateException) { + // viewTreeObserver is already dead + } } internal fun View?.removeOnDrawListenerSafe(listener: ViewTreeObserver.OnDrawListener) { if (this == null || viewTreeObserver == null || !viewTreeObserver.isAlive) { return } - viewTreeObserver.removeOnDrawListener(listener) + try { + viewTreeObserver.removeOnDrawListener(listener) + } catch (e: IllegalStateException) { + // viewTreeObserver is already dead + } } diff --git a/sentry-android-replay/src/main/java/io/sentry/android/replay/video/SimpleVideoEncoder.kt b/sentry-android-replay/src/main/java/io/sentry/android/replay/video/SimpleVideoEncoder.kt index 211decc098..0a535a439c 100644 --- a/sentry-android-replay/src/main/java/io/sentry/android/replay/video/SimpleVideoEncoder.kt +++ b/sentry-android-replay/src/main/java/io/sentry/android/replay/video/SimpleVideoEncoder.kt @@ -157,7 +157,10 @@ internal class SimpleVideoEncoder( fun encode(image: Bitmap) { // it seems that Xiaomi devices have problems with hardware canvas, so we have to use // lockCanvas instead https://stackoverflow.com/a/73520742 - val canvas = if (Build.MANUFACTURER.contains("xiaomi", ignoreCase = true)) { + val canvas = if ( + Build.MANUFACTURER.contains("xiaomi", ignoreCase = true) || + Build.MANUFACTURER.contains("motorola", ignoreCase = true) + ) { surface?.lockCanvas(null) } else { surface?.lockHardwareCanvas() diff --git a/sentry-android-replay/src/test/java/io/sentry/android/replay/ReplayCacheTest.kt b/sentry-android-replay/src/test/java/io/sentry/android/replay/ReplayCacheTest.kt index b2c8836d40..a3e17d4f73 100644 --- a/sentry-android-replay/src/test/java/io/sentry/android/replay/ReplayCacheTest.kt +++ b/sentry-android-replay/src/test/java/io/sentry/android/replay/ReplayCacheTest.kt @@ -285,6 +285,21 @@ class ReplayCacheTest { assertFalse(File(replayCache.replayCacheDir, ONGOING_SEGMENT).exists()) } + @Test + fun `when file does not exist upon persisting creates it`() { + val replayId = SentryId() + val replayCache = fixture.getSut( + tmpDir, + replayId + ) + + replayCache.ongoingSegmentFile?.delete() + + replayCache.persistSegmentValues("key", "value") + val segmentValues = File(replayCache.replayCacheDir, ONGOING_SEGMENT).readLines() + assertEquals("key=value", segmentValues[0]) + } + @Test fun `stores segment key value pairs`() { val replayId = SentryId() diff --git a/sentry-android-replay/src/test/java/io/sentry/android/replay/ReplayIntegrationTest.kt b/sentry-android-replay/src/test/java/io/sentry/android/replay/ReplayIntegrationTest.kt index 747519943a..1a07e35ada 100644 --- a/sentry-android-replay/src/test/java/io/sentry/android/replay/ReplayIntegrationTest.kt +++ b/sentry-android-replay/src/test/java/io/sentry/android/replay/ReplayIntegrationTest.kt @@ -277,6 +277,7 @@ class ReplayIntegrationTest { replay.register(fixture.scopes, fixture.options) replay.start() + replay.pause() replay.resume() verify(captureStrategy).resume() @@ -646,6 +647,7 @@ class ReplayIntegrationTest { replay.register(fixture.scopes, fixture.options) replay.start() + replay.onConnectionStatusChanged(DISCONNECTED) replay.onConnectionStatusChanged(CONNECTED) verify(recorder).resume() @@ -677,16 +679,190 @@ class ReplayIntegrationTest { context, recorderProvider = { recorder }, replayCaptureStrategyProvider = { captureStrategy }, - isRateLimited = false + isRateLimited = true ) replay.register(fixture.scopes, fixture.options) replay.start() + + replay.onRateLimitChanged(fixture.rateLimiter) + whenever(fixture.rateLimiter.isActiveForCategory(any())).thenReturn(false) replay.onRateLimitChanged(fixture.rateLimiter) verify(recorder).resume() } + @Test + fun `closed replay cannot be started`() { + val replay = fixture.getSut(context) + replay.register(fixture.scopes, fixture.options) + replay.start() + replay.close() + + replay.start() + + assertFalse(replay.isRecording) + } + + @Test + fun `if recording is paused in configChanges re-pauses it again`() { + var configChanged = false + val recorderConfig = mock() + val captureStrategy = mock() + val recorder = mock() + val replay = fixture.getSut( + context, + recorderProvider = { recorder }, + replayCaptureStrategyProvider = { captureStrategy }, + recorderConfigProvider = { configChanged = it; recorderConfig } + ) + + replay.register(fixture.scopes, fixture.options) + replay.start() + replay.pause() + replay.onConfigurationChanged(mock()) + + verify(recorder).stop() + verify(captureStrategy).onConfigurationChanged(eq(recorderConfig)) + verify(recorder, times(2)).start(eq(recorderConfig)) + verify(recorder, times(2)).pause() + assertTrue(configChanged) + } + + @Test + fun `onTouchEvent does nothing when not started or resumed`() { + val captureStrategy = mock() + val replay = fixture.getSut(context, replayCaptureStrategyProvider = { captureStrategy }) + + replay.register(fixture.scopes, fixture.options) + replay.start() + replay.pause() + replay.onTouchEvent(mock()) + + verify(captureStrategy, never()).onTouchEvent(any()) + } + + @Test + fun `when paused manually onConnectionStatusChanged does not resume`() { + val captureStrategy = getSessionCaptureStrategy(fixture.options) + val recorder = mock() + val replay = fixture.getSut( + context, + recorderProvider = { recorder }, + replayCaptureStrategyProvider = { captureStrategy } + ) + + replay.register(fixture.scopes, fixture.options) + replay.start() + replay.onConnectionStatusChanged(DISCONNECTED) + replay.pause() + replay.onConnectionStatusChanged(CONNECTED) + + verify(recorder, never()).resume() + } + + @Test + fun `when paused manually onRateLimitChanged does not resume`() { + val captureStrategy = getSessionCaptureStrategy(fixture.options) + val recorder = mock() + val replay = fixture.getSut( + context, + recorderProvider = { recorder }, + replayCaptureStrategyProvider = { captureStrategy }, + isRateLimited = true + ) + + replay.register(fixture.scopes, fixture.options) + replay.start() + + replay.onRateLimitChanged(fixture.rateLimiter) + replay.pause() + whenever(fixture.rateLimiter.isActiveForCategory(any())).thenReturn(false) + replay.onRateLimitChanged(fixture.rateLimiter) + + verify(recorder, never()).resume() + } + + @Test + fun `when rate limit is active manual resume does nothing`() { + val captureStrategy = getSessionCaptureStrategy(fixture.options) + val recorder = mock() + val replay = fixture.getSut( + context, + recorderProvider = { recorder }, + replayCaptureStrategyProvider = { captureStrategy }, + isRateLimited = true + ) + + replay.register(fixture.scopes, fixture.options) + replay.start() + + replay.pause() + replay.resume() + + verify(recorder, never()).resume() + } + + @Test + fun `when no connection manual resume does nothing`() { + val captureStrategy = getSessionCaptureStrategy(fixture.options) + val recorder = mock() + val replay = fixture.getSut( + context, + recorderProvider = { recorder }, + replayCaptureStrategyProvider = { captureStrategy }, + isOffline = true + ) + + replay.register(fixture.scopes, fixture.options) + replay.start() + + replay.pause() + replay.resume() + + verify(recorder, never()).resume() + } + + @Test + fun `when already paused does not pause again`() { + val captureStrategy = getSessionCaptureStrategy(fixture.options) + val recorder = mock() + val replay = fixture.getSut( + context, + recorderProvider = { recorder }, + replayCaptureStrategyProvider = { captureStrategy } + ) + + replay.register(fixture.scopes, fixture.options) + replay.start() + + replay.pause() + replay.pause() + + verify(recorder).pause() + } + + @Test + fun `when already resumed does not resume again`() { + val captureStrategy = getSessionCaptureStrategy(fixture.options) + val recorder = mock() + val replay = fixture.getSut( + context, + recorderProvider = { recorder }, + replayCaptureStrategyProvider = { captureStrategy } + ) + + replay.register(fixture.scopes, fixture.options) + replay.start() + + replay.pause() + replay.resume() + + replay.resume() + + verify(recorder).resume() + } + private fun getSessionCaptureStrategy(options: SentryOptions): SessionCaptureStrategy { return SessionCaptureStrategy( options, diff --git a/sentry-android-replay/src/test/java/io/sentry/android/replay/ReplayIntegrationWithRecorderTest.kt b/sentry-android-replay/src/test/java/io/sentry/android/replay/ReplayIntegrationWithRecorderTest.kt index 25713ad295..4e81d76df4 100644 --- a/sentry-android-replay/src/test/java/io/sentry/android/replay/ReplayIntegrationWithRecorderTest.kt +++ b/sentry-android-replay/src/test/java/io/sentry/android/replay/ReplayIntegrationWithRecorderTest.kt @@ -127,12 +127,12 @@ class ReplayIntegrationWithRecorderTest { replay.start() assertEquals(STARTED, recorder.state) - replay.resume() - assertEquals(RESUMED, recorder.state) - replay.pause() assertEquals(PAUSED, recorder.state) + replay.resume() + assertEquals(RESUMED, recorder.state) + replay.stop() assertEquals(STOPPED, recorder.state) diff --git a/sentry-android-replay/src/test/java/io/sentry/android/replay/ReplayLifecycleTest.kt b/sentry-android-replay/src/test/java/io/sentry/android/replay/ReplayLifecycleTest.kt new file mode 100644 index 0000000000..c989237452 --- /dev/null +++ b/sentry-android-replay/src/test/java/io/sentry/android/replay/ReplayLifecycleTest.kt @@ -0,0 +1,120 @@ +package io.sentry.android.replay + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class ReplayLifecycleTest { + @Test + fun `verify initial state`() { + val lifecycle = ReplayLifecycle() + assertEquals(ReplayState.INITIAL, lifecycle.currentState) + } + + @Test + fun `test transitions from INITIAL state`() { + val lifecycle = ReplayLifecycle() + + assertTrue(lifecycle.isAllowed(ReplayState.STARTED)) + assertTrue(lifecycle.isAllowed(ReplayState.CLOSED)) + + assertFalse(lifecycle.isAllowed(ReplayState.RESUMED)) + assertFalse(lifecycle.isAllowed(ReplayState.PAUSED)) + assertFalse(lifecycle.isAllowed(ReplayState.STOPPED)) + } + + @Test + fun `test transitions from STARTED state`() { + val lifecycle = ReplayLifecycle() + lifecycle.currentState = ReplayState.STARTED + + assertTrue(lifecycle.isAllowed(ReplayState.PAUSED)) + assertTrue(lifecycle.isAllowed(ReplayState.STOPPED)) + assertTrue(lifecycle.isAllowed(ReplayState.CLOSED)) + + assertFalse(lifecycle.isAllowed(ReplayState.RESUMED)) + assertFalse(lifecycle.isAllowed(ReplayState.INITIAL)) + } + + @Test + fun `test transitions from RESUMED state`() { + val lifecycle = ReplayLifecycle() + lifecycle.currentState = ReplayState.RESUMED + + assertTrue(lifecycle.isAllowed(ReplayState.PAUSED)) + assertTrue(lifecycle.isAllowed(ReplayState.STOPPED)) + assertTrue(lifecycle.isAllowed(ReplayState.CLOSED)) + + assertFalse(lifecycle.isAllowed(ReplayState.STARTED)) + assertFalse(lifecycle.isAllowed(ReplayState.INITIAL)) + } + + @Test + fun `test transitions from PAUSED state`() { + val lifecycle = ReplayLifecycle() + lifecycle.currentState = ReplayState.PAUSED + + assertTrue(lifecycle.isAllowed(ReplayState.RESUMED)) + assertTrue(lifecycle.isAllowed(ReplayState.STOPPED)) + assertTrue(lifecycle.isAllowed(ReplayState.CLOSED)) + + assertFalse(lifecycle.isAllowed(ReplayState.STARTED)) + assertFalse(lifecycle.isAllowed(ReplayState.INITIAL)) + } + + @Test + fun `test transitions from STOPPED state`() { + val lifecycle = ReplayLifecycle() + lifecycle.currentState = ReplayState.STOPPED + + assertTrue(lifecycle.isAllowed(ReplayState.STARTED)) + assertTrue(lifecycle.isAllowed(ReplayState.CLOSED)) + + assertFalse(lifecycle.isAllowed(ReplayState.RESUMED)) + assertFalse(lifecycle.isAllowed(ReplayState.PAUSED)) + assertFalse(lifecycle.isAllowed(ReplayState.INITIAL)) + } + + @Test + fun `test transitions from CLOSED state`() { + val lifecycle = ReplayLifecycle() + lifecycle.currentState = ReplayState.CLOSED + + assertFalse(lifecycle.isAllowed(ReplayState.INITIAL)) + assertFalse(lifecycle.isAllowed(ReplayState.STARTED)) + assertFalse(lifecycle.isAllowed(ReplayState.RESUMED)) + assertFalse(lifecycle.isAllowed(ReplayState.PAUSED)) + assertFalse(lifecycle.isAllowed(ReplayState.STOPPED)) + assertFalse(lifecycle.isAllowed(ReplayState.CLOSED)) + } + + @Test + fun `test touch recording is allowed only in STARTED and RESUMED states`() { + val lifecycle = ReplayLifecycle() + + // Initial state doesn't allow touch recording + assertFalse(lifecycle.isTouchRecordingAllowed()) + + // STARTED state allows touch recording + lifecycle.currentState = ReplayState.STARTED + assertTrue(lifecycle.isTouchRecordingAllowed()) + + // RESUMED state allows touch recording + lifecycle.currentState = ReplayState.RESUMED + assertTrue(lifecycle.isTouchRecordingAllowed()) + + // Other states don't allow touch recording + val otherStates = listOf( + ReplayState.INITIAL, + ReplayState.PAUSED, + ReplayState.STOPPED, + ReplayState.CLOSED + ) + + otherStates.forEach { state -> + lifecycle.currentState = state + assertFalse(lifecycle.isTouchRecordingAllowed()) + } + } +} From 91474b9e3f35274a95d9da9ca96931feb457bba6 Mon Sep 17 00:00:00 2001 From: Stefano Date: Thu, 6 Feb 2025 10:39:47 +0100 Subject: [PATCH 11/27] Create onCreate and onStart spans for all Activities (#4025) * ActivityLifecycleIntegration creates activity spans for all Activities, not only for appStart ones * updated changelog * Fix test * Changelog --------- Co-authored-by: Roman Zavarnitsyn --- CHANGELOG.md | 4 ++ .../core/ActivityLifecycleIntegration.java | 6 ++- .../ActivityLifecycleSpanHelper.java | 19 +++++----- .../core/ActivityLifecycleIntegrationTest.kt | 37 +++++++++++++++++++ 4 files changed, 54 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ce9260acd7..cfd31db09c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ - The Kotlin Language version is now set to 1.6 ([#3936](https://github.com/getsentry/sentry-java/pull/3936)) +### Features + +- Create onCreate and onStart spans for all Activities ([#4025](https://github.com/getsentry/sentry-java/pull/4025)) + ### Fixes - Do not log if `OtelContextScopesStorage` cannot be found ([#4127](https://github.com/getsentry/sentry-java/pull/4127)) diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java b/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java index eaf05e619d..3c0d8b3a5c 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java @@ -430,7 +430,8 @@ public void onActivityPostCreated( final @NotNull Activity activity, final @Nullable Bundle savedInstanceState) { final ActivityLifecycleSpanHelper helper = activitySpanHelpers.get(activity); if (helper != null) { - helper.createAndStopOnCreateSpan(appStartSpan); + helper.createAndStopOnCreateSpan( + appStartSpan != null ? appStartSpan : activitiesWithOngoingTransactions.get(activity)); } } @@ -468,7 +469,8 @@ public void onActivityStarted(final @NotNull Activity activity) { public void onActivityPostStarted(final @NotNull Activity activity) { final ActivityLifecycleSpanHelper helper = activitySpanHelpers.get(activity); if (helper != null) { - helper.createAndStopOnStartSpan(appStartSpan); + helper.createAndStopOnStartSpan( + appStartSpan != null ? appStartSpan : activitiesWithOngoingTransactions.get(activity)); // Needed to handle hybrid SDKs helper.saveSpanToAppStartMetrics(); } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/performance/ActivityLifecycleSpanHelper.java b/sentry-android-core/src/main/java/io/sentry/android/core/performance/ActivityLifecycleSpanHelper.java index 7fed5e0fdb..fead459ba8 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/performance/ActivityLifecycleSpanHelper.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/performance/ActivityLifecycleSpanHelper.java @@ -36,18 +36,18 @@ public void setOnStartStartTimestamp(final @NotNull SentryDate onStartStartTimes this.onStartStartTimestamp = onStartStartTimestamp; } - public void createAndStopOnCreateSpan(final @Nullable ISpan appStartSpan) { - if (onCreateStartTimestamp != null && appStartSpan != null) { + public void createAndStopOnCreateSpan(final @Nullable ISpan parentSpan) { + if (onCreateStartTimestamp != null && parentSpan != null) { onCreateSpan = - createLifecycleSpan(appStartSpan, activityName + ".onCreate", onCreateStartTimestamp); + createLifecycleSpan(parentSpan, activityName + ".onCreate", onCreateStartTimestamp); onCreateSpan.finish(); } } - public void createAndStopOnStartSpan(final @Nullable ISpan appStartSpan) { - if (onStartStartTimestamp != null && appStartSpan != null) { + public void createAndStopOnStartSpan(final @Nullable ISpan parentSpan) { + if (onStartStartTimestamp != null && parentSpan != null) { onStartSpan = - createLifecycleSpan(appStartSpan, activityName + ".onStart", onStartStartTimestamp); + createLifecycleSpan(parentSpan, activityName + ".onStart", onStartStartTimestamp); onStartSpan.finish(); } } @@ -106,19 +106,18 @@ public void saveSpanToAppStartMetrics() { } private @NotNull ISpan createLifecycleSpan( - final @NotNull ISpan appStartSpan, + final @NotNull ISpan parentSpan, final @NotNull String description, final @NotNull SentryDate startTimestamp) { final @NotNull ISpan span = - appStartSpan.startChild( + parentSpan.startChild( APP_METRICS_ACTIVITIES_OP, description, startTimestamp, Instrumenter.SENTRY); setDefaultStartSpanData(span); return span; } public void clear() { - // in case the appStartSpan isn't completed yet, we finish it as cancelled to avoid - // memory leak + // in case the parentSpan isn't completed yet, we finish it as cancelled to avoid memory leak if (onCreateSpan != null && !onCreateSpan.isFinished()) { onCreateSpan.finish(SpanStatus.CANCELLED); } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/ActivityLifecycleIntegrationTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/ActivityLifecycleIntegrationTest.kt index eae6e9cc1c..a14f62c3f0 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/ActivityLifecycleIntegrationTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/ActivityLifecycleIntegrationTest.kt @@ -1561,6 +1561,43 @@ class ActivityLifecycleIntegrationTest { assertFalse(appStartMetrics.activityLifecycleTimeSpans.isEmpty()) } + @Test + fun `Creates activity lifecycle spans even when no app start span is available`() { + val sut = fixture.getSut() + fixture.options.tracesSampleRate = 1.0 + val startDate = SentryNanotimeDate(Date(2), 0) + val appStartMetrics = AppStartMetrics.getInstance() + val activity = mock() + fixture.options.dateProvider = SentryDateProvider { startDate } + // Don't set app start time, so there's no app start span + // setAppStartTime(appStartDate) + + sut.register(fixture.scopes, fixture.options) + assertTrue(sut.activitySpanHelpers.isEmpty()) + + sut.onActivityPreCreated(activity, null) + + assertFalse(sut.activitySpanHelpers.isEmpty()) + val helper = sut.activitySpanHelpers.values.first() + assertNotNull(helper.onCreateStartTimestamp) + + sut.onActivityCreated(activity, null) + assertNull(sut.appStartSpan) + + sut.onActivityPostCreated(activity, null) + assertTrue(helper.onCreateSpan!!.isFinished) + + sut.onActivityPreStarted(activity) + assertNotNull(helper.onStartStartTimestamp) + + sut.onActivityStarted(activity) + assertTrue(appStartMetrics.activityLifecycleTimeSpans.isEmpty()) + + sut.onActivityPostStarted(activity) + assertTrue(helper.onStartSpan!!.isFinished) + assertFalse(appStartMetrics.activityLifecycleTimeSpans.isEmpty()) + } + @Test fun `Save activity lifecycle spans in AppStartMetrics onPostSarted`() { val sut = fixture.getSut() From b79b57cfa2265a74e997d1b619adbdcfcc960305 Mon Sep 17 00:00:00 2001 From: "YOUNG HO CHA (aka ganachoco)" Date: Fri, 7 Feb 2025 07:54:04 +0900 Subject: [PATCH 12/27] Add Split Apks info as extras (#3193) * Add Split Apks info as tags * Disable to get splitNames for Kitkat or below * Use extras instead of tags * Add testcase for splitapk info * Send split apks info as part of App context * Changelog * Remove redundant method --------- Co-authored-by: Roman Zavarnitsyn --- CHANGELOG.md | 1 + .../api/sentry-android-core.api | 1 + .../android/core/AnrV2EventProcessor.java | 13 +++++ .../io/sentry/android/core/ContextUtils.java | 56 +++++++++++++++++++ .../core/DefaultAndroidEventProcessor.java | 10 +++- .../sentry/android/core/DeviceInfoUtil.java | 7 +++ .../android/core/InternalSentrySdk.java | 2 +- .../sentry/android/core/ContextUtilsTest.kt | 36 ++++++++++++ sentry/api/sentry.api | 6 ++ .../src/main/java/io/sentry/protocol/App.java | 47 +++++++++++++++- 10 files changed, 175 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cfd31db09c..e51fb436fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ ### Features - Create onCreate and onStart spans for all Activities ([#4025](https://github.com/getsentry/sentry-java/pull/4025)) +- Add split apks info to the `App` context ([#3193](https://github.com/getsentry/sentry-java/pull/3193))) ### Fixes diff --git a/sentry-android-core/api/sentry-android-core.api b/sentry-android-core/api/sentry-android-core.api index 6832051aa9..8cabe0fd8e 100644 --- a/sentry-android-core/api/sentry-android-core.api +++ b/sentry-android-core/api/sentry-android-core.api @@ -195,6 +195,7 @@ public final class io/sentry/android/core/DeviceInfoUtil { public static fun getInstance (Landroid/content/Context;Lio/sentry/android/core/SentryAndroidOptions;)Lio/sentry/android/core/DeviceInfoUtil; public fun getOperatingSystem ()Lio/sentry/protocol/OperatingSystem; public fun getSideLoadedInfo ()Lio/sentry/android/core/ContextUtils$SideLoadedInfo; + public fun getSplitApksInfo ()Lio/sentry/android/core/ContextUtils$SplitApksInfo; public fun getTotalMemory ()Ljava/lang/Long; public static fun isCharging (Landroid/content/Intent;Lio/sentry/SentryOptions;)Ljava/lang/Boolean; public static fun resetInstance ()V diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2EventProcessor.java b/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2EventProcessor.java index 0810f1ac38..216a4424c2 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2EventProcessor.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2EventProcessor.java @@ -404,6 +404,19 @@ private void setApp(final @NotNull SentryBaseEvent event, final @NotNull Object } } + try { + final ContextUtils.SplitApksInfo splitApksInfo = + DeviceInfoUtil.getInstance(context, options).getSplitApksInfo(); + if (splitApksInfo != null) { + app.setSplitApks(splitApksInfo.isSplitApks()); + if (splitApksInfo.getSplitNames() != null) { + app.setSplitNames(Arrays.asList(splitApksInfo.getSplitNames())); + } + } + } catch (Throwable e) { + options.getLogger().log(SentryLevel.ERROR, "Error getting split apks info.", e); + } + event.getContexts().setApp(app); } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ContextUtils.java b/sentry-android-core/src/main/java/io/sentry/android/core/ContextUtils.java index 29b6a4e6fd..5dcd901a91 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/ContextUtils.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/ContextUtils.java @@ -26,6 +26,7 @@ import java.io.File; import java.io.FileReader; import java.io.IOException; +import java.util.Arrays; import java.util.HashMap; import java.util.Map; import org.jetbrains.annotations.ApiStatus; @@ -63,6 +64,27 @@ public boolean isSideLoaded() { } } + static class SplitApksInfo { + // https://github.com/google/bundletool/blob/master/src/main/java/com/android/tools/build/bundletool/model/AndroidManifest.java#L257-L263 + static final String SPLITS_REQUIRED = "com.android.vending.splits.required"; + + private final boolean isSplitApks; + private final String[] splitNames; + + public SplitApksInfo(final boolean isSplitApks, final String[] splitNames) { + this.isSplitApks = isSplitApks; + this.splitNames = splitNames; + } + + public boolean isSplitApks() { + return isSplitApks; + } + + public @Nullable String[] getSplitNames() { + return splitNames; + } + } + private ContextUtils() {} // to avoid doing a bunch of Binder calls we use LazyEvaluator to cache the values that are static @@ -322,6 +344,26 @@ public static boolean isForegroundImportance() { return null; } + @SuppressWarnings({"deprecation"}) + static @Nullable SplitApksInfo retrieveSplitApksInfo( + final @NotNull Context context, final @NotNull BuildInfoProvider buildInfoProvider) { + String[] splitNames = null; + final ApplicationInfo applicationInfo = getApplicationInfo(context, buildInfoProvider); + final PackageInfo packageInfo = getPackageInfo(context, buildInfoProvider); + + if (packageInfo != null) { + splitNames = packageInfo.splitNames; + boolean isSplitApks = false; + if (applicationInfo != null && applicationInfo.metaData != null) { + isSplitApks = applicationInfo.metaData.getBoolean(SplitApksInfo.SPLITS_REQUIRED); + } + + return new SplitApksInfo(isSplitApks, splitNames); + } + + return null; + } + /** * Get the human-facing Application name. * @@ -422,6 +464,7 @@ public static boolean isForegroundImportance() { static void setAppPackageInfo( final @NotNull PackageInfo packageInfo, final @NotNull BuildInfoProvider buildInfoProvider, + final @Nullable DeviceInfoUtil deviceInfoUtil, final @NotNull App app) { app.setAppIdentifier(packageInfo.packageName); app.setAppVersion(packageInfo.versionName); @@ -446,6 +489,19 @@ static void setAppPackageInfo( } } app.setPermissions(permissions); + + if (deviceInfoUtil != null) { + try { + final ContextUtils.SplitApksInfo splitApksInfo = deviceInfoUtil.getSplitApksInfo(); + if (splitApksInfo != null) { + app.setSplitApks(splitApksInfo.isSplitApks()); + if (splitApksInfo.getSplitNames() != null) { + app.setSplitNames(Arrays.asList(splitApksInfo.getSplitNames())); + } + } + } catch (Throwable e) { + } + } } /** diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/DefaultAndroidEventProcessor.java b/sentry-android-core/src/main/java/io/sentry/android/core/DefaultAndroidEventProcessor.java index e077073e74..646f3c91dc 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/DefaultAndroidEventProcessor.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/DefaultAndroidEventProcessor.java @@ -239,7 +239,15 @@ private void setPackageInfo(final @NotNull SentryBaseEvent event, final @NotNull String versionCode = ContextUtils.getVersionCode(packageInfo, buildInfoProvider); setDist(event, versionCode); - ContextUtils.setAppPackageInfo(packageInfo, buildInfoProvider, app); + + @Nullable DeviceInfoUtil deviceInfoUtil = null; + try { + deviceInfoUtil = this.deviceInfoUtil.get(); + } catch (Throwable e) { + options.getLogger().log(SentryLevel.ERROR, "Failed to retrieve device info", e); + } + + ContextUtils.setAppPackageInfo(packageInfo, buildInfoProvider, deviceInfoUtil, app); } } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/DeviceInfoUtil.java b/sentry-android-core/src/main/java/io/sentry/android/core/DeviceInfoUtil.java index 2293f21f3a..3fa131285e 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/DeviceInfoUtil.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/DeviceInfoUtil.java @@ -49,6 +49,7 @@ public final class DeviceInfoUtil { private final @NotNull BuildInfoProvider buildInfoProvider; private final @Nullable Boolean isEmulator; private final @Nullable ContextUtils.SideLoadedInfo sideLoadedInfo; + private final @Nullable ContextUtils.SplitApksInfo splitApksInfo; private final @NotNull OperatingSystem os; private final @Nullable Long totalMem; @@ -65,6 +66,7 @@ public DeviceInfoUtil( isEmulator = buildInfoProvider.isEmulator(); sideLoadedInfo = ContextUtils.retrieveSideLoadedInfo(context, options.getLogger(), buildInfoProvider); + splitApksInfo = ContextUtils.retrieveSplitApksInfo(context, buildInfoProvider); final @Nullable ActivityManager.MemoryInfo memInfo = ContextUtils.getMemInfo(context, options.getLogger()); if (memInfo != null) { @@ -188,6 +190,11 @@ public ContextUtils.SideLoadedInfo getSideLoadedInfo() { return sideLoadedInfo; } + @Nullable + public ContextUtils.SplitApksInfo getSplitApksInfo() { + return splitApksInfo; + } + private void setDeviceIO(final @NotNull Device device, final boolean includeDynamicData) { final Intent batteryIntent = getBatteryIntent(); if (batteryIntent != null) { diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/InternalSentrySdk.java b/sentry-android-core/src/main/java/io/sentry/android/core/InternalSentrySdk.java index 2e03184bc4..841edf8109 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/InternalSentrySdk.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/InternalSentrySdk.java @@ -124,7 +124,7 @@ public static Map serializeScope( ContextUtils.getPackageInfo( context, PackageManager.GET_PERMISSIONS, options.getLogger(), buildInfoProvider); if (packageInfo != null) { - ContextUtils.setAppPackageInfo(packageInfo, buildInfoProvider, app); + ContextUtils.setAppPackageInfo(packageInfo, buildInfoProvider, deviceInfoUtil, app); } scope.getContexts().setApp(app); diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/ContextUtilsTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/ContextUtilsTest.kt index 53989202aa..ea58171d0c 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/ContextUtilsTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/ContextUtilsTest.kt @@ -7,6 +7,9 @@ import android.app.ActivityManager.RunningAppProcessInfo import android.content.BroadcastReceiver import android.content.Context import android.content.IntentFilter +import android.content.pm.ApplicationInfo +import android.content.pm.PackageInfo +import android.content.pm.PackageManager import android.os.Build import android.os.Process import androidx.test.core.app.ApplicationProvider @@ -26,6 +29,7 @@ import org.robolectric.shadows.ShadowActivityManager import org.robolectric.shadows.ShadowBuild import kotlin.test.BeforeTest import kotlin.test.Test +import kotlin.test.assertContentEquals import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertNotNull @@ -116,6 +120,38 @@ class ContextUtilsTest { assertEquals("play.google.com", sideLoadedInfo.installerStore) } + @Test + fun `given a valid PackageInfo, returns valid splitNames`() { + val splitNames = arrayOf("config.arm64_v8a") + val mockedContext = mock() + val mockedPackageManager = mock() + val mockedApplicationInfo = mock() + val mockedPackageInfo = mock() + mockedPackageInfo.splitNames = splitNames + + whenever(mockedContext.packageName).thenReturn("dummy") + + whenever( + mockedPackageManager.getApplicationInfo( + any(), + any() + ) + ).thenReturn(mockedApplicationInfo) + + whenever( + mockedPackageManager.getPackageInfo( + any(), + any() + ) + ).thenReturn(mockedPackageInfo) + + whenever(mockedContext.packageManager).thenReturn(mockedPackageManager) + + val splitApksInfo = + ContextUtils.retrieveSplitApksInfo(mockedContext, BuildInfoProvider(logger)) + assertContentEquals(splitNames, splitApksInfo!!.splitNames) + } + @Test @Config(qualifiers = "w360dp-h640dp-xxhdpi") fun `when display metrics specified, getDisplayMetrics returns correct values`() { diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index ecf75c6b7b..662d87c265 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -4333,6 +4333,8 @@ public final class io/sentry/protocol/App : io/sentry/JsonSerializable, io/sentr public fun getDeviceAppHash ()Ljava/lang/String; public fun getInForeground ()Ljava/lang/Boolean; public fun getPermissions ()Ljava/util/Map; + public fun getSplitApks ()Ljava/lang/Boolean; + public fun getSplitNames ()Ljava/util/List; public fun getStartType ()Ljava/lang/String; public fun getUnknown ()Ljava/util/Map; public fun getViewNames ()Ljava/util/List; @@ -4347,6 +4349,8 @@ public final class io/sentry/protocol/App : io/sentry/JsonSerializable, io/sentr public fun setDeviceAppHash (Ljava/lang/String;)V public fun setInForeground (Ljava/lang/Boolean;)V public fun setPermissions (Ljava/util/Map;)V + public fun setSplitApks (Ljava/lang/Boolean;)V + public fun setSplitNames (Ljava/util/List;)V public fun setStartType (Ljava/lang/String;)V public fun setUnknown (Ljava/util/Map;)V public fun setViewNames (Ljava/util/List;)V @@ -4368,6 +4372,8 @@ public final class io/sentry/protocol/App$JsonKeys { public static final field BUILD_TYPE Ljava/lang/String; public static final field DEVICE_APP_HASH Ljava/lang/String; public static final field IN_FOREGROUND Ljava/lang/String; + public static final field IS_SPLIT_APKS Ljava/lang/String; + public static final field SPLIT_NAMES Ljava/lang/String; public static final field START_TYPE Ljava/lang/String; public static final field VIEW_NAMES Ljava/lang/String; public fun ()V diff --git a/sentry/src/main/java/io/sentry/protocol/App.java b/sentry/src/main/java/io/sentry/protocol/App.java index b949f93c1e..da43a617cb 100644 --- a/sentry/src/main/java/io/sentry/protocol/App.java +++ b/sentry/src/main/java/io/sentry/protocol/App.java @@ -49,6 +49,10 @@ public final class App implements JsonUnknown, JsonSerializable { * visible to the user. */ private @Nullable Boolean inForeground; + /** A flag indicating whether the app is split into multiple APKs */ + private @Nullable Boolean isSplitApks; + /* The list of split APKs */ + private @Nullable List splitNames; public App() {} @@ -64,6 +68,8 @@ public App() {} this.inForeground = app.inForeground; this.viewNames = CollectionUtils.newArrayList(app.viewNames); this.startType = app.startType; + this.isSplitApks = app.isSplitApks; + this.splitNames = app.splitNames; this.unknown = CollectionUtils.newConcurrentHashMap(app.unknown); } @@ -163,6 +169,22 @@ public void setStartType(final @Nullable String startType) { this.startType = startType; } + public @Nullable Boolean getSplitApks() { + return isSplitApks; + } + + public void setSplitApks(final @Nullable Boolean splitApks) { + isSplitApks = splitApks; + } + + public @Nullable List getSplitNames() { + return splitNames; + } + + public void setSplitNames(final @Nullable List splitNames) { + this.splitNames = splitNames; + } + @Override public boolean equals(Object o) { if (this == o) return true; @@ -178,7 +200,9 @@ public boolean equals(Object o) { && Objects.equals(permissions, app.permissions) && Objects.equals(inForeground, app.inForeground) && Objects.equals(viewNames, app.viewNames) - && Objects.equals(startType, app.startType); + && Objects.equals(startType, app.startType) + && Objects.equals(isSplitApks, app.isSplitApks) + && Objects.equals(splitNames, app.splitNames); } @Override @@ -194,7 +218,9 @@ public int hashCode() { permissions, inForeground, viewNames, - startType); + startType, + isSplitApks, + splitNames); } // region json @@ -222,6 +248,8 @@ public static final class JsonKeys { public static final String IN_FOREGROUND = "in_foreground"; public static final String VIEW_NAMES = "view_names"; public static final String START_TYPE = "start_type"; + public static final String IS_SPLIT_APKS = "is_split_apks"; + public static final String SPLIT_NAMES = "split_names"; } @Override @@ -261,6 +289,12 @@ public void serialize(final @NotNull ObjectWriter writer, final @NotNull ILogger if (startType != null) { writer.name(JsonKeys.START_TYPE).value(startType); } + if (isSplitApks != null) { + writer.name(JsonKeys.IS_SPLIT_APKS).value(isSplitApks); + } + if (splitNames != null && !splitNames.isEmpty()) { + writer.name(JsonKeys.SPLIT_NAMES).value(logger, splitNames); + } if (unknown != null) { for (String key : unknown.keySet()) { Object value = unknown.get(key); @@ -319,6 +353,15 @@ public static final class Deserializer implements JsonDeserializer { case JsonKeys.START_TYPE: app.startType = reader.nextStringOrNull(); break; + case JsonKeys.IS_SPLIT_APKS: + app.isSplitApks = reader.nextBooleanOrNull(); + break; + case JsonKeys.SPLIT_NAMES: + final @Nullable List splitNames = (List) reader.nextObjectOrNull(); + if (splitNames != null) { + app.setSplitNames(splitNames); + } + break; default: if (unknown == null) { unknown = new ConcurrentHashMap<>(); From a582fda12a36ee34d4d671b4b41e1106eba79349 Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Fri, 7 Feb 2025 09:06:34 +0100 Subject: [PATCH 13/27] Expose new `withSentryObservableEffect` method overload with `SentryNavigationListener` (#4143) * FEAT: Add version of withSentryObservableEffect that has better interop with fragment navigation * Changelog * pr id --------- Co-authored-by: Andy Zolyak --- CHANGELOG.md | 4 +- sentry-compose/api/android/sentry-compose.api | 1 + .../compose/SentryNavigationIntegration.kt | 46 +++++++++++++------ 3 files changed, 37 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e51fb436fd..326043e723 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,9 @@ ### Features - Create onCreate and onStart spans for all Activities ([#4025](https://github.com/getsentry/sentry-java/pull/4025)) -- Add split apks info to the `App` context ([#3193](https://github.com/getsentry/sentry-java/pull/3193))) +- Add split apks info to the `App` context ([#3193](https://github.com/getsentry/sentry-java/pull/3193)) +- Expose new `withSentryObservableEffect` method overload that accepts `SentryNavigationListener` as a parameter ([#4143](https://github.com/getsentry/sentry-java/pull/4143)) + - This allows sharing the same `SentryNavigationListener` instance across fragments and composables to preserve the trace ### Fixes diff --git a/sentry-compose/api/android/sentry-compose.api b/sentry-compose/api/android/sentry-compose.api index f8da7ecfb0..728e248dd3 100644 --- a/sentry-compose/api/android/sentry-compose.api +++ b/sentry-compose/api/android/sentry-compose.api @@ -18,6 +18,7 @@ public final class io/sentry/compose/SentryModifier { } public final class io/sentry/compose/SentryNavigationIntegrationKt { + public static final fun withSentryObservableEffect (Landroidx/navigation/NavHostController;Lio/sentry/android/navigation/SentryNavigationListener;Landroidx/compose/runtime/Composer;I)Landroidx/navigation/NavHostController; public static final fun withSentryObservableEffect (Landroidx/navigation/NavHostController;ZZLandroidx/compose/runtime/Composer;II)Landroidx/navigation/NavHostController; } diff --git a/sentry-compose/src/androidMain/kotlin/io/sentry/compose/SentryNavigationIntegration.kt b/sentry-compose/src/androidMain/kotlin/io/sentry/compose/SentryNavigationIntegration.kt index 4af368f065..0246a5b991 100644 --- a/sentry-compose/src/androidMain/kotlin/io/sentry/compose/SentryNavigationIntegration.kt +++ b/sentry-compose/src/androidMain/kotlin/io/sentry/compose/SentryNavigationIntegration.kt @@ -48,30 +48,22 @@ internal class SentryLifecycleObserver( * A [DisposableEffect] that captures a [Breadcrumb] and starts an [ITransaction] and sends * them to Sentry for every navigation event when being attached to the respective [NavHostController]. * - * @param enableNavigationBreadcrumbs Whether the integration should capture breadcrumbs for - * navigation events. - * @param enableNavigationTracing Whether the integration should start a new [ITransaction] - * with [SentryOptions.idleTimeout] for navigation events. + * @param navListener An instance of a [SentryNavigationListener] that is shared with other sentry integrations, like + * the fragment navigation integration. */ @Composable @NonRestartableComposable public fun NavHostController.withSentryObservableEffect( - enableNavigationBreadcrumbs: Boolean = true, - enableNavigationTracing: Boolean = true + navListener: SentryNavigationListener ): NavHostController { - val enableBreadcrumbsSnapshot by rememberUpdatedState(enableNavigationBreadcrumbs) - val enableTracingSnapshot by rememberUpdatedState(enableNavigationTracing) + val navListenerSnapshot by rememberUpdatedState(navListener) // As described in https://developer.android.com/codelabs/jetpack-compose-advanced-state-side-effects#6 val lifecycle = LocalLifecycleOwner.current.lifecycle DisposableEffect(lifecycle, this) { val observer = SentryLifecycleObserver( this@withSentryObservableEffect, - navListener = SentryNavigationListener( - enableNavigationBreadcrumbs = enableBreadcrumbsSnapshot, - enableNavigationTracing = enableTracingSnapshot, - traceOriginAppendix = TRACE_ORIGIN_APPENDIX - ) + navListener = navListenerSnapshot ) lifecycle.addObserver(observer) @@ -84,6 +76,34 @@ public fun NavHostController.withSentryObservableEffect( return this } +/** + * A [DisposableEffect] that captures a [Breadcrumb] and starts an [ITransaction] and sends + * them to Sentry for every navigation event when being attached to the respective [NavHostController]. + * This version of withSentryObservableEffect should be used if you are working purely with Compose. + * + * @param enableNavigationBreadcrumbs Whether the integration should capture breadcrumbs for + * navigation events. + * @param enableNavigationTracing Whether the integration should start a new [ITransaction] + * with [SentryOptions.idleTimeout] for navigation events. + */ +@Composable +@NonRestartableComposable +public fun NavHostController.withSentryObservableEffect( + enableNavigationBreadcrumbs: Boolean = true, + enableNavigationTracing: Boolean = true +): NavHostController { + val enableBreadcrumbsSnapshot by rememberUpdatedState(enableNavigationBreadcrumbs) + val enableTracingSnapshot by rememberUpdatedState(enableNavigationTracing) + + return withSentryObservableEffect( + navListener = SentryNavigationListener( + enableNavigationBreadcrumbs = enableBreadcrumbsSnapshot, + enableNavigationTracing = enableTracingSnapshot, + traceOriginAppendix = TRACE_ORIGIN_APPENDIX + ) + ) +} + /** * A [DisposableEffect] that captures a [Breadcrumb] and starts an [ITransaction] and sends * them to Sentry for every navigation event when being attached to the respective [NavHostController]. From 95020ab9497f96b8f000386f422630ee4459c812 Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Fri, 7 Feb 2025 11:10:46 +0100 Subject: [PATCH 14/27] Cherry-pick: Modifier.sentryTag uses Modifier.Node (#4029) (#4144) * Modifier.sentryTag uses Modifier.Node (#4029) * Modifier.sentryTag uses Modifier.Node * Update Changelog * Add UI test for SentryModifier * Make sentrymodifier a robolectric test * Remove redundant dep --------- Co-authored-by: Markus Hintersteiner Co-authored-by: Roman Zavarnitsyn * Fix compose test version --------- Co-authored-by: Richard Z Co-authored-by: Markus Hintersteiner --- CHANGELOG.md | 2 + buildSrc/src/main/java/Config.kt | 1 + sentry-compose/build.gradle.kts | 5 ++ .../io/sentry/compose/SentryModifier.kt | 41 +++++++++++-- .../compose/SentryModifierComposeTest.kt | 59 +++++++++++++++++++ 5 files changed, 102 insertions(+), 6 deletions(-) create mode 100644 sentry-compose/src/androidUnitTest/kotlin/io/sentry/compose/SentryModifierComposeTest.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 326043e723..9b6e1d8b5a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,8 @@ - Fix `IllegalStateException` when registering `onDrawListener` - Fix SIGABRT native crashes on Motorola devices when encoding a video - Mention javadoc and sources for published artifacts in Gradle `.module` metadata ([#3936](https://github.com/getsentry/sentry-java/pull/3936)) +- (Jetpack Compose) Modifier.sentryTag now uses Modifier.Node ([#4029](https://github.com/getsentry/sentry-java/pull/4029)) + - This allows Composables that use this modifier to be skippable ### Dependencies diff --git a/buildSrc/src/main/java/Config.kt b/buildSrc/src/main/java/Config.kt index f508b21b1d..a8c3f760ea 100644 --- a/buildSrc/src/main/java/Config.kt +++ b/buildSrc/src/main/java/Config.kt @@ -208,6 +208,7 @@ object Config { val javaFaker = "com.github.javafaker:javafaker:1.0.2" val msgpack = "org.msgpack:msgpack-core:0.9.8" val leakCanaryInstrumentation = "com.squareup.leakcanary:leakcanary-android-instrumentation:2.14" + val composeUiTestJunit4 = "androidx.compose.ui:ui-test-junit4:1.6.8" } object QualityPlugins { diff --git a/sentry-compose/build.gradle.kts b/sentry-compose/build.gradle.kts index 597a619154..e782807a26 100644 --- a/sentry-compose/build.gradle.kts +++ b/sentry-compose/build.gradle.kts @@ -60,6 +60,11 @@ kotlin { implementation(Config.TestLibs.mockitoKotlin) implementation(Config.TestLibs.mockitoInline) implementation(Config.Libs.composeNavigation) + implementation(Config.TestLibs.robolectric) + implementation(Config.TestLibs.androidxRunner) + implementation(Config.TestLibs.androidxJunit) + implementation(Config.TestLibs.androidxTestRules) + implementation(Config.TestLibs.composeUiTestJunit4) } } } diff --git a/sentry-compose/src/androidMain/kotlin/io/sentry/compose/SentryModifier.kt b/sentry-compose/src/androidMain/kotlin/io/sentry/compose/SentryModifier.kt index f1f43c9c8b..39ac321661 100644 --- a/sentry-compose/src/androidMain/kotlin/io/sentry/compose/SentryModifier.kt +++ b/sentry-compose/src/androidMain/kotlin/io/sentry/compose/SentryModifier.kt @@ -1,8 +1,13 @@ package io.sentry.compose import androidx.compose.ui.Modifier +import androidx.compose.ui.node.ModifierNodeElement +import androidx.compose.ui.node.SemanticsModifierNode +import androidx.compose.ui.platform.InspectorInfo +import androidx.compose.ui.semantics.SemanticsConfiguration +import androidx.compose.ui.semantics.SemanticsModifier import androidx.compose.ui.semantics.SemanticsPropertyKey -import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.SemanticsPropertyReceiver public object SentryModifier { @@ -19,11 +24,35 @@ public object SentryModifier { ) @JvmStatic - public fun Modifier.sentryTag(tag: String): Modifier { - return semantics( - properties = { - this[SentryTag] = tag + public fun Modifier.sentryTag(tag: String): Modifier = + this then SentryTagModifierNodeElement(tag) + + private data class SentryTagModifierNodeElement(val tag: String) : + ModifierNodeElement(), SemanticsModifier { + + override val semanticsConfiguration: SemanticsConfiguration = + SemanticsConfiguration().also { + it[SentryTag] = tag } - ) + + override fun create(): SentryTagModifierNode = SentryTagModifierNode(tag) + + override fun update(node: SentryTagModifierNode) { + node.tag = tag + } + + override fun InspectorInfo.inspectableProperties() { + name = "sentryTag" + properties["tag"] = tag + } + } + + private class SentryTagModifierNode(var tag: String) : + Modifier.Node(), + SemanticsModifierNode { + + override fun SemanticsPropertyReceiver.applySemantics() { + this[SentryTag] = tag + } } } diff --git a/sentry-compose/src/androidUnitTest/kotlin/io/sentry/compose/SentryModifierComposeTest.kt b/sentry-compose/src/androidUnitTest/kotlin/io/sentry/compose/SentryModifierComposeTest.kt new file mode 100644 index 0000000000..38aa2585d3 --- /dev/null +++ b/sentry-compose/src/androidUnitTest/kotlin/io/sentry/compose/SentryModifierComposeTest.kt @@ -0,0 +1,59 @@ +package io.sentry.compose + +import android.app.Application +import android.content.ComponentName +import androidx.activity.ComponentActivity +import androidx.compose.foundation.layout.Box +import androidx.compose.ui.Modifier +import androidx.compose.ui.test.SemanticsMatcher +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.sentry.compose.SentryModifier.sentryTag +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TestWatcher +import org.junit.runner.Description +import org.junit.runner.RunWith +import org.robolectric.Shadows +import org.robolectric.annotation.Config + +@RunWith(AndroidJUnit4::class) +@Config(sdk = [30]) +class SentryModifierComposeTest { + + companion object { + private const val TAG_VALUE = "ExampleTagValue" + } + + // workaround for robolectric tests with composeRule + // from https://github.com/robolectric/robolectric/pull/4736#issuecomment-1831034882 + @get:Rule(order = 1) + val addActivityToRobolectricRule = object : TestWatcher() { + override fun starting(description: Description?) { + super.starting(description) + val appContext: Application = ApplicationProvider.getApplicationContext() + Shadows.shadowOf(appContext.packageManager).addActivityIfNotPresent( + ComponentName( + appContext.packageName, + ComponentActivity::class.java.name + ) + ) + } + } + + @get:Rule(order = 2) + val rule = createComposeRule() + + @Test + fun sentryModifierAppliesTag() { + rule.setContent { + Box(modifier = Modifier.sentryTag(TAG_VALUE)) + } + rule.onNode( + SemanticsMatcher(TAG_VALUE) { + it.config.find { (key, _) -> key.name == SentryModifier.TAG }?.value == TAG_VALUE + } + ).assertExists() + } +} From 6cd406da2503830b4afdcbbda46b8e1abfa6bffd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Feb 2025 16:03:48 +0100 Subject: [PATCH 15/27] Bump actions/create-github-app-token from 1.11.2 to 1.11.3 (#4151) Bumps [actions/create-github-app-token](https://github.com/actions/create-github-app-token) from 1.11.2 to 1.11.3. - [Release notes](https://github.com/actions/create-github-app-token/releases) - [Commits](https://github.com/actions/create-github-app-token/compare/136412a57a7081aa63c935a2cc2918f76c34f514...67e27a7eb7db372a1c61a7f9bdab8699e9ee57f7) --- updated-dependencies: - dependency-name: actions/create-github-app-token dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6b473ad5ed..5f8ddf9f10 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -19,7 +19,7 @@ jobs: steps: - name: Get auth token id: token - uses: actions/create-github-app-token@136412a57a7081aa63c935a2cc2918f76c34f514 # v1.11.2 + uses: actions/create-github-app-token@67e27a7eb7db372a1c61a7f9bdab8699e9ee57f7 # v1.11.3 with: app-id: ${{ vars.SENTRY_RELEASE_BOT_CLIENT_ID }} private-key: ${{ secrets.SENTRY_RELEASE_BOT_PRIVATE_KEY }} From 33f1664aa648300e04e98a274ed1acfe14336857 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Feb 2025 15:25:40 +0000 Subject: [PATCH 16/27] Bump github/codeql-action from 3.28.8 to 3.28.9 (#4149) Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.28.8 to 3.28.9. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/dd746615b3b9d728a6a37ca2045b68ca76d4841a...9e8d0789d4a0fa9ceb6b1738f7e269594bdd67f0) --- updated-dependencies: - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/codeql-analysis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 98cc67159a..7652d0ff64 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -40,7 +40,7 @@ jobs: cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }} - name: Initialize CodeQL - uses: github/codeql-action/init@dd746615b3b9d728a6a37ca2045b68ca76d4841a # pin@v2 + uses: github/codeql-action/init@9e8d0789d4a0fa9ceb6b1738f7e269594bdd67f0 # pin@v2 with: languages: 'java' @@ -49,4 +49,4 @@ jobs: ./gradlew buildForCodeQL - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@dd746615b3b9d728a6a37ca2045b68ca76d4841a # pin@v2 + uses: github/codeql-action/analyze@9e8d0789d4a0fa9ceb6b1738f7e269594bdd67f0 # pin@v2 From f4162ef9982e0e4bd860c1ecd415fc61fa885b05 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Feb 2025 15:29:57 +0000 Subject: [PATCH 17/27] Bump gradle/actions from 6962c6c931ff9effc947259cc1b9c6edba90b9d3 to aa23778d2dc6f6556fcc7164e99babbd8c3134e4 (#4150) Bumps [gradle/actions](https://github.com/gradle/actions) from 6962c6c931ff9effc947259cc1b9c6edba90b9d3 to aa23778d2dc6f6556fcc7164e99babbd8c3134e4. - [Release notes](https://github.com/gradle/actions/releases) - [Commits](https://github.com/gradle/actions/compare/6962c6c931ff9effc947259cc1b9c6edba90b9d3...aa23778d2dc6f6556fcc7164e99babbd8c3134e4) --- updated-dependencies: - dependency-name: gradle/actions dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/agp-matrix.yml | 2 +- .github/workflows/build.yml | 2 +- .github/workflows/codeql-analysis.yml | 2 +- .github/workflows/enforce-license-compliance.yml | 2 +- .github/workflows/generate-javadocs.yml | 2 +- .github/workflows/integration-tests-benchmarks.yml | 4 ++-- .github/workflows/integration-tests-ui-critical.yml | 2 +- .github/workflows/integration-tests-ui.yml | 2 +- .github/workflows/release-build.yml | 2 +- .github/workflows/system-tests-backend.yml | 2 +- 10 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/agp-matrix.yml b/.github/workflows/agp-matrix.yml index 75253e7251..99dd460a56 100644 --- a/.github/workflows/agp-matrix.yml +++ b/.github/workflows/agp-matrix.yml @@ -39,7 +39,7 @@ jobs: java-version: '17' - name: Setup Gradle - uses: gradle/actions/setup-gradle@6962c6c931ff9effc947259cc1b9c6edba90b9d3 # pin@v3 + uses: gradle/actions/setup-gradle@aa23778d2dc6f6556fcc7164e99babbd8c3134e4 # pin@v3 with: gradle-home-cache-cleanup: true cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }} diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b83f9de0b1..7de498e3e5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -30,7 +30,7 @@ jobs: java-version: '17' - name: Setup Gradle - uses: gradle/actions/setup-gradle@6962c6c931ff9effc947259cc1b9c6edba90b9d3 # pin@v3 + uses: gradle/actions/setup-gradle@aa23778d2dc6f6556fcc7164e99babbd8c3134e4 # pin@v3 with: gradle-home-cache-cleanup: true cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }} diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 7652d0ff64..39dfbdb090 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -34,7 +34,7 @@ jobs: java-version: '17' - name: Setup Gradle - uses: gradle/actions/setup-gradle@6962c6c931ff9effc947259cc1b9c6edba90b9d3 # pin@v3 + uses: gradle/actions/setup-gradle@aa23778d2dc6f6556fcc7164e99babbd8c3134e4 # pin@v3 with: gradle-home-cache-cleanup: true cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }} diff --git a/.github/workflows/enforce-license-compliance.yml b/.github/workflows/enforce-license-compliance.yml index f7e1aad059..6b793131f3 100644 --- a/.github/workflows/enforce-license-compliance.yml +++ b/.github/workflows/enforce-license-compliance.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Setup Gradle - uses: gradle/actions/setup-gradle@6962c6c931ff9effc947259cc1b9c6edba90b9d3 # pin@v3 + uses: gradle/actions/setup-gradle@aa23778d2dc6f6556fcc7164e99babbd8c3134e4 # pin@v3 with: gradle-home-cache-cleanup: true diff --git a/.github/workflows/generate-javadocs.yml b/.github/workflows/generate-javadocs.yml index 286df36dfe..62c8c30181 100644 --- a/.github/workflows/generate-javadocs.yml +++ b/.github/workflows/generate-javadocs.yml @@ -20,7 +20,7 @@ jobs: java-version: '17' - name: Setup Gradle - uses: gradle/actions/setup-gradle@6962c6c931ff9effc947259cc1b9c6edba90b9d3 # pin@v3 + uses: gradle/actions/setup-gradle@aa23778d2dc6f6556fcc7164e99babbd8c3134e4 # pin@v3 with: gradle-home-cache-cleanup: true diff --git a/.github/workflows/integration-tests-benchmarks.yml b/.github/workflows/integration-tests-benchmarks.yml index 8d0e0381d6..de40849718 100644 --- a/.github/workflows/integration-tests-benchmarks.yml +++ b/.github/workflows/integration-tests-benchmarks.yml @@ -38,7 +38,7 @@ jobs: java-version: '17' - name: Setup Gradle - uses: gradle/actions/setup-gradle@6962c6c931ff9effc947259cc1b9c6edba90b9d3 # pin@v3 + uses: gradle/actions/setup-gradle@aa23778d2dc6f6556fcc7164e99babbd8c3134e4 # pin@v3 with: gradle-home-cache-cleanup: true cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }} @@ -89,7 +89,7 @@ jobs: java-version: '17' - name: Setup Gradle - uses: gradle/actions/setup-gradle@6962c6c931ff9effc947259cc1b9c6edba90b9d3 # pin@v3 + uses: gradle/actions/setup-gradle@aa23778d2dc6f6556fcc7164e99babbd8c3134e4 # pin@v3 with: gradle-home-cache-cleanup: true cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }} diff --git a/.github/workflows/integration-tests-ui-critical.yml b/.github/workflows/integration-tests-ui-critical.yml index 917d34732f..05cd811f5a 100644 --- a/.github/workflows/integration-tests-ui-critical.yml +++ b/.github/workflows/integration-tests-ui-critical.yml @@ -36,7 +36,7 @@ jobs: java-version: '17' - name: Setup Gradle - uses: gradle/actions/setup-gradle@6962c6c931ff9effc947259cc1b9c6edba90b9d3 # pin@v3 + uses: gradle/actions/setup-gradle@aa23778d2dc6f6556fcc7164e99babbd8c3134e4 # pin@v3 with: gradle-home-cache-cleanup: true cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }} diff --git a/.github/workflows/integration-tests-ui.yml b/.github/workflows/integration-tests-ui.yml index b50420533a..f62408e7f4 100644 --- a/.github/workflows/integration-tests-ui.yml +++ b/.github/workflows/integration-tests-ui.yml @@ -33,7 +33,7 @@ jobs: java-version: '17' - name: Setup Gradle - uses: gradle/actions/setup-gradle@6962c6c931ff9effc947259cc1b9c6edba90b9d3 # pin@v3 + uses: gradle/actions/setup-gradle@aa23778d2dc6f6556fcc7164e99babbd8c3134e4 # pin@v3 with: gradle-home-cache-cleanup: true cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }} diff --git a/.github/workflows/release-build.yml b/.github/workflows/release-build.yml index cbf6997405..975ab2ff1a 100644 --- a/.github/workflows/release-build.yml +++ b/.github/workflows/release-build.yml @@ -26,7 +26,7 @@ jobs: java-version: '17' - name: Setup Gradle - uses: gradle/actions/setup-gradle@6962c6c931ff9effc947259cc1b9c6edba90b9d3 # pin@v3 + uses: gradle/actions/setup-gradle@aa23778d2dc6f6556fcc7164e99babbd8c3134e4 # pin@v3 with: gradle-home-cache-cleanup: true diff --git a/.github/workflows/system-tests-backend.yml b/.github/workflows/system-tests-backend.yml index c5d364375e..aed5fd2864 100644 --- a/.github/workflows/system-tests-backend.yml +++ b/.github/workflows/system-tests-backend.yml @@ -56,7 +56,7 @@ jobs: java-version: '17' - name: Setup Gradle - uses: gradle/actions/setup-gradle@6962c6c931ff9effc947259cc1b9c6edba90b9d3 # pin@v3 + uses: gradle/actions/setup-gradle@aa23778d2dc6f6556fcc7164e99babbd8c3134e4 # pin@v3 with: gradle-home-cache-cleanup: true From 5e31a6b9243794a70f7b6c333c1efa4baa0154ba Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 11 Feb 2025 14:22:15 +0100 Subject: [PATCH 18/27] feat(android-ndk): add api for getting debug images by addresses (#4089) * Add fetching debug images by addresses * update test * clean up * update test * update comments * remove file * fix imports * update docs * revert doc * use hashset * Format code * update naming * apiDump * Improve Nullability, consider case of null imageSize * Add fetching debug images by addresses update test clean up update test update comments remove file fix imports update docs revert doc use hashset Format code update naming apiDump Improve Nullability, consider case of null imageSize Update tests, ensure code_file and debug_file are set * Fix test * Update javadoc * Update Changelog --------- Co-authored-by: Sentry Github Bot Co-authored-by: Markus Hintersteiner --- CHANGELOG.md | 1 + .../api/sentry-android-core.api | 1 + .../android/core/IDebugImagesLoader.java | 4 + .../android/core/NoOpDebugImagesLoader.java | 6 + .../android/core/SentryAndroidOptionsTest.kt | 2 + sentry-android-ndk/api/sentry-android-ndk.api | 1 + .../sentry/android/ndk/DebugImagesLoader.java | 93 ++++++++++++++- .../android/ndk/DebugImagesLoaderTest.kt | 110 +++++++++++++++++- 8 files changed, 214 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b6e1d8b5a..199bf7caa3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ - Add split apks info to the `App` context ([#3193](https://github.com/getsentry/sentry-java/pull/3193)) - Expose new `withSentryObservableEffect` method overload that accepts `SentryNavigationListener` as a parameter ([#4143](https://github.com/getsentry/sentry-java/pull/4143)) - This allows sharing the same `SentryNavigationListener` instance across fragments and composables to preserve the trace +- (Internal) Add API to filter native debug images based on stacktrace addresses ([#4089](https://github.com/getsentry/sentry-java/pull/4089)) ### Fixes diff --git a/sentry-android-core/api/sentry-android-core.api b/sentry-android-core/api/sentry-android-core.api index 8cabe0fd8e..ff982b51f6 100644 --- a/sentry-android-core/api/sentry-android-core.api +++ b/sentry-android-core/api/sentry-android-core.api @@ -212,6 +212,7 @@ public abstract class io/sentry/android/core/EnvelopeFileObserverIntegration : i public abstract interface class io/sentry/android/core/IDebugImagesLoader { public abstract fun clearDebugImages ()V public abstract fun loadDebugImages ()Ljava/util/List; + public abstract fun loadDebugImagesForAddresses (Ljava/util/Set;)Ljava/util/Set; } public final class io/sentry/android/core/InternalSentrySdk { diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/IDebugImagesLoader.java b/sentry-android-core/src/main/java/io/sentry/android/core/IDebugImagesLoader.java index 902f7efc2b..7b98147aab 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/IDebugImagesLoader.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/IDebugImagesLoader.java @@ -2,6 +2,7 @@ import io.sentry.protocol.DebugImage; import java.util.List; +import java.util.Set; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.Nullable; @@ -11,5 +12,8 @@ public interface IDebugImagesLoader { @Nullable List loadDebugImages(); + @Nullable + Set loadDebugImagesForAddresses(Set addresses); + void clearDebugImages(); } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/NoOpDebugImagesLoader.java b/sentry-android-core/src/main/java/io/sentry/android/core/NoOpDebugImagesLoader.java index 70451972a7..193b734219 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/NoOpDebugImagesLoader.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/NoOpDebugImagesLoader.java @@ -2,6 +2,7 @@ import io.sentry.protocol.DebugImage; import java.util.List; +import java.util.Set; import org.jetbrains.annotations.Nullable; final class NoOpDebugImagesLoader implements IDebugImagesLoader { @@ -19,6 +20,11 @@ public static NoOpDebugImagesLoader getInstance() { return null; } + @Override + public @Nullable Set loadDebugImagesForAddresses(Set addresses) { + return null; + } + @Override public void clearDebugImages() {} } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidOptionsTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidOptionsTest.kt index efef393dc7..d3e8e9a239 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidOptionsTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidOptionsTest.kt @@ -182,6 +182,8 @@ class SentryAndroidOptionsTest { private class CustomDebugImagesLoader : IDebugImagesLoader { override fun loadDebugImages(): List? = null + override fun loadDebugImagesForAddresses(addresses: Set?): Set? = null + override fun clearDebugImages() {} } } diff --git a/sentry-android-ndk/api/sentry-android-ndk.api b/sentry-android-ndk/api/sentry-android-ndk.api index 155a368b11..eb5f48a9bd 100644 --- a/sentry-android-ndk/api/sentry-android-ndk.api +++ b/sentry-android-ndk/api/sentry-android-ndk.api @@ -10,6 +10,7 @@ public final class io/sentry/android/ndk/DebugImagesLoader : io/sentry/android/c public fun (Lio/sentry/android/core/SentryAndroidOptions;Lio/sentry/ndk/NativeModuleListLoader;)V public fun clearDebugImages ()V public fun loadDebugImages ()Ljava/util/List; + public fun loadDebugImagesForAddresses (Ljava/util/Set;)Ljava/util/Set; } public final class io/sentry/android/ndk/NdkScopeObserver : io/sentry/ScopeObserverAdapter { diff --git a/sentry-android-ndk/src/main/java/io/sentry/android/ndk/DebugImagesLoader.java b/sentry-android-ndk/src/main/java/io/sentry/android/ndk/DebugImagesLoader.java index 1257325091..602981c1d0 100644 --- a/sentry-android-ndk/src/main/java/io/sentry/android/ndk/DebugImagesLoader.java +++ b/sentry-android-ndk/src/main/java/io/sentry/android/ndk/DebugImagesLoader.java @@ -10,7 +10,9 @@ import io.sentry.util.AutoClosableReentrantLock; import io.sentry.util.Objects; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; +import java.util.Set; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.VisibleForTesting; @@ -25,7 +27,7 @@ public final class DebugImagesLoader implements IDebugImagesLoader { private final @NotNull NativeModuleListLoader moduleListLoader; - private static @Nullable List debugImages; + private static volatile @Nullable List debugImages; /** we need to lock it because it could be called from different threads */ protected static final @NotNull AutoClosableReentrantLock debugImagesLock = @@ -54,6 +56,8 @@ public DebugImagesLoader( debugImages = new ArrayList<>(debugImagesArr.length); for (io.sentry.ndk.DebugImage d : debugImagesArr) { final DebugImage debugImage = new DebugImage(); + debugImage.setCodeFile(d.getCodeFile()); + debugImage.setDebugFile(d.getDebugFile()); debugImage.setUuid(d.getUuid()); debugImage.setType(d.getType()); debugImage.setDebugId(d.getDebugId()); @@ -75,7 +79,92 @@ public DebugImagesLoader( return debugImages; } - /** Clears the caching of debug images on sentry-native and here. */ + /** + * Loads debug images for the given set of addresses. + * + * @param addresses Set of memory addresses to find debug images for + * @return Set of matching debug images, or null if debug images couldn't be loaded + */ + public @Nullable Set loadDebugImagesForAddresses( + final @NotNull Set addresses) { + try (final @NotNull ISentryLifecycleToken ignored = debugImagesLock.acquire()) { + final @Nullable List allDebugImages = loadDebugImages(); + if (allDebugImages == null) { + return null; + } + if (addresses.isEmpty()) { + return null; + } + + final Set referencedImages = filterImagesByAddresses(allDebugImages, addresses); + if (referencedImages.isEmpty()) { + options + .getLogger() + .log( + SentryLevel.WARNING, + "No debug images found for any of the %d addresses.", + addresses.size()); + return null; + } + + return referencedImages; + } + } + + /** + * Finds all debug image containing the given addresses. Assumes that the images are sorted by + * address, which should always be true on Linux/Android and Windows platforms + * + * @return All matching debug images or null if none are found + */ + private @NotNull Set filterImagesByAddresses( + final @NotNull List images, final @NotNull Set addresses) { + final Set result = new HashSet<>(); + + for (int i = 0; i < images.size(); i++) { + final @NotNull DebugImage image = images.get(i); + final @Nullable DebugImage nextDebugImage = + (i + 1) < images.size() ? images.get(i + 1) : null; + final @Nullable String nextDebugImageAddress = + nextDebugImage != null ? nextDebugImage.getImageAddr() : null; + + for (final @NotNull String rawAddress : addresses) { + try { + final long address = Long.parseLong(rawAddress.replace("0x", ""), 16); + + final @Nullable String imageAddress = image.getImageAddr(); + if (imageAddress != null) { + try { + final long imageStart = Long.parseLong(imageAddress.replace("0x", ""), 16); + final long imageEnd; + + final @Nullable Long imageSize = image.getImageSize(); + if (imageSize != null) { + imageEnd = imageStart + imageSize; + } else if (nextDebugImageAddress != null) { + imageEnd = Long.parseLong(nextDebugImageAddress.replace("0x", ""), 16); + } else { + imageEnd = Long.MAX_VALUE; + } + if (address >= imageStart && address < imageEnd) { + result.add(image); + // once image is added we can skip the remaining addresses and go straight to the + // next + // image + break; + } + } catch (NumberFormatException e) { + // ignored, invalid debug image address + } + } + } catch (NumberFormatException e) { + // ignored, invalid address supplied + } + } + } + return result; + } + @Override public void clearDebugImages() { try (final @NotNull ISentryLifecycleToken ignored = debugImagesLock.acquire()) { diff --git a/sentry-android-ndk/src/test/java/io/sentry/android/ndk/DebugImagesLoaderTest.kt b/sentry-android-ndk/src/test/java/io/sentry/android/ndk/DebugImagesLoaderTest.kt index 927ce98c3b..912a2911c5 100644 --- a/sentry-android-ndk/src/test/java/io/sentry/android/ndk/DebugImagesLoaderTest.kt +++ b/sentry-android-ndk/src/test/java/io/sentry/android/ndk/DebugImagesLoaderTest.kt @@ -6,6 +6,7 @@ import org.mockito.kotlin.mock import org.mockito.kotlin.verify import org.mockito.kotlin.whenever import kotlin.test.Test +import kotlin.test.assertEquals import kotlin.test.assertNotNull import kotlin.test.assertNull import kotlin.test.assertTrue @@ -16,11 +17,13 @@ class DebugImagesLoaderTest { val options = SentryAndroidOptions() fun getSut(): DebugImagesLoader { - return DebugImagesLoader(options, nativeLoader) + val loader = DebugImagesLoader(options, nativeLoader) + loader.clearDebugImages() + return loader } } - private val fixture = Fixture() + private var fixture = Fixture() @Test fun `get images returns image list`() { @@ -77,4 +80,107 @@ class DebugImagesLoaderTest { assertNull(sut.cachedDebugImages) } + + @Test + fun `find images by address`() { + val sut = fixture.getSut() + + val image1 = io.sentry.ndk.DebugImage().apply { + imageAddr = "0x1000" + imageSize = 0x1000L + } + + val image2 = io.sentry.ndk.DebugImage().apply { + imageAddr = "0x2000" + imageSize = 0x1000L + } + + val image3 = io.sentry.ndk.DebugImage().apply { + imageAddr = "0x3000" + imageSize = 0x1000L + } + + whenever(fixture.nativeLoader.loadModuleList()).thenReturn(arrayOf(image1, image2, image3)) + + val result = sut.loadDebugImagesForAddresses( + setOf("0x1500", "0x2500") + ) + + assertNotNull(result) + assertEquals(2, result.size) + assertTrue(result.any { it.imageAddr == image1.imageAddr }) + assertTrue(result.any { it.imageAddr == image2.imageAddr }) + } + + @Test + fun `find images with invalid addresses are not added to the result`() { + val sut = fixture.getSut() + + val image1 = io.sentry.ndk.DebugImage().apply { + imageAddr = "0x1000" + imageSize = 0x1000L + } + + val image2 = io.sentry.ndk.DebugImage().apply { + imageAddr = "0x2000" + imageSize = 0x1000L + } + + whenever(fixture.nativeLoader.loadModuleList()).thenReturn(arrayOf(image1, image2)) + + val hexAddresses = setOf("0xINVALID", "0x1500") + val result = sut.loadDebugImagesForAddresses(hexAddresses) + + assertEquals(1, result!!.size) + } + + @Test + fun `find images by address returns null if result is empty`() { + val sut = fixture.getSut() + + val image1 = io.sentry.ndk.DebugImage().apply { + imageAddr = "0x1000" + imageSize = 0x1000L + } + + val image2 = io.sentry.ndk.DebugImage().apply { + imageAddr = "0x2000" + imageSize = 0x1000L + } + + whenever(fixture.nativeLoader.loadModuleList()).thenReturn(arrayOf(image1, image2)) + + val hexAddresses = setOf("0x100", "0x10500") + val result = sut.loadDebugImagesForAddresses(hexAddresses) + + assertNull(result) + } + + @Test + fun `invalid image adresses are ignored for loadDebugImagesForAddresses`() { + val sut = fixture.getSut() + + val image1 = io.sentry.ndk.DebugImage().apply { + imageAddr = "0xNotANumber" + imageSize = 0x1000L + } + + val image2 = io.sentry.ndk.DebugImage().apply { + imageAddr = "0x2000" + imageSize = null + } + + val image3 = io.sentry.ndk.DebugImage().apply { + imageAddr = "0x5000" + imageSize = 0x1000L + } + + whenever(fixture.nativeLoader.loadModuleList()).thenReturn(arrayOf(image1, image2, image3)) + + val hexAddresses = setOf("0x100", "0x2000", "0x2000", "0x5000") + val result = sut.loadDebugImagesForAddresses(hexAddresses) + + assertNotNull(result) + assertEquals(2, result.size) + } } From c2c78dec7b7cccc78e1d1e9d69ac0cfb20c4331e Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Tue, 11 Feb 2025 16:57:00 +0100 Subject: [PATCH 19/27] Fix config cache for build command (#4157) * Fix config cache for build command * Fix shadowing * Add gradle key for config cache in system tests * Cache buildSrc on CI --- .github/workflows/build.yml | 7 ++++++ .github/workflows/system-tests-backend.yml | 6 +++-- build.gradle.kts | 22 +++---------------- buildSrc/src/main/java/Config.kt | 20 ++++++++++++++++- buildSrc/src/main/java/Publication.kt | 16 ++++++++------ gradle.properties | 2 ++ sentry-android-core/build.gradle.kts | 4 +++- sentry-android-fragment/build.gradle.kts | 4 +++- sentry-android-navigation/build.gradle.kts | 4 +++- sentry-android-ndk/build.gradle.kts | 4 +++- sentry-android-replay/build.gradle.kts | 4 +++- sentry-android-sqlite/build.gradle.kts | 4 +++- sentry-android-timber/build.gradle.kts | 4 +++- sentry-android/build.gradle.kts | 4 +++- sentry-compose/build.gradle.kts | 5 ++++- .../build.gradle.kts | 10 ++++----- 16 files changed, 77 insertions(+), 43 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7de498e3e5..c3dea6dd3b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -29,6 +29,13 @@ jobs: distribution: 'temurin' java-version: '17' + # Workaround for https://github.com/gradle/actions/issues/21 to use config cache + - name: Cache buildSrc + uses: actions/cache@v4 + with: + path: buildSrc/build + key: build-logic-${{ hashFiles('buildSrc/src/**', 'buildSrc/build.gradle.kts','buildSrc/settings.gradle.kts') }} + - name: Setup Gradle uses: gradle/actions/setup-gradle@aa23778d2dc6f6556fcc7164e99babbd8c3134e4 # pin@v3 with: diff --git a/.github/workflows/system-tests-backend.yml b/.github/workflows/system-tests-backend.yml index aed5fd2864..0656a5331e 100644 --- a/.github/workflows/system-tests-backend.yml +++ b/.github/workflows/system-tests-backend.yml @@ -16,6 +16,7 @@ jobs: continue-on-error: true env: SENTRY_URL: http://127.0.0.1:8000 + GRADLE_ENCRYPTION_KEY: ${{ secrets.GRADLE_ENCRYPTION_KEY }} strategy: fail-fast: false matrix: @@ -59,6 +60,7 @@ jobs: uses: gradle/actions/setup-gradle@aa23778d2dc6f6556fcc7164e99babbd8c3134e4 # pin@v3 with: gradle-home-cache-cleanup: true + cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }} - name: Exclude android modules from build run: | @@ -91,11 +93,11 @@ jobs: - name: Build server jar run: | - ./gradlew :sentry-samples:${{ matrix.sample }}:bootJar --no-configuration-cache + ./gradlew :sentry-samples:${{ matrix.sample }}:bootJar - name: Build agent jar run: | - ./gradlew :sentry-opentelemetry:sentry-opentelemetry-agent:assemble --no-configuration-cache + ./gradlew :sentry-opentelemetry:sentry-opentelemetry-agent:assemble - name: Start server and run integration test for sentry-cli commands run: | diff --git a/build.gradle.kts b/build.gradle.kts index bb60529161..1b2eb5614b 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -179,8 +179,8 @@ subprojects { tasks.named("distZip").configure { this.dependsOn("publishToMavenLocal") + val file = this.project.layout.buildDirectory.file("distributions${sep}${this.project.name}-${this.project.version}.zip").get().asFile this.doLast { - val file = this.project.layout.buildDirectory.file("distributions${sep}${this.project.name}-${this.project.version}.zip").get().asFile if (!file.exists()) throw IllegalStateException("Distribution file: ${file.absolutePath} does not exist") if (file.length() == 0L) throw IllegalStateException("Distribution file: ${file.absolutePath} is empty") } @@ -300,21 +300,6 @@ gradle.taskGraph.whenReady { } } -private val androidLibs = setOf( - "sentry-android-core", - "sentry-android-ndk", - "sentry-android-fragment", - "sentry-android-navigation", - "sentry-android-timber", - "sentry-compose-android", - "sentry-android-sqlite", - "sentry-android-replay" -) - -private val androidXLibs = listOf( - "androidx.core:core" -) - /* * Adapted from https://github.com/androidx/androidx/blob/c799cba927a71f01ea6b421a8f83c181682633fb/buildSrc/private/src/main/kotlin/androidx/build/MavenUploadHelper.kt#L524-L549 * @@ -334,7 +319,6 @@ private val androidXLibs = listOf( */ // Workaround for https://github.com/gradle/gradle/issues/3170 -@Suppress("UnstableApiUsage") fun MavenPublishBaseExtension.assignAarTypes() { pom { withXml { @@ -356,9 +340,9 @@ fun MavenPublishBaseExtension.assignAarTypes() { } as? Node val artifactIdValue = artifactId?.children()?.firstOrNull() as? String - if (artifactIdValue in androidLibs) { + if (artifactIdValue in Config.BuildScript.androidLibs) { dep.appendNode("type", "aar") - } else if ("$groupValue:$artifactIdValue" in androidXLibs) { + } else if ("$groupValue:$artifactIdValue" in Config.BuildScript.androidXLibs) { dep.appendNode("type", "aar") } } diff --git a/buildSrc/src/main/java/Config.kt b/buildSrc/src/main/java/Config.kt index a8c3f760ea..0db77e349e 100644 --- a/buildSrc/src/main/java/Config.kt +++ b/buildSrc/src/main/java/Config.kt @@ -27,8 +27,9 @@ object Config { val grettyVersion = "4.0.0" val gradleMavenPublishPlugin = "com.vanniktech.maven.publish" val gradleMavenPublishPluginVersion = "0.30.0" - val dokkaPlugin = "org.jetbrains.dokka:dokka-gradle-plugin:1.9.20" + val dokkaPlugin = "org.jetbrains.dokka:dokka-gradle-plugin:2.0.0" val dokkaPluginAlias = "org.jetbrains.dokka" + val dokkaPluginJavadocAlias = "org.jetbrains.dokka-javadoc" val composeGradlePlugin = "org.jetbrains.compose:compose-gradle-plugin:$composeVersion" val commonsCompressOverride = "org.apache.commons:commons-compress:1.25.0" } @@ -272,4 +273,21 @@ object Config { val errorprone = "com.google.errorprone:error_prone_core:2.11.0" val errorProneNullAway = "com.uber.nullaway:nullaway:0.9.5" } + + object BuildScript { + val androidLibs = setOf( + "sentry-android-core", + "sentry-android-ndk", + "sentry-android-fragment", + "sentry-android-navigation", + "sentry-android-timber", + "sentry-compose-android", + "sentry-android-sqlite", + "sentry-android-replay" + ) + + val androidXLibs = listOf( + "androidx.core:core" + ) + } } diff --git a/buildSrc/src/main/java/Publication.kt b/buildSrc/src/main/java/Publication.kt index bae398e27c..0aa717a563 100644 --- a/buildSrc/src/main/java/Publication.kt +++ b/buildSrc/src/main/java/Publication.kt @@ -11,10 +11,11 @@ private object Consts { fun DistributionContainer.configureForMultiplatform(project: Project) { val sep = File.separator val version = project.properties["versionName"].toString() + val name = project.name this.maybeCreate("android").contents { from("build${sep}publications${sep}androidRelease") { - renameModule(project.name, "android", version = version) + renameModule(name, "android", version = version) } from("build${sep}outputs${sep}aar") { include("*-release*") @@ -32,7 +33,7 @@ fun DistributionContainer.configureForMultiplatform(project: Project) { } this.getByName("main").contents { from("build${sep}publications${sep}kotlinMultiplatform") { - renameModule(project.name, version = version) + renameModule(name, version = version) } from("build${sep}kotlinToolingMetadata") from("build${sep}libs") { @@ -48,7 +49,7 @@ fun DistributionContainer.configureForMultiplatform(project: Project) { this.maybeCreate("desktop").contents { // kotlin multiplatform modules from("build${sep}publications${sep}desktop") { - renameModule(project.name, "desktop", version = version) + renameModule(name, "desktop", version = version) } from("build${sep}libs") { include("*desktop*") @@ -69,27 +70,28 @@ fun DistributionContainer.configureForMultiplatform(project: Project) { fun DistributionContainer.configureForJvm(project: Project) { val sep = File.separator val version = project.properties["versionName"].toString() + val name = project.name this.getByName("main").contents { // non android modules from("build${sep}libs") from("build${sep}publications${sep}maven") { - renameModule(project.name, version = version) + renameModule(name, version = version) } // android modules from("build${sep}outputs${sep}aar") { include("*-release*") } from("build${sep}publications${sep}release") { - renameModule(project.name, version = version) + renameModule(name, version = version) } from("build${sep}intermediates${sep}java_doc_jar${sep}release") { include("*javadoc*") - rename { it.replace("release", "${project.name}-$version") } + rename { it.replace("release", "$name-$version") } } from("build${sep}intermediates${sep}source_jar${sep}release") { include("*sources*") - rename { it.replace("release", "${project.name}-$version") } + rename { it.replace("release", "$name-$version") } } } } diff --git a/gradle.properties b/gradle.properties index 39c9cb9985..0e6fa3890b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,6 +5,8 @@ org.gradle.parallel=true org.gradle.configureondemand=true org.gradle.configuration-cache=true +org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled + # Daemons workers org.gradle.workers.max=2 diff --git a/sentry-android-core/build.gradle.kts b/sentry-android-core/build.gradle.kts index 15692681cd..7203a0327f 100644 --- a/sentry-android-core/build.gradle.kts +++ b/sentry-android-core/build.gradle.kts @@ -26,7 +26,9 @@ android { } buildTypes { - getByName("debug") + getByName("debug") { + consumerProguardFiles("proguard-rules.pro") + } getByName("release") { consumerProguardFiles("proguard-rules.pro") } diff --git a/sentry-android-fragment/build.gradle.kts b/sentry-android-fragment/build.gradle.kts index 37bba3e7ca..34eaee68c6 100644 --- a/sentry-android-fragment/build.gradle.kts +++ b/sentry-android-fragment/build.gradle.kts @@ -21,7 +21,9 @@ android { } buildTypes { - getByName("debug") + getByName("debug") { + consumerProguardFiles("proguard-rules.pro") + } getByName("release") { consumerProguardFiles("proguard-rules.pro") } diff --git a/sentry-android-navigation/build.gradle.kts b/sentry-android-navigation/build.gradle.kts index ddf8e1f6c0..dd37e35b88 100644 --- a/sentry-android-navigation/build.gradle.kts +++ b/sentry-android-navigation/build.gradle.kts @@ -21,7 +21,9 @@ android { } buildTypes { - getByName("debug") + getByName("debug") { + consumerProguardFiles("proguard-rules.pro") + } getByName("release") { consumerProguardFiles("proguard-rules.pro") } diff --git a/sentry-android-ndk/build.gradle.kts b/sentry-android-ndk/build.gradle.kts index 0d75024ddc..6988c3367b 100644 --- a/sentry-android-ndk/build.gradle.kts +++ b/sentry-android-ndk/build.gradle.kts @@ -26,7 +26,9 @@ android { } buildTypes { - getByName("debug") + getByName("debug") { + consumerProguardFiles("proguard-rules.pro") + } getByName("release") { consumerProguardFiles("proguard-rules.pro") } diff --git a/sentry-android-replay/build.gradle.kts b/sentry-android-replay/build.gradle.kts index 45a51906eb..655247f41f 100644 --- a/sentry-android-replay/build.gradle.kts +++ b/sentry-android-replay/build.gradle.kts @@ -35,7 +35,9 @@ android { } buildTypes { - getByName("debug") + getByName("debug") { + consumerProguardFiles("proguard-rules.pro") + } getByName("release") { consumerProguardFiles("proguard-rules.pro") } diff --git a/sentry-android-sqlite/build.gradle.kts b/sentry-android-sqlite/build.gradle.kts index fe983f1dd8..7053fa70e3 100644 --- a/sentry-android-sqlite/build.gradle.kts +++ b/sentry-android-sqlite/build.gradle.kts @@ -22,7 +22,9 @@ android { } buildTypes { - getByName("debug") + getByName("debug") { + consumerProguardFiles("proguard-rules.pro") + } getByName("release") { consumerProguardFiles("proguard-rules.pro") } diff --git a/sentry-android-timber/build.gradle.kts b/sentry-android-timber/build.gradle.kts index 8f8afafd79..c2145cdfa1 100644 --- a/sentry-android-timber/build.gradle.kts +++ b/sentry-android-timber/build.gradle.kts @@ -25,7 +25,9 @@ android { } buildTypes { - getByName("debug") + getByName("debug") { + consumerProguardFiles("proguard-rules.pro") + } getByName("release") { consumerProguardFiles("proguard-rules.pro") } diff --git a/sentry-android/build.gradle.kts b/sentry-android/build.gradle.kts index 43d07bd44d..bcc214ae47 100644 --- a/sentry-android/build.gradle.kts +++ b/sentry-android/build.gradle.kts @@ -18,7 +18,9 @@ android { } buildTypes { - getByName("debug") + getByName("debug") { + consumerProguardFiles("proguard-rules.pro") + } getByName("release") { consumerProguardFiles("proguard-rules.pro") } diff --git a/sentry-compose/build.gradle.kts b/sentry-compose/build.gradle.kts index e782807a26..df8c4dae20 100644 --- a/sentry-compose/build.gradle.kts +++ b/sentry-compose/build.gradle.kts @@ -10,6 +10,7 @@ plugins { id(Config.QualityPlugins.gradleVersions) id(Config.QualityPlugins.detektPlugin) id(Config.BuildPlugins.dokkaPluginAlias) + id(Config.BuildPlugins.dokkaPluginJavadocAlias) `maven-publish` // necessary for publishMavenLocal task to publish correct artifacts } @@ -86,7 +87,9 @@ android { } buildTypes { - getByName("debug") + getByName("debug") { + consumerProguardFiles("proguard-rules.pro") + } getByName("release") { consumerProguardFiles("proguard-rules.pro") } diff --git a/sentry-opentelemetry/sentry-opentelemetry-agent/build.gradle.kts b/sentry-opentelemetry/sentry-opentelemetry-agent/build.gradle.kts index 88569f69b6..747f6eb786 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-agent/build.gradle.kts +++ b/sentry-opentelemetry/sentry-opentelemetry-agent/build.gradle.kts @@ -2,7 +2,7 @@ import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar plugins { `java-library` - id("com.github.johnrengelman.shadow") version "7.1.2" + id("com.gradleup.shadow") version "8.3.6" } fun relocatePackages(shadowJar: ShadowJar) { @@ -89,7 +89,7 @@ tasks { // building the final javaagent jar is done in 3 steps: // 1. all distro specific javaagent libs are relocated - create("relocateJavaagentLibs", ShadowJar::class.java) { + register("relocateJavaagentLibs", ShadowJar::class.java) { configurations = listOf(javaagentLibs) duplicatesStrategy = DuplicatesStrategy.FAIL @@ -114,7 +114,7 @@ tasks { // having a separate task for isolating javaagent libs is required to avoid duplicates with the upstream javaagent // duplicatesStrategy in shadowJar won't be applied when adding files with with(CopySpec) because each CopySpec has // its own duplicatesStrategy - create("isolateJavaagentLibs", Copy::class.java) { + register("isolateJavaagentLibs", Copy::class.java) { dependsOn(findByName("relocateJavaagentLibs")) with(isolateClasses(findByName("relocateJavaagentLibs")!!.outputs.files)) @@ -123,8 +123,8 @@ tasks { // 3. the relocated and isolated javaagent libs are merged together with the bootstrap libs (which undergo relocation // in this task) and the upstream javaagent jar; duplicates are removed - shadowJar { - configurations = listOf(bootstrapLibs, upstreamAgent) + named("shadowJar", ShadowJar::class) { + configurations = listOf(bootstrapLibs) + listOf(upstreamAgent) dependsOn(findByName("isolateJavaagentLibs")) from(findByName("isolateJavaagentLibs")!!.outputs) From 367d8b9d1d97030a075813ecb0543f523f3e8a12 Mon Sep 17 00:00:00 2001 From: Lorenzo Cian Date: Wed, 12 Feb 2025 11:42:38 +0100 Subject: [PATCH 20/27] Log a warning when envelope or items are dropped due to rate limiting (#4148) * Log a warning when items or whole envelope are dropped due to rate limiting * changelog --- CHANGELOG.md | 1 + .../src/main/java/io/sentry/transport/RateLimiter.java | 9 +++++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 199bf7caa3..d2254534c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ ### Fixes +- Log a warning when envelope or items are dropped due to rate limiting ([#4148](https://github.com/getsentry/sentry-java/pull/4148)) - Do not log if `OtelContextScopesStorage` cannot be found ([#4127](https://github.com/getsentry/sentry-java/pull/4127)) - Previously `java.lang.ClassNotFoundException: io.sentry.opentelemetry.OtelContextScopesStorage` was shown in the log if the class could not be found. - This is just a lookup the SDK performs to configure itself. The SDK also works without OpenTelemetry. diff --git a/sentry/src/main/java/io/sentry/transport/RateLimiter.java b/sentry/src/main/java/io/sentry/transport/RateLimiter.java index 3fc8293bf1..4e667e97a7 100644 --- a/sentry/src/main/java/io/sentry/transport/RateLimiter.java +++ b/sentry/src/main/java/io/sentry/transport/RateLimiter.java @@ -75,7 +75,10 @@ public RateLimiter(final @NotNull SentryOptions options) { if (dropItems != null) { options .getLogger() - .log(SentryLevel.INFO, "%d items will be dropped due rate limiting.", dropItems.size()); + .log( + SentryLevel.WARNING, + "%d envelope items will be dropped due rate limiting.", + dropItems.size()); // Need a new envelope List toSend = new ArrayList<>(); @@ -87,7 +90,9 @@ public RateLimiter(final @NotNull SentryOptions options) { // no reason to continue if (toSend.isEmpty()) { - options.getLogger().log(SentryLevel.INFO, "Envelope discarded due all items rate limited."); + options + .getLogger() + .log(SentryLevel.WARNING, "Envelope discarded due all items rate limited."); markHintWhenSendingFailed(hint, false); return null; From dc851689e543dcf525ea1bd52644b650250f49b0 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Wed, 12 Feb 2025 17:50:48 +0100 Subject: [PATCH 21/27] Propagate sampling random value (#4153) * wip * wip2 * wip3 * format + api * cleanup * revert change to demo * make baggage final again * remove ObjectToString suppressing * remove outdated comment * remove getPropagationContext from Scopes again * remove noop baggage and propagation context again * fix outbox sender; test; cleanup api file * review changes * fix test for old span processor * review feedback * Format code * changelog --------- Co-authored-by: Sentry Github Bot --- CHANGELOG.md | 2 + .../api/sentry-opentelemetry-bootstrap.api | 1 + .../InternalSemanticAttributes.java | 2 + .../sentry/opentelemetry/OtelSpanFactory.java | 2 + .../opentelemetry/OtelSamplingUtil.java | 7 +- .../opentelemetry/OtelSentryPropagator.java | 24 +-- .../OtelSentrySpanProcessor.java | 7 +- .../sentry/opentelemetry/OtelSpanWrapper.java | 24 ++- .../sentry/opentelemetry/SentrySampler.java | 3 +- .../opentelemetry/SentrySamplingResult.java | 1 + .../opentelemetry/SentrySpanExporter.java | 1 + .../test/kotlin/SentrySpanProcessorTest.kt | 6 +- sentry/api/sentry.api | 21 ++- sentry/src/main/java/io/sentry/Baggage.java | 82 ++++++++-- .../java/io/sentry/CombinedScopeView.java | 1 + .../src/main/java/io/sentry/OutboxSender.java | 11 +- .../java/io/sentry/PropagationContext.java | 27 ++-- .../main/java/io/sentry/SamplingContext.java | 21 +++ sentry/src/main/java/io/sentry/Scopes.java | 21 ++- sentry/src/main/java/io/sentry/Sentry.java | 4 +- .../src/main/java/io/sentry/SentryTracer.java | 30 ++-- .../src/main/java/io/sentry/TraceContext.java | 46 +++++- .../main/java/io/sentry/TracesSampler.java | 42 ++--- .../io/sentry/TracesSamplingDecision.java | 24 ++- .../java/io/sentry/TransactionContext.java | 30 ++-- .../java/io/sentry/util/SampleRateUtils.java | 38 +++++ .../java/io/sentry/util/TracingUtils.java | 79 ++++++++-- sentry/src/test/java/io/sentry/BaggageTest.kt | 61 +++++++- .../test/java/io/sentry/JsonSerializerTest.kt | 8 +- .../test/java/io/sentry/OutboxSenderTest.kt | 59 +++++++ .../java/io/sentry/PropagationContextTest.kt | 43 +++++ .../sentry/TraceContextSerializationTest.kt | 3 +- .../test/java/io/sentry/TracesSamplerTest.kt | 119 ++++++++------ .../java/io/sentry/TransactionContextTest.kt | 30 ++++ .../java/io/sentry/util/SampleRateUtilTest.kt | 61 ++++++++ .../java/io/sentry/util/TracingUtilsTest.kt | 148 ++++++++++++++++-- .../envelope-transaction-with-sample-rand.txt | 3 + .../json/sentry_envelope_header.json | 1 + .../src/test/resources/json/trace_state.json | 1 + 39 files changed, 881 insertions(+), 213 deletions(-) create mode 100644 sentry/src/test/java/io/sentry/PropagationContextTest.kt create mode 100644 sentry/src/test/resources/envelope-transaction-with-sample-rand.txt diff --git a/CHANGELOG.md b/CHANGELOG.md index d2254534c9..f681818c9b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ - Expose new `withSentryObservableEffect` method overload that accepts `SentryNavigationListener` as a parameter ([#4143](https://github.com/getsentry/sentry-java/pull/4143)) - This allows sharing the same `SentryNavigationListener` instance across fragments and composables to preserve the trace - (Internal) Add API to filter native debug images based on stacktrace addresses ([#4089](https://github.com/getsentry/sentry-java/pull/4089)) +- Propagate sampling random value ([#4153](https://github.com/getsentry/sentry-java/pull/4153)) + - The random value used for sampling traces is now sent to Sentry and attached to the `baggage` header on outgoing requests ### Fixes diff --git a/sentry-opentelemetry/sentry-opentelemetry-bootstrap/api/sentry-opentelemetry-bootstrap.api b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/api/sentry-opentelemetry-bootstrap.api index df7db47c65..adb976adc0 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-bootstrap/api/sentry-opentelemetry-bootstrap.api +++ b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/api/sentry-opentelemetry-bootstrap.api @@ -20,6 +20,7 @@ public final class io/sentry/opentelemetry/InternalSemanticAttributes { public static final field PROFILE_SAMPLED Lio/opentelemetry/api/common/AttributeKey; public static final field PROFILE_SAMPLE_RATE Lio/opentelemetry/api/common/AttributeKey; public static final field SAMPLED Lio/opentelemetry/api/common/AttributeKey; + public static final field SAMPLE_RAND Lio/opentelemetry/api/common/AttributeKey; public static final field SAMPLE_RATE Lio/opentelemetry/api/common/AttributeKey; public fun ()V } diff --git a/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/InternalSemanticAttributes.java b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/InternalSemanticAttributes.java index cb64d7bfff..4795401266 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/InternalSemanticAttributes.java +++ b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/InternalSemanticAttributes.java @@ -8,6 +8,8 @@ public final class InternalSemanticAttributes { public static final AttributeKey SAMPLED = AttributeKey.booleanKey("sentry.sampled"); public static final AttributeKey SAMPLE_RATE = AttributeKey.doubleKey("sentry.sample_rate"); + public static final AttributeKey SAMPLE_RAND = + AttributeKey.doubleKey("sentry.sample_rand"); public static final AttributeKey PARENT_SAMPLED = AttributeKey.booleanKey("sentry.parent_sampled"); public static final AttributeKey PROFILE_SAMPLED = diff --git a/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/OtelSpanFactory.java b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/OtelSpanFactory.java index 547463dcdc..7a51c3f337 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/OtelSpanFactory.java +++ b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/OtelSpanFactory.java @@ -136,6 +136,8 @@ public OtelSpanFactory() { spanBuilder.setAttribute(InternalSemanticAttributes.SAMPLED, samplingDecision.getSampled()); spanBuilder.setAttribute( InternalSemanticAttributes.SAMPLE_RATE, samplingDecision.getSampleRate()); + spanBuilder.setAttribute( + InternalSemanticAttributes.SAMPLE_RAND, samplingDecision.getSampleRand()); spanBuilder.setAttribute( InternalSemanticAttributes.PROFILE_SAMPLED, samplingDecision.getProfileSampled()); spanBuilder.setAttribute( diff --git a/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/OtelSamplingUtil.java b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/OtelSamplingUtil.java index 01f414f902..324f06b539 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/OtelSamplingUtil.java +++ b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/OtelSamplingUtil.java @@ -14,13 +14,18 @@ public final class OtelSamplingUtil { final @Nullable Boolean sampled = attributes.get(InternalSemanticAttributes.SAMPLED); if (sampled != null) { final @Nullable Double sampleRate = attributes.get(InternalSemanticAttributes.SAMPLE_RATE); + final @Nullable Double sampleRand = attributes.get(InternalSemanticAttributes.SAMPLE_RAND); final @Nullable Boolean profileSampled = attributes.get(InternalSemanticAttributes.PROFILE_SAMPLED); final @Nullable Double profileSampleRate = attributes.get(InternalSemanticAttributes.PROFILE_SAMPLE_RATE); return new TracesSamplingDecision( - sampled, sampleRate, profileSampled == null ? false : profileSampled, profileSampleRate); + sampled, + sampleRate, + sampleRand, + profileSampled == null ? false : profileSampled, + profileSampleRate); } else { return null; } diff --git a/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/OtelSentryPropagator.java b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/OtelSentryPropagator.java index c5802df245..fc2e3d426b 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/OtelSentryPropagator.java +++ b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/OtelSentryPropagator.java @@ -13,12 +13,12 @@ import io.sentry.Baggage; import io.sentry.BaggageHeader; import io.sentry.IScopes; -import io.sentry.PropagationContext; import io.sentry.ScopesAdapter; import io.sentry.Sentry; import io.sentry.SentryLevel; import io.sentry.SentryTraceHeader; import io.sentry.exception.InvalidSentryTraceHeaderException; +import io.sentry.util.TracingUtils; import java.util.Arrays; import java.util.Collection; import java.util.Collections; @@ -73,12 +73,17 @@ public void inject(final Context context, final C carrier, final TextMapSett return; } - final @NotNull SentryTraceHeader sentryTraceHeader = sentrySpan.toSentryTrace(); - setter.set(carrier, sentryTraceHeader.getName(), sentryTraceHeader.getValue()); - final @Nullable BaggageHeader baggageHeader = - sentrySpan.toBaggageHeader(Collections.emptyList()); - if (baggageHeader != null) { - setter.set(carrier, baggageHeader.getName(), baggageHeader.getValue()); + // TODO can we use traceIfAllowed? do we have the URL here? need to access span attrs + final @Nullable TracingUtils.TracingHeaders tracingHeaders = + TracingUtils.trace(scopes, Collections.emptyList(), sentrySpan); + + if (tracingHeaders != null) { + final @NotNull SentryTraceHeader sentryTraceHeader = tracingHeaders.getSentryTraceHeader(); + setter.set(carrier, sentryTraceHeader.getName(), sentryTraceHeader.getValue()); + final @Nullable BaggageHeader baggageHeader = tracingHeaders.getBaggageHeader(); + if (baggageHeader != null) { + setter.set(carrier, baggageHeader.getName(), baggageHeader.getValue()); + } } } @@ -125,11 +130,6 @@ public Context extract( .getLogger() .log(SentryLevel.DEBUG, "Continuing Sentry trace %s", sentryTraceHeader.getTraceId()); - final @NotNull PropagationContext propagationContext = - PropagationContext.fromHeaders( - scopes.getOptions().getLogger(), sentryTraceString, baggageString); - scopesToUse.getIsolationScope().setPropagationContext(propagationContext); - return modifiedContext; } catch (InvalidSentryTraceHeaderException e) { scopes diff --git a/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/OtelSentrySpanProcessor.java b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/OtelSentrySpanProcessor.java index 521bc9020c..37293569ae 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/OtelSentrySpanProcessor.java +++ b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/OtelSentrySpanProcessor.java @@ -70,15 +70,10 @@ public void onStart(final @NotNull Context parentContext, final @NotNull ReadWri baggage = baggageFromContext; } - final @Nullable Boolean baggageMutable = - otelSpan.getAttribute(InternalSemanticAttributes.BAGGAGE_MUTABLE); final @Nullable String baggageString = otelSpan.getAttribute(InternalSemanticAttributes.BAGGAGE); if (baggageString != null) { baggage = Baggage.fromHeader(baggageString); - if (baggageMutable == true) { - baggage.freeze(); - } } final @Nullable Boolean sampled = isSampled(otelSpan, samplingDecision); @@ -87,6 +82,8 @@ public void onStart(final @NotNull Context parentContext, final @NotNull ReadWri new PropagationContext( new SentryId(traceId), sentrySpanId, sentryParentSpanId, baggage, sampled); + baggage = propagationContext.getBaggage(); + updatePropagationContext(scopes, propagationContext); } diff --git a/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/OtelSpanWrapper.java b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/OtelSpanWrapper.java index 090d317485..34f2d2a4d7 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/OtelSpanWrapper.java +++ b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/OtelSpanWrapper.java @@ -64,7 +64,6 @@ public final class OtelSpanWrapper implements IOtelSpanWrapper { private final @NotNull Contexts contexts = new Contexts(); private @Nullable String transactionName; private @Nullable TransactionNameSource transactionNameSource; - private final @Nullable Baggage baggage; private final @NotNull AutoClosableReentrantLock lock = new AutoClosableReentrantLock(); private final @NotNull Map data = new ConcurrentHashMap<>(); @@ -86,17 +85,12 @@ public OtelSpanWrapper( this.scopes = Objects.requireNonNull(scopes, "scopes are required"); this.span = new WeakReference<>(span); this.startTimestamp = startTimestamp; - - if (parentSpan != null) { - this.baggage = parentSpan.getSpanContext().getBaggage(); - } else if (baggage != null) { - this.baggage = baggage; - } else { - this.baggage = null; - } - + final @Nullable Baggage baggageToUse = + baggage != null + ? baggage + : (parentSpan != null ? parentSpan.getSpanContext().getBaggage() : null); this.context = - new OtelSpanContext(span, samplingDecision, parentSpan, parentSpanId, this.baggage); + new OtelSpanContext(span, samplingDecision, parentSpan, parentSpanId, baggageToUse); } @Override @@ -207,15 +201,16 @@ public OtelSpanWrapper( @Override public @Nullable TraceContext traceContext() { if (scopes.getOptions().isTraceSampling()) { + final @Nullable Baggage baggage = context.getBaggage(); if (baggage != null) { - updateBaggageValues(); + updateBaggageValues(baggage); return baggage.toTraceContext(); } } return null; } - private void updateBaggageValues() { + private void updateBaggageValues(final @NotNull Baggage baggage) { try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { if (baggage != null && baggage.isMutable()) { final AtomicReference replayIdAtomicReference = new AtomicReference<>(); @@ -238,8 +233,9 @@ private void updateBaggageValues() { @Override public @Nullable BaggageHeader toBaggageHeader(@Nullable List thirdPartyBaggageHeaders) { if (scopes.getOptions().isTraceSampling()) { + final @Nullable Baggage baggage = context.getBaggage(); if (baggage != null) { - updateBaggageValues(); + updateBaggageValues(baggage); return BaggageHeader.fromBaggageAndOutgoingHeader(baggage, thirdPartyBaggageHeaders); } } diff --git a/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SentrySampler.java b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SentrySampler.java index 6f35fcb9c5..8949932129 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SentrySampler.java +++ b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SentrySampler.java @@ -95,7 +95,8 @@ public SamplingResult shouldSample( scopes .getOptions() .getInternalTracesSampler() - .sample(new SamplingContext(transactionContext, null)); + .sample( + new SamplingContext(transactionContext, null, propagationContext.getSampleRand())); if (!sentryDecision.getSampled()) { scopes diff --git a/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SentrySamplingResult.java b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SentrySamplingResult.java index 69acf52134..e29601faf5 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SentrySamplingResult.java +++ b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SentrySamplingResult.java @@ -29,6 +29,7 @@ public Attributes getAttributes() { return Attributes.builder() .put(InternalSemanticAttributes.SAMPLED, sentryDecision.getSampled()) .put(InternalSemanticAttributes.SAMPLE_RATE, sentryDecision.getSampleRate()) + .put(InternalSemanticAttributes.SAMPLE_RAND, sentryDecision.getSampleRand()) .put(InternalSemanticAttributes.PROFILE_SAMPLED, sentryDecision.getProfileSampled()) .put(InternalSemanticAttributes.PROFILE_SAMPLE_RATE, sentryDecision.getProfileSampleRate()) .build(); diff --git a/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SentrySpanExporter.java b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SentrySpanExporter.java index 4d2e7545c6..693b94fe38 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SentrySpanExporter.java +++ b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SentrySpanExporter.java @@ -62,6 +62,7 @@ public final class SentrySpanExporter implements SpanExporter { InternalSemanticAttributes.BAGGAGE_MUTABLE.getKey(), InternalSemanticAttributes.SAMPLED.getKey(), InternalSemanticAttributes.SAMPLE_RATE.getKey(), + InternalSemanticAttributes.SAMPLE_RAND.getKey(), InternalSemanticAttributes.PROFILE_SAMPLED.getKey(), InternalSemanticAttributes.PROFILE_SAMPLE_RATE.getKey(), InternalSemanticAttributes.PARENT_SAMPLED.getKey(), diff --git a/sentry-opentelemetry/sentry-opentelemetry-core/src/test/kotlin/SentrySpanProcessorTest.kt b/sentry-opentelemetry/sentry-opentelemetry-core/src/test/kotlin/SentrySpanProcessorTest.kt index 053f80e537..f7bcd22656 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-core/src/test/kotlin/SentrySpanProcessorTest.kt +++ b/sentry-opentelemetry/sentry-opentelemetry-core/src/test/kotlin/SentrySpanProcessorTest.kt @@ -409,15 +409,15 @@ class SentrySpanProcessorTest { assertEquals("1", it.baggage?.sampleRate) assertEquals("HTTP GET", it.baggage?.transaction) assertEquals("502f25099c204a2fbf4cb16edc5975d1", it.baggage?.publicKey) + assertFalse(it.baggage!!.isMutable) } else { assertNotNull(it.baggage) assertNull(it.baggage?.traceId) assertNull(it.baggage?.sampleRate) assertNull(it.baggage?.transaction) assertNull(it.baggage?.publicKey) - assertFalse(it.baggage!!.isMutable) + assertTrue(it.baggage!!.isMutable) } - assertFalse(it.baggage!!.isMutable) }, check { assertNotNull(it.startTimestamp) @@ -434,7 +434,7 @@ class SentrySpanProcessorTest { assertEquals(otelSpan.spanContext.traceId, it.traceId.toString()) assertNull(it.parentSpanId) assertNull(it.parentSamplingDecision) - assertNull(it.baggage) + assertNotNull(it.baggage) }, check { assertNotNull(it.startTimestamp) diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 662d87c265..eb63ba38c4 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -32,7 +32,7 @@ public abstract interface class io/sentry/BackfillingEventProcessor : io/sentry/ public final class io/sentry/Baggage { public fun (Lio/sentry/Baggage;)V public fun (Lio/sentry/ILogger;)V - public fun (Ljava/util/Map;Ljava/lang/String;ZLio/sentry/ILogger;)V + public fun (Ljava/util/Map;Ljava/lang/String;ZZLio/sentry/ILogger;)V public fun freeze ()V public static fun fromEvent (Lio/sentry/SentryEvent;Lio/sentry/SentryOptions;)Lio/sentry/Baggage; public static fun fromHeader (Ljava/lang/String;)Lio/sentry/Baggage; @@ -46,6 +46,8 @@ public final class io/sentry/Baggage { public fun getPublicKey ()Ljava/lang/String; public fun getRelease ()Ljava/lang/String; public fun getReplayId ()Ljava/lang/String; + public fun getSampleRand ()Ljava/lang/String; + public fun getSampleRandDouble ()Ljava/lang/Double; public fun getSampleRate ()Ljava/lang/String; public fun getSampleRateDouble ()Ljava/lang/Double; public fun getSampled ()Ljava/lang/String; @@ -55,11 +57,14 @@ public final class io/sentry/Baggage { public fun getUnknown ()Ljava/util/Map; public fun getUserId ()Ljava/lang/String; public fun isMutable ()Z + public fun isShouldFreeze ()Z public fun set (Ljava/lang/String;Ljava/lang/String;)V public fun setEnvironment (Ljava/lang/String;)V public fun setPublicKey (Ljava/lang/String;)V public fun setRelease (Ljava/lang/String;)V public fun setReplayId (Ljava/lang/String;)V + public fun setSampleRand (Ljava/lang/String;)V + public fun setSampleRandDouble (Ljava/lang/Double;)V public fun setSampleRate (Ljava/lang/String;)V public fun setSampled (Ljava/lang/String;)V public fun setTraceId (Ljava/lang/String;)V @@ -78,6 +83,7 @@ public final class io/sentry/Baggage$DSCKeys { public static final field RELEASE Ljava/lang/String; public static final field REPLAY_ID Ljava/lang/String; public static final field SAMPLED Ljava/lang/String; + public static final field SAMPLE_RAND Ljava/lang/String; public static final field SAMPLE_RATE Ljava/lang/String; public static final field TRACE_ID Ljava/lang/String; public static final field TRANSACTION Ljava/lang/String; @@ -1947,10 +1953,10 @@ public final class io/sentry/PropagationContext { public static fun fromHeaders (Lio/sentry/SentryTraceHeader;Lio/sentry/Baggage;Lio/sentry/SpanId;)Lio/sentry/PropagationContext; public fun getBaggage ()Lio/sentry/Baggage; public fun getParentSpanId ()Lio/sentry/SpanId; + public fun getSampleRand ()Ljava/lang/Double; public fun getSpanId ()Lio/sentry/SpanId; public fun getTraceId ()Lio/sentry/protocol/SentryId; public fun isSampled ()Ljava/lang/Boolean; - public fun setBaggage (Lio/sentry/Baggage;)V public fun setParentSpanId (Lio/sentry/SpanId;)V public fun setSampled (Ljava/lang/Boolean;)V public fun setSpanId (Lio/sentry/SpanId;)V @@ -2007,7 +2013,9 @@ public final class io/sentry/RequestDetails { public final class io/sentry/SamplingContext { public fun (Lio/sentry/TransactionContext;Lio/sentry/CustomSamplingContext;)V + public fun (Lio/sentry/TransactionContext;Lio/sentry/CustomSamplingContext;Ljava/lang/Double;)V public fun getCustomSamplingContext ()Lio/sentry/CustomSamplingContext; + public fun getSampleRand ()Ljava/lang/Double; public fun getTransactionContext ()Lio/sentry/TransactionContext; } @@ -3634,6 +3642,7 @@ public final class io/sentry/TraceContext : io/sentry/JsonSerializable, io/sentr public fun getPublicKey ()Ljava/lang/String; public fun getRelease ()Ljava/lang/String; public fun getReplayId ()Lio/sentry/protocol/SentryId; + public fun getSampleRand ()Ljava/lang/String; public fun getSampleRate ()Ljava/lang/String; public fun getSampled ()Ljava/lang/String; public fun getTraceId ()Lio/sentry/protocol/SentryId; @@ -3656,6 +3665,7 @@ public final class io/sentry/TraceContext$JsonKeys { public static final field RELEASE Ljava/lang/String; public static final field REPLAY_ID Ljava/lang/String; public static final field SAMPLED Ljava/lang/String; + public static final field SAMPLE_RAND Ljava/lang/String; public static final field SAMPLE_RATE Ljava/lang/String; public static final field TRACE_ID Ljava/lang/String; public static final field TRANSACTION Ljava/lang/String; @@ -3672,8 +3682,11 @@ public final class io/sentry/TracesSamplingDecision { public fun (Ljava/lang/Boolean;)V public fun (Ljava/lang/Boolean;Ljava/lang/Double;)V public fun (Ljava/lang/Boolean;Ljava/lang/Double;Ljava/lang/Boolean;Ljava/lang/Double;)V + public fun (Ljava/lang/Boolean;Ljava/lang/Double;Ljava/lang/Double;)V + public fun (Ljava/lang/Boolean;Ljava/lang/Double;Ljava/lang/Double;Ljava/lang/Boolean;Ljava/lang/Double;)V public fun getProfileSampleRate ()Ljava/lang/Double; public fun getProfileSampled ()Ljava/lang/Boolean; + public fun getSampleRand ()Ljava/lang/Double; public fun getSampleRate ()Ljava/lang/Double; public fun getSampled ()Ljava/lang/Boolean; } @@ -6275,6 +6288,8 @@ public final class io/sentry/util/Random : java/io/Serializable { public final class io/sentry/util/SampleRateUtils { public fun ()V + public static fun backfilledSampleRand (Lio/sentry/TracesSamplingDecision;)Lio/sentry/TracesSamplingDecision; + public static fun backfilledSampleRand (Ljava/lang/Double;Ljava/lang/Double;Ljava/lang/Boolean;)Ljava/lang/Double; public static fun isValidProfilesSampleRate (Ljava/lang/Double;)Z public static fun isValidSampleRate (Ljava/lang/Double;)Z public static fun isValidTracesSampleRate (Ljava/lang/Double;)Z @@ -6310,6 +6325,8 @@ public final class io/sentry/util/StringUtils { public final class io/sentry/util/TracingUtils { public fun ()V + public static fun ensureBaggage (Lio/sentry/Baggage;Lio/sentry/TracesSamplingDecision;)Lio/sentry/Baggage; + public static fun ensureBaggage (Lio/sentry/Baggage;Ljava/lang/Boolean;Ljava/lang/Double;Ljava/lang/Double;)Lio/sentry/Baggage; public static fun isIgnored (Ljava/util/List;Ljava/lang/String;)Z public static fun maybeUpdateBaggage (Lio/sentry/IScope;Lio/sentry/SentryOptions;)Lio/sentry/PropagationContext; public static fun startNewTrace (Lio/sentry/IScopes;)V diff --git a/sentry/src/main/java/io/sentry/Baggage.java b/sentry/src/main/java/io/sentry/Baggage.java index 05a59d7053..71149a81da 100644 --- a/sentry/src/main/java/io/sentry/Baggage.java +++ b/sentry/src/main/java/io/sentry/Baggage.java @@ -35,6 +35,7 @@ public final class Baggage { final @NotNull Map keyValues; final @Nullable String thirdPartyHeader; private boolean mutable; + private boolean shouldFreeze; final @NotNull ILogger logger; @NotNull @@ -85,7 +86,7 @@ public static Baggage fromHeader( final @NotNull ILogger logger) { final @NotNull Map keyValues = new HashMap<>(); final @NotNull List thirdPartyKeyValueStrings = new ArrayList<>(); - boolean mutable = true; + boolean shouldFreeze = false; if (headerValue != null) { try { @@ -103,7 +104,20 @@ public static Baggage fromHeader( final String valueDecoded = decode(value); keyValues.put(keyDecoded, valueDecoded); - mutable = false; + + // Without ignoring SAMPLE_RAND here, we'd be freezing baggage that we're transporting + // via OTel span attributes. + // This is done when a transaction is created via Sentry API. + // In that case Baggage is created before the OTel span is created and we put it on + // the span attributes. + // It does however only contain the sample random value as its only value. + // The OTel code then uses it to create a propagation context from it and ends up + // freezing it, + // preventing outgoing requests (to other systems or Sentry) from adding info to + // baggage and only then freeze it. + if (!DSCKeys.SAMPLE_RAND.equalsIgnoreCase(key)) { + shouldFreeze = true; + } } catch (Throwable e) { logger.log( SentryLevel.ERROR, @@ -123,7 +137,12 @@ public static Baggage fromHeader( thirdPartyKeyValueStrings.isEmpty() ? null : StringUtils.join(",", thirdPartyKeyValueStrings); - return new Baggage(keyValues, thirdPartyHeader, mutable, logger); + /* + can't freeze Baggage right away as we might have to backfill sampleRand + also we don't receive sentry-trace header here or in ctor so we can't + backfill then freeze here unless we pass sentry-trace header. + */ + return new Baggage(keyValues, thirdPartyHeader, true, shouldFreeze, logger); } @ApiStatus.Internal @@ -140,6 +159,7 @@ public static Baggage fromEvent( // we don't persist sample rate baggage.setSampleRate(null); baggage.setSampled(null); + baggage.setSampleRand(null); final @Nullable Object replayId = event.getContexts().get(REPLAY_ID); if (replayId != null && !replayId.toString().equals(SentryId.EMPTY_ID.toString())) { baggage.setReplayId(replayId.toString()); @@ -152,12 +172,17 @@ public static Baggage fromEvent( @ApiStatus.Internal public Baggage(final @NotNull ILogger logger) { - this(new HashMap<>(), null, true, logger); + this(new HashMap<>(), null, true, false, logger); } @ApiStatus.Internal public Baggage(final @NotNull Baggage baggage) { - this(baggage.keyValues, baggage.thirdPartyHeader, baggage.mutable, baggage.logger); + this( + baggage.keyValues, + baggage.thirdPartyHeader, + baggage.mutable, + baggage.shouldFreeze, + baggage.logger); } @ApiStatus.Internal @@ -165,11 +190,13 @@ public Baggage( final @NotNull Map keyValues, final @Nullable String thirdPartyHeader, boolean isMutable, + boolean shouldFreeze, final @NotNull ILogger logger) { this.keyValues = keyValues; this.logger = logger; - this.mutable = isMutable; this.thirdPartyHeader = thirdPartyHeader; + this.mutable = isMutable; + this.shouldFreeze = shouldFreeze; } @ApiStatus.Internal @@ -182,6 +209,11 @@ public boolean isMutable() { return mutable; } + @ApiStatus.Internal + public boolean isShouldFreeze() { + return shouldFreeze; + } + @Nullable public String getThirdPartyHeader() { return thirdPartyHeader; @@ -335,6 +367,21 @@ public void setSampleRate(final @Nullable String sampleRate) { set(DSCKeys.SAMPLE_RATE, sampleRate); } + @ApiStatus.Internal + public @Nullable String getSampleRand() { + return get(DSCKeys.SAMPLE_RAND); + } + + @ApiStatus.Internal + public void setSampleRand(final @Nullable String sampleRand) { + set(DSCKeys.SAMPLE_RAND, sampleRand); + } + + @ApiStatus.Internal + public void setSampleRandDouble(final @Nullable Double sampleRand) { + setSampleRand(sampleRateToString(sampleRand)); + } + @ApiStatus.Internal public void setSampled(final @Nullable String sampled) { set(DSCKeys.SAMPLED, sampled); @@ -445,12 +492,20 @@ private static boolean isHighQualityTransactionName( @ApiStatus.Internal public @Nullable Double getSampleRateDouble() { - final String sampleRateString = getSampleRate(); - if (sampleRateString != null) { + return toDouble(getSampleRate()); + } + + @ApiStatus.Internal + public @Nullable Double getSampleRandDouble() { + return toDouble(getSampleRand()); + } + + private @Nullable Double toDouble(final @Nullable String stringValue) { + if (stringValue != null) { try { - double sampleRate = Double.parseDouble(sampleRateString); - if (SampleRateUtils.isValidTracesSampleRate(sampleRate, false)) { - return sampleRate; + double doubleValue = Double.parseDouble(stringValue); + if (SampleRateUtils.isValidTracesSampleRate(doubleValue, false)) { + return doubleValue; } } catch (NumberFormatException e) { return null; @@ -477,7 +532,8 @@ public TraceContext toTraceContext() { getTransaction(), getSampleRate(), getSampled(), - replayIdString == null ? null : new SentryId(replayIdString)); + replayIdString == null ? null : new SentryId(replayIdString), + getSampleRand()); traceContext.setUnknown(getUnknown()); return traceContext; } else { @@ -494,6 +550,7 @@ public static final class DSCKeys { public static final String ENVIRONMENT = "sentry-environment"; public static final String TRANSACTION = "sentry-transaction"; public static final String SAMPLE_RATE = "sentry-sample_rate"; + public static final String SAMPLE_RAND = "sentry-sample_rand"; public static final String SAMPLED = "sentry-sampled"; public static final String REPLAY_ID = "sentry-replay_id"; @@ -506,6 +563,7 @@ public static final class DSCKeys { ENVIRONMENT, TRANSACTION, SAMPLE_RATE, + SAMPLE_RAND, SAMPLED, REPLAY_ID); } diff --git a/sentry/src/main/java/io/sentry/CombinedScopeView.java b/sentry/src/main/java/io/sentry/CombinedScopeView.java index 3523afa4d3..129066450f 100644 --- a/sentry/src/main/java/io/sentry/CombinedScopeView.java +++ b/sentry/src/main/java/io/sentry/CombinedScopeView.java @@ -426,6 +426,7 @@ public void setPropagationContext(@NotNull PropagationContext propagationContext getDefaultWriteScope().setPropagationContext(propagationContext); } + @ApiStatus.Internal @Override public @NotNull PropagationContext getPropagationContext() { return getDefaultWriteScope().getPropagationContext(); diff --git a/sentry/src/main/java/io/sentry/OutboxSender.java b/sentry/src/main/java/io/sentry/OutboxSender.java index 4e223da03d..cbe4f6ee00 100644 --- a/sentry/src/main/java/io/sentry/OutboxSender.java +++ b/sentry/src/main/java/io/sentry/OutboxSender.java @@ -244,7 +244,16 @@ private void processEnvelope(final @NotNull SentryEnvelope envelope, final @NotN "Invalid sample rate parsed from TraceContext: %s", sampleRateString); } else { - return new TracesSamplingDecision(true, sampleRate); + final @Nullable String sampleRandString = traceContext.getSampleRand(); + if (sampleRandString != null) { + final Double sampleRand = Double.parseDouble(sampleRandString); + if (SampleRateUtils.isValidTracesSampleRate(sampleRand, false)) { + return new TracesSamplingDecision(true, sampleRate, sampleRand); + } + } + + return SampleRateUtils.backfilledSampleRand( + new TracesSamplingDecision(true, sampleRate)); } } catch (Exception e) { logger.log( diff --git a/sentry/src/main/java/io/sentry/PropagationContext.java b/sentry/src/main/java/io/sentry/PropagationContext.java index b0debc2a9d..791cb1d3d3 100644 --- a/sentry/src/main/java/io/sentry/PropagationContext.java +++ b/sentry/src/main/java/io/sentry/PropagationContext.java @@ -2,6 +2,7 @@ import io.sentry.exception.InvalidSentryTraceHeaderException; import io.sentry.protocol.SentryId; +import io.sentry.util.TracingUtils; import java.util.Arrays; import java.util.List; import org.jetbrains.annotations.ApiStatus; @@ -56,7 +57,7 @@ public static PropagationContext fromHeaders( private @Nullable Boolean sampled; - private @Nullable Baggage baggage; + private final @NotNull Baggage baggage; public PropagationContext() { this(new SentryId(), new SpanId(), null, null, null); @@ -67,18 +68,10 @@ public PropagationContext(final @NotNull PropagationContext propagationContext) propagationContext.getTraceId(), propagationContext.getSpanId(), propagationContext.getParentSpanId(), - cloneBaggage(propagationContext.getBaggage()), + propagationContext.getBaggage(), propagationContext.isSampled()); } - private static @Nullable Baggage cloneBaggage(final @Nullable Baggage baggage) { - if (baggage != null) { - return new Baggage(baggage); - } - - return null; - } - public PropagationContext( final @NotNull SentryId traceId, final @NotNull SpanId spanId, @@ -88,7 +81,7 @@ public PropagationContext( this.traceId = traceId; this.spanId = spanId; this.parentSpanId = parentSpanId; - this.baggage = baggage; + this.baggage = TracingUtils.ensureBaggage(baggage, sampled, null, null); this.sampled = sampled; } @@ -116,14 +109,10 @@ public void setParentSpanId(final @Nullable SpanId parentSpanId) { this.parentSpanId = parentSpanId; } - public @Nullable Baggage getBaggage() { + public @NotNull Baggage getBaggage() { return baggage; } - public void setBaggage(final @Nullable Baggage baggage) { - this.baggage = baggage; - } - public @Nullable Boolean isSampled() { return sampled; } @@ -145,4 +134,10 @@ public void setSampled(final @Nullable Boolean sampled) { spanContext.setOrigin("auto"); return spanContext; } + + public @NotNull Double getSampleRand() { + final @Nullable Double sampleRand = baggage.getSampleRandDouble(); + // should never be null since we ensure it in ctor + return sampleRand == null ? 0.0 : sampleRand; + } } diff --git a/sentry/src/main/java/io/sentry/SamplingContext.java b/sentry/src/main/java/io/sentry/SamplingContext.java index 60944fbd43..711c03e21c 100644 --- a/sentry/src/main/java/io/sentry/SamplingContext.java +++ b/sentry/src/main/java/io/sentry/SamplingContext.java @@ -1,6 +1,8 @@ package io.sentry; import io.sentry.util.Objects; +import io.sentry.util.SentryRandom; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -11,13 +13,28 @@ public final class SamplingContext { private final @NotNull TransactionContext transactionContext; private final @Nullable CustomSamplingContext customSamplingContext; + private final @NotNull Double sampleRand; + @Deprecated + @SuppressWarnings("InlineMeSuggester") + /** + * @deprecated creating a SamplingContext is something only the SDK should do + */ public SamplingContext( final @NotNull TransactionContext transactionContext, final @Nullable CustomSamplingContext customSamplingContext) { + this(transactionContext, customSamplingContext, SentryRandom.current().nextDouble()); + } + + @ApiStatus.Internal + public SamplingContext( + final @NotNull TransactionContext transactionContext, + final @Nullable CustomSamplingContext customSamplingContext, + final @NotNull Double sampleRand) { this.transactionContext = Objects.requireNonNull(transactionContext, "transactionContexts is required"); this.customSamplingContext = customSamplingContext; + this.sampleRand = sampleRand; } public @Nullable CustomSamplingContext getCustomSamplingContext() { @@ -27,4 +44,8 @@ public SamplingContext( public @NotNull TransactionContext getTransactionContext() { return transactionContext; } + + public @NotNull Double getSampleRand() { + return sampleRand; + } } diff --git a/sentry/src/main/java/io/sentry/Scopes.java b/sentry/src/main/java/io/sentry/Scopes.java index 2b0d510368..14025a1d77 100644 --- a/sentry/src/main/java/io/sentry/Scopes.java +++ b/sentry/src/main/java/io/sentry/Scopes.java @@ -857,8 +857,10 @@ public void flush(long timeoutMillis) { SentryLevel.INFO, "Tracing is disabled and this 'startTransaction' returns a no-op."); transaction = NoOpTransaction.getInstance(); } else { + final Double sampleRand = getSampleRand(transactionContext); final SamplingContext samplingContext = - new SamplingContext(transactionContext, transactionOptions.getCustomSamplingContext()); + new SamplingContext( + transactionContext, transactionOptions.getCustomSamplingContext(), sampleRand); final @NotNull TracesSampler tracesSampler = getOptions().getInternalTracesSampler(); @NotNull TracesSamplingDecision samplingDecision = tracesSampler.sample(samplingContext); transactionContext.setSamplingDecision(samplingDecision); @@ -894,6 +896,18 @@ public void flush(long timeoutMillis) { return transaction; } + private @NotNull Double getSampleRand(final @NotNull TransactionContext transactionContext) { + final @Nullable Baggage baggage = transactionContext.getBaggage(); + if (baggage != null) { + final @Nullable Double sampleRandFromBaggageMaybe = baggage.getSampleRandDouble(); + if (sampleRandFromBaggageMaybe != null) { + return sampleRandFromBaggageMaybe; + } + } + + return getCombinedScopeView().getPropagationContext().getSampleRand(); + } + @Override @ApiStatus.Internal public void setSpanContext( @@ -963,7 +977,10 @@ public void reportFullyDisplayed() { PropagationContext.fromHeaders(getOptions().getLogger(), sentryTrace, baggageHeaders); configureScope( (scope) -> { - scope.setPropagationContext(propagationContext); + scope.withPropagationContext( + oldPropagationContext -> { + scope.setPropagationContext(propagationContext); + }); }); if (getOptions().isTracingEnabled()) { return TransactionContext.fromPropagationContext(propagationContext); diff --git a/sentry/src/main/java/io/sentry/Sentry.java b/sentry/src/main/java/io/sentry/Sentry.java index 94007e42c5..bd5f296b7c 100644 --- a/sentry/src/main/java/io/sentry/Sentry.java +++ b/sentry/src/main/java/io/sentry/Sentry.java @@ -22,6 +22,7 @@ import io.sentry.util.InitUtil; import io.sentry.util.LoadClass; import io.sentry.util.Platform; +import io.sentry.util.SentryRandom; import io.sentry.util.thread.IThreadChecker; import io.sentry.util.thread.NoOpThreadChecker; import io.sentry.util.thread.ThreadChecker; @@ -458,7 +459,8 @@ private static void handleAppStartProfilingConfig( final @NotNull SentryOptions options) { TransactionContext appStartTransactionContext = new TransactionContext("app.launch", "profile"); appStartTransactionContext.setForNextAppStart(true); - SamplingContext appStartSamplingContext = new SamplingContext(appStartTransactionContext, null); + SamplingContext appStartSamplingContext = + new SamplingContext(appStartTransactionContext, null, SentryRandom.current().nextDouble()); return options.getInternalTracesSampler().sample(appStartSamplingContext); } diff --git a/sentry/src/main/java/io/sentry/SentryTracer.java b/sentry/src/main/java/io/sentry/SentryTracer.java index 65901a2e1a..36329db7a7 100644 --- a/sentry/src/main/java/io/sentry/SentryTracer.java +++ b/sentry/src/main/java/io/sentry/SentryTracer.java @@ -46,7 +46,6 @@ public final class SentryTracer implements ITransaction { private final @NotNull AtomicBoolean isIdleFinishTimerRunning = new AtomicBoolean(false); private final @NotNull AtomicBoolean isDeadlineTimerRunning = new AtomicBoolean(false); - private final @NotNull Baggage baggage; private @NotNull TransactionNameSource transactionNameSource; private final @NotNull Instrumenter instrumenter; private final @NotNull Contexts contexts = new Contexts(); @@ -81,12 +80,6 @@ public SentryTracer( this.transactionNameSource = context.getTransactionNameSource(); this.transactionOptions = transactionOptions; - if (context.getBaggage() != null) { - this.baggage = context.getBaggage(); - } else { - this.baggage = new Baggage(scopes.getOptions().getLogger()); - } - // We are currently sending the performance data only in profiles, but we are always sending // performance measurements. if (transactionPerformanceCollector != null) { @@ -642,14 +635,16 @@ public void finish(@Nullable SpanStatus status, @Nullable SentryDate finishDate) @Override public @Nullable TraceContext traceContext() { if (scopes.getOptions().isTraceSampling()) { - updateBaggageValues(); - return baggage.toTraceContext(); - } else { - return null; + final @Nullable Baggage baggage = getSpanContext().getBaggage(); + if (baggage != null) { + updateBaggageValues(baggage); + return baggage.toTraceContext(); + } } + return null; } - private void updateBaggageValues() { + private void updateBaggageValues(final @NotNull Baggage baggage) { try (final @NotNull ISentryLifecycleToken ignored = tracerLock.acquire()) { if (baggage.isMutable()) { final AtomicReference replayId = new AtomicReference<>(); @@ -672,12 +667,13 @@ private void updateBaggageValues() { @Override public @Nullable BaggageHeader toBaggageHeader(@Nullable List thirdPartyBaggageHeaders) { if (scopes.getOptions().isTraceSampling()) { - updateBaggageValues(); - - return BaggageHeader.fromBaggageAndOutgoingHeader(baggage, thirdPartyBaggageHeaders); - } else { - return null; + final @Nullable Baggage baggage = getSpanContext().getBaggage(); + if (baggage != null) { + updateBaggageValues(baggage); + return BaggageHeader.fromBaggageAndOutgoingHeader(baggage, thirdPartyBaggageHeaders); + } } + return null; } private boolean hasAllChildrenFinished() { diff --git a/sentry/src/main/java/io/sentry/TraceContext.java b/sentry/src/main/java/io/sentry/TraceContext.java index bb32022f60..b10954f528 100644 --- a/sentry/src/main/java/io/sentry/TraceContext.java +++ b/sentry/src/main/java/io/sentry/TraceContext.java @@ -19,6 +19,7 @@ public final class TraceContext implements JsonUnknown, JsonSerializable { private final @Nullable String userId; private final @Nullable String transaction; private final @Nullable String sampleRate; + private final @Nullable String sampleRand; private final @Nullable String sampled; private final @Nullable SentryId replayId; @@ -29,6 +30,11 @@ public final class TraceContext implements JsonUnknown, JsonSerializable { this(traceId, publicKey, null, null, null, null, null, null, null); } + @SuppressWarnings("InlineMeSuggester") + /** + * @deprecated please use the constructor than also takes sampleRand + */ + @Deprecated TraceContext( @NotNull SentryId traceId, @NotNull String publicKey, @@ -39,6 +45,30 @@ public final class TraceContext implements JsonUnknown, JsonSerializable { @Nullable String sampleRate, @Nullable String sampled, @Nullable SentryId replayId) { + this( + traceId, + publicKey, + release, + environment, + userId, + transaction, + sampleRate, + sampled, + replayId, + null); + } + + TraceContext( + @NotNull SentryId traceId, + @NotNull String publicKey, + @Nullable String release, + @Nullable String environment, + @Nullable String userId, + @Nullable String transaction, + @Nullable String sampleRate, + @Nullable String sampled, + @Nullable SentryId replayId, + @Nullable String sampleRand) { this.traceId = traceId; this.publicKey = publicKey; this.release = release; @@ -48,6 +78,7 @@ public final class TraceContext implements JsonUnknown, JsonSerializable { this.sampleRate = sampleRate; this.sampled = sampled; this.replayId = replayId; + this.sampleRand = sampleRand; } @SuppressWarnings("UnusedMethod") @@ -88,6 +119,10 @@ public final class TraceContext implements JsonUnknown, JsonSerializable { return sampleRate; } + public @Nullable String getSampleRand() { + return sampleRand; + } + public @Nullable String getSampled() { return sampled; } @@ -117,6 +152,7 @@ public static final class JsonKeys { public static final String USER_ID = "user_id"; public static final String TRANSACTION = "transaction"; public static final String SAMPLE_RATE = "sample_rate"; + public static final String SAMPLE_RAND = "sample_rand"; public static final String SAMPLED = "sampled"; public static final String REPLAY_ID = "replay_id"; } @@ -142,6 +178,9 @@ public void serialize(final @NotNull ObjectWriter writer, final @NotNull ILogger if (sampleRate != null) { writer.name(TraceContext.JsonKeys.SAMPLE_RATE).value(sampleRate); } + if (sampleRand != null) { + writer.name(TraceContext.JsonKeys.SAMPLE_RAND).value(sampleRand); + } if (sampled != null) { writer.name(TraceContext.JsonKeys.SAMPLED).value(sampled); } @@ -171,6 +210,7 @@ public static final class Deserializer implements JsonDeserializer String userId = null; String transaction = null; String sampleRate = null; + String sampleRand = null; String sampled = null; SentryId replayId = null; @@ -199,6 +239,9 @@ public static final class Deserializer implements JsonDeserializer case TraceContext.JsonKeys.SAMPLE_RATE: sampleRate = reader.nextStringOrNull(); break; + case TraceContext.JsonKeys.SAMPLE_RAND: + sampleRand = reader.nextStringOrNull(); + break; case TraceContext.JsonKeys.SAMPLED: sampled = reader.nextStringOrNull(); break; @@ -229,7 +272,8 @@ public static final class Deserializer implements JsonDeserializer transaction, sampleRate, sampled, - replayId); + replayId, + sampleRand); traceContext.setUnknown(unknown); reader.endObject(); return traceContext; diff --git a/sentry/src/main/java/io/sentry/TracesSampler.java b/sentry/src/main/java/io/sentry/TracesSampler.java index 3ce28ab745..b3da8d63cc 100644 --- a/sentry/src/main/java/io/sentry/TracesSampler.java +++ b/sentry/src/main/java/io/sentry/TracesSampler.java @@ -1,35 +1,27 @@ package io.sentry; import io.sentry.util.Objects; -import io.sentry.util.Random; -import io.sentry.util.SentryRandom; +import io.sentry.util.SampleRateUtils; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import org.jetbrains.annotations.TestOnly; @ApiStatus.Internal public final class TracesSampler { private final @NotNull SentryOptions options; - private final @Nullable Random random; public TracesSampler(final @NotNull SentryOptions options) { - this(Objects.requireNonNull(options, "options are required"), null); - } - - @TestOnly - TracesSampler(final @NotNull SentryOptions options, final @Nullable Random random) { - this.options = options; - this.random = random; + this.options = Objects.requireNonNull(options, "options are required"); } @SuppressWarnings("deprecation") @NotNull public TracesSamplingDecision sample(final @NotNull SamplingContext samplingContext) { + final @NotNull Double sampleRand = samplingContext.getSampleRand(); final TracesSamplingDecision samplingContextSamplingDecision = samplingContext.getTransactionContext().getSamplingDecision(); if (samplingContextSamplingDecision != null) { - return samplingContextSamplingDecision; + return SampleRateUtils.backfilledSampleRand(samplingContextSamplingDecision); } Double profilesSampleRate = null; @@ -45,7 +37,7 @@ public TracesSamplingDecision sample(final @NotNull SamplingContext samplingCont if (profilesSampleRate == null) { profilesSampleRate = options.getProfilesSampleRate(); } - Boolean profilesSampled = profilesSampleRate != null && sample(profilesSampleRate); + Boolean profilesSampled = profilesSampleRate != null && sample(profilesSampleRate, sampleRand); if (options.getTracesSampler() != null) { Double samplerResult = null; @@ -58,14 +50,18 @@ public TracesSamplingDecision sample(final @NotNull SamplingContext samplingCont } if (samplerResult != null) { return new TracesSamplingDecision( - sample(samplerResult), samplerResult, profilesSampled, profilesSampleRate); + sample(samplerResult, sampleRand), + samplerResult, + sampleRand, + profilesSampled, + profilesSampleRate); } } final TracesSamplingDecision parentSamplingDecision = samplingContext.getTransactionContext().getParentSamplingDecision(); if (parentSamplingDecision != null) { - return parentSamplingDecision; + return SampleRateUtils.backfilledSampleRand(parentSamplingDecision); } final @Nullable Double tracesSampleRateFromOptions = options.getTracesSampleRate(); @@ -76,23 +72,17 @@ public TracesSamplingDecision sample(final @NotNull SamplingContext samplingCont if (downsampledTracesSampleRate != null) { return new TracesSamplingDecision( - sample(downsampledTracesSampleRate), + sample(downsampledTracesSampleRate, sampleRand), downsampledTracesSampleRate, + sampleRand, profilesSampled, profilesSampleRate); } - return new TracesSamplingDecision(false, null, false, null); + return new TracesSamplingDecision(false, null, sampleRand, false, null); } - private boolean sample(final @NotNull Double aDouble) { - return !(aDouble < getRandom().nextDouble()); - } - - private Random getRandom() { - if (random == null) { - return SentryRandom.current(); - } - return random; + private boolean sample(final @NotNull Double sampleRate, final @NotNull Double sampleRand) { + return !(sampleRate < sampleRand); } } diff --git a/sentry/src/main/java/io/sentry/TracesSamplingDecision.java b/sentry/src/main/java/io/sentry/TracesSamplingDecision.java index 8010537d5c..e9e9a7a490 100644 --- a/sentry/src/main/java/io/sentry/TracesSamplingDecision.java +++ b/sentry/src/main/java/io/sentry/TracesSamplingDecision.java @@ -7,6 +7,7 @@ public final class TracesSamplingDecision { private final @NotNull Boolean sampled; private final @Nullable Double sampleRate; + private final @Nullable Double sampleRand; private final @NotNull Boolean profileSampled; private final @Nullable Double profileSampleRate; @@ -15,16 +16,33 @@ public TracesSamplingDecision(final @NotNull Boolean sampled) { } public TracesSamplingDecision(final @NotNull Boolean sampled, final @Nullable Double sampleRate) { - this(sampled, sampleRate, false, null); + this(sampled, sampleRate, null, false, null); } public TracesSamplingDecision( final @NotNull Boolean sampled, final @Nullable Double sampleRate, + final @Nullable Double sampleRand) { + this(sampled, sampleRate, sampleRand, false, null); + } + + public TracesSamplingDecision( + final @NotNull Boolean sampled, + final @Nullable Double sampleRate, + final @NotNull Boolean profileSampled, + final @Nullable Double profileSampleRate) { + this(sampled, sampleRate, null, profileSampled, profileSampleRate); + } + + public TracesSamplingDecision( + final @NotNull Boolean sampled, + final @Nullable Double sampleRate, + final @Nullable Double sampleRand, final @NotNull Boolean profileSampled, final @Nullable Double profileSampleRate) { this.sampled = sampled; this.sampleRate = sampleRate; + this.sampleRand = sampleRand; // A profile can be sampled only if the transaction is sampled this.profileSampled = sampled && profileSampled; this.profileSampleRate = profileSampleRate; @@ -38,6 +56,10 @@ public TracesSamplingDecision( return sampleRate; } + public @Nullable Double getSampleRand() { + return sampleRand; + } + public @NotNull Boolean getProfileSampled() { return profileSampled; } diff --git a/sentry/src/main/java/io/sentry/TransactionContext.java b/sentry/src/main/java/io/sentry/TransactionContext.java index aec0b8927d..866c1c00da 100644 --- a/sentry/src/main/java/io/sentry/TransactionContext.java +++ b/sentry/src/main/java/io/sentry/TransactionContext.java @@ -3,6 +3,7 @@ import io.sentry.protocol.SentryId; import io.sentry.protocol.TransactionNameSource; import io.sentry.util.Objects; +import io.sentry.util.TracingUtils; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -20,24 +21,14 @@ public final class TransactionContext extends SpanContext { @ApiStatus.Internal public static TransactionContext fromPropagationContext( final @NotNull PropagationContext propagationContext) { - @Nullable Boolean parentSampled = propagationContext.isSampled(); - TracesSamplingDecision samplingDecision = - parentSampled == null ? null : new TracesSamplingDecision(parentSampled); - - @Nullable Baggage baggage = propagationContext.getBaggage(); - - if (baggage != null) { - baggage.freeze(); - - Double sampleRate = baggage.getSampleRateDouble(); - if (parentSampled != null) { - if (sampleRate != null) { - samplingDecision = new TracesSamplingDecision(parentSampled.booleanValue(), sampleRate); - } else { - samplingDecision = new TracesSamplingDecision(parentSampled.booleanValue()); - } - } - } + final @Nullable Boolean parentSampled = propagationContext.isSampled(); + final @NotNull Baggage baggage = propagationContext.getBaggage(); + final @Nullable Double sampleRate = baggage.getSampleRateDouble(); + final @Nullable TracesSamplingDecision samplingDecision = + parentSampled == null + ? null + : new TracesSamplingDecision( + parentSampled, sampleRate, propagationContext.getSampleRand()); return new TransactionContext( propagationContext.getTraceId(), @@ -90,6 +81,7 @@ public TransactionContext( this.name = Objects.requireNonNull(name, "name is required"); this.transactionNameSource = transactionNameSource; this.setSamplingDecision(samplingDecision); + this.baggage = TracingUtils.ensureBaggage(null, samplingDecision); } @ApiStatus.Internal @@ -103,7 +95,7 @@ public TransactionContext( this.name = DEFAULT_TRANSACTION_NAME; this.parentSamplingDecision = parentSamplingDecision; this.transactionNameSource = DEFAULT_NAME_SOURCE; - this.baggage = baggage; + this.baggage = TracingUtils.ensureBaggage(baggage, parentSamplingDecision); } public @NotNull String getName() { diff --git a/sentry/src/main/java/io/sentry/util/SampleRateUtils.java b/sentry/src/main/java/io/sentry/util/SampleRateUtils.java index ed011ff842..225ce58a3b 100644 --- a/sentry/src/main/java/io/sentry/util/SampleRateUtils.java +++ b/sentry/src/main/java/io/sentry/util/SampleRateUtils.java @@ -1,6 +1,8 @@ package io.sentry.util; +import io.sentry.TracesSamplingDecision; import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @ApiStatus.Internal @@ -23,6 +25,42 @@ public static boolean isValidProfilesSampleRate(@Nullable Double profilesSampleR return isValidRate(profilesSampleRate, true); } + public static @NotNull Double backfilledSampleRand( + final @Nullable Double sampleRand, + final @Nullable Double sampleRate, + final @Nullable Boolean sampled) { + if (sampleRand != null) { + return sampleRand; + } + + double newSampleRand = SentryRandom.current().nextDouble(); + if (sampleRate != null && sampled != null) { + if (sampled) { + return newSampleRand * sampleRate; + } else { + return sampleRate + (newSampleRand * (1 - sampleRate)); + } + } + + return newSampleRand; + } + + public static @NotNull TracesSamplingDecision backfilledSampleRand( + final @NotNull TracesSamplingDecision samplingDecision) { + if (samplingDecision.getSampleRand() != null) { + return samplingDecision; + } + + final @NotNull Double sampleRand = + backfilledSampleRand(null, samplingDecision.getSampleRate(), samplingDecision.getSampled()); + return new TracesSamplingDecision( + samplingDecision.getSampled(), + samplingDecision.getSampleRate(), + sampleRand, + samplingDecision.getProfileSampled(), + samplingDecision.getProfileSampleRate()); + } + private static boolean isValidRate(final @Nullable Double rate, final boolean allowNull) { if (rate == null) { return allowNull; diff --git a/sentry/src/main/java/io/sentry/util/TracingUtils.java b/sentry/src/main/java/io/sentry/util/TracingUtils.java index 16655be634..8673b358a9 100644 --- a/sentry/src/main/java/io/sentry/util/TracingUtils.java +++ b/sentry/src/main/java/io/sentry/util/TracingUtils.java @@ -6,9 +6,11 @@ import io.sentry.IScope; import io.sentry.IScopes; import io.sentry.ISpan; +import io.sentry.NoOpLogger; import io.sentry.PropagationContext; import io.sentry.SentryOptions; import io.sentry.SentryTraceHeader; +import io.sentry.TracesSamplingDecision; import java.util.List; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; @@ -57,12 +59,9 @@ public static void startNewTrace(final @NotNull IScopes scopes) { if (returnValue.propagationContext != null) { final @NotNull PropagationContext propagationContext = returnValue.propagationContext; - final @Nullable Baggage baggage = propagationContext.getBaggage(); - @Nullable BaggageHeader baggageHeader = null; - if (baggage != null) { - baggageHeader = - BaggageHeader.fromBaggageAndOutgoingHeader(baggage, thirdPartyBaggageHeaders); - } + final @NotNull Baggage baggage = propagationContext.getBaggage(); + final @NotNull BaggageHeader baggageHeader = + BaggageHeader.fromBaggageAndOutgoingHeader(baggage, thirdPartyBaggageHeaders); return new TracingHeaders( new SentryTraceHeader( @@ -80,11 +79,7 @@ public static void startNewTrace(final @NotNull IScopes scopes) { final @NotNull IScope scope, final @NotNull SentryOptions sentryOptions) { return scope.withPropagationContext( propagationContext -> { - @Nullable Baggage baggage = propagationContext.getBaggage(); - if (baggage == null) { - baggage = new Baggage(sentryOptions.getLogger()); - propagationContext.setBaggage(baggage); - } + @NotNull Baggage baggage = propagationContext.getBaggage(); if (baggage.isMutable()) { baggage.setValuesFromScope(scope, sentryOptions); baggage.freeze(); @@ -152,4 +147,66 @@ public static boolean isIgnored( return false; } + + /** + * Ensures a non null baggage instance is present by creating a new Baggage instance if null is + * passed in. + * + *

Also ensures there is a sampleRand value present on the baggage if it is still mutable. If + * the baggage should be frozen, it also takes care of freezing it. + * + * @param incomingBaggage a nullable baggage instance, if null a new one will be created + * @param decision a TracesSamplingDecision for potentially backfilling sampleRand to match that + * decision + * @return previous baggage instance or a new one + */ + @ApiStatus.Internal + public static @NotNull Baggage ensureBaggage( + final @Nullable Baggage incomingBaggage, final @Nullable TracesSamplingDecision decision) { + final @Nullable Boolean decisionSampled = decision == null ? null : decision.getSampled(); + final @Nullable Double decisionSampleRate = decision == null ? null : decision.getSampleRate(); + final @Nullable Double decisionSampleRand = decision == null ? null : decision.getSampleRand(); + + return ensureBaggage(incomingBaggage, decisionSampled, decisionSampleRate, decisionSampleRand); + } + + /** + * Ensures a non null baggage instance is present by creating a new Baggage instance if null is + * passed in. + * + *

Also ensures there is a sampleRand value present on the baggage if it is still mutable. If + * the baggage should be frozen, it also takes care of freezing it. + * + * @param incomingBaggage a nullable baggage instance, if null a new one will be created + * @param decisionSampled sampled decision for potential backfilling + * @param decisionSampleRate sampleRate for potential backfilling + * @param decisionSampleRand sampleRand to be used if none in baggage + * @return previous baggage instance or a new one + */ + @ApiStatus.Internal + public static @NotNull Baggage ensureBaggage( + final @Nullable Baggage incomingBaggage, + final @Nullable Boolean decisionSampled, + final @Nullable Double decisionSampleRate, + final @Nullable Double decisionSampleRand) { + final @NotNull Baggage baggage = + incomingBaggage == null ? new Baggage(NoOpLogger.getInstance()) : incomingBaggage; + + if (baggage.getSampleRand() == null) { + final @Nullable Double baggageSampleRate = baggage.getSampleRateDouble(); + final @Nullable Double sampleRateMaybe = + baggageSampleRate == null ? decisionSampleRate : baggageSampleRate; + final @NotNull Double sampleRand = + SampleRateUtils.backfilledSampleRand( + decisionSampleRand, sampleRateMaybe, decisionSampled); + baggage.setSampleRandDouble(sampleRand); + } + if (baggage.isMutable()) { + if (baggage.isShouldFreeze()) { + baggage.freeze(); + } + } + + return baggage; + } } diff --git a/sentry/src/test/java/io/sentry/BaggageTest.kt b/sentry/src/test/java/io/sentry/BaggageTest.kt index 8beae33668..fedd782624 100644 --- a/sentry/src/test/java/io/sentry/BaggageTest.kt +++ b/sentry/src/test/java/io/sentry/BaggageTest.kt @@ -8,7 +8,9 @@ import java.util.UUID import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertFalse import kotlin.test.assertNotNull +import kotlin.test.assertNull import kotlin.test.assertTrue class BaggageTest { @@ -340,8 +342,9 @@ class BaggageTest { } @Test - fun `setting values if header contains sentry values has no effect`() { + fun `setting values on frozen baggage has no effect`() { val baggage = Baggage.fromHeader("sentry-trace_id=a,sentry-transaction=sentryTransaction", logger) + baggage.freeze() baggage.traceId = "b" baggage.traceId = "c" @@ -535,6 +538,62 @@ class BaggageTest { assertEquals("abc", traceContext.unknown!!["anewkey"]) } + @Test + fun `header with sentry values is marked for freezing`() { + val baggage = + Baggage.fromHeader("sentry-trace_id=a,sentry-transaction=sentryTransaction") + assertTrue(baggage.isShouldFreeze) + } + + @Test + fun `header with sentry sample rand only is not marked for freezing`() { + val baggage = + Baggage.fromHeader("sentry-sample_rand=0.3") + assertFalse(baggage.isShouldFreeze) + } + + @Test + fun `header without sentry values is not marked for freezing`() { + val baggage = + Baggage.fromHeader("a=b,c=d") + assertFalse(baggage.isShouldFreeze) + } + + @Test + fun `sample rate can be retrieved as double`() { + val baggage = Baggage.fromHeader("a=b,c=d") + baggage.sampleRate = "0.1" + assertEquals(0.1, baggage.sampleRateDouble) + } + + @Test + fun `sample rand can be retrieved as double`() { + val baggage = Baggage.fromHeader("a=b,c=d") + baggage.sampleRand = "0.1" + assertEquals(0.1, baggage.sampleRandDouble) + } + + @Test + fun `sample rand can be set as double`() { + val baggage = Baggage.fromHeader("a=b,c=d") + baggage.sampleRandDouble = 0.1 + assertEquals("0.1", baggage.sampleRand) + } + + @Test + fun `broken sample rand returns null double`() { + val baggage = Baggage.fromHeader("a=b,c=d") + baggage.sampleRand = "a0.1" + assertNull(baggage.sampleRandDouble) + } + + @Test + fun `broken sample rate returns null double`() { + val baggage = Baggage.fromHeader("a=b,c=d") + baggage.sampleRate = "a0.1" + assertNull(baggage.sampleRateDouble) + } + /** * token = 1*tchar * tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" diff --git a/sentry/src/test/java/io/sentry/JsonSerializerTest.kt b/sentry/src/test/java/io/sentry/JsonSerializerTest.kt index e9d12ff4b1..8e3140faa1 100644 --- a/sentry/src/test/java/io/sentry/JsonSerializerTest.kt +++ b/sentry/src/test/java/io/sentry/JsonSerializerTest.kt @@ -456,16 +456,16 @@ class JsonSerializerTest { @Test fun `serializes trace context`() { - val traceContext = SentryEnvelopeHeader(null, null, TraceContext(SentryId("3367f5196c494acaae85bbbd535379ac"), "key", "release", "environment", "userId", "transaction", "0.5", "true", SentryId("3367f5196c494acaae85bbbd535379aa"))) - val expected = """{"trace":{"trace_id":"3367f5196c494acaae85bbbd535379ac","public_key":"key","release":"release","environment":"environment","user_id":"userId","transaction":"transaction","sample_rate":"0.5","sampled":"true","replay_id":"3367f5196c494acaae85bbbd535379aa"}}""" + val traceContext = SentryEnvelopeHeader(null, null, TraceContext(SentryId("3367f5196c494acaae85bbbd535379ac"), "key", "release", "environment", "userId", "transaction", "0.5", "true", SentryId("3367f5196c494acaae85bbbd535379aa"), "0.25")) + val expected = """{"trace":{"trace_id":"3367f5196c494acaae85bbbd535379ac","public_key":"key","release":"release","environment":"environment","user_id":"userId","transaction":"transaction","sample_rate":"0.5","sample_rand":"0.25","sampled":"true","replay_id":"3367f5196c494acaae85bbbd535379aa"}}""" val json = serializeToString(traceContext) assertEquals(expected, json) } @Test fun `serializes trace context with user having null id`() { - val traceContext = SentryEnvelopeHeader(null, null, TraceContext(SentryId("3367f5196c494acaae85bbbd535379ac"), "key", "release", "environment", null, "transaction", "0.6", "false", SentryId("3367f5196c494acaae85bbbd535379aa"))) - val expected = """{"trace":{"trace_id":"3367f5196c494acaae85bbbd535379ac","public_key":"key","release":"release","environment":"environment","transaction":"transaction","sample_rate":"0.6","sampled":"false","replay_id":"3367f5196c494acaae85bbbd535379aa"}}""" + val traceContext = SentryEnvelopeHeader(null, null, TraceContext(SentryId("3367f5196c494acaae85bbbd535379ac"), "key", "release", "environment", null, "transaction", "0.6", "false", SentryId("3367f5196c494acaae85bbbd535379aa"), "0.3")) + val expected = """{"trace":{"trace_id":"3367f5196c494acaae85bbbd535379ac","public_key":"key","release":"release","environment":"environment","transaction":"transaction","sample_rate":"0.6","sample_rand":"0.3","sampled":"false","replay_id":"3367f5196c494acaae85bbbd535379aa"}}""" val json = serializeToString(traceContext) assertEquals(expected, json) } diff --git a/sentry/src/test/java/io/sentry/OutboxSenderTest.kt b/sentry/src/test/java/io/sentry/OutboxSenderTest.kt index 8a1850e7dd..9136494ddf 100644 --- a/sentry/src/test/java/io/sentry/OutboxSenderTest.kt +++ b/sentry/src/test/java/io/sentry/OutboxSenderTest.kt @@ -23,6 +23,7 @@ import java.util.Date import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFalse +import kotlin.test.assertNotNull import kotlin.test.assertTrue class OutboxSenderTest { @@ -141,6 +142,63 @@ class OutboxSenderTest { whenever(fixture.scopes.options).thenReturn(fixture.options) whenever(fixture.options.transactionProfiler).thenReturn(NoOpTransactionProfiler.getInstance()) + val transactionContext = TransactionContext("fixture-name", "http") + transactionContext.description = "fixture-request" + transactionContext.status = SpanStatus.OK + transactionContext.setTag("fixture-tag", "fixture-value") + transactionContext.samplingDecision = TracesSamplingDecision(true, 0.00000021, 0.021) + + val sentryTracer = SentryTracer(transactionContext, fixture.scopes) + val span = sentryTracer.startChild("child") + span.finish(SpanStatus.OK) + sentryTracer.finish() + + val sentryTracerSpy = spy(sentryTracer) + whenever(sentryTracerSpy.eventId).thenReturn(SentryId("3367f5196c494acaae85bbbd535379ac")) + + val expected = SentryTransaction(sentryTracerSpy) + whenever(fixture.serializer.deserialize(any(), eq(SentryTransaction::class.java))).thenReturn(expected) + + val sut = fixture.getSut() + val path = getTempEnvelope(fileName = "envelope-transaction-with-sample-rand.txt") + assertTrue(File(path).exists()) + + val hints = HintUtils.createWithTypeCheckHint(mock()) + sut.processEnvelopeFile(path, hints) + + verify(fixture.scopes).captureTransaction( + check { + assertEquals(expected, it) + assertTrue(it.isSampled) + assertEquals(0.00000021, it.samplingDecision?.sampleRate) + assertEquals(0.021, it.samplingDecision?.sampleRand) + assertTrue(it.samplingDecision!!.sampled) + }, + check { + assertEquals("b156a475de54423d9c1571df97ec7eb6", it.traceId.toString()) + assertEquals("key", it.publicKey) + assertEquals("0.00000021", it.sampleRate) + assertEquals("1.0-beta.1", it.release) + assertEquals("prod", it.environment) + assertEquals("usr1", it.userId) + assertEquals("tx1", it.transaction) + }, + any() + ) + assertFalse(File(path).exists()) + + // Additionally make sure we have no errors logged + verify(fixture.logger, never()).log(eq(SentryLevel.ERROR), any(), any()) + verify(fixture.logger, never()).log(eq(SentryLevel.ERROR), any(), any()) + } + + @Test + fun `backfills sampleRand`() { + fixture.envelopeReader = EnvelopeReader(JsonSerializer(fixture.options)) + whenever(fixture.options.maxSpans).thenReturn(1000) + whenever(fixture.scopes.options).thenReturn(fixture.options) + whenever(fixture.options.transactionProfiler).thenReturn(NoOpTransactionProfiler.getInstance()) + val transactionContext = TransactionContext("fixture-name", "http") transactionContext.description = "fixture-request" transactionContext.status = SpanStatus.OK @@ -170,6 +228,7 @@ class OutboxSenderTest { assertEquals(expected, it) assertTrue(it.isSampled) assertEquals(0.00000021, it.samplingDecision?.sampleRate) + assertNotNull(it.samplingDecision?.sampleRand) assertTrue(it.samplingDecision!!.sampled) }, check { diff --git a/sentry/src/test/java/io/sentry/PropagationContextTest.kt b/sentry/src/test/java/io/sentry/PropagationContextTest.kt new file mode 100644 index 0000000000..39fffe9f89 --- /dev/null +++ b/sentry/src/test/java/io/sentry/PropagationContextTest.kt @@ -0,0 +1,43 @@ +package io.sentry + +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +class PropagationContextTest { + + @Test + fun `freezes baggage with sentry values`() { + val propagationContext = PropagationContext.fromHeaders( + NoOpLogger.getInstance(), + "2722d9f6ec019ade60c776169d9a8904-cedf5b7571cb4972-1", + "sentry-trace_id=a,sentry-transaction=sentryTransaction" + ) + assertFalse(propagationContext.baggage.isMutable) + assertTrue(propagationContext.baggage.isShouldFreeze) + } + + @Test + fun `does not freeze baggage without sentry values`() { + val propagationContext = PropagationContext.fromHeaders( + NoOpLogger.getInstance(), + "2722d9f6ec019ade60c776169d9a8904-cedf5b7571cb4972-1", + "a=b" + ) + assertTrue(propagationContext.baggage.isMutable) + assertFalse(propagationContext.baggage.isShouldFreeze) + } + + @Test + fun `creates new baggage if none passed`() { + val propagationContext = PropagationContext.fromHeaders( + NoOpLogger.getInstance(), + "2722d9f6ec019ade60c776169d9a8904-cedf5b7571cb4972-1", + null as? String? + ) + assertNotNull(propagationContext.baggage) + assertTrue(propagationContext.baggage.isMutable) + assertFalse(propagationContext.baggage.isShouldFreeze) + } +} diff --git a/sentry/src/test/java/io/sentry/TraceContextSerializationTest.kt b/sentry/src/test/java/io/sentry/TraceContextSerializationTest.kt index 8b00df543d..ce0e4a4ae4 100644 --- a/sentry/src/test/java/io/sentry/TraceContextSerializationTest.kt +++ b/sentry/src/test/java/io/sentry/TraceContextSerializationTest.kt @@ -24,7 +24,8 @@ class TraceContextSerializationTest { "0252ec25-cd0a-4230-bd2f-936a4585637e", "0.00000021", "true", - SentryId("3367f5196c494acaae85bbbd535379aa") + SentryId("3367f5196c494acaae85bbbd535379aa"), + "0.00000012" ) } private val fixture = Fixture() diff --git a/sentry/src/test/java/io/sentry/TracesSamplerTest.kt b/sentry/src/test/java/io/sentry/TracesSamplerTest.kt index 06eb60aece..0fbc8e2f67 100644 --- a/sentry/src/test/java/io/sentry/TracesSamplerTest.kt +++ b/sentry/src/test/java/io/sentry/TracesSamplerTest.kt @@ -1,31 +1,25 @@ package io.sentry -import io.sentry.util.Random import org.mockito.kotlin.any import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.verify -import org.mockito.kotlin.whenever import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFalse +import kotlin.test.assertNotNull import kotlin.test.assertNull import kotlin.test.assertTrue class TracesSamplerTest { class Fixture { internal fun getSut( - randomResult: Double? = null, tracesSampleRate: Double? = null, profilesSampleRate: Double? = null, tracesSamplerCallback: SentryOptions.TracesSamplerCallback? = null, profilesSamplerCallback: SentryOptions.ProfilesSamplerCallback? = null, logger: ILogger? = null ): TracesSampler { - val random = mock() - if (randomResult != null) { - whenever(random.nextDouble()).thenReturn(randomResult) - } val options = SentryOptions() if (tracesSampleRate != null) { options.tracesSampleRate = tracesSampleRate @@ -43,7 +37,7 @@ class TracesSamplerTest { options.isDebug = true options.setLogger(logger) } - return TracesSampler(options, random) + return TracesSampler(options) } } @@ -51,103 +45,115 @@ class TracesSamplerTest { @Test fun `when tracesSampleRate is set and random returns greater number returns false`() { - val sampler = fixture.getSut(randomResult = 0.9, tracesSampleRate = 0.2, profilesSampleRate = 0.2) - val samplingDecision = sampler.sample(SamplingContext(TransactionContext("name", "op"), null)) + val sampler = fixture.getSut(tracesSampleRate = 0.2, profilesSampleRate = 0.2) + val samplingDecision = sampler.sample(SamplingContext(TransactionContext("name", "op"), null, 0.9)) assertFalse(samplingDecision.sampled) assertEquals(0.2, samplingDecision.sampleRate) + assertEquals(0.9, samplingDecision.sampleRand) } @Test fun `when tracesSampleRate is set and random returns lower number returns true`() { - val sampler = fixture.getSut(randomResult = 0.1, tracesSampleRate = 0.2, profilesSampleRate = 0.2) - val samplingDecision = sampler.sample(SamplingContext(TransactionContext("name", "op"), null)) + val sampler = fixture.getSut(tracesSampleRate = 0.2, profilesSampleRate = 0.2) + val samplingDecision = sampler.sample(SamplingContext(TransactionContext("name", "op"), null, 0.1)) assertTrue(samplingDecision.sampled) assertEquals(0.2, samplingDecision.sampleRate) + assertEquals(0.1, samplingDecision.sampleRand) } @Test fun `when profilesSampleRate is set and random returns greater number returns false`() { - val sampler = fixture.getSut(randomResult = 0.9, tracesSampleRate = 1.0, profilesSampleRate = 0.2) - val samplingDecision = sampler.sample(SamplingContext(TransactionContext("name", "op"), null)) + val sampler = fixture.getSut(tracesSampleRate = 1.0, profilesSampleRate = 0.2) + val samplingDecision = sampler.sample(SamplingContext(TransactionContext("name", "op"), null, 0.9)) assertTrue(samplingDecision.sampled) assertFalse(samplingDecision.profileSampled) assertEquals(0.2, samplingDecision.profileSampleRate) + assertEquals(0.9, samplingDecision.sampleRand) } @Test fun `when profilesSampleRate is set and random returns lower number returns true`() { - val sampler = fixture.getSut(randomResult = 0.1, tracesSampleRate = 1.0, profilesSampleRate = 0.2) - val samplingDecision = sampler.sample(SamplingContext(TransactionContext("name", "op"), null)) + val sampler = fixture.getSut(tracesSampleRate = 1.0, profilesSampleRate = 0.2) + val samplingDecision = sampler.sample(SamplingContext(TransactionContext("name", "op"), null, 0.1)) assertTrue(samplingDecision.sampled) assertTrue(samplingDecision.profileSampled) assertEquals(0.2, samplingDecision.profileSampleRate) + assertEquals(0.1, samplingDecision.sampleRand) } @Test fun `when trace is not sampled, profile is not sampled`() { - val sampler = fixture.getSut(randomResult = 0.3, tracesSampleRate = 0.0, profilesSampleRate = 1.0) - val samplingDecision = sampler.sample(SamplingContext(TransactionContext("name", "op"), null)) + val sampler = fixture.getSut(tracesSampleRate = 0.0, profilesSampleRate = 1.0) + val samplingDecision = sampler.sample(SamplingContext(TransactionContext("name", "op"), null, 0.3)) assertFalse(samplingDecision.sampled) assertFalse(samplingDecision.profileSampled) assertEquals(1.0, samplingDecision.profileSampleRate) + assertEquals(0.3, samplingDecision.sampleRand) } @Test fun `when tracesSampleRate is not set, tracesSampler is set and random returns lower number returns true`() { val sampler = fixture.getSut( - randomResult = 0.1, tracesSamplerCallback = { 0.2 }, profilesSamplerCallback = { 0.2 } ) val samplingDecision = sampler.sample( SamplingContext( TransactionContext("name", "op"), - CustomSamplingContext() + CustomSamplingContext(), + 0.1 ) ) assertTrue(samplingDecision.sampled) assertEquals(0.2, samplingDecision.sampleRate) + assertEquals(0.1, samplingDecision.sampleRand) } @Test fun `when profilesSampleRate is not set, profilesSampler is set and random returns lower number returns true`() { - val sampler = fixture.getSut(randomResult = 0.1, tracesSampleRate = 1.0, profilesSamplerCallback = { 0.2 }) + val sampler = fixture.getSut(tracesSampleRate = 1.0, profilesSamplerCallback = { 0.2 }) val samplingDecision = sampler.sample( SamplingContext( TransactionContext("name", "op"), - CustomSamplingContext() + CustomSamplingContext(), + 0.1 ) ) assertTrue(samplingDecision.sampled) assertTrue(samplingDecision.profileSampled) assertEquals(0.2, samplingDecision.profileSampleRate) + assertEquals(0.1, samplingDecision.sampleRand) } @Test fun `when tracesSampleRate is not set, tracesSampler is set and random returns greater number returns false`() { - val sampler = fixture.getSut(randomResult = 0.9, tracesSamplerCallback = { 0.2 }) + val sampler = fixture.getSut(tracesSamplerCallback = { 0.2 }) val samplingDecision = sampler.sample( SamplingContext( TransactionContext("name", "op"), - CustomSamplingContext() + CustomSamplingContext(), + 0.9 ) ) assertFalse(samplingDecision.sampled) assertEquals(0.2, samplingDecision.sampleRate) + assertEquals(0.9, samplingDecision.sampleRand) } @Test fun `when profilesSampleRate is not set, profilesSampler is set and random returns greater number returns false`() { - val sampler = fixture.getSut(randomResult = 0.9, tracesSampleRate = 1.0, profilesSamplerCallback = { 0.2 }) + val sampler = fixture.getSut(tracesSampleRate = 1.0, profilesSamplerCallback = { 0.2 }) val samplingDecision = sampler.sample( SamplingContext( TransactionContext("name", "op"), - CustomSamplingContext() + CustomSamplingContext(), + 0.9 ) ) assertTrue(samplingDecision.sampled) assertFalse(samplingDecision.profileSampled) assertEquals(0.2, samplingDecision.profileSampleRate) + assertEquals(0.9, samplingDecision.sampleRand) } @Test @@ -158,11 +164,13 @@ class TracesSamplerTest { val samplingDecision = sampler.sample( SamplingContext( transactionContextParentSampled, - CustomSamplingContext() + CustomSamplingContext(), + 0.1 ) ) assertTrue(samplingDecision.sampled) assertNull(samplingDecision.sampleRate) + assertNotNull(samplingDecision.sampleRand) } @Test @@ -173,34 +181,39 @@ class TracesSamplerTest { val samplingDecision = sampler.sample( SamplingContext( transactionContextParentSampled, - CustomSamplingContext() + CustomSamplingContext(), + 0.1 ) ) assertTrue(samplingDecision.sampled) assertTrue(samplingDecision.profileSampled) assertNull(samplingDecision.profileSampleRate) + assertNotNull(samplingDecision.sampleRand) } @Test fun `when tracesSampler returns null and tracesSampleRate is set sampler uses it as a sampling decision`() { - val sampler = fixture.getSut(randomResult = 0.1, tracesSampleRate = 0.2, tracesSamplerCallback = null) + val sampler = fixture.getSut(tracesSampleRate = 0.2, tracesSamplerCallback = null) val samplingDecision = sampler.sample( SamplingContext( TransactionContext("name", "op"), - CustomSamplingContext() + CustomSamplingContext(), + 0.1 ) ) assertTrue(samplingDecision.sampled) assertEquals(0.2, samplingDecision.sampleRate) + assertEquals(0.1, samplingDecision.sampleRand) } @Test fun `when profilesSampler returns null and profilesSampleRate is set sampler uses it as a sampling decision`() { - val sampler = fixture.getSut(randomResult = 0.1, tracesSampleRate = 1.0, profilesSampleRate = 0.2, profilesSamplerCallback = null) + val sampler = fixture.getSut(tracesSampleRate = 1.0, profilesSampleRate = 0.2, profilesSamplerCallback = null) val samplingDecision = sampler.sample( SamplingContext( TransactionContext("name", "op"), - CustomSamplingContext() + CustomSamplingContext(), + 0.1 ) ) assertTrue(samplingDecision.sampled) @@ -210,29 +223,33 @@ class TracesSamplerTest { @Test fun `when tracesSampleRate is not set, and tracesSampler is not set returns false`() { - val sampler = fixture.getSut(randomResult = 0.1) + val sampler = fixture.getSut() val samplingDecision = sampler.sample( SamplingContext( TransactionContext("name", "op"), - CustomSamplingContext() + CustomSamplingContext(), + 0.1 ) ) assertFalse(samplingDecision.sampled) assertNull(samplingDecision.sampleRate) + assertEquals(0.1, samplingDecision.sampleRand) } @Test fun `when profilesSampleRate is not set, and profilesSampler is not set returns false`() { - val sampler = fixture.getSut(randomResult = 0.1, tracesSampleRate = 1.0) + val sampler = fixture.getSut(tracesSampleRate = 1.0) val samplingDecision = sampler.sample( SamplingContext( TransactionContext("name", "op"), - CustomSamplingContext() + CustomSamplingContext(), + 0.1 ) ) assertTrue(samplingDecision.sampled) assertFalse(samplingDecision.profileSampled) assertNull(samplingDecision.profileSampleRate) + assertEquals(0.1, samplingDecision.sampleRand) } @Test @@ -243,11 +260,13 @@ class TracesSamplerTest { val samplingDecision = sampler.sample( SamplingContext( transactionContextParentNotSampled, - CustomSamplingContext() + CustomSamplingContext(), + 0.1 ) ) assertFalse(samplingDecision.sampled) assertNull(samplingDecision.sampleRate) + assertNotNull(samplingDecision.sampleRand) assertFalse(samplingDecision.profileSampled) assertNull(samplingDecision.profileSampleRate) @@ -256,11 +275,13 @@ class TracesSamplerTest { val samplingDecisionParentSampled = sampler.sample( SamplingContext( transactionContextParentSampled, - CustomSamplingContext() + CustomSamplingContext(), + 0.1 ) ) assertTrue(samplingDecisionParentSampled.sampled) assertNull(samplingDecisionParentSampled.sampleRate) + assertNotNull(samplingDecisionParentSampled.sampleRand) assertTrue(samplingDecisionParentSampled.profileSampled) assertNull(samplingDecisionParentSampled.profileSampleRate) } @@ -288,27 +309,30 @@ class TracesSamplerTest { val transactionContextNotSampled = TransactionContext("name", "op") transactionContextNotSampled.sampled = false val samplingDecision = - sampler.sample(SamplingContext(transactionContextNotSampled, CustomSamplingContext())) + sampler.sample(SamplingContext(transactionContextNotSampled, CustomSamplingContext(), 0.1)) assertFalse(samplingDecision.sampled) assertNull(samplingDecision.sampleRate) + assertNotNull(samplingDecision.sampleRand) assertFalse(samplingDecision.profileSampled) assertNull(samplingDecision.profileSampleRate) val transactionContextSampled = TransactionContext("name", "op") transactionContextSampled.setSampled(true, true) val samplingDecisionContextSampled = - sampler.sample(SamplingContext(transactionContextSampled, CustomSamplingContext())) + sampler.sample(SamplingContext(transactionContextSampled, CustomSamplingContext(), 0.1)) assertTrue(samplingDecisionContextSampled.sampled) assertNull(samplingDecisionContextSampled.sampleRate) + assertNotNull(samplingDecisionContextSampled.sampleRand) assertTrue(samplingDecisionContextSampled.profileSampled) assertNull(samplingDecisionContextSampled.profileSampleRate) val transactionContextUnsampledWithProfile = TransactionContext("name", "op") transactionContextUnsampledWithProfile.setSampled(false, true) val samplingDecisionContextUnsampledWithProfile = - sampler.sample(SamplingContext(transactionContextUnsampledWithProfile, CustomSamplingContext())) + sampler.sample(SamplingContext(transactionContextUnsampledWithProfile, CustomSamplingContext(), 0.1)) assertFalse(samplingDecisionContextUnsampledWithProfile.sampled) assertNull(samplingDecisionContextUnsampledWithProfile.sampleRate) + assertNotNull(samplingDecisionContextUnsampledWithProfile.sampleRand) assertFalse(samplingDecisionContextUnsampledWithProfile.profileSampled) assertNull(samplingDecisionContextUnsampledWithProfile.profileSampleRate) } @@ -326,7 +350,7 @@ class TracesSamplerTest { logger = logger ) val decision = sampler.sample( - SamplingContext(TransactionContext("name", "op"), null) + SamplingContext(TransactionContext("name", "op"), null, 0.1) ) assertFalse(decision.profileSampled) verify(logger).log(eq(SentryLevel.ERROR), any(), eq(exception)) @@ -336,7 +360,6 @@ class TracesSamplerTest { fun `when a profilingRate and a ProfilesSamplerCallback is set but the callback throws an exception then profiling should still be enabled`() { val exception = Exception("faulty ProfilesSamplerCallback") val sampler = fixture.getSut( - randomResult = 0.0, tracesSampleRate = 1.0, profilesSampleRate = 1.0, profilesSamplerCallback = { @@ -344,9 +367,10 @@ class TracesSamplerTest { } ) val decision = sampler.sample( - SamplingContext(TransactionContext("name", "op"), null) + SamplingContext(TransactionContext("name", "op"), null, 0.0) ) assertTrue(decision.profileSampled) + assertEquals(0.0, decision.sampleRand) } @Test @@ -361,9 +385,10 @@ class TracesSamplerTest { logger = logger ) val decision = sampler.sample( - SamplingContext(TransactionContext("name", "op"), null) + SamplingContext(TransactionContext("name", "op"), null, 0.1) ) assertFalse(decision.sampled) + assertEquals(0.1, decision.sampleRand) verify(logger).log(eq(SentryLevel.ERROR), any(), eq(exception)) } @@ -371,15 +396,15 @@ class TracesSamplerTest { fun `when a tracesSampleRate and a TracesSamplerCallback is set but the callback throws an exception then tracing should still be enabled`() { val exception = Exception("faulty TracesSamplerCallback") val sampler = fixture.getSut( - randomResult = 0.0, tracesSampleRate = 1.0, tracesSamplerCallback = { throw exception } ) val decision = sampler.sample( - SamplingContext(TransactionContext("name", "op"), null) + SamplingContext(TransactionContext("name", "op"), null, 0.0) ) assertTrue(decision.sampled) + assertEquals(0.0, decision.sampleRand) } } diff --git a/sentry/src/test/java/io/sentry/TransactionContextTest.kt b/sentry/src/test/java/io/sentry/TransactionContextTest.kt index d6b715bd84..8a66870bc2 100644 --- a/sentry/src/test/java/io/sentry/TransactionContextTest.kt +++ b/sentry/src/test/java/io/sentry/TransactionContextTest.kt @@ -1,10 +1,12 @@ package io.sentry import io.sentry.protocol.SentryId +import io.sentry.protocol.TransactionNameSource import org.mockito.kotlin.mock import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFalse +import kotlin.test.assertNotNull import kotlin.test.assertNull import kotlin.test.assertTrue @@ -91,4 +93,32 @@ class TransactionContextTest { context.isForNextAppStart = true assertTrue(context.isForNextAppStart) } + + @Test + fun `when passing null baggage creates a new one`() { + val context = TransactionContext(SentryId(), SpanId(), null, null, null) + assertNotNull(context.baggage) + assertNotNull(context.baggage?.sampleRand) + } + + @Test + fun `when passing null baggage creates a new one and uses parent sampling decision`() { + val context = TransactionContext(SentryId(), SpanId(), null, TracesSamplingDecision(true, 0.1, 0.2), null) + assertNotNull(context.baggage) + assertEquals("0.2", context.baggage?.sampleRand) + } + + @Test + fun `when using few param ctor creates a new baggage`() { + val context = TransactionContext("name", "op") + assertNotNull(context.baggage) + assertNotNull(context.baggage?.sampleRand) + } + + @Test + fun `when using few param ctor creates a new baggage and uses sampling decision`() { + val context = TransactionContext("name", TransactionNameSource.CUSTOM, "op", TracesSamplingDecision(true, 0.1, 0.2)) + assertNotNull(context.baggage) + assertEquals("0.2", context.baggage?.sampleRand) + } } diff --git a/sentry/src/test/java/io/sentry/util/SampleRateUtilTest.kt b/sentry/src/test/java/io/sentry/util/SampleRateUtilTest.kt index e5c81bc70e..8576e9b10f 100644 --- a/sentry/src/test/java/io/sentry/util/SampleRateUtilTest.kt +++ b/sentry/src/test/java/io/sentry/util/SampleRateUtilTest.kt @@ -1,7 +1,10 @@ package io.sentry.util +import io.sentry.TracesSamplingDecision import kotlin.test.Test +import kotlin.test.assertEquals import kotlin.test.assertFalse +import kotlin.test.assertNotNull import kotlin.test.assertTrue class SampleRateUtilTest { @@ -130,4 +133,62 @@ class SampleRateUtilTest { fun `accepts null profiles sample rate`() { assertTrue(SampleRateUtils.isValidProfilesSampleRate(null)) } + + @Test + fun `fills sample rand on decision if missing`() { + val decision = SampleRateUtils.backfilledSampleRand(TracesSamplingDecision(true)) + assertNotNull(decision.sampleRand) + } + + @Test + fun `keeps sample rand on decision if present`() { + val decision = SampleRateUtils.backfilledSampleRand(TracesSamplingDecision(true, 0.1, 0.5)) + assertEquals(0.5, decision.sampleRand) + } + + @Test + fun `uses sampleRand and does not backfill`() { + val sampleRand = SampleRateUtils.backfilledSampleRand(0.3, null, null) + assertEquals(0.3, sampleRand) + } + + @Test + fun `backfills sampleRand if missing`() { + val sampleRand = SampleRateUtils.backfilledSampleRand(null, null, null) + assertNotNull(sampleRand) + assertTrue(sampleRand >= 0) + assertTrue(sampleRand < 1) + } + + @Test + fun `backfills sampleRand if missing with sampled true`() { + val sampleRand = SampleRateUtils.backfilledSampleRand(null, null, true) + assertNotNull(sampleRand) + assertTrue(sampleRand >= 0) + assertTrue(sampleRand < 1) + } + + @Test + fun `backfills sampleRand if missing with sampled false`() { + val sampleRand = SampleRateUtils.backfilledSampleRand(null, null, false) + assertNotNull(sampleRand) + assertTrue(sampleRand >= 0) + assertTrue(sampleRand < 1) + } + + @Test + fun `backfills sampleRand if missing with sampled true below sample rate`() { + val sampleRand = SampleRateUtils.backfilledSampleRand(null, 0.0001, true) + assertNotNull(sampleRand) + assertTrue(sampleRand >= 0) + assertTrue(sampleRand < 0.0001) + } + + @Test + fun `backfills sampleRand if missing with sampled false above sample rate`() { + val sampleRand = SampleRateUtils.backfilledSampleRand(null, 0.9999, false) + assertNotNull(sampleRand) + assertTrue(sampleRand >= 0.9999) + assertTrue(sampleRand < 1) + } } diff --git a/sentry/src/test/java/io/sentry/util/TracingUtilsTest.kt b/sentry/src/test/java/io/sentry/util/TracingUtilsTest.kt index e5403f54d9..ed378ee960 100644 --- a/sentry/src/test/java/io/sentry/util/TracingUtilsTest.kt +++ b/sentry/src/test/java/io/sentry/util/TracingUtilsTest.kt @@ -2,15 +2,19 @@ package io.sentry.util import io.sentry.Baggage import io.sentry.IScopes +import io.sentry.NoOpLogger import io.sentry.NoOpSpan +import io.sentry.PropagationContext import io.sentry.Scope import io.sentry.ScopeCallback import io.sentry.SentryOptions import io.sentry.SentryTracer import io.sentry.Span +import io.sentry.SpanId import io.sentry.SpanOptions import io.sentry.TracesSamplingDecision import io.sentry.TransactionContext +import io.sentry.protocol.SentryId import org.junit.Test import org.mockito.kotlin.any import org.mockito.kotlin.doAnswer @@ -21,6 +25,7 @@ import kotlin.test.assertFalse import kotlin.test.assertNotEquals import kotlin.test.assertNotNull import kotlin.test.assertNull +import kotlin.test.assertSame import kotlin.test.assertTrue class TracingUtilsTest { @@ -129,7 +134,7 @@ class TracingUtilsTest { @Test fun `returns headers if allowed from scope without span leaving frozen baggage alone`() { - fixture.scope.propagationContext.baggage = Baggage.fromHeader("sentry-public_key=502f25099c204a2fbf4cb16edc5975d1,sentry-sample_rate=1,sentry-trace_id=2722d9f6ec019ade60c776169d9a8904,sentry-transaction=HTTP%20GET").also { it.freeze() } + fixture.scope.propagationContext = PropagationContext(SentryId(), SpanId(), null, Baggage.fromHeader("sentry-public_key=502f25099c204a2fbf4cb16edc5975d1,sentry-sample_rate=1,sentry-trace_id=2722d9f6ec019ade60c776169d9a8904,sentry-transaction=HTTP%20GET").also { it.freeze() }, true) fixture.setup() val headers = TracingUtils.traceIfAllowed(fixture.scopes, "https://sentry.io/hello", fixture.preExistingBaggage, null) @@ -208,23 +213,11 @@ class TracingUtilsTest { assertNotEquals(propagationContextBefore.spanId, fixture.scope.propagationContext.spanId) } - @Test - fun `creates new baggage if none present`() { - fixture.setup() - assertNull(fixture.scope.propagationContext.baggage) - - TracingUtils.maybeUpdateBaggage(fixture.scope, fixture.options) - - assertNotNull(fixture.scope.propagationContext.baggage) - assertEquals(fixture.scope.propagationContext.traceId.toString(), fixture.scope.propagationContext.baggage!!.traceId) - assertFalse(fixture.scope.propagationContext.baggage!!.isMutable) - } - @Test fun `updates mutable baggage`() { fixture.setup() // not frozen because it doesn't contain sentry-* keys - fixture.scope.propagationContext.baggage = Baggage.fromHeader(fixture.preExistingBaggage) + fixture.scope.propagationContext = PropagationContext(SentryId(), SpanId(), null, Baggage.fromHeader(fixture.preExistingBaggage), true) TracingUtils.maybeUpdateBaggage(fixture.scope, fixture.options) @@ -236,11 +229,136 @@ class TracingUtilsTest { fun `does not change frozen baggage`() { fixture.setup() // frozen automatically because it contains sentry-* keys - fixture.scope.propagationContext.baggage = Baggage.fromHeader("sentry-public_key=502f25099c204a2fbf4cb16edc5975d1,sentry-sample_rate=1,sentry-trace_id=2722d9f6ec019ade60c776169d9a8904,sentry-transaction=HTTP%20GET") + fixture.scope.propagationContext = PropagationContext(SentryId(), SpanId(), null, Baggage.fromHeader("sentry-public_key=502f25099c204a2fbf4cb16edc5975d1,sentry-sample_rate=1,sentry-trace_id=2722d9f6ec019ade60c776169d9a8904,sentry-transaction=HTTP%20GET"), true) TracingUtils.maybeUpdateBaggage(fixture.scope, fixture.options) assertEquals("2722d9f6ec019ade60c776169d9a8904", fixture.scope.propagationContext.baggage!!.traceId) assertFalse(fixture.scope.propagationContext.baggage!!.isMutable) } + + @Test + fun `returns baggage if passed in`() { + val incomingBaggage = Baggage(NoOpLogger.getInstance()) + val baggage = TracingUtils.ensureBaggage( + incomingBaggage, + null as? TracesSamplingDecision? + ) + assertSame(incomingBaggage, baggage) + } + + @Test + fun `crates new baggage if null passed in that has sampleRand set and is mutable`() { + val baggage = TracingUtils.ensureBaggage( + null, + null as? TracesSamplingDecision? + ) + assertNotNull(baggage) + assertNotNull(baggage.sampleRand) + assertTrue(baggage.isMutable) + assertFalse(baggage.isShouldFreeze) + } + + @Test + fun `backfills sampleRand on passed in baggage if missing`() { + val incomingBaggage = Baggage(NoOpLogger.getInstance()) + val baggage = TracingUtils.ensureBaggage( + incomingBaggage, + null as? TracesSamplingDecision? + ) + assertSame(incomingBaggage, baggage) + assertNotNull(baggage.sampleRand) + assertTrue(baggage.isMutable) + } + + @Test + fun `keeps sampleRand on passed in baggage if present`() { + val incomingBaggage = Baggage(NoOpLogger.getInstance()) + incomingBaggage.sampleRand = "0.3" + val baggage = TracingUtils.ensureBaggage( + incomingBaggage, + null as? TracesSamplingDecision? + ) + assertSame(incomingBaggage, baggage) + assertEquals("0.3", baggage.sampleRand) + assertTrue(baggage.isMutable) + } + + @Test + fun `does not backfill sampleRand on passed in baggage if frozen`() { + val incomingBaggage = Baggage(NoOpLogger.getInstance()) + incomingBaggage.freeze() + val baggage = TracingUtils.ensureBaggage( + incomingBaggage, + null as? TracesSamplingDecision? + ) + assertSame(incomingBaggage, baggage) + assertNull(baggage.sampleRand) + assertFalse(baggage.isMutable) + } + + @Test + fun `freezes passed in baggage if should be frozen`() { + // markes as shouldFreeze=true due to sentry values being present in header + val incomingBaggage = Baggage.fromHeader("sentry-trace_id=a,sentry-transaction=sentryTransaction") + val baggage = TracingUtils.ensureBaggage( + incomingBaggage, + null as? TracesSamplingDecision? + ) + assertSame(incomingBaggage, baggage) + assertNotNull(baggage.sampleRand) + assertFalse(baggage.isMutable) + } + + @Test + fun `does not freeze passed in baggage if should not be frozen`() { + // markes as shouldFreeze=false due to no sentry values being present in header + val incomingBaggage = Baggage.fromHeader("a=b,c=d") + val baggage = TracingUtils.ensureBaggage( + incomingBaggage, + null as? TracesSamplingDecision? + ) + assertSame(incomingBaggage, baggage) + assertNotNull(baggage.sampleRand) + assertTrue(baggage.isMutable) + } + + @Test + fun `uses sample rand if passed in`() { + val incomingBaggage = Baggage(NoOpLogger.getInstance()) + val baggage = TracingUtils.ensureBaggage( + incomingBaggage, + TracesSamplingDecision(true, null, 0.123) + ) + assertSame(incomingBaggage, baggage) + assertEquals("0.123", baggage.sampleRand) + } + + @Test + fun `uses sample rate and sampled flag true if passed in`() { + val incomingBaggage = Baggage(NoOpLogger.getInstance()) + val baggage = TracingUtils.ensureBaggage( + incomingBaggage, + TracesSamplingDecision(true, 0.0001, null) + ) + assertSame(incomingBaggage, baggage) + val sampleRand = baggage.sampleRandDouble + assertNotNull(sampleRand) + assertTrue(sampleRand < 0.0001) + assertTrue(sampleRand >= 0.0) + } + + @Test + fun `uses sample rate and sampled flag false if passed in`() { + val incomingBaggage = Baggage(NoOpLogger.getInstance()) + val baggage = TracingUtils.ensureBaggage( + incomingBaggage, + TracesSamplingDecision(false, 0.9999, null) + ) + assertSame(incomingBaggage, baggage) + val sampleRand = baggage.sampleRandDouble + assertNotNull(sampleRand) + assertTrue(sampleRand < 1.0) + assertTrue(sampleRand >= 0.9999) + } } diff --git a/sentry/src/test/resources/envelope-transaction-with-sample-rand.txt b/sentry/src/test/resources/envelope-transaction-with-sample-rand.txt new file mode 100644 index 0000000000..1a6b3120b8 --- /dev/null +++ b/sentry/src/test/resources/envelope-transaction-with-sample-rand.txt @@ -0,0 +1,3 @@ +{"event_id":"3367f5196c494acaae85bbbd535379ac","trace":{"trace_id":"b156a475de54423d9c1571df97ec7eb6","public_key":"key","release":"1.0-beta.1","environment":"prod","user_id":"usr1","transaction":"tx1","sample_rate":"0.00000021","sample_rand":"0.021"}} +{"type":"transaction","length":640,"content_type":"application/json"} +{"transaction":"a-transaction","type":"transaction","start_timestamp":"2020-10-23T10:24:01.791Z","timestamp":"2020-10-23T10:24:02.791Z","event_id":"3367f5196c494acaae85bbbd535379ac","contexts":{"trace":{"trace_id":"b156a475de54423d9c1571df97ec7eb6","span_id":"0a53026963414893","op":"http","status":"ok"},"custom":{"some-key":"some-value"}},"spans":[{"start_timestamp":"2021-03-05T08:51:12.838Z","timestamp":"2021-03-05T08:51:12.949Z","trace_id":"2b099185293344a5bfdd7ad89ebf9416","span_id":"5b95c29a5ded4281","parent_span_id":"a3b2d1d58b344b07","op":"PersonService.create","description":"desc","status":"aborted","tags":{"name":"value"}}]} diff --git a/sentry/src/test/resources/json/sentry_envelope_header.json b/sentry/src/test/resources/json/sentry_envelope_header.json index 626e9cbbc2..23580aab66 100644 --- a/sentry/src/test/resources/json/sentry_envelope_header.json +++ b/sentry/src/test/resources/json/sentry_envelope_header.json @@ -26,6 +26,7 @@ "user_id": "c052c566-6619-45f5-a61f-172802afa39a", "transaction": "0252ec25-cd0a-4230-bd2f-936a4585637e", "sample_rate": "0.00000021", + "sample_rand": "0.00000012", "sampled": "true", "replay_id": "3367f5196c494acaae85bbbd535379aa" }, diff --git a/sentry/src/test/resources/json/trace_state.json b/sentry/src/test/resources/json/trace_state.json index db745e5213..a5eabb3583 100644 --- a/sentry/src/test/resources/json/trace_state.json +++ b/sentry/src/test/resources/json/trace_state.json @@ -6,6 +6,7 @@ "user_id": "c052c566-6619-45f5-a61f-172802afa39a", "transaction": "0252ec25-cd0a-4230-bd2f-936a4585637e", "sample_rate": "0.00000021", + "sample_rand": "0.00000012", "sampled": "true", "replay_id": "3367f5196c494acaae85bbbd535379aa" } From 80eda8c315b4354acc72a626a7861a9f882e1245 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Wed, 12 Feb 2025 18:29:03 +0100 Subject: [PATCH 22/27] Update `sampleRate` in DSC (#4158) * wip * wip2 * wip3 * format + api * cleanup * revert change to demo * make baggage final again * remove ObjectToString suppressing * remove outdated comment * remove getPropagationContext from Scopes again * remove noop baggage and propagation context again * fix outbox sender; test; cleanup api file * Update sampleRate in DSC * wip * wip2 * wip3 * format + api * cleanup * revert change to demo * make baggage final again * remove ObjectToString suppressing * remove outdated comment * remove getPropagationContext from Scopes again * remove noop baggage and propagation context again * fix outbox sender; test; cleanup api file * review changes * Format code * changelog * fix build --------- Co-authored-by: Sentry Github Bot --- CHANGELOG.md | 2 + .../OtelSentrySpanProcessor.java | 1 + sentry/api/sentry.api | 2 + sentry/src/main/java/io/sentry/Baggage.java | 45 ++++++++++++++++- .../src/main/java/io/sentry/SpanContext.java | 7 ++- sentry/src/test/java/io/sentry/BaggageTest.kt | 50 +++++++++++++++++++ .../test/java/io/sentry/SpanContextTest.kt | 11 ++++ 7 files changed, 115 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f681818c9b..dd6c239f1b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,8 @@ - (Internal) Add API to filter native debug images based on stacktrace addresses ([#4089](https://github.com/getsentry/sentry-java/pull/4089)) - Propagate sampling random value ([#4153](https://github.com/getsentry/sentry-java/pull/4153)) - The random value used for sampling traces is now sent to Sentry and attached to the `baggage` header on outgoing requests +- Update `sampleRate` that is sent to Sentry and attached to the `baggage` header on outgoing requests ([#4158](https://github.com/getsentry/sentry-java/pull/4158)) + - If the SDK uses its `sampleRate` or `tracesSampler` callback, it now updates the `sampleRate` in Dynamic Sampling Context. ### Fixes diff --git a/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/OtelSentrySpanProcessor.java b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/OtelSentrySpanProcessor.java index 37293569ae..6469ea9209 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/OtelSentrySpanProcessor.java +++ b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/OtelSentrySpanProcessor.java @@ -83,6 +83,7 @@ public void onStart(final @NotNull Context parentContext, final @NotNull ReadWri new SentryId(traceId), sentrySpanId, sentryParentSpanId, baggage, sampled); baggage = propagationContext.getBaggage(); + baggage.setValuesFromSamplingDecision(samplingDecision); updatePropagationContext(scopes, propagationContext); } diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index eb63ba38c4..51ff5b390a 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -33,6 +33,7 @@ public final class io/sentry/Baggage { public fun (Lio/sentry/Baggage;)V public fun (Lio/sentry/ILogger;)V public fun (Ljava/util/Map;Ljava/lang/String;ZZLio/sentry/ILogger;)V + public fun forceSetSampleRate (Ljava/lang/String;)V public fun freeze ()V public static fun fromEvent (Lio/sentry/SentryEvent;Lio/sentry/SentryOptions;)Lio/sentry/Baggage; public static fun fromHeader (Ljava/lang/String;)Lio/sentry/Baggage; @@ -70,6 +71,7 @@ public final class io/sentry/Baggage { public fun setTraceId (Ljava/lang/String;)V public fun setTransaction (Ljava/lang/String;)V public fun setUserId (Ljava/lang/String;)V + public fun setValuesFromSamplingDecision (Lio/sentry/TracesSamplingDecision;)V public fun setValuesFromScope (Lio/sentry/IScope;Lio/sentry/SentryOptions;)V public fun setValuesFromTransaction (Lio/sentry/protocol/SentryId;Lio/sentry/protocol/SentryId;Lio/sentry/SentryOptions;Lio/sentry/TracesSamplingDecision;Ljava/lang/String;Lio/sentry/protocol/TransactionNameSource;)V public fun toHeaderString (Ljava/lang/String;)Ljava/lang/String; diff --git a/sentry/src/main/java/io/sentry/Baggage.java b/sentry/src/main/java/io/sentry/Baggage.java index 71149a81da..4b83b91d40 100644 --- a/sentry/src/main/java/io/sentry/Baggage.java +++ b/sentry/src/main/java/io/sentry/Baggage.java @@ -367,6 +367,11 @@ public void setSampleRate(final @Nullable String sampleRate) { set(DSCKeys.SAMPLE_RATE, sampleRate); } + @ApiStatus.Internal + public void forceSetSampleRate(final @Nullable String sampleRate) { + set(DSCKeys.SAMPLE_RATE, sampleRate, true); + } + @ApiStatus.Internal public @Nullable String getSampleRand() { return get(DSCKeys.SAMPLE_RAND); @@ -399,7 +404,18 @@ public void setReplayId(final @Nullable String replayId) { @ApiStatus.Internal public void set(final @NotNull String key, final @Nullable String value) { - if (mutable) { + set(key, value, false); + } + + /** + * Sets / updates a value + * + * @param key key + * @param value value to set + * @param force ignores mutability of this baggage and sets the value anyways + */ + private void set(final @NotNull String key, final @Nullable String value, final boolean force) { + if (mutable || force) { this.keyValues.put(key, value); } } @@ -439,6 +455,25 @@ public void setValuesFromTransaction( } setSampleRate(sampleRateToString(sampleRate(samplingDecision))); setSampled(StringUtils.toString(sampled(samplingDecision))); + setSampleRand(sampleRateToString(sampleRand(samplingDecision))); // TODO check + } + + @ApiStatus.Internal + public void setValuesFromSamplingDecision( + final @Nullable TracesSamplingDecision samplingDecision) { + if (samplingDecision == null) { + return; + } + + setSampled(StringUtils.toString(sampled(samplingDecision))); + + if (samplingDecision.getSampleRand() != null) { + setSampleRand(sampleRateToString(sampleRand(samplingDecision))); + } + + if (samplingDecision.getSampleRate() != null) { + forceSetSampleRate(sampleRateToString(sampleRate(samplingDecision))); + } } @ApiStatus.Internal @@ -466,6 +501,14 @@ public void setValuesFromScope( return samplingDecision.getSampleRate(); } + private static @Nullable Double sampleRand(@Nullable TracesSamplingDecision samplingDecision) { + if (samplingDecision == null) { + return null; + } + + return samplingDecision.getSampleRand(); + } + private static @Nullable String sampleRateToString(@Nullable Double sampleRateAsDouble) { if (!SampleRateUtils.isValidTracesSampleRate(sampleRateAsDouble, false)) { return null; diff --git a/sentry/src/main/java/io/sentry/SpanContext.java b/sentry/src/main/java/io/sentry/SpanContext.java index 91e3abd956..6f1e4e4eaf 100644 --- a/sentry/src/main/java/io/sentry/SpanContext.java +++ b/sentry/src/main/java/io/sentry/SpanContext.java @@ -91,10 +91,10 @@ public SpanContext( this.spanId = Objects.requireNonNull(spanId, "spanId is required"); this.op = Objects.requireNonNull(operation, "operation is required"); this.parentSpanId = parentSpanId; - this.samplingDecision = samplingDecision; this.description = description; this.status = status; this.origin = origin; + setSamplingDecision(samplingDecision); } /** @@ -106,7 +106,7 @@ public SpanContext(final @NotNull SpanContext spanContext) { this.traceId = spanContext.traceId; this.spanId = spanContext.spanId; this.parentSpanId = spanContext.parentSpanId; - this.samplingDecision = spanContext.samplingDecision; + setSamplingDecision(spanContext.samplingDecision); this.op = spanContext.op; this.description = spanContext.description; this.status = spanContext.status; @@ -209,6 +209,9 @@ public void setSampled(final @Nullable Boolean sampled, final @Nullable Boolean @ApiStatus.Internal public void setSamplingDecision(final @Nullable TracesSamplingDecision samplingDecision) { this.samplingDecision = samplingDecision; + if (this.baggage != null) { + this.baggage.setValuesFromSamplingDecision(this.samplingDecision); + } } public @Nullable String getOrigin() { diff --git a/sentry/src/test/java/io/sentry/BaggageTest.kt b/sentry/src/test/java/io/sentry/BaggageTest.kt index fedd782624..27cfcfd49e 100644 --- a/sentry/src/test/java/io/sentry/BaggageTest.kt +++ b/sentry/src/test/java/io/sentry/BaggageTest.kt @@ -355,6 +355,18 @@ class BaggageTest { assertEquals("sentry-trace_id=a,sentry-transaction=sentryTransaction", baggage.toHeaderString(null)) } + @Test + fun `if header contains sentry values baggage is marked as shouldFreeze`() { + val baggage = Baggage.fromHeader("sentry-trace_id=a,sentry-transaction=sentryTransaction", logger) + assertTrue(baggage.isShouldFreeze) + } + + @Test + fun `if header does not contain sentry values baggage is not marked as shouldFreeze`() { + val baggage = Baggage.fromHeader("a=b", logger) + assertFalse(baggage.isShouldFreeze) + } + @Test fun `value may contain = sign`() { val baggage = Baggage(logger) @@ -560,6 +572,44 @@ class BaggageTest { } @Test + fun `sets values from traces sampling decision`() { + val baggage = Baggage.fromHeader("a=b,c=d") + baggage.setValuesFromSamplingDecision(TracesSamplingDecision(true, 0.021, 0.025)) + + assertEquals("true", baggage.sampled) + assertEquals("0.021", baggage.sampleRate) + assertEquals("0.025", baggage.sampleRand) + } + + @Test + fun `handles null traces sampling decision`() { + val baggage = Baggage.fromHeader("a=b,c=d") + baggage.setValuesFromSamplingDecision(null) + } + + @Test + fun `sets values from traces sampling decision only if non null`() { + val baggage = Baggage.fromHeader("a=b,c=d") + baggage.setValuesFromSamplingDecision(TracesSamplingDecision(true, 0.021, 0.025)) + baggage.setValuesFromSamplingDecision(TracesSamplingDecision(false, null, null)) + + assertEquals("false", baggage.sampled) + assertEquals("0.021", baggage.sampleRate) + assertEquals("0.025", baggage.sampleRand) + } + + @Test + fun `replaces only sample rate if already frozen`() { + val baggage = Baggage.fromHeader("a=b,c=d") + baggage.setValuesFromSamplingDecision(TracesSamplingDecision(true, 0.021, 0.025)) + baggage.freeze() + baggage.setValuesFromSamplingDecision(TracesSamplingDecision(false, 0.121, 0.125)) + + assertEquals("true", baggage.sampled) + assertEquals("0.121", baggage.sampleRate) + assertEquals("0.025", baggage.sampleRand) + } + fun `sample rate can be retrieved as double`() { val baggage = Baggage.fromHeader("a=b,c=d") baggage.sampleRate = "0.1" diff --git a/sentry/src/test/java/io/sentry/SpanContextTest.kt b/sentry/src/test/java/io/sentry/SpanContextTest.kt index 5e7ba9de25..0935c10e1f 100644 --- a/sentry/src/test/java/io/sentry/SpanContextTest.kt +++ b/sentry/src/test/java/io/sentry/SpanContextTest.kt @@ -19,4 +19,15 @@ class SpanContextTest { trace.setTag("tagName", "tagValue") assertEquals("tagValue", trace.tags["tagName"]) } + + @Test + fun `updates sampling decision on baggage`() { + val trace = SpanContext("op") + trace.baggage = Baggage.fromHeader("a=b") + trace.samplingDecision = TracesSamplingDecision(true, 0.1, 0.2) + + assertEquals("true", trace.baggage?.sampled) + assertEquals("0.1", trace.baggage?.sampleRate) + assertEquals("0.2", trace.baggage?.sampleRand) + } } From f2910984ad5905276410aac83fc78a311dc8f28c Mon Sep 17 00:00:00 2001 From: getsentry-bot Date: Wed, 12 Feb 2025 17:29:53 +0000 Subject: [PATCH 23/27] release: 8.2.0 --- CHANGELOG.md | 2 +- gradle.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dd6c239f1b..d6f2529ff4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## Unreleased +## 8.2.0 ### Breaking Changes diff --git a/gradle.properties b/gradle.properties index 0e6fa3890b..5087fc4c95 100644 --- a/gradle.properties +++ b/gradle.properties @@ -14,7 +14,7 @@ org.gradle.workers.max=2 android.useAndroidX=true # Release information -versionName=8.1.0 +versionName=8.2.0 # Override the SDK name on native crashes on Android sentryAndroidSdkName=sentry.native.android From 46cb541ec78df2db668b6460f4f4a79881a06bc0 Mon Sep 17 00:00:00 2001 From: Lorenzo Cian Date: Mon, 17 Feb 2025 12:26:16 +0100 Subject: [PATCH 24/27] Refactor `ReactorUtils` into its own `sentry-reactor` module (#4155) * Refactor `ReactorUtils` into its own `sentry-reactor` module * comment out failing tests * add dependency to sentry-samples-spring-boot-webflux-jakarta * fixes (#4160) * update * update * update * changelog * update * readme * Update README.md * Update CHANGELOG.md --------- Co-authored-by: Alexander Dinauer --- .github/ISSUE_TEMPLATE/bug_report_java.yml | 1 + CHANGELOG.md | 8 ++ buildSrc/src/main/java/Config.kt | 1 + sentry-reactor/README.md | 68 ++++++++++++++ sentry-reactor/api/sentry-reactor.api | 26 +++++ sentry-reactor/build.gradle.kts | 94 +++++++++++++++++++ .../SentryReactorThreadLocalAccessor.java | 4 +- .../io/sentry/reactor/SentryReactorUtils.java | 8 +- .../io.micrometer.context.ThreadLocalAccessor | 1 + .../sentry/reactor/SentryReactorUtilsTest.kt | 13 +-- .../spring/boot/jakarta/TodoController.java | 4 +- .../spring/boot/jakarta/TodoController.java | 4 +- .../spring/boot/jakarta/TodoController.java | 4 +- sentry-spring-boot-jakarta/build.gradle.kts | 2 + .../api/sentry-spring-jakarta.api | 25 +---- sentry-spring-jakarta/build.gradle.kts | 2 + .../webflux/SentryWebExceptionHandler.java | 3 +- ...entryWebFilterWithThreadLocalAccessor.java | 3 +- .../jakarta/webflux/reactor/ReactorUtils.java | 9 ++ .../io.micrometer.context.ThreadLocalAccessor | 1 - settings.gradle.kts | 1 + 21 files changed, 239 insertions(+), 43 deletions(-) create mode 100644 sentry-reactor/README.md create mode 100644 sentry-reactor/api/sentry-reactor.api create mode 100644 sentry-reactor/build.gradle.kts rename {sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/webflux => sentry-reactor/src/main/java/io/sentry/reactor}/SentryReactorThreadLocalAccessor.java (85%) rename sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/webflux/ReactorUtils.java => sentry-reactor/src/main/java/io/sentry/reactor/SentryReactorUtils.java (97%) create mode 100644 sentry-reactor/src/main/resources/META-INF/services/io.micrometer.context.ThreadLocalAccessor rename sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/webflux/ReactorUtilsTest.kt => sentry-reactor/src/test/kotlin/io/sentry/reactor/SentryReactorUtilsTest.kt (90%) create mode 100644 sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/webflux/reactor/ReactorUtils.java delete mode 100644 sentry-spring-jakarta/src/main/resources/META-INF/services/io.micrometer.context.ThreadLocalAccessor diff --git a/.github/ISSUE_TEMPLATE/bug_report_java.yml b/.github/ISSUE_TEMPLATE/bug_report_java.yml index d71d61c074..18ebe6b620 100644 --- a/.github/ISSUE_TEMPLATE/bug_report_java.yml +++ b/.github/ISSUE_TEMPLATE/bug_report_java.yml @@ -34,6 +34,7 @@ body: - sentry-openfeign - sentry-apache-http-client-5 - sentry-okhttp + - sentry-reactor - other validations: required: true diff --git a/CHANGELOG.md b/CHANGELOG.md index d6f2529ff4..aed6b1efc9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## Unreleased + +### Behavioural Changes + +- The class `io.sentry.spring.jakarta.webflux.ReactorUtils` is now deprecated, please use `io.sentry.reactor.SentryReactorUtils` in the new `sentry-reactor` module instead ([#4155](https://github.com/getsentry/sentry-java/pull/4155)) + - The new module will be exposed as an `api` dependency when using `sentry-spring-boot-jakarta` (Spring Boot 3) or `sentry-spring-jakarta` (Spring 6). + Therefore, if you're using one of those modules, changing your imports will suffice. + ## 8.2.0 ### Breaking Changes diff --git a/buildSrc/src/main/java/Config.kt b/buildSrc/src/main/java/Config.kt index 0db77e349e..ac379bcb41 100644 --- a/buildSrc/src/main/java/Config.kt +++ b/buildSrc/src/main/java/Config.kt @@ -259,6 +259,7 @@ object Config { val SENTRY_SERVLET_JAKARTA_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.servlet.jakarta" val SENTRY_COMPOSE_HELPER_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.compose.helper" val SENTRY_OKHTTP_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.okhttp" + val SENTRY_REACTOR_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.reactor" val group = "io.sentry" val description = "SDK for sentry.io" val versionNameProp = "versionName" diff --git a/sentry-reactor/README.md b/sentry-reactor/README.md new file mode 100644 index 0000000000..d0b69e2217 --- /dev/null +++ b/sentry-reactor/README.md @@ -0,0 +1,68 @@ +# sentry-reactor + +This module provides a set of utilities to use Sentry with [Reactor](https://projectreactor.io/). + +## Setup + +Please refer to the documentation on how to set up our [Java SDK](https://docs.sentry.io/platforms/java/), +or our [Spring](https://docs.sentry.io/platforms/java/guides/spring/) +or [Spring Boot](https://docs.sentry.io/platforms/java/guides/spring-boot/) integrations if you're using Spring WebFlux. + +If you're using our Spring Boot SDK with Spring Boot (`sentry-spring-boot` or `sentry-spring-boot-jakarta`), this module will be available and used under the hood to automatically instrument WebFlux. +If you're using our Spring SDK (`sentry-spring` or `sentry-spring-jakarta`), you need to configure WebFlux as we do in [SentryWebFluxAutoConfiguration](https://github.com/getsentry/sentry-java/blob/a5098280b52aec28c71c150e286b5c937767634d/sentry-spring-boot-jakarta/src/main/java/io/sentry/spring/boot/jakarta/SentryWebfluxAutoConfiguration.java) for Spring Boot. + +Otherwise, read on to find out how to set up and use the integration. + +Add the latest version of `io.sentry.reactor` as a dependency. +Make sure you're using `io.micrometer:context-propagation:1.0.2` or later, and `io.projectreactor:reactor-core:3.5.3` or later. + +Then, enable automatic context propagation: +```java +import reactor.core.publisher.Hooks; +// ... +Hooks.enableAutomaticContextPropagation(); +``` + +## Usage + +You can use the utilities provided by this module to wrap `Mono` and `Flux` objects to enable correct errors, breadcrumbs and tracing in your application. + +For normal use cases, you should wrap your operations on `Mono` or `Flux` objects using the `withSentry` function. +This will fork the *current scopes* and use them throughout the stream's execution context. + +For example: +```java +import reactor.core.publisher.Mono; +import io.sentry.Sentry; +import io.sentry.ISpan; +import io.sentry.ITransaction; +import io.sentry.TransactionOptions; + +TransactionOptions txOptions = new TransactionOptions(); +txOptions.setBindToScope(true); +ITransaction tx = Sentry.startTransaction("Transaction", "op", txOptions); +ISpan child = tx.startChild("Outside Mono", "op") +Sentry.captureMessage("Message outside Mono") +child.finish() +String result = SentryReactorUtils.withSentry( + Mono.just("hello") + .map({ (it) -> + ISpan span = Sentry.getCurrentScopes().transaction.startChild("Inside Mono", "map"); + Sentry.captureMessage("Message inside Mono"); + span.finish(); + return it; + }) +).block(); +System.out.println(result); +tx.finish(); +``` + +For more complex use cases, you can also use `withSentryForkedRoots` to fork the root scopes or `withSentryScopes` to wrap the operation in arbitrary scopes. + +For more information on scopes and scope forking, please consult our [scopes documentation](https://docs.sentry.io/platforms/java/enriching-events/scopes). + +Examples of usage of this module (with Spring WebFlux) are provided in +[sentry-samples-spring-boot-webflux](https://github.com/getsentry/sentry-java/tree/main/sentry-samples/sentry-samples-spring-boot-webflux) +and +[sentry-samples-spring-boot-webflux-jakarta](https://github.com/getsentry/sentry-java/tree/main/sentry-samples/sentry-samples-spring-boot-webflux-jakarta) +. diff --git a/sentry-reactor/api/sentry-reactor.api b/sentry-reactor/api/sentry-reactor.api new file mode 100644 index 0000000000..bb38ca1df4 --- /dev/null +++ b/sentry-reactor/api/sentry-reactor.api @@ -0,0 +1,26 @@ +public final class io/sentry/reactor/BuildConfig { + public static final field SENTRY_REACTOR_SDK_NAME Ljava/lang/String; + public static final field VERSION_NAME Ljava/lang/String; +} + +public final class io/sentry/reactor/SentryReactorThreadLocalAccessor : io/micrometer/context/ThreadLocalAccessor { + public static final field KEY Ljava/lang/String; + public fun ()V + public fun getValue ()Lio/sentry/IScopes; + public synthetic fun getValue ()Ljava/lang/Object; + public fun key ()Ljava/lang/Object; + public fun reset ()V + public fun setValue (Lio/sentry/IScopes;)V + public synthetic fun setValue (Ljava/lang/Object;)V +} + +public class io/sentry/reactor/SentryReactorUtils { + public fun ()V + public static fun withSentry (Lreactor/core/publisher/Flux;)Lreactor/core/publisher/Flux; + public static fun withSentry (Lreactor/core/publisher/Mono;)Lreactor/core/publisher/Mono; + public static fun withSentryForkedRoots (Lreactor/core/publisher/Flux;)Lreactor/core/publisher/Flux; + public static fun withSentryForkedRoots (Lreactor/core/publisher/Mono;)Lreactor/core/publisher/Mono; + public static fun withSentryScopes (Lreactor/core/publisher/Flux;Lio/sentry/IScopes;)Lreactor/core/publisher/Flux; + public static fun withSentryScopes (Lreactor/core/publisher/Mono;Lio/sentry/IScopes;)Lreactor/core/publisher/Mono; +} + diff --git a/sentry-reactor/build.gradle.kts b/sentry-reactor/build.gradle.kts new file mode 100644 index 0000000000..83e7bbdaff --- /dev/null +++ b/sentry-reactor/build.gradle.kts @@ -0,0 +1,94 @@ +import net.ltgt.gradle.errorprone.errorprone +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + `java-library` + kotlin("jvm") + jacoco + id(Config.QualityPlugins.errorProne) + id(Config.QualityPlugins.gradleVersions) + id(Config.BuildPlugins.buildConfig) version Config.BuildPlugins.buildConfigVersion +} + +configure { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 +} + +tasks.withType().configureEach { + kotlinOptions.jvmTarget = JavaVersion.VERSION_17.toString() + kotlinOptions.languageVersion = Config.kotlinCompatibleLanguageVersion +} + +dependencies { + api(projects.sentry) + compileOnly(Config.Libs.reactorCore) + compileOnly(Config.Libs.contextPropagation) + + compileOnly(Config.CompileOnly.nopen) + errorprone(Config.CompileOnly.nopenChecker) + errorprone(Config.CompileOnly.errorprone) + errorprone(Config.CompileOnly.errorProneNullAway) + compileOnly(Config.CompileOnly.jetbrainsAnnotations) + + // tests + testImplementation(projects.sentryTestSupport) + testImplementation(kotlin(Config.kotlinStdLib)) + testImplementation(Config.TestLibs.kotlinTestJunit) + testImplementation(Config.TestLibs.mockitoKotlin) + + testImplementation(Config.Libs.reactorCore) + testImplementation(Config.Libs.contextPropagation) + + testImplementation(platform("org.junit:junit-bom:5.10.0")) + testImplementation("org.junit.jupiter:junit-jupiter") +} + +configure { + test { + java.srcDir("src/test/java") + } +} + +jacoco { + toolVersion = Config.QualityPlugins.Jacoco.version +} + +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) + } +} + +buildConfig { + useJavaOutput() + packageName("io.sentry.reactor") + buildConfigField("String", "SENTRY_REACTOR_SDK_NAME", "\"${Config.Sentry.SENTRY_REACTOR_SDK_NAME}\"") + buildConfigField("String", "VERSION_NAME", "\"${project.version}\"") +} + +val generateBuildConfig by tasks +tasks.withType().configureEach { + dependsOn(generateBuildConfig) + options.errorprone { + check("NullAway", net.ltgt.gradle.errorprone.CheckSeverity.ERROR) + option("NullAway:AnnotatedPackages", "io.sentry") + } +} + +repositories { + mavenCentral() +} diff --git a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/webflux/SentryReactorThreadLocalAccessor.java b/sentry-reactor/src/main/java/io/sentry/reactor/SentryReactorThreadLocalAccessor.java similarity index 85% rename from sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/webflux/SentryReactorThreadLocalAccessor.java rename to sentry-reactor/src/main/java/io/sentry/reactor/SentryReactorThreadLocalAccessor.java index 9b7e51db73..7ef4bb9bd1 100644 --- a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/webflux/SentryReactorThreadLocalAccessor.java +++ b/sentry-reactor/src/main/java/io/sentry/reactor/SentryReactorThreadLocalAccessor.java @@ -1,12 +1,10 @@ -package io.sentry.spring.jakarta.webflux; +package io.sentry.reactor; import io.micrometer.context.ThreadLocalAccessor; import io.sentry.IScopes; import io.sentry.NoOpScopes; import io.sentry.Sentry; -import org.jetbrains.annotations.ApiStatus; -@ApiStatus.Experimental public final class SentryReactorThreadLocalAccessor implements ThreadLocalAccessor { public static final String KEY = "sentry-scopes"; diff --git a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/webflux/ReactorUtils.java b/sentry-reactor/src/main/java/io/sentry/reactor/SentryReactorUtils.java similarity index 97% rename from sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/webflux/ReactorUtils.java rename to sentry-reactor/src/main/java/io/sentry/reactor/SentryReactorUtils.java index 1c2bb0afcf..16fed92bd0 100644 --- a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/webflux/ReactorUtils.java +++ b/sentry-reactor/src/main/java/io/sentry/reactor/SentryReactorUtils.java @@ -1,15 +1,15 @@ -package io.sentry.spring.jakarta.webflux; +package io.sentry.reactor; +import com.jakewharton.nopen.annotation.Open; import io.sentry.IScopes; import io.sentry.Sentry; -import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.util.context.Context; -@ApiStatus.Experimental -public final class ReactorUtils { +@Open +public class SentryReactorUtils { /** * Writes the current Sentry {@link IScopes} to the {@link Context} and uses {@link diff --git a/sentry-reactor/src/main/resources/META-INF/services/io.micrometer.context.ThreadLocalAccessor b/sentry-reactor/src/main/resources/META-INF/services/io.micrometer.context.ThreadLocalAccessor new file mode 100644 index 0000000000..8f6a41322d --- /dev/null +++ b/sentry-reactor/src/main/resources/META-INF/services/io.micrometer.context.ThreadLocalAccessor @@ -0,0 +1 @@ +io.sentry.reactor.SentryReactorThreadLocalAccessor diff --git a/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/webflux/ReactorUtilsTest.kt b/sentry-reactor/src/test/kotlin/io/sentry/reactor/SentryReactorUtilsTest.kt similarity index 90% rename from sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/webflux/ReactorUtilsTest.kt rename to sentry-reactor/src/test/kotlin/io/sentry/reactor/SentryReactorUtilsTest.kt index f3bd5d2653..e2edfa9d53 100644 --- a/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/webflux/ReactorUtilsTest.kt +++ b/sentry-reactor/src/test/kotlin/io/sentry/reactor/SentryReactorUtilsTest.kt @@ -1,4 +1,4 @@ -package io.sentry.spring.jakarta.webflux +package io.sentry.reactor import io.sentry.IScopes import io.sentry.NoOpScopes @@ -18,11 +18,12 @@ import kotlin.test.assertEquals import kotlin.test.assertNotSame import kotlin.test.assertSame -class ReactorUtilsTest { +class SentryReactorUtilsTest { @BeforeTest fun setup() { Hooks.enableAutomaticContextPropagation() + Sentry.init("https://key@sentry.io/proj") } @AfterTest @@ -34,7 +35,7 @@ class ReactorUtilsTest { fun `propagates scopes inside mono`() { val scopesToUse = mock() var scopesInside: IScopes? = null - val mono = ReactorUtils.withSentryScopes( + val mono = SentryReactorUtils.withSentryScopes( Mono.just("hello") .publishOn(Schedulers.boundedElastic()) .map { it -> @@ -52,7 +53,7 @@ class ReactorUtilsTest { fun `propagates scopes inside flux`() { val scopesToUse = mock() var scopesInside: IScopes? = null - val flux = ReactorUtils.withSentryScopes( + val flux = SentryReactorUtils.withSentryScopes( Flux.just("hello") .publishOn(Schedulers.boundedElastic()) .map { it -> @@ -101,7 +102,7 @@ class ReactorUtilsTest { val mockScopes = mock() whenever(mockScopes.forkedCurrentScope(any())).thenReturn(mock()) Sentry.setCurrentScopes(mockScopes) - ReactorUtils.withSentry(Mono.just("hello")).block() + SentryReactorUtils.withSentry(Mono.just("hello")).block() verify(mockScopes).forkedCurrentScope(any()) } @@ -111,7 +112,7 @@ class ReactorUtilsTest { val mockScopes = mock() whenever(mockScopes.forkedCurrentScope(any())).thenReturn(mock()) Sentry.setCurrentScopes(mockScopes) - ReactorUtils.withSentry(Flux.just("hello")).blockFirst() + SentryReactorUtils.withSentry(Flux.just("hello")).blockFirst() verify(mockScopes).forkedCurrentScope(any()) } diff --git a/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/TodoController.java b/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/TodoController.java index 8d86ddcb86..0fa450a879 100644 --- a/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/TodoController.java +++ b/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/TodoController.java @@ -5,7 +5,7 @@ import io.opentelemetry.context.Scope; import io.sentry.ISpan; import io.sentry.Sentry; -import io.sentry.spring.jakarta.webflux.ReactorUtils; +import io.sentry.reactor.SentryReactorUtils; import org.jetbrains.annotations.NotNull; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -51,7 +51,7 @@ Todo todo(@PathVariable Long id) { @GetMapping("/todo-webclient/{id}") Todo todoWebClient(@PathVariable Long id) { Hooks.enableAutomaticContextPropagation(); - return ReactorUtils.withSentry( + return SentryReactorUtils.withSentry( Mono.just(true) .publishOn(Schedulers.boundedElastic()) .flatMap( diff --git a/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/TodoController.java b/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/TodoController.java index 8d86ddcb86..0fa450a879 100644 --- a/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/TodoController.java +++ b/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/TodoController.java @@ -5,7 +5,7 @@ import io.opentelemetry.context.Scope; import io.sentry.ISpan; import io.sentry.Sentry; -import io.sentry.spring.jakarta.webflux.ReactorUtils; +import io.sentry.reactor.SentryReactorUtils; import org.jetbrains.annotations.NotNull; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -51,7 +51,7 @@ Todo todo(@PathVariable Long id) { @GetMapping("/todo-webclient/{id}") Todo todoWebClient(@PathVariable Long id) { Hooks.enableAutomaticContextPropagation(); - return ReactorUtils.withSentry( + return SentryReactorUtils.withSentry( Mono.just(true) .publishOn(Schedulers.boundedElastic()) .flatMap( diff --git a/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/TodoController.java b/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/TodoController.java index 88a6b11d4a..987d516936 100644 --- a/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/TodoController.java +++ b/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/TodoController.java @@ -1,6 +1,6 @@ package io.sentry.samples.spring.boot.jakarta; -import io.sentry.spring.jakarta.webflux.ReactorUtils; +import io.sentry.reactor.SentryReactorUtils; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RestController; @@ -32,7 +32,7 @@ Todo todo(@PathVariable Long id) { @GetMapping("/todo-webclient/{id}") Todo todoWebClient(@PathVariable Long id) { Hooks.enableAutomaticContextPropagation(); - return ReactorUtils.withSentry( + return SentryReactorUtils.withSentry( Mono.just(true) .publishOn(Schedulers.boundedElastic()) .flatMap( diff --git a/sentry-spring-boot-jakarta/build.gradle.kts b/sentry-spring-boot-jakarta/build.gradle.kts index 53cb078bcc..67afbaf9b9 100644 --- a/sentry-spring-boot-jakarta/build.gradle.kts +++ b/sentry-spring-boot-jakarta/build.gradle.kts @@ -44,6 +44,7 @@ dependencies { compileOnly(Config.Libs.OpenTelemetry.otelSdk) compileOnly(projects.sentryOpentelemetry.sentryOpentelemetryCore) compileOnly(projects.sentryOpentelemetry.sentryOpentelemetryAgentcustomization) + api(projects.sentryReactor) annotationProcessor(platform(SpringBootPlugin.BOM_COORDINATES)) annotationProcessor(Config.AnnotationProcessors.springBootAutoConfigure) @@ -85,6 +86,7 @@ dependencies { testImplementation(projects.sentryOpentelemetry.sentryOpentelemetryAgent) testImplementation(projects.sentryOpentelemetry.sentryOpentelemetryAgentcustomization) testImplementation(projects.sentryOpentelemetry.sentryOpentelemetryBootstrap) + testImplementation(projects.sentryReactor) } configure { diff --git a/sentry-spring-jakarta/api/sentry-spring-jakarta.api b/sentry-spring-jakarta/api/sentry-spring-jakarta.api index 1da9c6f03a..cb27fb7f82 100644 --- a/sentry-spring-jakarta/api/sentry-spring-jakarta.api +++ b/sentry-spring-jakarta/api/sentry-spring-jakarta.api @@ -306,27 +306,6 @@ public abstract class io/sentry/spring/jakarta/webflux/AbstractSentryWebFilter : protected fun startTransaction (Lio/sentry/IScopes;Lorg/springframework/http/server/reactive/ServerHttpRequest;Lio/sentry/TransactionContext;Ljava/lang/String;)Lio/sentry/ITransaction; } -public final class io/sentry/spring/jakarta/webflux/ReactorUtils { - public fun ()V - public static fun withSentry (Lreactor/core/publisher/Flux;)Lreactor/core/publisher/Flux; - public static fun withSentry (Lreactor/core/publisher/Mono;)Lreactor/core/publisher/Mono; - public static fun withSentryForkedRoots (Lreactor/core/publisher/Flux;)Lreactor/core/publisher/Flux; - public static fun withSentryForkedRoots (Lreactor/core/publisher/Mono;)Lreactor/core/publisher/Mono; - public static fun withSentryScopes (Lreactor/core/publisher/Flux;Lio/sentry/IScopes;)Lreactor/core/publisher/Flux; - public static fun withSentryScopes (Lreactor/core/publisher/Mono;Lio/sentry/IScopes;)Lreactor/core/publisher/Mono; -} - -public final class io/sentry/spring/jakarta/webflux/SentryReactorThreadLocalAccessor : io/micrometer/context/ThreadLocalAccessor { - public static final field KEY Ljava/lang/String; - public fun ()V - public fun getValue ()Lio/sentry/IScopes; - public synthetic fun getValue ()Ljava/lang/Object; - public fun key ()Ljava/lang/Object; - public fun reset ()V - public fun setValue (Lio/sentry/IScopes;)V - public synthetic fun setValue (Ljava/lang/Object;)V -} - public class io/sentry/spring/jakarta/webflux/SentryRequestResolver { public fun (Lio/sentry/IScopes;)V public fun resolveSentryRequest (Lorg/springframework/http/server/reactive/ServerHttpRequest;)Lio/sentry/protocol/Request; @@ -355,3 +334,7 @@ public final class io/sentry/spring/jakarta/webflux/SentryWebFilterWithThreadLoc public fun filter (Lorg/springframework/web/server/ServerWebExchange;Lorg/springframework/web/server/WebFilterChain;)Lreactor/core/publisher/Mono; } +public final class io/sentry/spring/jakarta/webflux/reactor/ReactorUtils : io/sentry/reactor/SentryReactorUtils { + public fun ()V +} + diff --git a/sentry-spring-jakarta/build.gradle.kts b/sentry-spring-jakarta/build.gradle.kts index 97aff65a84..fcae34caf8 100644 --- a/sentry-spring-jakarta/build.gradle.kts +++ b/sentry-spring-jakarta/build.gradle.kts @@ -49,6 +49,7 @@ dependencies { compileOnly(projects.sentryQuartz) compileOnly(projects.sentryOpentelemetry.sentryOpentelemetryAgentcustomization) compileOnly(projects.sentryOpentelemetry.sentryOpentelemetryBootstrap) + api(projects.sentryReactor) // tests testImplementation(projects.sentryTestSupport) @@ -66,6 +67,7 @@ dependencies { testImplementation(Config.Libs.contextPropagation) testImplementation(Config.TestLibs.awaitility) testImplementation(Config.Libs.graphQlJava22) + testImplementation(projects.sentryReactor) } tasks.withType { diff --git a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/webflux/SentryWebExceptionHandler.java b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/webflux/SentryWebExceptionHandler.java index 15c73ab625..1e1e387eb2 100644 --- a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/webflux/SentryWebExceptionHandler.java +++ b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/webflux/SentryWebExceptionHandler.java @@ -10,6 +10,7 @@ import io.sentry.SentryLevel; import io.sentry.exception.ExceptionMechanismException; import io.sentry.protocol.Mechanism; +import io.sentry.reactor.SentryReactorUtils; import io.sentry.util.Objects; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; @@ -40,7 +41,7 @@ public SentryWebExceptionHandler(final @NotNull IScopes scopes) { serverWebExchange.getAttributeOrDefault(SentryWebFilter.SENTRY_SCOPES_KEY, null); final @NotNull IScopes scopesToUse = requestScopes != null ? requestScopes : scopes; - return ReactorUtils.withSentryScopes( + return SentryReactorUtils.withSentryScopes( Mono.just(ex) .map( it -> { diff --git a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/webflux/SentryWebFilterWithThreadLocalAccessor.java b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/webflux/SentryWebFilterWithThreadLocalAccessor.java index 5408f6dbec..7748d43a01 100644 --- a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/webflux/SentryWebFilterWithThreadLocalAccessor.java +++ b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/webflux/SentryWebFilterWithThreadLocalAccessor.java @@ -4,6 +4,7 @@ import io.sentry.IScopes; import io.sentry.ITransaction; import io.sentry.Sentry; +import io.sentry.reactor.SentryReactorUtils; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -26,7 +27,7 @@ public Mono filter( final @NotNull ServerWebExchange serverWebExchange, final @NotNull WebFilterChain webFilterChain) { final @NotNull TransactionContainer transactionContainer = new TransactionContainer(); - return ReactorUtils.withSentryForkedRoots( + return SentryReactorUtils.withSentryForkedRoots( webFilterChain .filter(serverWebExchange) .doFinally( diff --git a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/webflux/reactor/ReactorUtils.java b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/webflux/reactor/ReactorUtils.java new file mode 100644 index 0000000000..b324957036 --- /dev/null +++ b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/webflux/reactor/ReactorUtils.java @@ -0,0 +1,9 @@ +package io.sentry.spring.jakarta.webflux.reactor; + +import io.sentry.reactor.SentryReactorUtils; + +/** + * @deprecated Please use {@link SentryReactorUtils} directly. + */ +@Deprecated +public final class ReactorUtils extends SentryReactorUtils {} diff --git a/sentry-spring-jakarta/src/main/resources/META-INF/services/io.micrometer.context.ThreadLocalAccessor b/sentry-spring-jakarta/src/main/resources/META-INF/services/io.micrometer.context.ThreadLocalAccessor deleted file mode 100644 index cd0c8f35d9..0000000000 --- a/sentry-spring-jakarta/src/main/resources/META-INF/services/io.micrometer.context.ThreadLocalAccessor +++ /dev/null @@ -1 +0,0 @@ -io.sentry.spring.jakarta.webflux.SentryReactorThreadLocalAccessor diff --git a/settings.gradle.kts b/settings.gradle.kts index d99f0f0e0a..28644604f0 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -52,6 +52,7 @@ include( "sentry-opentelemetry:sentry-opentelemetry-agentless-spring", "sentry-quartz", "sentry-okhttp", + "sentry-reactor", "sentry-samples:sentry-samples-android", "sentry-samples:sentry-samples-console", "sentry-samples:sentry-samples-console-opentelemetry-noagent", From f0eb0a277e2746e0ec736c00d4735e294755405a Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Mon, 17 Feb 2025 13:00:53 +0100 Subject: [PATCH 25/27] `options.tracePropagationTargets` should not be internal (#4170) * options.tracePropagationTargets should not be internal * changelog --- CHANGELOG.md | 4 ++++ sentry/src/main/java/io/sentry/SentryOptions.java | 1 - 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aed6b1efc9..48a37ffdcf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Fixes + +- `SentryOptions.setTracePropagationTargets` is no longer marked internal ([#4170](https://github.com/getsentry/sentry-java/pull/4170)) + ### Behavioural Changes - The class `io.sentry.spring.jakarta.webflux.ReactorUtils` is now deprecated, please use `io.sentry.reactor.SentryReactorUtils` in the new `sentry-reactor` module instead ([#4155](https://github.com/getsentry/sentry-java/pull/4155)) diff --git a/sentry/src/main/java/io/sentry/SentryOptions.java b/sentry/src/main/java/io/sentry/SentryOptions.java index 79a0c188a1..384b097ee6 100644 --- a/sentry/src/main/java/io/sentry/SentryOptions.java +++ b/sentry/src/main/java/io/sentry/SentryOptions.java @@ -1845,7 +1845,6 @@ public void setProfilesSampleRate(final @Nullable Double profilesSampleRate) { return tracePropagationTargets; } - @ApiStatus.Internal public void setTracePropagationTargets(final @Nullable List tracePropagationTargets) { if (tracePropagationTargets == null) { this.tracePropagationTargets = null; From cf3d1e263cbbd6bef1ceaf05c63447de84e0dc0c Mon Sep 17 00:00:00 2001 From: Markus Hintersteiner Date: Mon, 17 Feb 2025 17:49:49 +0100 Subject: [PATCH 26/27] Mention ip-address change in 7.21.0 changelog as well (#4174) * Mention ip-address change in 7.21.0 changelog as well #skip-changelog * Update CHANGELOG.md Co-authored-by: Karl Heinz Struggl --------- Co-authored-by: Karl Heinz Struggl --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 48a37ffdcf..157f9b5cbc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -416,6 +416,8 @@ If you have been using `8.0.0-rc.4` of the Java SDK, here's the new changes that ### Behavioural Changes +- (changed in [7.20.1](https://github.com/getsentry/sentry-java/releases/tag/7.20.1)) The user ip-address is now only set to `"{{auto}}"` if sendDefaultPii is enabled ([#4071](https://github.com/getsentry/sentry-java/pull/4071)) + - This change gives you control over IP address collection directly on the client - Reduce the number of broadcasts the SDK is subscribed for ([#4052](https://github.com/getsentry/sentry-java/pull/4052)) - Drop `TempSensorBreadcrumbsIntegration` - Drop `PhoneStateBreadcrumbsIntegration` @@ -466,7 +468,6 @@ If you would like to keep some of the default broadcast events as breadcrumbs, c - The user ip-address is now only set to `"{{auto}}"` if sendDefaultPii is enabled ([#4071](https://github.com/getsentry/sentry-java/pull/4071)) - This change gives you control over IP address collection directly on the client - ## 7.20.0 ### Features From 1e5daad7b006c6a5bf51462dd46286fcbc7029b3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Feb 2025 21:18:25 +0000 Subject: [PATCH 27/27] Bump actions/create-github-app-token from 1.11.3 to 1.11.5 (#4167) Bumps [actions/create-github-app-token](https://github.com/actions/create-github-app-token) from 1.11.3 to 1.11.5. - [Release notes](https://github.com/actions/create-github-app-token/releases) - [Commits](https://github.com/actions/create-github-app-token/compare/67e27a7eb7db372a1c61a7f9bdab8699e9ee57f7...0d564482f06ca65fa9e77e2510873638c82206f2) --- updated-dependencies: - dependency-name: actions/create-github-app-token dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5f8ddf9f10..af873abff7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -19,7 +19,7 @@ jobs: steps: - name: Get auth token id: token - uses: actions/create-github-app-token@67e27a7eb7db372a1c61a7f9bdab8699e9ee57f7 # v1.11.3 + uses: actions/create-github-app-token@0d564482f06ca65fa9e77e2510873638c82206f2 # v1.11.5 with: app-id: ${{ vars.SENTRY_RELEASE_BOT_CLIENT_ID }} private-key: ${{ secrets.SENTRY_RELEASE_BOT_PRIVATE_KEY }}