diff --git a/.craft.yml b/.craft.yml index 7dbf0382589..cf59692fc94 100644 --- a/.craft.yml +++ b/.craft.yml @@ -34,6 +34,7 @@ targets: maven:io.sentry:sentry-apache-http-client-5: maven:io.sentry:sentry-android: maven:io.sentry:sentry-android-core: + maven:io.sentry:sentry-android-distribution: maven:io.sentry:sentry-android-ndk: maven:io.sentry:sentry-android-timber: maven:io.sentry:sentry-kotlin-extensions: diff --git a/.github/workflows/agp-matrix.yml b/.github/workflows/agp-matrix.yml index cb0b528cf55..bd85cff2eac 100644 --- a/.github/workflows/agp-matrix.yml +++ b/.github/workflows/agp-matrix.yml @@ -94,7 +94,7 @@ jobs: - name: Upload test results if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: test-results-AGP${{ matrix.agp }}-Integrations${{ matrix.integrations }} path: | diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c851c853724..adf002a9f63 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -53,7 +53,7 @@ jobs: - name: Upload test results if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: test-results-build path: | diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 21ba09907b0..c133df12fa1 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -36,7 +36,7 @@ jobs: cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }} - name: Initialize CodeQL - uses: github/codeql-action/init@f443b600d91635bebf5b0d9ebc620189c0d6fba5 # pin@v2 + uses: github/codeql-action/init@4e94bd11f71e507f7f87df81788dff88d1dacbfb # pin@v2 with: languages: 'java' @@ -45,4 +45,4 @@ jobs: ./gradlew buildForCodeQL --no-build-cache - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@f443b600d91635bebf5b0d9ebc620189c0d6fba5 # pin@v2 + uses: github/codeql-action/analyze@4e94bd11f71e507f7f87df81788dff88d1dacbfb # pin@v2 diff --git a/.github/workflows/integration-tests-ui-critical.yml b/.github/workflows/integration-tests-ui-critical.yml index c1fb7169757..6c78279c748 100644 --- a/.github/workflows/integration-tests-ui-critical.yml +++ b/.github/workflows/integration-tests-ui-critical.yml @@ -44,7 +44,7 @@ jobs: run: make assembleUiTestCriticalRelease - name: Upload APK artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: ${{env.APK_ARTIFACT_NAME}} path: "${{env.BASE_PATH}}/${{env.BUILD_PATH}}/${{env.APK_NAME}}" @@ -110,7 +110,7 @@ jobs: script: echo "Generated AVD snapshot for caching." - name: Download APK artifact - uses: actions/download-artifact@v5 + uses: actions/download-artifact@v6 with: name: ${{env.APK_ARTIFACT_NAME}} @@ -137,7 +137,7 @@ jobs: - name: Upload Maestro test results if: failure() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: maestro-logs path: "${{env.BASE_PATH}}/maestro-logs" diff --git a/.github/workflows/release-build.yml b/.github/workflows/release-build.yml index 3c2e6c2adad..66a690a66d7 100644 --- a/.github/workflows/release-build.yml +++ b/.github/workflows/release-build.yml @@ -32,7 +32,7 @@ jobs: run: make publish - name: Upload artifacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: ${{ github.sha }} if-no-files-found: error diff --git a/.github/workflows/spring-boot-2-matrix.yml b/.github/workflows/spring-boot-2-matrix.yml index ad6062bbf9d..b21ef0ea64b 100644 --- a/.github/workflows/spring-boot-2-matrix.yml +++ b/.github/workflows/spring-boot-2-matrix.yml @@ -148,7 +148,7 @@ jobs: - name: Upload test results if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: test-results-springboot-2-${{ matrix.springboot-version }} path: | diff --git a/.github/workflows/spring-boot-3-matrix.yml b/.github/workflows/spring-boot-3-matrix.yml index 268f0f92129..6e23294ba81 100644 --- a/.github/workflows/spring-boot-3-matrix.yml +++ b/.github/workflows/spring-boot-3-matrix.yml @@ -148,7 +148,7 @@ jobs: - name: Upload test results if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: test-results-springboot-3-${{ matrix.springboot-version }} path: | diff --git a/.github/workflows/spring-boot-4-matrix.yml b/.github/workflows/spring-boot-4-matrix.yml index 6d0b9264b28..0e3ff39a35b 100644 --- a/.github/workflows/spring-boot-4-matrix.yml +++ b/.github/workflows/spring-boot-4-matrix.yml @@ -149,7 +149,7 @@ jobs: - name: Upload test results if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: test-results-springboot-4-${{ matrix.springboot-version }} path: | diff --git a/.github/workflows/system-tests-backend.yml b/.github/workflows/system-tests-backend.yml index 38671763169..eab62ec6c42 100644 --- a/.github/workflows/system-tests-backend.yml +++ b/.github/workflows/system-tests-backend.yml @@ -147,7 +147,7 @@ jobs: - name: Upload test results if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: test-results-${{ matrix.sample }}-${{ matrix.agent }}-${{ matrix.agent-auto-init }}-system-test path: | diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c2eb2a58e1..3e1005ae460 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,26 @@ # Changelog +## 8.25.0 + +### Fixes + +- [ANR] Removed AndroidTransactionProfiler lock ([#4817](https://github.com/getsentry/sentry-java/pull/4817)) +- Avoid ExecutorService for DefaultCompositePerformanceCollector timeout ([#4841](https://github.com/getsentry/sentry-java/pull/4841)) + - This avoids infinite data collection for never stopped transactions, leading to OOMs +- Fix wrong .super() call in SentryTimberTree ([#4844](https://github.com/getsentry/sentry-java/pull/4844)) + +### Improvements + +- [ANR] Defer some class availability checks ([#4825](https://github.com/getsentry/sentry-java/pull/4825)) +- Collect PerformanceCollectionData only for sampled transactions ([#4834](https://github.com/getsentry/sentry-java/pull/4834)) + - **Breaking change**: Transactions with a deferred sampling decision (`sampled == null`) won't be collecting any performance data anymore (CPU, RAM, slow/frozen frames). + +### Dependencies + +- Bump Native SDK from v0.11.2 to v0.11.3 ([#4810](https://github.com/getsentry/sentry-java/pull/4810)) + - [changelog](https://github.com/getsentry/sentry-native/blob/master/CHANGELOG.md#0113) + - [diff](https://github.com/getsentry/sentry-native/compare/0.11.2...0.11.3) + ## 8.24.0 ### Features diff --git a/README.md b/README.md index edd447a8013..741783f239c 100644 --- a/README.md +++ b/README.md @@ -18,51 +18,51 @@ Sentry SDK for Java and Android | Packages | Maven Central | Minimum Android API Version | |-----------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| ------- | -| sentry-android | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-android/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-android) | 21 | -| sentry-android-core | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-android-core/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-android-core) | 21 | -| sentry-android-distribution | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-android-distribution/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-android-distribution) | 21 | -| sentry-android-ndk | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-android-ndk/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-android-ndk) | 21 | -| sentry-android-timber | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-android-timber/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-android-timber) | 21 | -| sentry-android-fragment | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-android-fragment/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-android-fragment) | 21 | -| sentry-android-navigation | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-android-navigation/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-android-navigation) | 21 | -| sentry-android-sqlite | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-android-sqlite/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-android-sqlite) | 21 | -| sentry-android-replay | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-android-replay/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-android-replay) | 26 | -| sentry-compose-android | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-compose-android/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-compose-android) | 21 | -| sentry-compose-desktop | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-compose-desktop/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-compose-desktop) | -| sentry-compose | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-compose/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-compose) | -| sentry-apache-http-client-5 | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-apache-http-client-5/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-apache-http-client-5) | -| sentry | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry) | 21 | -| sentry-jul | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-jul/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-jul) | -| sentry-jdbc | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-jdbc/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-jdbc) | -| sentry-apollo | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-apollo/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-apollo) | 21 | -| sentry-apollo-3 | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-apollo-3/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-apollo-3) | 21 | -| sentry-apollo-4 | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-apollo-4/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-apollo-4) | 21 | -| sentry-kotlin-extensions | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-kotlin-extensions/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-kotlin-extensions) | 21 | -| sentry-ktor-client | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-ktor-client/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-ktor-client) | 21 | -| sentry-servlet | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-servlet/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-servlet) | | -| sentry-servlet-jakarta | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-servlet-jakarta/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-servlet-jakarta) | | -| sentry-spring-boot | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-spring-boot/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-spring-boot) | -| sentry-spring-boot-jakarta | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-spring-boot-jakarta/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-spring-boot-jakarta) | -| sentry-spring-boot-4 | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-spring-boot-4/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-spring-boot-4) | -| sentry-spring-boot-4-starter | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-spring-boot-4-starter/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-spring-boot-4-starter) | -| sentry-spring-boot-starter | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-spring-boot-starter/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-spring-boot-starter) | -| sentry-spring-boot-starter-jakarta | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-spring-boot-starter-jakarta/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-spring-boot-starter-jakarta) | -| sentry-spring | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-spring/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-spring) | -| sentry-spring-jakarta | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-spring-jakarta/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-spring-jakarta) | -| sentry-spring-7 | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-spring-7/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-spring-7) | -| sentry-logback | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-logback/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-logback) | -| sentry-log4j2 | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-log4j2/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-log4j2) | -| sentry-bom | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-bom/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-bom) | -| sentry-graphql | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-graphql/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-graphql) | -| sentry-graphql-core | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-graphql-core/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-graphql-core) | -| sentry-graphql-22 | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-graphql-22/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-graphql-22) | -| sentry-quartz | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-quartz/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-quartz) | -| sentry-openfeign | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-openfeign/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-openfeign) | -| sentry-opentelemetry-agent | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-opentelemetry-agent/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-opentelemetry-agent) | -| sentry-opentelemetry-agentcustomization | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-opentelemetry-agentcustomization/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-opentelemetry-agentcustomization) | -| sentry-opentelemetry-core | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-opentelemetry-core/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-opentelemetry-core) | -| sentry-okhttp | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-okhttp/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-okhttp) | -| sentry-reactor | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-reactor/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-reactor) | +| sentry-android | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-android?style=for-the-badge&logo=sentry&color=green) | 21 | +| sentry-android-core | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-android-core?style=for-the-badge&logo=sentry&color=green) | 21 | +| sentry-android-distribution | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-android-distribution?style=for-the-badge&logo=sentry&color=green) | 21 | +| sentry-android-ndk | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-android-ndk?style=for-the-badge&logo=sentry&color=green) | 21 | +| sentry-android-timber | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-android-timber?style=for-the-badge&logo=sentry&color=green) | 21 | +| sentry-android-fragment | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-android-fragment?style=for-the-badge&logo=sentry&color=green) | 21 | +| sentry-android-navigation | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-android-navigation?style=for-the-badge&logo=sentry&color=green) | 21 | +| sentry-android-sqlite | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-android-sqlite?style=for-the-badge&logo=sentry&color=green) | 21 | +| sentry-android-replay | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-android-replay?style=for-the-badge&logo=sentry&color=green) | 26 | +| sentry-compose-android | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-compose-android?style=for-the-badge&logo=sentry&color=green) | 21 | +| sentry-compose-desktop | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-compose-desktop?style=for-the-badge&logo=sentry&color=green) | +| sentry-compose | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-compose?style=for-the-badge&logo=sentry&color=green) | +| sentry-apache-http-client-5 | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-apache-http-client-5?style=for-the-badge&logo=sentry&color=green) | +| sentry | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry?style=for-the-badge&logo=sentry&color=green) | 21 | +| sentry-jul | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-jul?style=for-the-badge&logo=sentry&color=green) | +| sentry-jdbc | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-jdbc?style=for-the-badge&logo=sentry&color=green) | +| sentry-apollo | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-apollo?style=for-the-badge&logo=sentry&color=green) | 21 | +| sentry-apollo-3 | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-apollo-3?style=for-the-badge&logo=sentry&color=green) | 21 | +| sentry-apollo-4 | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-apollo-4?style=for-the-badge&logo=sentry&color=green) | 21 | +| sentry-kotlin-extensions | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-kotlin-extensions?style=for-the-badge&logo=sentry&color=green) | 21 | +| sentry-ktor-client | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-ktor-client?style=for-the-badge&logo=sentry&color=green) | 21 | +| sentry-servlet | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-servlet?style=for-the-badge&logo=sentry&color=green) | | +| sentry-servlet-jakarta | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-servlet-jakarta?style=for-the-badge&logo=sentry&color=green) | | +| sentry-spring-boot | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-spring-boot?style=for-the-badge&logo=sentry&color=green) | +| sentry-spring-boot-jakarta | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-spring-boot-jakarta?style=for-the-badge&logo=sentry&color=green) | +| sentry-spring-boot-4 | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-spring-boot-4?style=for-the-badge&logo=sentry&color=green) | +| sentry-spring-boot-4-starter | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-spring-boot-4-starter?style=for-the-badge&logo=sentry&color=green) | +| sentry-spring-boot-starter | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-spring-boot-starter?style=for-the-badge&logo=sentry&color=green) | +| sentry-spring-boot-starter-jakarta | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-spring-boot-starter-jakarta?style=for-the-badge&logo=sentry&color=green) | +| sentry-spring | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-spring?style=for-the-badge&logo=sentry&color=green) | +| sentry-spring-jakarta | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-spring-jakarta?style=for-the-badge&logo=sentry&color=green) | +| sentry-spring-7 | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-spring-7?style=for-the-badge&logo=sentry&color=green) | +| sentry-logback | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-logback?style=for-the-badge&logo=sentry&color=green) | +| sentry-log4j2 | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-log4j2?style=for-the-badge&logo=sentry&color=green) | +| sentry-bom | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-bom?style=for-the-badge&logo=sentry&color=green) | +| sentry-graphql | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-graphql?style=for-the-badge&logo=sentry&color=green) | +| sentry-graphql-core | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-graphql-core?style=for-the-badge&logo=sentry&color=green) | +| sentry-graphql-22 | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-graphql-22?style=for-the-badge&logo=sentry&color=green) | +| sentry-quartz | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-quartz?style=for-the-badge&logo=sentry&color=green) | +| sentry-openfeign | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-openfeign?style=for-the-badge&logo=sentry&color=green) | +| sentry-opentelemetry-agent | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-opentelemetry-agent?style=for-the-badge&logo=sentry&color=green) | +| sentry-opentelemetry-agentcustomization | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-opentelemetry-agentcustomization?style=for-the-badge&logo=sentry&color=green) | +| sentry-opentelemetry-core | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-opentelemetry-core?style=for-the-badge&logo=sentry&color=green) | +| sentry-okhttp | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-okhttp?style=for-the-badge&logo=sentry&color=green) | +| sentry-reactor | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-reactor?style=for-the-badge&logo=sentry&color=green) | # Releases diff --git a/gradle.properties b/gradle.properties index 6c35a01713f..3e0bce74540 100644 --- a/gradle.properties +++ b/gradle.properties @@ -11,7 +11,7 @@ org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled android.useAndroidX=true # Release information -versionName=8.24.0 +versionName=8.25.0 # Override the SDK name on native crashes on Android sentryAndroidSdkName=sentry.native.android diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6672de2d6e7..a95d7e459c3 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -134,7 +134,7 @@ quartz = { module = "org.quartz-scheduler:quartz", version = "2.3.0" } reactor-core = { module = "io.projectreactor:reactor-core", version = "3.5.3" } retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" } retrofit-gson = { module = "com.squareup.retrofit2:converter-gson", version.ref = "retrofit" } -sentry-native-ndk = { module = "io.sentry:sentry-native-ndk", version = "0.11.2" } +sentry-native-ndk = { module = "io.sentry:sentry-native-ndk", version = "0.11.3" } servlet-api = { module = "javax.servlet:javax.servlet-api", version = "3.1.0" } servlet-jakarta-api = { module = "jakarta.servlet:jakarta.servlet-api", version = "6.1.0" } slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ActivityFramesTracker.java b/sentry-android-core/src/main/java/io/sentry/android/core/ActivityFramesTracker.java index ade8fdd37c7..3895819fc94 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/ActivityFramesTracker.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/ActivityFramesTracker.java @@ -10,6 +10,7 @@ import io.sentry.protocol.MeasurementValue; import io.sentry.protocol.SentryId; import io.sentry.util.AutoClosableReentrantLock; +import io.sentry.util.LazyEvaluator; import java.util.HashMap; import java.util.Map; import java.util.WeakHashMap; @@ -30,7 +31,7 @@ */ public final class ActivityFramesTracker { - private @Nullable FrameMetricsAggregator frameMetricsAggregator = null; + private @NotNull LazyEvaluator frameMetricsAggregator; private @NotNull final SentryAndroidOptions options; private final @NotNull Map> @@ -41,17 +42,18 @@ public final class ActivityFramesTracker { private final @NotNull MainLooperHandler handler; protected @NotNull AutoClosableReentrantLock lock = new AutoClosableReentrantLock(); + private final @NotNull LazyEvaluator androidXAvailable; + public ActivityFramesTracker( final @NotNull io.sentry.util.LoadClass loadClass, final @NotNull SentryAndroidOptions options, final @NotNull MainLooperHandler handler) { - final boolean androidXAvailable = - loadClass.isClassAvailable("androidx.core.app.FrameMetricsAggregator", options.getLogger()); + androidXAvailable = + loadClass.isClassAvailableLazy( + "androidx.core.app.FrameMetricsAggregator", options.getLogger()); + frameMetricsAggregator = new LazyEvaluator<>(() -> new FrameMetricsAggregator()); - if (androidXAvailable) { - frameMetricsAggregator = new FrameMetricsAggregator(); - } this.options = options; this.handler = handler; } @@ -67,15 +69,15 @@ public ActivityFramesTracker( final @NotNull io.sentry.util.LoadClass loadClass, final @NotNull SentryAndroidOptions options, final @NotNull MainLooperHandler handler, - final @Nullable FrameMetricsAggregator frameMetricsAggregator) { + final @NotNull FrameMetricsAggregator frameMetricsAggregator) { this(loadClass, options, handler); - this.frameMetricsAggregator = frameMetricsAggregator; + this.frameMetricsAggregator = new LazyEvaluator<>(() -> frameMetricsAggregator); } @VisibleForTesting public boolean isFrameMetricsAggregatorAvailable() { - return frameMetricsAggregator != null + return androidXAvailable.getValue() && options.isEnableFramesTracking() && !options.isEnablePerformanceV2(); } @@ -87,7 +89,8 @@ public void addActivity(final @NotNull Activity activity) { return; } - runSafelyOnUiThread(() -> frameMetricsAggregator.add(activity), "FrameMetricsAggregator.add"); + runSafelyOnUiThread( + () -> frameMetricsAggregator.getValue().add(activity), "FrameMetricsAggregator.add"); snapshotFrameCountsAtStart(activity); } } @@ -104,11 +107,11 @@ private void snapshotFrameCountsAtStart(final @NotNull Activity activity) { return null; } - if (frameMetricsAggregator == null) { + if (!androidXAvailable.getValue()) { return null; } - final @Nullable SparseIntArray[] framesRates = frameMetricsAggregator.getMetrics(); + final @Nullable SparseIntArray[] framesRates = frameMetricsAggregator.getValue().getMetrics(); int totalFrames = 0; int slowFrames = 0; @@ -153,7 +156,7 @@ public void setMetrics(final @NotNull Activity activity, final @NotNull SentryId // there was no // Observers, See // https://android.googlesource.com/platform/frameworks/base/+/140ff5ea8e2d99edc3fbe63a43239e459334c76b - runSafelyOnUiThread(() -> frameMetricsAggregator.remove(activity), null); + runSafelyOnUiThread(() -> frameMetricsAggregator.getValue().remove(activity), null); final @Nullable FrameCounts frameCounts = diffFrameCountsAtEnd(activity); @@ -215,8 +218,9 @@ public void setMetrics(final @NotNull Activity activity, final @NotNull SentryId public void stop() { try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { if (isFrameMetricsAggregatorAvailable()) { - runSafelyOnUiThread(() -> frameMetricsAggregator.stop(), "FrameMetricsAggregator.stop"); - frameMetricsAggregator.reset(); + runSafelyOnUiThread( + () -> frameMetricsAggregator.getValue().stop(), "FrameMetricsAggregator.stop"); + frameMetricsAggregator.getValue().reset(); } activityMeasurements.clear(); } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java index 7f98b9fbb14..2b7a32820c3 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java @@ -200,8 +200,8 @@ static void initializeIntegrationsAndProcessors( options.setVersionDetector(new DefaultVersionDetector(options)); } - final boolean isAndroidXScrollViewAvailable = - loadClass.isClassAvailable("androidx.core.view.ScrollingView", options); + final @NotNull LazyEvaluator isAndroidXScrollViewAvailable = + loadClass.isClassAvailableLazy("androidx.core.view.ScrollingView", options); final boolean isComposeUpstreamAvailable = loadClass.isClassAvailable(COMPOSE_CLASS_NAME, options); diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidProfiler.java b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidProfiler.java index c6772529816..2ae1d0d3db1 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidProfiler.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidProfiler.java @@ -94,7 +94,7 @@ public ProfileEndData( private final @NotNull Map measurementsMap = new HashMap<>(); private final @Nullable ISentryExecutorService timeoutExecutorService; private final @NotNull ILogger logger; - private boolean isRunning = false; + private volatile boolean isRunning = false; protected final @NotNull AutoClosableReentrantLock lock = new AutoClosableReentrantLock(); public AndroidProfiler( @@ -354,4 +354,8 @@ private void putPerformanceCollectionDataInMeasurements( } } } + + boolean isRunning() { + return isRunning; + } } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidTransactionProfiler.java b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidTransactionProfiler.java index 0aa678d5195..a6d07ba0699 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidTransactionProfiler.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidTransactionProfiler.java @@ -5,8 +5,6 @@ import android.annotation.SuppressLint; import android.content.Context; import android.os.Build; -import android.os.Process; -import android.os.SystemClock; import io.sentry.DateUtils; import io.sentry.ILogger; import io.sentry.ISentryExecutorService; @@ -26,9 +24,9 @@ import java.util.ArrayList; import java.util.Date; import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import org.jetbrains.annotations.TestOnly; final class AndroidTransactionProfiler implements ITransactionProfiler { private final @NotNull Context context; @@ -39,10 +37,16 @@ final class AndroidTransactionProfiler implements ITransactionProfiler { private final @NotNull ISentryExecutorService executorService; private final @NotNull BuildInfoProvider buildInfoProvider; private boolean isInitialized = false; - private int transactionsCounter = 0; + private final @NotNull AtomicBoolean isRunning = new AtomicBoolean(false); private final @NotNull SentryFrameMetricsCollector frameMetricsCollector; - private @Nullable ProfilingTransactionData currentProfilingTransactionData; - private @Nullable AndroidProfiler profiler = null; + private volatile @Nullable ProfilingTransactionData currentProfilingTransactionData; + + /** + * The underlying profiler instance. It is thread safe to call it after checking if it's not null, + * because we never nullify it after instantiation. + */ + private volatile @Nullable AndroidProfiler profiler = null; + private long profileStartNanos; private long profileStartCpuMillis; private @NotNull Date profileStartTimestamp; @@ -95,6 +99,7 @@ private void init() { return; } isInitialized = true; + if (!isProfilingEnabled) { logger.log(SentryLevel.INFO, "Profiling is disabled in options."); return; @@ -124,22 +129,30 @@ private void init() { @Override public void start() { - try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { - // Debug.startMethodTracingSampling() is only available since Lollipop, but Android Profiler - // causes crashes on api 21 -> https://github.com/getsentry/sentry-java/issues/3392 - if (buildInfoProvider.getSdkInfoVersion() < Build.VERSION_CODES.LOLLIPOP_MR1) return; + // Debug.startMethodTracingSampling() is only available since Lollipop, but Android Profiler + // causes crashes on api 21 -> https://github.com/getsentry/sentry-java/issues/3392 + if (buildInfoProvider.getSdkInfoVersion() < Build.VERSION_CODES.LOLLIPOP_MR1) return; + // When the first transaction is starting, we can start profiling + if (!isRunning.getAndSet(true)) { // Let's initialize trace folder and profiling interval init(); - transactionsCounter++; - // When the first transaction is starting, we can start profiling - if (transactionsCounter == 1 && onFirstStart()) { + if (onFirstStart()) { logger.log(SentryLevel.DEBUG, "Profiler started."); } else { - transactionsCounter--; - logger.log( - SentryLevel.WARNING, "A profile is already running. This profile will be ignored."); + // If profiler is not null and is running, it means that a profile is already running + if (profiler != null && profiler.isRunning()) { + logger.log( + SentryLevel.WARNING, "A profile is already running. This profile will be ignored."); + } else { + try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { + // Ensure we unbind any transaction data, just in case of concurrent starts + currentProfilingTransactionData = null; + } + // Otherwise we update the flag, because it means the profiler is not running + isRunning.set(false); + } } } } @@ -164,11 +177,14 @@ private boolean onFirstStart() { @Override public void bindTransaction(final @NotNull ITransaction transaction) { - try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { - // If the profiler is running, but no profilingTransactionData is set, we bind it here - if (transactionsCounter > 0 && currentProfilingTransactionData == null) { - currentProfilingTransactionData = - new ProfilingTransactionData(transaction, profileStartNanos, profileStartCpuMillis); + // If the profiler is running, but no profilingTransactionData is set, we bind it here + if (isRunning.get() && currentProfilingTransactionData == null) { + try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { + // If the profiler is running, but no profilingTransactionData is set, we bind it here + if (isRunning.get() && currentProfilingTransactionData == null) { + currentProfilingTransactionData = + new ProfilingTransactionData(transaction, profileStartNanos, profileStartCpuMillis); + } } } } @@ -178,15 +194,13 @@ public void bindTransaction(final @NotNull ITransaction transaction) { final @NotNull ITransaction transaction, final @Nullable List performanceCollectionData, final @NotNull SentryOptions options) { - try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { - return onTransactionFinish( - transaction.getName(), - transaction.getEventId().toString(), - transaction.getSpanContext().getTraceId().toString(), - false, - performanceCollectionData, - options); - } + return onTransactionFinish( + transaction.getName(), + transaction.getEventId().toString(), + transaction.getSpanContext().getTraceId().toString(), + false, + performanceCollectionData, + options); } @SuppressLint("NewApi") @@ -197,20 +211,23 @@ public void bindTransaction(final @NotNull ITransaction transaction) { final boolean isTimeout, final @Nullable List performanceCollectionData, final @NotNull SentryOptions options) { - try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { - // check if profiler was created - if (profiler == null) { - return null; - } - // onTransactionStart() is only available since Lollipop_MR1 - // and SystemClock.elapsedRealtimeNanos() since Jelly Bean - // and SUPPORTED_ABIS since KITKAT - if (buildInfoProvider.getSdkInfoVersion() < Build.VERSION_CODES.LOLLIPOP_MR1) return null; + // onTransactionStart() is only available since Lollipop_MR1 + // and SystemClock.elapsedRealtimeNanos() since Jelly Bean + // and SUPPORTED_ABIS since KITKAT + if (buildInfoProvider.getSdkInfoVersion() < Build.VERSION_CODES.LOLLIPOP_MR1) return null; + + // check if profiler was created + if (profiler == null) { + return null; + } + + final ProfilingTransactionData txData; + try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { + txData = currentProfilingTransactionData; // Transaction finished, but it's not in the current profile - if (currentProfilingTransactionData == null - || !currentProfilingTransactionData.getId().equals(transactionId)) { + if (txData == null || !txData.getId().equals(transactionId)) { // A transaction is finishing, but it's not profiled. We can skip it logger.log( SentryLevel.INFO, @@ -219,118 +236,90 @@ public void bindTransaction(final @NotNull ITransaction transaction) { traceId); return null; } + currentProfilingTransactionData = null; + } - if (transactionsCounter > 0) { - transactionsCounter--; - } + logger.log(SentryLevel.DEBUG, "Transaction %s (%s) finished.", transactionName, traceId); - logger.log(SentryLevel.DEBUG, "Transaction %s (%s) finished.", transactionName, traceId); + final AndroidProfiler.ProfileEndData endData = + profiler.endAndCollect(false, performanceCollectionData); - if (transactionsCounter != 0) { - // We notify the data referring to this transaction that it finished - if (currentProfilingTransactionData != null) { - currentProfilingTransactionData.notifyFinish( - SystemClock.elapsedRealtimeNanos(), - profileStartNanos, - Process.getElapsedCpuTime(), - profileStartCpuMillis); - } - return null; - } - - final AndroidProfiler.ProfileEndData endData = - profiler.endAndCollect(false, performanceCollectionData); - // check if profiler end successfully - if (endData == null) { - return null; - } + isRunning.set(false); - long transactionDurationNanos = endData.endNanos - profileStartNanos; + // check if profiler end successfully + if (endData == null) { + return null; + } - List transactionList = new ArrayList<>(1); - final ProfilingTransactionData txData = currentProfilingTransactionData; - if (txData != null) { - transactionList.add(txData); - } - currentProfilingTransactionData = null; - // We clear the counter in case of a timeout - transactionsCounter = 0; - - String totalMem = "0"; - final @Nullable Long memory = - (options instanceof SentryAndroidOptions) - ? DeviceInfoUtil.getInstance(context, (SentryAndroidOptions) options).getTotalMemory() - : null; - if (memory != null) { - totalMem = Long.toString(memory); - } - String[] abis = Build.SUPPORTED_ABIS; + long transactionDurationNanos = endData.endNanos - profileStartNanos; - // We notify all transactions data that all transactions finished. - // Some may not have been really finished, in case of a timeout - for (ProfilingTransactionData t : transactionList) { - t.notifyFinish( - endData.endNanos, profileStartNanos, endData.endCpuMillis, profileStartCpuMillis); - } + final @NotNull List transactionList = new ArrayList<>(1); + transactionList.add(txData); + txData.notifyFinish( + endData.endNanos, profileStartNanos, endData.endCpuMillis, profileStartCpuMillis); - // cpu max frequencies are read with a lambda because reading files is involved, so it will be - // done in the background when the trace file is read - return new ProfilingTraceData( - endData.traceFile, - profileStartTimestamp, - transactionList, - transactionName, - transactionId, - traceId, - Long.toString(transactionDurationNanos), - buildInfoProvider.getSdkInfoVersion(), - abis != null && abis.length > 0 ? abis[0] : "", - () -> CpuInfoUtils.getInstance().readMaxFrequencies(), - buildInfoProvider.getManufacturer(), - buildInfoProvider.getModel(), - buildInfoProvider.getVersionRelease(), - buildInfoProvider.isEmulator(), - totalMem, - options.getProguardUuid(), - options.getRelease(), - options.getEnvironment(), - (endData.didTimeout || isTimeout) - ? ProfilingTraceData.TRUNCATION_REASON_TIMEOUT - : ProfilingTraceData.TRUNCATION_REASON_NORMAL, - endData.measurementsMap); + String totalMem = "0"; + final @Nullable Long memory = + (options instanceof SentryAndroidOptions) + ? DeviceInfoUtil.getInstance(context, (SentryAndroidOptions) options).getTotalMemory() + : null; + if (memory != null) { + totalMem = Long.toString(memory); } + final String[] abis = Build.SUPPORTED_ABIS; + + // cpu max frequencies are read with a lambda because reading files is involved, so it will be + // done in the background when the trace file is read + return new ProfilingTraceData( + endData.traceFile, + profileStartTimestamp, + transactionList, + transactionName, + transactionId, + traceId, + Long.toString(transactionDurationNanos), + buildInfoProvider.getSdkInfoVersion(), + abis != null && abis.length > 0 ? abis[0] : "", + () -> CpuInfoUtils.getInstance().readMaxFrequencies(), + buildInfoProvider.getManufacturer(), + buildInfoProvider.getModel(), + buildInfoProvider.getVersionRelease(), + buildInfoProvider.isEmulator(), + totalMem, + options.getProguardUuid(), + options.getRelease(), + options.getEnvironment(), + (endData.didTimeout || isTimeout) + ? ProfilingTraceData.TRUNCATION_REASON_TIMEOUT + : ProfilingTraceData.TRUNCATION_REASON_NORMAL, + endData.measurementsMap); } @Override public boolean isRunning() { - return transactionsCounter != 0; + return isRunning.get(); } @Override public void close() { + final @Nullable ProfilingTransactionData txData = currentProfilingTransactionData; // we stop profiling - if (currentProfilingTransactionData != null) { + if (txData != null) { onTransactionFinish( - currentProfilingTransactionData.getName(), - currentProfilingTransactionData.getId(), - currentProfilingTransactionData.getTraceId(), + txData.getName(), + txData.getId(), + txData.getTraceId(), true, null, ScopesAdapter.getInstance().getOptions()); - } else if (transactionsCounter != 0) { - // in case the app start profiling is running, and it's not bound to a transaction, we still - // stop profiling, but we also have to manually update the counter. - transactionsCounter--; } + // in case the app start profiling is running, and it's not bound to a transaction, we still + // stop profiling, but we also have to manually update the flag. + isRunning.set(false); // we have to first stop profiling otherwise we would lost the last profile if (profiler != null) { profiler.close(); } } - - @TestOnly - int getTransactionsCounter() { - return transactionsCounter; - } } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/internal/gestures/AndroidViewGestureTargetLocator.java b/sentry-android-core/src/main/java/io/sentry/android/core/internal/gestures/AndroidViewGestureTargetLocator.java index f271c3da9e3..c85fb80dc35 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/internal/gestures/AndroidViewGestureTargetLocator.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/internal/gestures/AndroidViewGestureTargetLocator.java @@ -8,6 +8,7 @@ import io.sentry.android.core.internal.util.ClassUtil; import io.sentry.internal.gestures.GestureTargetLocator; import io.sentry.internal.gestures.UiElement; +import io.sentry.util.LazyEvaluator; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -17,9 +18,10 @@ public final class AndroidViewGestureTargetLocator implements GestureTargetLocat private static final String ORIGIN = "old_view_system"; - private final boolean isAndroidXAvailable; + private final @NotNull LazyEvaluator isAndroidXAvailable; - public AndroidViewGestureTargetLocator(final boolean isAndroidXAvailable) { + public AndroidViewGestureTargetLocator( + final @NotNull LazyEvaluator isAndroidXAvailable) { this.isAndroidXAvailable = isAndroidXAvailable; } @@ -33,7 +35,7 @@ public AndroidViewGestureTargetLocator(final boolean isAndroidXAvailable) { if (targetType == UiElement.Type.CLICKABLE && isViewTappable(view)) { return createUiElement(view); } else if (targetType == UiElement.Type.SCROLLABLE - && isViewScrollable(view, isAndroidXAvailable)) { + && isViewScrollable(view, isAndroidXAvailable.getValue())) { return createUiElement(view); } return null; diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/ActivityFramesTrackerTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/ActivityFramesTrackerTest.kt index 8bfeb1c726d..55af0d71b0e 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/ActivityFramesTrackerTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/ActivityFramesTrackerTest.kt @@ -5,8 +5,10 @@ import android.util.SparseIntArray import androidx.core.app.FrameMetricsAggregator import androidx.test.ext.junit.runners.AndroidJUnit4 import io.sentry.ILogger +import io.sentry.SentryOptions import io.sentry.protocol.MeasurementValue import io.sentry.protocol.SentryId +import io.sentry.util.LazyEvaluator import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFalse @@ -34,12 +36,14 @@ class ActivityFramesTrackerTest { options.isEnablePerformanceV2 = false } - fun getSut(mockAggregator: Boolean = true): ActivityFramesTracker = - if (mockAggregator) { - ActivityFramesTracker(loadClass, options, handler, aggregator) - } else { - ActivityFramesTracker(loadClass, options, handler) - } + fun getSut(isAndroidxAvailable: Boolean = true): ActivityFramesTracker { + whenever(loadClass.isClassAvailableLazy(any(), any())) + .thenReturn(LazyEvaluator { isAndroidxAvailable }) + whenever(loadClass.isClassAvailableLazy(any(), any())) + .thenReturn(LazyEvaluator { isAndroidxAvailable }) + + return ActivityFramesTracker(loadClass, options, handler, aggregator) + } } private val fixture = Fixture() @@ -340,7 +344,6 @@ class ActivityFramesTrackerTest { @Test fun `addActivity does not throw if no AndroidX`() { - whenever(fixture.loadClass.isClassAvailable(any(), any())).thenReturn(false) val sut = fixture.getSut(false) sut.addActivity(fixture.activity) @@ -348,7 +351,6 @@ class ActivityFramesTrackerTest { @Test fun `setMetrics does not throw if no AndroidX`() { - whenever(fixture.loadClass.isClassAvailable(any(), any())).thenReturn(false) val sut = fixture.getSut(false) sut.setMetrics(fixture.activity, fixture.sentryId) @@ -356,7 +358,6 @@ class ActivityFramesTrackerTest { @Test fun `addActivity and setMetrics combined do not throw if no AndroidX`() { - whenever(fixture.loadClass.isClassAvailable(any(), any())).thenReturn(false) val sut = fixture.getSut(false) sut.addActivity(fixture.activity) @@ -373,7 +374,6 @@ class ActivityFramesTrackerTest { @Test fun `stop does not throw if no AndroidX`() { - whenever(fixture.loadClass.isClassAvailable(any(), any())).thenReturn(false) val sut = fixture.getSut(false) sut.stop() @@ -390,9 +390,13 @@ class ActivityFramesTrackerTest { @Test fun `takeMetrics returns null if no AndroidX`() { - whenever(fixture.loadClass.isClassAvailable(any(), any())).thenReturn(false) val sut = fixture.getSut(false) + whenever(fixture.aggregator.metrics).thenReturn(emptyArray(), getArray()) + + sut.addActivity(fixture.activity) + sut.setMetrics(fixture.activity, fixture.sentryId) + assertNull(sut.takeMetrics(fixture.sentryId)) } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidTransactionProfilerTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidTransactionProfilerTest.kt index 50c7ba3c7d3..c7a0f2b4567 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidTransactionProfilerTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidTransactionProfilerTest.kt @@ -185,7 +185,7 @@ class AndroidTransactionProfilerTest { fun `profiler start update inner counter`() { val profiler = fixture.getSut(context) profiler.start() - assertEquals(1, profiler.transactionsCounter) + assertTrue(profiler.isRunning) } @Test @@ -193,10 +193,10 @@ class AndroidTransactionProfilerTest { val profiler = fixture.getSut(context) profiler.start() profiler.bindTransaction(fixture.transaction1) - assertEquals(1, profiler.transactionsCounter) + assertTrue(profiler.isRunning) assertTrue(profiler.isRunning) profiler.onTransactionFinish(fixture.transaction1, null, fixture.options) - assertEquals(0, profiler.transactionsCounter) + assertFalse(profiler.isRunning) assertFalse(profiler.isRunning) } @@ -205,19 +205,19 @@ class AndroidTransactionProfilerTest { val profiler = fixture.getSut(context) profiler.start() profiler.start() - assertEquals(1, profiler.transactionsCounter) + assertTrue(profiler.isRunning) } @Test fun `profiler bind set current transaction`() { val profiler = fixture.getSut(context) - assertEquals(0, profiler.transactionsCounter) + assertFalse(profiler.isRunning) profiler.start() profiler.bindTransaction(fixture.transaction1) - assertEquals(1, profiler.transactionsCounter) + assertTrue(profiler.isRunning) val profilingTraceData = profiler.onTransactionFinish(fixture.transaction1, null, fixture.options) - assertEquals(0, profiler.transactionsCounter) + assertFalse(profiler.isRunning) assertNotNull(profilingTraceData) assertEquals(profilingTraceData.transactionId, fixture.transaction1.eventId.toString()) @@ -226,18 +226,18 @@ class AndroidTransactionProfilerTest { @Test fun `profiler multiple binds are ignored`() { val profiler = fixture.getSut(context) - assertEquals(0, profiler.transactionsCounter) + assertFalse(profiler.isRunning) profiler.start() profiler.bindTransaction(fixture.transaction1) profiler.bindTransaction(fixture.transaction2) - assertEquals(1, profiler.transactionsCounter) + assertTrue(profiler.isRunning) val profilingTraceData2 = profiler.onTransactionFinish(fixture.transaction2, null, fixture.options) - assertEquals(1, profiler.transactionsCounter) + assertTrue(profiler.isRunning) val profilingTraceData = profiler.onTransactionFinish(fixture.transaction1, null, fixture.options) - assertEquals(0, profiler.transactionsCounter) + assertFalse(profiler.isRunning) assertNotNull(profilingTraceData) assertNull(profilingTraceData2) @@ -252,7 +252,7 @@ class AndroidTransactionProfilerTest { } val profiler = fixture.getSut(context, buildInfo) profiler.start() - assertEquals(0, profiler.transactionsCounter) + assertFalse(profiler.isRunning) } @Test @@ -260,7 +260,7 @@ class AndroidTransactionProfilerTest { fixture.options.apply { profilesSampleRate = 0.0 } val profiler = fixture.getSut(context) profiler.start() - assertEquals(0, profiler.transactionsCounter) + assertFalse(profiler.isRunning) } @Test @@ -323,7 +323,7 @@ class AndroidTransactionProfilerTest { fixture.options.apply { cacheDirPath = null } val profiler = fixture.getSut(context) profiler.start() - assertEquals(0, profiler.transactionsCounter) + assertFalse(profiler.isRunning) } @Test @@ -331,7 +331,7 @@ class AndroidTransactionProfilerTest { fixture.options.apply { cacheDirPath = null } val profiler = fixture.getSut(context) profiler.start() - assertEquals(0, profiler.transactionsCounter) + assertFalse(profiler.isRunning) } @Test @@ -339,7 +339,7 @@ class AndroidTransactionProfilerTest { fixture.options.apply { profilingTracesHz = 0 } val profiler = fixture.getSut(context) profiler.start() - assertEquals(0, profiler.transactionsCounter) + assertFalse(profiler.isRunning) } @Test @@ -507,10 +507,10 @@ class AndroidTransactionProfilerTest { val profiler = fixture.getSut(context) profiler.start() profiler.bindTransaction(fixture.transaction1) - assertEquals(1, profiler.transactionsCounter) + assertTrue(profiler.isRunning) profiler.close() - assertEquals(0, profiler.transactionsCounter) + assertFalse(profiler.isRunning) // The timeout scheduled job should be cleared val androidProfiler = profiler.getProperty("profiler") @@ -527,10 +527,10 @@ class AndroidTransactionProfilerTest { fun `profiler stops profiling on close, even if not bound to a transaction`() { val profiler = fixture.getSut(context) profiler.start() - assertEquals(1, profiler.transactionsCounter) + assertTrue(profiler.isRunning) profiler.close() - assertEquals(0, profiler.transactionsCounter) + assertFalse(profiler.isRunning) // The timeout scheduled job should be cleared val androidProfiler = profiler.getProperty("profiler") diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/internal/gestures/SentryGestureListenerClickTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/internal/gestures/SentryGestureListenerClickTest.kt index efe651e4389..81950647fa9 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/internal/gestures/SentryGestureListenerClickTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/internal/gestures/SentryGestureListenerClickTest.kt @@ -17,6 +17,7 @@ import io.sentry.Scope.IWithPropagationContext import io.sentry.ScopeCallback import io.sentry.SentryLevel.INFO import io.sentry.android.core.SentryAndroidOptions +import io.sentry.util.LazyEvaluator import kotlin.test.Test import kotlin.test.assertEquals import org.mockito.kotlin.any @@ -38,7 +39,7 @@ class SentryGestureListenerClickTest { SentryAndroidOptions().apply { isEnableUserInteractionBreadcrumbs = true isEnableUserInteractionTracing = true - gestureTargetLocators = listOf(AndroidViewGestureTargetLocator(true)) + gestureTargetLocators = listOf(AndroidViewGestureTargetLocator(LazyEvaluator { true })) dsn = "https://key@sentry.io/proj" } val scopes = mock() diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/internal/gestures/SentryGestureListenerScrollTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/internal/gestures/SentryGestureListenerScrollTest.kt index 3dd1f726d7b..633bb2fdb86 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/internal/gestures/SentryGestureListenerScrollTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/internal/gestures/SentryGestureListenerScrollTest.kt @@ -20,6 +20,7 @@ import io.sentry.ScopeCallback import io.sentry.SentryLevel import io.sentry.SentryLevel.INFO import io.sentry.android.core.SentryAndroidOptions +import io.sentry.util.LazyEvaluator import kotlin.test.Test import kotlin.test.assertEquals import org.mockito.kotlin.any @@ -46,7 +47,7 @@ class SentryGestureListenerScrollTest { dsn = "https://key@sentry.io/proj" isEnableUserInteractionBreadcrumbs = true isEnableUserInteractionTracing = true - gestureTargetLocators = listOf(AndroidViewGestureTargetLocator(true)) + gestureTargetLocators = listOf(AndroidViewGestureTargetLocator(LazyEvaluator { true })) } val scopes = mock() val scope = mock() diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/internal/gestures/SentryGestureListenerTracingTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/internal/gestures/SentryGestureListenerTracingTest.kt index 3f8ba2d3003..fe994f4a828 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/internal/gestures/SentryGestureListenerTracingTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/internal/gestures/SentryGestureListenerTracingTest.kt @@ -23,6 +23,7 @@ import io.sentry.TransactionOptions import io.sentry.android.core.SentryAndroidOptions import io.sentry.protocol.SentryId import io.sentry.protocol.TransactionNameSource +import io.sentry.util.LazyEvaluator import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertNotEquals @@ -65,7 +66,8 @@ class SentryGestureListenerTracingTest { options.tracesSampleRate = tracesSampleRate options.isEnableUserInteractionTracing = isEnableUserInteractionTracing options.isEnableUserInteractionBreadcrumbs = true - options.gestureTargetLocators = listOf(AndroidViewGestureTargetLocator(true)) + options.gestureTargetLocators = + listOf(AndroidViewGestureTargetLocator(LazyEvaluator { true })) options.isEnableAutoTraceIdGeneration = isEnableAutoTraceIdGeneration whenever(scopes.options).thenReturn(options) 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 743501d664f..caca9aa8228 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 @@ -66,7 +66,7 @@ public class SentryTimberTree( /** Log an info message with optional format args. */ override fun i(message: String?, vararg args: Any?) { - super.d(message, *args) + super.i(message, *args) logWithSentry(Log.INFO, null, message, *args) } 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 2e610cf2798..37b8894dcc5 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 @@ -1,5 +1,6 @@ package io.sentry.android.timber +import android.util.Log import io.sentry.Breadcrumb import io.sentry.Scopes import io.sentry.SentryLevel @@ -23,18 +24,19 @@ import timber.log.Timber class SentryTimberTreeTest { private class Fixture { - val scopes = mock() - val logs = mock() - - init { - whenever(scopes.logger()).thenReturn(logs) - } + lateinit var scopes: Scopes + lateinit var logs: ILoggerApi fun getSut( minEventLevel: SentryLevel = SentryLevel.ERROR, minBreadcrumbLevel: SentryLevel = SentryLevel.INFO, minLogsLevel: SentryLogLevel = SentryLogLevel.INFO, - ): SentryTimberTree = SentryTimberTree(scopes, minEventLevel, minBreadcrumbLevel, minLogsLevel) + ): SentryTimberTree { + logs = mock() + scopes = mock() + whenever(scopes.logger()).thenReturn(logs) + return SentryTimberTree(scopes, minEventLevel, minBreadcrumbLevel, minLogsLevel) + } } private val fixture = Fixture() @@ -139,6 +141,56 @@ class SentryTimberTreeTest { verify(fixture.scopes).captureEvent(check { assertEquals("tag", it.getTag("TimberTag")) }) } + @Test + fun `Tree captures an event with TimberTag tag for debug events`() { + val sut = fixture.getSut(minEventLevel = SentryLevel.INFO) + Timber.plant(sut) + // only available thru static class + Timber.tag("infoTag").i("message") + verify(fixture.scopes).captureEvent(check { assertEquals("infoTag", it.getTag("TimberTag")) }) + } + + @Test + fun `Tree captures an event with chained tag usage`() { + val sut = fixture.getSut(minEventLevel = SentryLevel.INFO) + Timber.plant(sut) + // only available thru static class + Timber.tag("infoTag").log(Log.INFO, "message") + verify(fixture.scopes).captureEvent(check { assertEquals("infoTag", it.getTag("TimberTag")) }) + } + + @Test + fun `Tree properly propagates all levels`() { + val levels = + listOf( + Pair(Log.DEBUG, SentryLevel.DEBUG), + Pair(Log.VERBOSE, SentryLevel.DEBUG), + Pair(Log.INFO, SentryLevel.INFO), + Pair(Log.WARN, SentryLevel.WARNING), + Pair(Log.ERROR, SentryLevel.ERROR), + Pair(Log.ASSERT, SentryLevel.FATAL), + ) + + for (level in levels) { + Timber.uprootAll() + + val logLevel = level.first + val sentryLevel = level.second + + val sut = fixture.getSut(minEventLevel = sentryLevel) + Timber.plant(sut) + // only available thru static class + Timber.tag("tag").log(logLevel, "message") + verify(fixture.scopes) + .captureEvent( + check { + assertEquals("tag", it.getTag("TimberTag")) + assertEquals(sentryLevel, it.level) + } + ) + } + } + @Test fun `Tree captures an event without TimberTag tag`() { val sut = fixture.getSut() diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 2b05a5b0d01..2211f053912 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -7086,6 +7086,8 @@ public class io/sentry/util/LoadClass { public fun ()V public fun isClassAvailable (Ljava/lang/String;Lio/sentry/ILogger;)Z public fun isClassAvailable (Ljava/lang/String;Lio/sentry/SentryOptions;)Z + public fun isClassAvailableLazy (Ljava/lang/String;Lio/sentry/ILogger;)Lio/sentry/util/LazyEvaluator; + public fun isClassAvailableLazy (Ljava/lang/String;Lio/sentry/SentryOptions;)Lio/sentry/util/LazyEvaluator; public fun loadClass (Ljava/lang/String;Lio/sentry/ILogger;)Ljava/lang/Class; } diff --git a/sentry/src/main/java/io/sentry/DefaultCompositePerformanceCollector.java b/sentry/src/main/java/io/sentry/DefaultCompositePerformanceCollector.java index 4189ec7b6a2..1861381a853 100644 --- a/sentry/src/main/java/io/sentry/DefaultCompositePerformanceCollector.java +++ b/sentry/src/main/java/io/sentry/DefaultCompositePerformanceCollector.java @@ -8,7 +8,7 @@ import java.util.Timer; import java.util.TimerTask; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; @@ -20,8 +20,7 @@ public final class DefaultCompositePerformanceCollector implements CompositePerf private static final long TRANSACTION_COLLECTION_TIMEOUT_MILLIS = 30000; private final @NotNull AutoClosableReentrantLock timerLock = new AutoClosableReentrantLock(); private volatile @Nullable Timer timer = null; - private final @NotNull Map> performanceDataMap = - new ConcurrentHashMap<>(); + private final @NotNull Map compositeDataMap = new ConcurrentHashMap<>(); private final @NotNull List snapshotCollectors; private final @NotNull List continuousCollectors; private final boolean hasNoCollectors; @@ -65,23 +64,11 @@ public void start(final @NotNull ITransaction transaction) { collector.onSpanStarted(transaction); } - if (!performanceDataMap.containsKey(transaction.getEventId().toString())) { - performanceDataMap.put(transaction.getEventId().toString(), new ArrayList<>()); - // We schedule deletion of collected performance data after a timeout - try { - options - .getExecutorService() - .schedule(() -> stop(transaction), TRANSACTION_COLLECTION_TIMEOUT_MILLIS); - } catch (RejectedExecutionException e) { - options - .getLogger() - .log( - SentryLevel.ERROR, - "Failed to call the executor. Performance collector will not be automatically finished. Did you call Sentry.close()?", - e); - } + final @NotNull String id = transaction.getEventId().toString(); + if (!compositeDataMap.containsKey(id)) { + compositeDataMap.put(id, new CompositeData(transaction)); } - start(transaction.getEventId().toString()); + start(id); } @Override @@ -95,8 +82,10 @@ public void start(final @NotNull String id) { return; } - if (!performanceDataMap.containsKey(id)) { - performanceDataMap.put(id, new ArrayList<>()); + if (!compositeDataMap.containsKey(id)) { + // Transactions are added in start(ITransaction). If we are here, it means we don't come from + // a transaction + compositeDataMap.put(id, new CompositeData(null)); } if (!isStarted.getAndSet(true)) { try (final @NotNull ISentryLifecycleToken ignored = timerLock.acquire()) { @@ -118,6 +107,7 @@ public void run() { // and collect() calls. // This way ICollectors that collect average stats based on time intervals, like // AndroidCpuCollector, can have an actual time interval to evaluate. + final @NotNull List timedOutTransactions = new ArrayList<>(); TimerTask timerTask = new TimerTask() { @Override @@ -129,16 +119,31 @@ public void run() { if (now - lastCollectionTimestamp <= 10) { return; } + timedOutTransactions.clear(); + lastCollectionTimestamp = now; final @NotNull PerformanceCollectionData tempData = - new PerformanceCollectionData(new SentryNanotimeDate().nanoTimestamp()); + new PerformanceCollectionData(options.getDateProvider().now().nanoTimestamp()); + // Enrich tempData using collectors for (IPerformanceSnapshotCollector collector : snapshotCollectors) { collector.collect(tempData); } - for (List data : performanceDataMap.values()) { - data.add(tempData); + // Add the enriched tempData to all transactions/profiles/objects that collect data. + // Then Check if that object timed out. + for (CompositeData data : compositeDataMap.values()) { + if (data.addDataAndCheckTimeout(tempData)) { + // timed out + if (data.transaction != null) { + timedOutTransactions.add(data.transaction); + } + } + } + // Stop timed out transactions outside compositeDataMap loop, as stop() modifies the + // map + for (final @NotNull ITransaction t : timedOutTransactions) { + stop(t); } } }; @@ -183,13 +188,14 @@ public void onSpanFinished(@NotNull ISpan span) { @Override public @Nullable List stop(final @NotNull String id) { - final @Nullable List data = performanceDataMap.remove(id); + final @Nullable CompositeData data = compositeDataMap.remove(id); + options.getLogger().log(SentryLevel.DEBUG, "stop collecting performance info for " + id); - // close if they are no more running requests - if (performanceDataMap.isEmpty()) { + // close if there are no more running requests + if (compositeDataMap.isEmpty()) { close(); } - return data; + return data != null ? data.dataList : null; } @Override @@ -198,7 +204,7 @@ public void close() { .getLogger() .log(SentryLevel.DEBUG, "stop collecting all performance info for transactions"); - performanceDataMap.clear(); + compositeDataMap.clear(); for (final @NotNull IPerformanceContinuousCollector collector : continuousCollectors) { collector.clear(); } @@ -211,4 +217,30 @@ public void close() { } } } + + private class CompositeData { + private final @NotNull List dataList; + private final @Nullable ITransaction transaction; + private final long startTimestamp; + + private CompositeData(final @Nullable ITransaction transaction) { + this.dataList = new ArrayList<>(); + this.transaction = transaction; + this.startTimestamp = options.getDateProvider().now().nanoTimestamp(); + } + + /** + * Adds the data to the internal list of PerformanceCollectionData. Then it checks if data + * collection timed out (for transactions only). + * + * @return true if data collection timed out (for transactions only). + */ + boolean addDataAndCheckTimeout(final @NotNull PerformanceCollectionData data) { + dataList.add(data); + return transaction != null + && options.getDateProvider().now().nanoTimestamp() + > startTimestamp + + TimeUnit.MILLISECONDS.toNanos(TRANSACTION_COLLECTION_TIMEOUT_MILLIS); + } + } } diff --git a/sentry/src/main/java/io/sentry/SentryTracer.java b/sentry/src/main/java/io/sentry/SentryTracer.java index 21d5088a181..bb89b3d748c 100644 --- a/sentry/src/main/java/io/sentry/SentryTracer.java +++ b/sentry/src/main/java/io/sentry/SentryTracer.java @@ -77,7 +77,9 @@ public SentryTracer( this.name = context.getName(); this.instrumenter = context.getInstrumenter(); this.scopes = scopes; - this.compositePerformanceCollector = compositePerformanceCollector; + // Let's collect performance data (cpu, ram, frames) only when the transaction is sampled + this.compositePerformanceCollector = + Boolean.TRUE.equals(isSampled()) ? compositePerformanceCollector : null; this.transactionNameSource = context.getTransactionNameSource(); this.transactionOptions = transactionOptions; @@ -90,9 +92,9 @@ public SentryTracer( } // We are currently sending the performance data only in profiles, but we are always sending - // performance measurements. - if (compositePerformanceCollector != null) { - compositePerformanceCollector.start(this); + // performance measurements (frames data in spans). + if (this.compositePerformanceCollector != null) { + this.compositePerformanceCollector.start(this); } if (transactionOptions.getIdleTimeout() != null diff --git a/sentry/src/main/java/io/sentry/util/LoadClass.java b/sentry/src/main/java/io/sentry/util/LoadClass.java index a48225abe8c..1946ce8381f 100644 --- a/sentry/src/main/java/io/sentry/util/LoadClass.java +++ b/sentry/src/main/java/io/sentry/util/LoadClass.java @@ -45,4 +45,14 @@ public boolean isClassAvailable( final @NotNull String clazz, final @Nullable SentryOptions options) { return isClassAvailable(clazz, options != null ? options.getLogger() : null); } + + public LazyEvaluator isClassAvailableLazy( + final @NotNull String clazz, final @Nullable ILogger logger) { + return new LazyEvaluator<>(() -> isClassAvailable(clazz, logger)); + } + + public LazyEvaluator isClassAvailableLazy( + final @NotNull String clazz, final @Nullable SentryOptions options) { + return new LazyEvaluator<>(() -> isClassAvailable(clazz, options)); + } } diff --git a/sentry/src/test/java/io/sentry/DefaultCompositePerformanceCollectorTest.kt b/sentry/src/test/java/io/sentry/DefaultCompositePerformanceCollectorTest.kt index 99b2822aedd..46c304358df 100644 --- a/sentry/src/test/java/io/sentry/DefaultCompositePerformanceCollectorTest.kt +++ b/sentry/src/test/java/io/sentry/DefaultCompositePerformanceCollectorTest.kt @@ -1,12 +1,12 @@ package io.sentry -import io.sentry.test.DeferredExecutorService import io.sentry.test.getCtor import io.sentry.test.getProperty import io.sentry.test.injectForField import io.sentry.util.thread.ThreadChecker +import java.util.Date import java.util.Timer -import java.util.concurrent.RejectedExecutionException +import java.util.concurrent.TimeUnit import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFailsWith @@ -36,7 +36,6 @@ class DefaultCompositePerformanceCollectorTest { val scopes: IScopes = mock() val options = SentryOptions() var mockTimer: Timer? = null - val deferredExecutorService = DeferredExecutorService() val mockCpuCollector: IPerformanceSnapshotCollector = object : IPerformanceSnapshotCollector { @@ -54,16 +53,17 @@ class DefaultCompositePerformanceCollectorTest { fun getSut( memoryCollector: IPerformanceSnapshotCollector? = JavaMemoryCollector(), cpuCollector: IPerformanceSnapshotCollector? = mockCpuCollector, - executorService: ISentryExecutorService = deferredExecutorService, + optionsConfiguration: Sentry.OptionsConfiguration = + Sentry.OptionsConfiguration {}, ): CompositePerformanceCollector { options.dsn = "https://key@sentry.io/proj" - options.executorService = executorService if (cpuCollector != null) { options.addPerformanceCollector(cpuCollector) } if (memoryCollector != null) { options.addPerformanceCollector(memoryCollector) } + optionsConfiguration.configure(options) transaction1 = SentryTracer(TransactionContext("", ""), scopes) transaction2 = SentryTracer(TransactionContext("", ""), scopes) val collector = DefaultCompositePerformanceCollector(options) @@ -184,21 +184,74 @@ class DefaultCompositePerformanceCollectorTest { @Test fun `collector times out after 30 seconds`() { - val collector = fixture.getSut() + val mockDateProvider = mock() + val mockCollector = mock() + val dates = + listOf( + SentryNanotimeDate( + Date().apply { time = TimeUnit.SECONDS.toMillis(100) }, + TimeUnit.SECONDS.toNanos(100), + ), + SentryNanotimeDate( + Date().apply { time = TimeUnit.SECONDS.toMillis(131) }, + TimeUnit.SECONDS.toNanos(131), + ), + ) + whenever(mockDateProvider.now()).thenReturn(dates[0], dates[0], dates[0], dates[1]) + val collector = + fixture.getSut { + it.dateProvider = mockDateProvider + it.addPerformanceCollector(mockCollector) + } collector.start(fixture.transaction1) + verify(fixture.mockTimer, never())!!.cancel() + // Let's sleep to make the collector get values Thread.sleep(300) - verify(fixture.mockTimer, never())!!.cancel() - // Let the timeout job stop the collector - fixture.deferredExecutorService.runAll() + // When the collector gets the values, it checks the current date, set 31 seconds after the + // begin. This means it should stop itself verify(fixture.mockTimer)!!.cancel() + // When the collector times out, the data collection for spans is stopped, too + verify(mockCollector).onSpanFinished(eq(fixture.transaction1)) + // Data is deleted after the collector times out val data1 = collector.stop(fixture.transaction1) assertNull(data1) } + @Test + fun `collector collects for 30 seconds`() { + val mockDateProvider = mock() + val dates = + listOf( + SentryNanotimeDate( + Date().apply { time = TimeUnit.SECONDS.toMillis(100) }, + TimeUnit.SECONDS.toNanos(100), + ), + SentryNanotimeDate( + Date().apply { time = TimeUnit.SECONDS.toMillis(130) }, + TimeUnit.SECONDS.toNanos(130), + ), + ) + whenever(mockDateProvider.now()).thenReturn(dates[0], dates[0], dates[0], dates[1]) + val collector = fixture.getSut { it.dateProvider = mockDateProvider } + collector.start(fixture.transaction1) + verify(fixture.mockTimer, never())!!.cancel() + + // Let's sleep to make the collector get values + Thread.sleep(300) + + // When the collector gets the values, it checks the current date, set 30 seconds after the + // begin. This means it should continue without being cancelled + verify(fixture.mockTimer, never())!!.cancel() + + // Data is deleted after the collector times out + val data1 = collector.stop(fixture.transaction1) + assertNotNull(data1) + } + @Test fun `collector has no IPerformanceCollector by default`() { val collector = fixture.getSut(null, null) @@ -270,25 +323,6 @@ class DefaultCompositePerformanceCollectorTest { assertNull(collector.stop(fixture.transaction1)) } - @Test - fun `start does not throw on executor shut down`() { - val executorService = mock() - whenever(executorService.schedule(any(), any())).thenThrow(RejectedExecutionException()) - val logger = mock() - fixture.options.setLogger(logger) - fixture.options.isDebug = true - val sut = fixture.getSut(executorService = executorService) - sut.start(fixture.transaction1) - verify(logger) - .log( - eq(SentryLevel.ERROR), - eq( - "Failed to call the executor. Performance collector will not be automatically finished. Did you call Sentry.close()?" - ), - any(), - ) - } - @Test fun `Continuous collectors are notified properly`() { val collector = mock() diff --git a/sentry/src/test/java/io/sentry/SentryTracerTest.kt b/sentry/src/test/java/io/sentry/SentryTracerTest.kt index 51c292b9b6b..1ccbcf2f318 100644 --- a/sentry/src/test/java/io/sentry/SentryTracerTest.kt +++ b/sentry/src/test/java/io/sentry/SentryTracerTest.kt @@ -4,6 +4,7 @@ import io.sentry.protocol.SentryId import io.sentry.protocol.TransactionNameSource import io.sentry.protocol.User import io.sentry.test.createTestScopes +import io.sentry.test.getProperty import io.sentry.util.thread.IThreadChecker import java.time.LocalDateTime import java.time.ZoneOffset @@ -1170,9 +1171,20 @@ class SentryTracerTest { } @Test - fun `when transaction is created, but not profiled, compositePerformanceCollector is started anyway`() { - val transaction = fixture.getSut() + fun `when transaction is created and sampled, but not profiled, compositePerformanceCollector is started anyway`() { + val transaction = fixture.getSut(samplingDecision = TracesSamplingDecision(true)) verify(fixture.compositePerformanceCollector).start(anyOrNull()) + assertEquals( + fixture.compositePerformanceCollector, + transaction.getProperty("compositePerformanceCollector"), + ) + } + + @Test + fun `when transaction is created, but not sampled, compositePerformanceCollector is not started nor set`() { + val transaction = fixture.getSut(samplingDecision = TracesSamplingDecision(false)) + verify(fixture.compositePerformanceCollector, never()).start(anyOrNull()) + assertNull(transaction.getProperty("compositePerformanceCollector")) } @Test @@ -1188,7 +1200,7 @@ class SentryTracerTest { @Test fun `when transaction is finished, compositePerformanceCollector is stopped`() { - val transaction = fixture.getSut() + val transaction = fixture.getSut(samplingDecision = TracesSamplingDecision(true)) transaction.finish() verify(fixture.compositePerformanceCollector) .stop(check { assertEquals(transaction, it) }) @@ -1196,7 +1208,7 @@ class SentryTracerTest { @Test fun `when a span is started and finished the compositePerformanceCollector gets notified`() { - val transaction = fixture.getSut() + val transaction = fixture.getSut(samplingDecision = TracesSamplingDecision(true)) val span = transaction.startChild("op.span") span.finish() @@ -1205,6 +1217,17 @@ class SentryTracerTest { verify(fixture.compositePerformanceCollector).onSpanFinished(check { assertEquals(span, it) }) } + @Test + fun `when a span is started and finished the compositePerformanceCollector gets never notified if not sampled`() { + val transaction = fixture.getSut(samplingDecision = TracesSamplingDecision(false)) + + val span = transaction.startChild("op.span") + span.finish() + + verify(fixture.compositePerformanceCollector, never()).onSpanStarted(any()) + verify(fixture.compositePerformanceCollector, never()).onSpanFinished(any()) + } + @Test fun `changing transaction name without source sets source to custom`() { val transaction = fixture.getSut() @@ -1393,6 +1416,7 @@ class SentryTracerTest { } val transaction = fixture.getSut( + samplingDecision = TracesSamplingDecision(true), optionsConfiguration = { it.profilesSampleRate = 1.0 }, performanceCollector = mockPerformanceCollector, )