diff --git a/.github/file-filters.yml b/.github/file-filters.yml new file mode 100644 index 0000000000..2b81e2f0b6 --- /dev/null +++ b/.github/file-filters.yml @@ -0,0 +1,12 @@ +# This is used by the action https://github.com/dorny/paths-filter + +high_risk_code: &high_risk_code + # Transport classes + - "sentry/src/main/java/io/sentry/transport/AsyncHttpTransport.java" + - "sentry/src/main/java/io/sentry/transport/HttpConnection.java" + - "sentry/src/main/java/io/sentry/transport/QueuedThreadPoolExecutor.java" + - "sentry/src/main/java/io/sentry/transport/RateLimiter.java" + - "sentry-apache-http-client-5/src/main/java/io/sentry/transport/apache/ApacheHttpClientTransport.java" + + # Class used by hybrid SDKs + - "sentry-android-core/src/main/java/io/sentry/android/core/InternalSentrySdk.java" diff --git a/.github/workflows/agp-matrix.yml b/.github/workflows/agp-matrix.yml index b43d40697a..182c0fa19b 100644 --- a/.github/workflows/agp-matrix.yml +++ b/.github/workflows/agp-matrix.yml @@ -38,7 +38,7 @@ jobs: java-version: '17' - name: Setup Gradle - uses: gradle/actions/setup-gradle@0d30c9111cf47a838eb69c06d13f3f51ab2ed76f # pin@v3 + uses: gradle/actions/setup-gradle@bb0c460cbf5354b0cddd15bacdf0d6aaa3e5a32b # pin@v3 with: gradle-home-cache-cleanup: true @@ -59,7 +59,7 @@ jobs: # We tried to use the cache action to cache gradle stuff, but it made tests slower and timeout - name: Run instrumentation tests - uses: reactivecircus/android-emulator-runner@f0d1ed2dcad93c7479e8b2f2226c83af54494915 # pin@v2 + uses: reactivecircus/android-emulator-runner@62dbb605bba737720e10b196cb4220d374026a6d # pin@v2 with: api-level: 30 force-avd-creation: false diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f4b8d8431c..6b885942e9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -27,7 +27,7 @@ jobs: java-version: '17' - name: Setup Gradle - uses: gradle/actions/setup-gradle@0d30c9111cf47a838eb69c06d13f3f51ab2ed76f # pin@v3 + uses: gradle/actions/setup-gradle@bb0c460cbf5354b0cddd15bacdf0d6aaa3e5a32b # pin@v3 with: gradle-home-cache-cleanup: true @@ -35,7 +35,7 @@ jobs: run: make preMerge - name: Upload coverage to Codecov - uses: codecov/codecov-action@e28ff129e5465c2c0dcc6f003fc735cb6ae0c673 # pin@v4 + uses: codecov/codecov-action@b9fd7d16f6d7d1b5d2bec1a2887e65ceed900238 # pin@v4 with: name: sentry-java fail_ci_if_error: false diff --git a/.github/workflows/changes-in-high-risk-code.yml b/.github/workflows/changes-in-high-risk-code.yml new file mode 100644 index 0000000000..64decbe48f --- /dev/null +++ b/.github/workflows/changes-in-high-risk-code.yml @@ -0,0 +1,49 @@ +name: Changes In High Risk Code +on: + pull_request: + +# https://docs.github.com/en/actions/using-jobs/using-concurrency#example-using-a-fallback-value +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + files-changed: + name: Detect changed files + runs-on: ubuntu-latest + # Map a step output to a job output + outputs: + high_risk_code: ${{ steps.changes.outputs.high_risk_code }} + high_risk_code_files: ${{ steps.changes.outputs.high_risk_code_files }} + steps: + - uses: actions/checkout@v4 + - name: Get changed files + id: changes + uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2 + with: + token: ${{ github.token }} + filters: .github/file-filters.yml + + # Enable listing of files matching each filter. + # Paths to files will be available in `${FILTER_NAME}_files` output variable. + list-files: csv + + validate-high-risk-code: + if: needs.files-changed.outputs.high_risk_code == 'true' + needs: files-changed + runs-on: ubuntu-latest + steps: + - name: Comment on PR to notify of changes in high risk files + uses: actions/github-script@v7 + env: + high_risk_code: ${{ needs.files-changed.outputs.high_risk_code_files }} + with: + script: | + const highRiskFiles = process.env.high_risk_code; + const fileList = highRiskFiles.split(',').map(file => `- [ ] ${file}`).join('\n'); + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: `### 🚨 Detected changes in high risk code 🚨 \n High-risk code has higher potential to break the SDK and may be hard to test. To prevent severe bugs, apply the rollout process for releasing such changes and be extra careful when changing and reviewing these files:\n ${fileList}` + }) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 144c89ed48..99abfbdca6 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -34,12 +34,12 @@ jobs: java-version: '17' - name: Setup Gradle - uses: gradle/actions/setup-gradle@0d30c9111cf47a838eb69c06d13f3f51ab2ed76f # pin@v3 + uses: gradle/actions/setup-gradle@bb0c460cbf5354b0cddd15bacdf0d6aaa3e5a32b # pin@v3 with: gradle-home-cache-cleanup: true - name: Initialize CodeQL - uses: github/codeql-action/init@294a9d92911152fe08befb9ec03e240add280cb3 # pin@v2 + uses: github/codeql-action/init@662472033e021d55d94146f66f6058822b0b39fd # pin@v2 with: languages: ${{ matrix.language }} @@ -48,4 +48,4 @@ jobs: ./gradlew buildForCodeQL - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@294a9d92911152fe08befb9ec03e240add280cb3 # pin@v2 + uses: github/codeql-action/analyze@662472033e021d55d94146f66f6058822b0b39fd # pin@v2 diff --git a/.github/workflows/enforce-license-compliance.yml b/.github/workflows/enforce-license-compliance.yml index c2ddec5865..2b87c1d278 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@0d30c9111cf47a838eb69c06d13f3f51ab2ed76f # pin@v3 + uses: gradle/actions/setup-gradle@bb0c460cbf5354b0cddd15bacdf0d6aaa3e5a32b # pin@v3 with: gradle-home-cache-cleanup: true diff --git a/.github/workflows/generate-javadocs.yml b/.github/workflows/generate-javadocs.yml index dd171af5a2..b540441fb7 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@0d30c9111cf47a838eb69c06d13f3f51ab2ed76f # pin@v3 + uses: gradle/actions/setup-gradle@bb0c460cbf5354b0cddd15bacdf0d6aaa3e5a32b # pin@v3 with: gradle-home-cache-cleanup: true @@ -28,7 +28,7 @@ jobs: run: | ./gradlew aggregateJavadocs - name: Deploy - uses: JamesIves/github-pages-deploy-action@920cbb300dcd3f0568dbc42700c61e2fd9e6139c # pin@4.6.4 + uses: JamesIves/github-pages-deploy-action@881db5376404c5c8d621010bcbec0310b58d5e29 # pin@4.6.8 with: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} BRANCH: gh-pages diff --git a/.github/workflows/integration-tests-benchmarks.yml b/.github/workflows/integration-tests-benchmarks.yml index f0beaa60b5..58fc933752 100644 --- a/.github/workflows/integration-tests-benchmarks.yml +++ b/.github/workflows/integration-tests-benchmarks.yml @@ -37,7 +37,7 @@ jobs: java-version: '17' - name: Setup Gradle - uses: gradle/actions/setup-gradle@0d30c9111cf47a838eb69c06d13f3f51ab2ed76f # pin@v3 + uses: gradle/actions/setup-gradle@bb0c460cbf5354b0cddd15bacdf0d6aaa3e5a32b # pin@v3 with: gradle-home-cache-cleanup: true @@ -86,7 +86,7 @@ jobs: java-version: '17' - name: Setup Gradle - uses: gradle/actions/setup-gradle@0d30c9111cf47a838eb69c06d13f3f51ab2ed76f # pin@v3 + uses: gradle/actions/setup-gradle@bb0c460cbf5354b0cddd15bacdf0d6aaa3e5a32b # pin@v3 with: gradle-home-cache-cleanup: true diff --git a/.github/workflows/integration-tests-ui-critical.yml b/.github/workflows/integration-tests-ui-critical.yml new file mode 100644 index 0000000000..815adf9b61 --- /dev/null +++ b/.github/workflows/integration-tests-ui-critical.yml @@ -0,0 +1,128 @@ +name: UI Tests Critical + +on: + push: + branches: + - main + pull_request: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + BASE_PATH: "sentry-android-integration-tests/sentry-uitest-android-critical" + BUILD_PATH: "build/outputs/apk/release" + APK_NAME: "sentry-uitest-android-critical-release.apk" + APK_ARTIFACT_NAME: "sentry-uitest-android-critical-release" + MAESTRO_VERSION: "1.39.0" + +jobs: + build: + name: Build + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Java 17 + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '17' + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@bb0c460cbf5354b0cddd15bacdf0d6aaa3e5a32b # pin@v3 + with: + gradle-home-cache-cleanup: true + + - name: Build debug APK + run: make assembleUiTestCriticalRelease + + - name: Upload APK artifact + uses: actions/upload-artifact@v4 + with: + name: ${{env.APK_ARTIFACT_NAME}} + path: "${{env.BASE_PATH}}/${{env.BUILD_PATH}}/${{env.APK_NAME}}" + retention-days: 1 + + run-maestro-tests: + name: Run Tests for API Level ${{ matrix.api-level }} + needs: build + runs-on: ubuntu-latest + strategy: + # we want that the matrix keeps running, default is to cancel them if it fails. + fail-fast: false + matrix: + include: + - api-level: 30 # Android 11 + target: aosp_atd + channel: canary # Necessary for ATDs + arch: x86_64 + - api-level: 31 # Android 12 + target: aosp_atd + channel: canary # Necessary for ATDs + arch: x86_64 + - api-level: 33 # Android 13 + target: aosp_atd + channel: canary # Necessary for ATDs + arch: x86_64 + - api-level: 34 # Android 14 + target: aosp_atd + channel: canary # Necessary for ATDs + arch: x86_64 + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup KVM + shell: bash + run: | + # check if virtualization is supported... + sudo apt install -y --no-install-recommends cpu-checker coreutils && echo "CPUs=$(nproc --all)" && kvm-ok + # allow access to KVM to run the emulator + echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' \ + | sudo tee /etc/udev/rules.d/99-kvm4all.rules + sudo udevadm control --reload-rules + sudo udevadm trigger --name-match=kvm + + - name: Download APK artifact + uses: actions/download-artifact@v4 + with: + name: ${{env.APK_ARTIFACT_NAME}} + + - name: Install Maestro + uses: dniHze/maestro-test-action@bda8a93211c86d0a05b7a4597c5ad134566fbde4 # pin@v1.0.0 + with: + version: ${{env.MAESTRO_VERSION}} + + - name: Run tests + uses: reactivecircus/android-emulator-runner@62dbb605bba737720e10b196cb4220d374026a6d # pin@v2.33.0 + with: + api-level: ${{ matrix.api-level }} + force-avd-creation: false + disable-animations: true + disable-spellchecker: true + target: ${{ matrix.target }} + channel: ${{ matrix.channel }} + arch: ${{ matrix.arch }} + emulator-options: > + -no-window + -no-snapshot-save + -gpu swiftshader_indirect + -noaudio + -no-boot-anim + -camera-back none + -camera-front none + -timezone US/Pacific + script: | + adb install -r -d "${{env.APK_NAME}}" + maestro test "${{env.BASE_PATH}}/maestro" --debug-output "${{env.BASE_PATH}}/maestro-logs" + + - name: Upload Maestro test results + if: failure() + uses: actions/upload-artifact@v4 + with: + name: maestro-logs + path: "${{env.BASE_PATH}}/maestro-logs" + retention-days: 1 diff --git a/.github/workflows/integration-tests-ui.yml b/.github/workflows/integration-tests-ui.yml index 771b4b5c8c..6c4ad6564f 100644 --- a/.github/workflows/integration-tests-ui.yml +++ b/.github/workflows/integration-tests-ui.yml @@ -32,7 +32,7 @@ jobs: java-version: '17' - name: Setup Gradle - uses: gradle/actions/setup-gradle@0d30c9111cf47a838eb69c06d13f3f51ab2ed76f # pin@v3 + uses: gradle/actions/setup-gradle@bb0c460cbf5354b0cddd15bacdf0d6aaa3e5a32b # pin@v3 with: gradle-home-cache-cleanup: true diff --git a/.github/workflows/release-build.yml b/.github/workflows/release-build.yml index cb6752bb93..83a3c82f8c 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@0d30c9111cf47a838eb69c06d13f3f51ab2ed76f # pin@v3 + uses: gradle/actions/setup-gradle@bb0c460cbf5354b0cddd15bacdf0d6aaa3e5a32b # 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 4b84c3a18e..bf8a8c7561 100644 --- a/.github/workflows/system-tests-backend.yml +++ b/.github/workflows/system-tests-backend.yml @@ -43,17 +43,38 @@ jobs: java-version: '17' - name: Setup Gradle - uses: gradle/actions/setup-gradle@0d30c9111cf47a838eb69c06d13f3f51ab2ed76f # pin@v3 + uses: gradle/actions/setup-gradle@bb0c460cbf5354b0cddd15bacdf0d6aaa3e5a32b # pin@v3 with: gradle-home-cache-cleanup: true - name: Exclude android modules from build run: | - sed -i -e '/.*"sentry-android-ndk",/d' -e '/.*"sentry-android",/d' -e '/.*"sentry-compose",/d' -e '/.*"sentry-android-core",/d' -e '/.*"sentry-android-fragment",/d' -e '/.*"sentry-android-navigation",/d' -e '/.*"sentry-android-sqlite",/d' -e '/.*"sentry-android-timber",/d' -e '/.*"sentry-android-integration-tests:sentry-uitest-android-benchmark",/d' -e '/.*"sentry-android-integration-tests:sentry-uitest-android",/d' -e '/.*"sentry-android-integration-tests:test-app-sentry",/d' -e '/.*"sentry-samples:sentry-samples-android",/d' -e '/.*"sentry-android-replay",/d' settings.gradle.kts + sed -i \ + -e '/.*"sentry-android-ndk",/d' \ + -e '/.*"sentry-android",/d' \ + -e '/.*"sentry-compose",/d' \ + -e '/.*"sentry-android-core",/d' \ + -e '/.*"sentry-android-fragment",/d' \ + -e '/.*"sentry-android-navigation",/d' \ + -e '/.*"sentry-android-sqlite",/d' \ + -e '/.*"sentry-android-timber",/d' \ + -e '/.*"sentry-android-integration-tests:sentry-uitest-android-benchmark",/d' \ + -e '/.*"sentry-android-integration-tests:sentry-uitest-android",/d' \ + -e '/.*"sentry-android-integration-tests:sentry-uitest-android-critical",/d' \ + -e '/.*"sentry-android-integration-tests:test-app-sentry",/d' \ + -e '/.*"sentry-samples:sentry-samples-android",/d' \ + -e '/.*"sentry-android-replay",/d' \ + settings.gradle.kts - name: Exclude android modules from ignore list run: | - sed -i -e '/.*"sentry-uitest-android",/d' -e '/.*"sentry-uitest-android-benchmark",/d' -e '/.*"test-app-sentry",/d' -e '/.*"sentry-samples-android",/d' build.gradle.kts + sed -i \ + -e '/.*"sentry-uitest-android",/d' \ + -e '/.*"sentry-uitest-android-benchmark",/d' \ + -e '/.*"sentry-uitest-android-critical",/d' \ + -e '/.*"test-app-sentry",/d' \ + -e '/.*"sentry-samples-android",/d' \ + build.gradle.kts - name: Build server jar run: | @@ -65,7 +86,12 @@ jobs: - name: Start server and run integration test for sentry-cli commands run: | - test/system-test-sentry-server-start.sh > sentry-mock-server.txt 2>&1 & test/system-test-spring-server-start.sh "${{ matrix.sample }}" "${{ matrix.agent }}" > spring-server.txt 2>&1 & test/wait-for-spring.sh && ./gradlew :sentry-samples:${{ matrix.sample }}:systemTest + test/system-test-sentry-server-start.sh \ + > sentry-mock-server.txt 2>&1 & \ + test/system-test-spring-server-start.sh "${{ matrix.sample }}" "${{ matrix.agent }}" \ + > spring-server.txt 2>&1 & \ + test/wait-for-spring.sh && \ + ./gradlew :sentry-samples:${{ matrix.sample }}:systemTest - name: Upload test results if: always() diff --git a/.sauce/sentry-uitest-android-benchmark.yml b/.sauce/sentry-uitest-android-benchmark.yml index 6b8120cfa7..48737b5fa5 100644 --- a/.sauce/sentry-uitest-android-benchmark.yml +++ b/.sauce/sentry-uitest-android-benchmark.yml @@ -33,7 +33,7 @@ suites: useTestOrchestrator: true devices: - id: Samsung_Galaxy_S10_Plus_11_real_us # Samsung Galaxy S10+ - api 30 (11) - high end - - id: Samsung_Galaxy_A71_5G_real_us # Samsung Galaxy A71 5G - api 30 (11) - mid end + - id: Google_Pixel_4a_real_us # Google Pixel 4a - api 30 (11) - mid end - id: Google_Pixel_3a_real # Google Pixel 3a - api 30 (11) - low end - name: "Android 10 (api 29)" @@ -42,7 +42,7 @@ suites: useTestOrchestrator: true devices: - id: Google_Pixel_3a_XL_real # Google Pixel 3a XL - api 29 (10) - - id: Motorola_Moto_G_Power_real_us # Motorola Moto G Power - api 29 (10) + - id: OnePlus_6T_real # OnePlus 6T - api 29 (10) # At the time of writing (July, 4, 2022), the market share per android version is: # 12.0 = 17.54%, 11.0 = 31.65%, 10.0 = 21.92% diff --git a/CHANGELOG.md b/CHANGELOG.md index b757475e07..7fa5ba47d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ - `globalHubMode` used to only be a param on `Sentry.init`. To make it easier to be used in e.g. Desktop environments, we now additionally added it as an option on SentryOptions that can also be set via `sentry.properties`. - If both the param on `Sentry.init` and the option are set, the option will win. By default the option is set to `null` meaning whatever is passed to `Sentry.init` takes effect. - Lazy uuid generation for SentryId and SpanId ([#3770](https://github.com/getsentry/sentry-java/pull/3770)) +- Use a separate `Random` instance per thread to improve SDK performance ([#3835](https://github.com/getsentry/sentry-java/pull/3835)) - Android 15: Add support for 16KB page sizes ([#3851](https://github.com/getsentry/sentry-java/pull/3851)) - See https://developer.android.com/guide/practices/page-sizes for more details @@ -20,6 +21,8 @@ - Add `auto.graphql.graphql22` to ignored span origins when using OpenTelemetry ([#3828](https://github.com/getsentry/sentry-java/pull/3828)) - The Spring Boot 3 WebFlux sample now uses our GraphQL v22 integration ([#3828](https://github.com/getsentry/sentry-java/pull/3828)) +- Accept manifest integer values when requiring floating values ([#3823](https://github.com/getsentry/sentry-java/pull/3823)) + ### Dependencies @@ -276,6 +279,48 @@ You may also use `LifecycleHelper.close(token)`, e.g. in case you need to pass t - Report exceptions returned by Throwable.getSuppressed() to Sentry as exception groups ([#3396] https://github.com/getsentry/sentry-java/pull/3396) +## 7.16.0 + +### Features + +- Add meta option to attach ANR thread dumps ([#3791](https://github.com/getsentry/sentry-java/pull/3791)) + +### Fixes + +- Cache parsed Dsn ([#3796](https://github.com/getsentry/sentry-java/pull/3796)) +- fix invalid profiles when the transaction name is empty ([#3747](https://github.com/getsentry/sentry-java/pull/3747)) +- Deprecate `enableTracing` option ([#3777](https://github.com/getsentry/sentry-java/pull/3777)) +- Vendor `java.util.Random` and replace `java.security.SecureRandom` usages ([#3783](https://github.com/getsentry/sentry-java/pull/3783)) +- Fix potential ANRs due to NDK scope sync ([#3754](https://github.com/getsentry/sentry-java/pull/3754)) +- Fix potential ANRs due to NDK System.loadLibrary calls ([#3670](https://github.com/getsentry/sentry-java/pull/3670)) +- Fix slow `Log` calls on app startup ([#3793](https://github.com/getsentry/sentry-java/pull/3793)) +- Fix slow Integration name parsing ([#3794](https://github.com/getsentry/sentry-java/pull/3794)) +- Session Replay: Reduce startup and capture overhead ([#3799](https://github.com/getsentry/sentry-java/pull/3799)) +- Load lazy fields on init in the background ([#3803](https://github.com/getsentry/sentry-java/pull/3803)) +- Replace setOf with HashSet.add ([#3801](https://github.com/getsentry/sentry-java/pull/3801)) + +### Breaking changes + +- The method `addIntegrationToSdkVersion(Ljava/lang/Class;)V` has been removed from the core (`io.sentry:sentry`) package. Please make sure all of the packages (e.g. `io.sentry:sentry-android-core`, `io.sentry:sentry-android-fragment`, `io.sentry:sentry-okhttp` and others) are all aligned and using the same version to prevent the `NoSuchMethodError` exception. + +## 7.16.0-alpha.1 + +### Features + +- Add meta option to attach ANR thread dumps ([#3791](https://github.com/getsentry/sentry-java/pull/3791)) + +### Fixes + +- Cache parsed Dsn ([#3796](https://github.com/getsentry/sentry-java/pull/3796)) +- fix invalid profiles when the transaction name is empty ([#3747](https://github.com/getsentry/sentry-java/pull/3747)) +- Deprecate `enableTracing` option ([#3777](https://github.com/getsentry/sentry-java/pull/3777)) +- Vendor `java.util.Random` and replace `java.security.SecureRandom` usages ([#3783](https://github.com/getsentry/sentry-java/pull/3783)) +- Fix potential ANRs due to NDK scope sync ([#3754](https://github.com/getsentry/sentry-java/pull/3754)) +- Fix potential ANRs due to NDK System.loadLibrary calls ([#3670](https://github.com/getsentry/sentry-java/pull/3670)) +- Fix slow `Log` calls on app startup ([#3793](https://github.com/getsentry/sentry-java/pull/3793)) +- Fix slow Integration name parsing ([#3794](https://github.com/getsentry/sentry-java/pull/3794)) +- Session Replay: Reduce startup and capture overhead ([#3799](https://github.com/getsentry/sentry-java/pull/3799)) + ## 7.15.0 ### Features diff --git a/Makefile b/Makefile index 2117e6da21..62e6e258f3 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: all clean compile javadocs dryRelease update stop checkFormat format api assembleBenchmarkTestRelease assembleUiTestRelease createCoverageReports check preMerge publish +.PHONY: all clean compile javadocs dryRelease update stop checkFormat format api assembleBenchmarkTestRelease assembleUiTestRelease assembleUiTestCriticalRelease createCoverageReports runUiTestCritical check preMerge publish all: stop clean javadocs compile createCoverageReports assembleBenchmarks: assembleBenchmarkTestRelease @@ -53,6 +53,14 @@ assembleUiTestRelease: ./gradlew :sentry-android-integration-tests:sentry-uitest-android:assembleRelease ./gradlew :sentry-android-integration-tests:sentry-uitest-android:assembleAndroidTest -DtestBuildType=release +# Assemble release of the uitest-android-critical module +assembleUiTestCriticalRelease: + ./gradlew :sentry-android-integration-tests:sentry-uitest-android-critical:assembleRelease + +# Run Maestro tests for the uitest-android-critical module +runUiTestCritical: + ./scripts/test-ui-critical.sh + # Create coverage reports # - Jacoco for Java & Android modules # - Kover for KMP modules e.g sentry-compose diff --git a/build.gradle.kts b/build.gradle.kts index b80ec8ac14..1e86a5cf31 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -65,6 +65,7 @@ apiValidation { "sentry-samples-spring-boot-webflux-jakarta", "sentry-uitest-android", "sentry-uitest-android-benchmark", + "sentry-uitest-android-critical", "test-app-plain", "test-app-sentry", "sentry-samples-netflix-dgs" diff --git a/buildSrc/src/main/java/Config.kt b/buildSrc/src/main/java/Config.kt index 2019a16f23..07f3f1debb 100644 --- a/buildSrc/src/main/java/Config.kt +++ b/buildSrc/src/main/java/Config.kt @@ -177,18 +177,17 @@ object Config { } object TestLibs { - private val androidxTestVersion = "1.5.0" private val espressoVersion = "3.5.0" val androidJUnitRunner = "androidx.test.runner.AndroidJUnitRunner" val kotlinTestJunit = "org.jetbrains.kotlin:kotlin-test-junit:$kotlinVersion" - val androidxCore = "androidx.test:core:$androidxTestVersion" - val androidxRunner = "androidx.test:runner:$androidxTestVersion" - val androidxTestCoreKtx = "androidx.test:core-ktx:$androidxTestVersion" - val androidxTestRules = "androidx.test:rules:$androidxTestVersion" + val androidxCore = "androidx.test:core:1.6.1" + val androidxRunner = "androidx.test:runner:1.6.2" + val androidxTestCoreKtx = "androidx.test:core-ktx:1.6.1" + val androidxTestRules = "androidx.test:rules:1.6.1" val espressoCore = "androidx.test.espresso:espresso-core:$espressoVersion" val espressoIdlingResource = "androidx.test.espresso:espresso-idling-resource:$espressoVersion" - val androidxTestOrchestrator = "androidx.test:orchestrator:1.4.2" + 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" diff --git a/gradle.properties b/gradle.properties index 58c3e92dd7..0deeb4e911 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,6 +3,9 @@ org.gradle.jvmargs=-Xmx12g -XX:MaxMetaspaceSize=4g -XX:+CrashOnOutOfMemoryError org.gradle.caching=true org.gradle.parallel=true +# Daemons workers +org.gradle.workers.max=2 + # AndroidX required by AGP >= 3.6.x android.useAndroidX=true diff --git a/scripts/test-ui-critical.sh b/scripts/test-ui-critical.sh new file mode 100755 index 0000000000..7bb36eebec --- /dev/null +++ b/scripts/test-ui-critical.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +set -e + +echo "Checking if ADB is installed..." +if ! command -v adb &> /dev/null; then + echo "ADB is not installed or not in PATH. Please install Android SDK platform tools and ensure ADB is in your PATH." + exit 1 +fi + +echo "Checking if an Android emulator is running..." +if ! adb devices | grep -q "emulator"; then + echo "No Android emulator is currently running. Please start an emulator before running this script." + exit 1 +fi + +echo "Checking if Maestro is installed..." +if ! command -v maestro &> /dev/null; then + echo "Maestro is not installed. Please install Maestro before running this script." + exit 1 +fi + +echo "Building the UI Test Critical app..." +make assembleUiTestCriticalRelease + +echo "Installing the UI Test Critical app on the emulator..." +baseDir="sentry-android-integration-tests/sentry-uitest-android-critical" +buildDir="build/outputs/apk/release" +apkName="sentry-uitest-android-critical-release.apk" +appPath="${baseDir}/${buildDir}/${apkName}" +adb install -r -d "$appPath" + +echo "Running the Maestro tests..." +maestro test \ + "${baseDir}/maestro" \ + --debug-output "${baseDir}/maestro-logs" diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ActivityBreadcrumbsIntegration.java b/sentry-android-core/src/main/java/io/sentry/android/core/ActivityBreadcrumbsIntegration.java index 886ad40c92..3cd7709c4b 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/ActivityBreadcrumbsIntegration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/ActivityBreadcrumbsIntegration.java @@ -50,7 +50,7 @@ public void register(final @NotNull IScopes scopes, final @NotNull SentryOptions if (enabled) { application.registerActivityLifecycleCallbacks(this); options.getLogger().log(SentryLevel.DEBUG, "ActivityBreadcrumbIntegration installed."); - addIntegrationToSdkVersion(getClass()); + addIntegrationToSdkVersion("ActivityBreadcrumbs"); } } 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 b69f8d4ea0..bc9658b5d1 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 @@ -122,7 +122,7 @@ public void register(final @NotNull IScopes scopes, final @NotNull SentryOptions application.registerActivityLifecycleCallbacks(this); this.options.getLogger().log(SentryLevel.DEBUG, "ActivityLifecycleIntegration installed."); - addIntegrationToSdkVersion(getClass()); + addIntegrationToSdkVersion("ActivityLifecycle"); } private boolean isPerformanceEnabled(final @NotNull SentryAndroidOptions options) { diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidLogger.java b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidLogger.java index b0f6b8ac92..ef943c1696 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidLogger.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidLogger.java @@ -26,7 +26,11 @@ public void log( final @NotNull SentryLevel level, final @NotNull String message, final @Nullable Object... args) { - Log.println(toLogcatLevel(level), tag, String.format(message, args)); + if (args == null || args.length == 0) { + Log.println(toLogcatLevel(level), tag, message); + } else { + Log.println(toLogcatLevel(level), tag, String.format(message, args)); + } } @SuppressWarnings("AnnotateFormatMethod") @@ -36,7 +40,11 @@ public void log( final @Nullable Throwable throwable, final @NotNull String message, final @Nullable Object... args) { - log(level, String.format(message, args), throwable); + if (args == null || args.length == 0) { + log(level, message, throwable); + } else { + log(level, String.format(message, args), throwable); + } } @Override diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AnrIntegration.java b/sentry-android-core/src/main/java/io/sentry/android/core/AnrIntegration.java index df8de1a7de..8243493a50 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AnrIntegration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AnrIntegration.java @@ -63,7 +63,7 @@ private void register( .log(SentryLevel.DEBUG, "AnrIntegration enabled: %s", options.isAnrEnabled()); if (options.isAnrEnabled()) { - addIntegrationToSdkVersion(getClass()); + addIntegrationToSdkVersion("Anr"); try { options .getExecutorService() 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 d41789cd5b..7295671e44 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 @@ -54,8 +54,8 @@ import io.sentry.protocol.SentryTransaction; import io.sentry.protocol.User; import io.sentry.util.HintUtils; +import io.sentry.util.SentryRandom; import java.io.File; -import java.security.SecureRandom; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -83,24 +83,13 @@ public final class AnrV2EventProcessor implements BackfillingEventProcessor { private final @NotNull SentryExceptionFactory sentryExceptionFactory; - private final @Nullable SecureRandom random; - public AnrV2EventProcessor( final @NotNull Context context, final @NotNull SentryAndroidOptions options, final @NotNull BuildInfoProvider buildInfoProvider) { - this(context, options, buildInfoProvider, null); - } - - AnrV2EventProcessor( - final @NotNull Context context, - final @NotNull SentryAndroidOptions options, - final @NotNull BuildInfoProvider buildInfoProvider, - final @Nullable SecureRandom random) { this.context = ContextUtils.getApplicationContext(context); this.options = options; this.buildInfoProvider = buildInfoProvider; - this.random = random; final SentryStackTraceFactory sentryStackTraceFactory = new SentryStackTraceFactory(this.options); @@ -180,9 +169,8 @@ private boolean sampleReplay(final @NotNull SentryEvent event) { try { // we have to sample here with the old sample rate, because it may change between app launches - final @NotNull SecureRandom random = this.random != null ? this.random : new SecureRandom(); final double replayErrorSampleRateDouble = Double.parseDouble(replayErrorSampleRate); - if (replayErrorSampleRateDouble < random.nextDouble()) { + if (replayErrorSampleRateDouble < SentryRandom.current().nextDouble()) { options .getLogger() .log( diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2Integration.java b/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2Integration.java index d52c5328ca..9efb1ad13b 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2Integration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2Integration.java @@ -95,7 +95,7 @@ public void register(@NotNull IScopes scopes, @NotNull SentryOptions options) { options.getLogger().log(SentryLevel.DEBUG, "Failed to start AnrProcessor.", e); } options.getLogger().log(SentryLevel.DEBUG, "AnrV2Integration installed."); - addIntegrationToSdkVersion(getClass()); + addIntegrationToSdkVersion("AnrV2"); } } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AppComponentsBreadcrumbsIntegration.java b/sentry-android-core/src/main/java/io/sentry/android/core/AppComponentsBreadcrumbsIntegration.java index 063334c021..ade16a329e 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AppComponentsBreadcrumbsIntegration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AppComponentsBreadcrumbsIntegration.java @@ -55,7 +55,7 @@ public void register(final @NotNull IScopes scopes, final @NotNull SentryOptions options .getLogger() .log(SentryLevel.DEBUG, "AppComponentsBreadcrumbsIntegration installed."); - addIntegrationToSdkVersion(getClass()); + addIntegrationToSdkVersion("AppComponentsBreadcrumbs"); } catch (Throwable e) { this.options.setEnableAppComponentBreadcrumbs(false); options.getLogger().log(SentryLevel.INFO, e, "ComponentCallbacks2 is not available."); diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AppLifecycleIntegration.java b/sentry-android-core/src/main/java/io/sentry/android/core/AppLifecycleIntegration.java index d26832af87..322f184a60 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AppLifecycleIntegration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AppLifecycleIntegration.java @@ -96,7 +96,7 @@ private void addObserver(final @NotNull IScopes scopes) { try { ProcessLifecycleOwner.get().getLifecycle().addObserver(watcher); options.getLogger().log(SentryLevel.DEBUG, "AppLifecycleIntegration installed."); - addIntegrationToSdkVersion(getClass()); + addIntegrationToSdkVersion("AppLifecycle"); } catch (Throwable e) { // This is to handle a potential 'AbstractMethodError' gracefully. The error is triggered in // connection with conflicting dependencies of the androidx.lifecycle. diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java b/sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java index 0edf0f5598..993d05d08a 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java @@ -27,8 +27,8 @@ final class ManifestMetadataReader { static final String SAMPLE_RATE = "io.sentry.sample-rate"; static final String ANR_ENABLE = "io.sentry.anr.enable"; static final String ANR_REPORT_DEBUG = "io.sentry.anr.report-debug"; - static final String ANR_TIMEOUT_INTERVAL_MILLIS = "io.sentry.anr.timeout-interval-millis"; + static final String ANR_ATTACH_THREAD_DUMPS = "io.sentry.anr.attach-thread-dumps"; static final String AUTO_INIT = "io.sentry.auto-init"; static final String NDK_ENABLE = "io.sentry.ndk.enable"; @@ -167,6 +167,9 @@ static void applyMetadata( ANR_TIMEOUT_INTERVAL_MILLIS, options.getAnrTimeoutIntervalMillis())); + options.setAttachAnrThreadDump( + readBool(metadata, logger, ANR_ATTACH_THREAD_DUMPS, options.isAttachAnrThreadDump())); + final String dsn = readString(metadata, logger, DSN, options.getDsn()); final boolean enabled = readBool(metadata, logger, ENABLE_SENTRY, options.isEnabled()); @@ -414,7 +417,7 @@ private static boolean readBool( final @NotNull String key, final boolean defaultValue) { final boolean value = metadata.getBoolean(key, defaultValue); - logger.log(SentryLevel.DEBUG, "%s read: %s", key, value); + logger.log(SentryLevel.DEBUG, key + " read: " + value); return value; } @@ -427,10 +430,10 @@ private static boolean readBool( if (metadata.getSerializable(key) != null) { final boolean nonNullDefault = defaultValue == null ? false : true; final boolean bool = metadata.getBoolean(key, nonNullDefault); - logger.log(SentryLevel.DEBUG, "%s read: %s", key, bool); + logger.log(SentryLevel.DEBUG, key + " read: " + bool); return bool; } else { - logger.log(SentryLevel.DEBUG, "%s used default %s", key, defaultValue); + logger.log(SentryLevel.DEBUG, key + " used default " + defaultValue); return defaultValue; } } @@ -441,7 +444,7 @@ private static boolean readBool( final @NotNull String key, final @Nullable String defaultValue) { final String value = metadata.getString(key, defaultValue); - logger.log(SentryLevel.DEBUG, "%s read: %s", key, value); + logger.log(SentryLevel.DEBUG, key + " read: " + value); return value; } @@ -451,14 +454,14 @@ private static boolean readBool( final @NotNull String key, final @NotNull String defaultValue) { final String value = metadata.getString(key, defaultValue); - logger.log(SentryLevel.DEBUG, "%s read: %s", key, value); + logger.log(SentryLevel.DEBUG, key + " read: " + value); return value; } private static @Nullable List readList( final @NotNull Bundle metadata, final @NotNull ILogger logger, final @NotNull String key) { final String value = metadata.getString(key); - logger.log(SentryLevel.DEBUG, "%s read: %s", key, value); + logger.log(SentryLevel.DEBUG, key + " read: " + value); if (value != null) { return Arrays.asList(value.split(",", -1)); } else { @@ -469,8 +472,8 @@ private static boolean readBool( private static @NotNull Double readDouble( final @NotNull Bundle metadata, final @NotNull ILogger logger, final @NotNull String key) { // manifest meta-data only reads float - final Double value = ((Float) metadata.getFloat(key, -1)).doubleValue(); - logger.log(SentryLevel.DEBUG, "%s read: %s", key, value); + final Double value = ((Number) metadata.getFloat(key, metadata.getInt(key, -1))).doubleValue(); + logger.log(SentryLevel.DEBUG, key + " read: " + value); return value; } @@ -481,7 +484,7 @@ private static long readLong( final long defaultValue) { // manifest meta-data only reads int if the value is not big enough final long value = metadata.getInt(key, (int) defaultValue); - logger.log(SentryLevel.DEBUG, "%s read: %s", key, value); + logger.log(SentryLevel.DEBUG, key + " read: " + value); return value; } @@ -501,7 +504,6 @@ static boolean isAutoInit(final @NotNull Context context, final @NotNull ILogger if (metadata != null) { autoInit = readBool(metadata, logger, AUTO_INIT, true); } - logger.log(SentryLevel.INFO, "Retrieving auto-init from AndroidManifest.xml"); } catch (Throwable e) { logger.log(SentryLevel.ERROR, "Failed to read auto-init from android manifest metadata.", e); } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/NdkIntegration.java b/sentry-android-core/src/main/java/io/sentry/android/core/NdkIntegration.java index dc464303c6..8353a32d65 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/NdkIntegration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/NdkIntegration.java @@ -56,7 +56,7 @@ public final void register(final @NotNull IScopes scopes, final @NotNull SentryO method.invoke(null, args); this.options.getLogger().log(SentryLevel.DEBUG, "NdkIntegration installed."); - addIntegrationToSdkVersion(getClass()); + addIntegrationToSdkVersion("Ndk"); } catch (NoSuchMethodException e) { disableNdkIntegration(this.options); this.options diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/NetworkBreadcrumbsIntegration.java b/sentry-android-core/src/main/java/io/sentry/android/core/NetworkBreadcrumbsIntegration.java index c7b5bb21d0..5cfb5df223 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/NetworkBreadcrumbsIntegration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/NetworkBreadcrumbsIntegration.java @@ -96,7 +96,7 @@ public void run() { context, logger, buildInfoProvider, networkCallback); if (registered) { logger.log(SentryLevel.DEBUG, "NetworkBreadcrumbsIntegration installed."); - addIntegrationToSdkVersion(getClass()); + addIntegrationToSdkVersion("NetworkBreadcrumbs"); } else { logger.log( SentryLevel.DEBUG, "NetworkBreadcrumbsIntegration not installed."); diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/PhoneStateBreadcrumbsIntegration.java b/sentry-android-core/src/main/java/io/sentry/android/core/PhoneStateBreadcrumbsIntegration.java index 81f1a52262..a0111b3c0a 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/PhoneStateBreadcrumbsIntegration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/PhoneStateBreadcrumbsIntegration.java @@ -83,7 +83,7 @@ private void startTelephonyListener( telephonyManager.listen(listener, android.telephony.PhoneStateListener.LISTEN_CALL_STATE); options.getLogger().log(SentryLevel.DEBUG, "PhoneStateBreadcrumbsIntegration installed."); - addIntegrationToSdkVersion(getClass()); + addIntegrationToSdkVersion("PhoneStateBreadcrumbs"); } catch (Throwable e) { options .getLogger() diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ScreenshotEventProcessor.java b/sentry-android-core/src/main/java/io/sentry/android/core/ScreenshotEventProcessor.java index 61ff9d290c..8585cb9614 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/ScreenshotEventProcessor.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/ScreenshotEventProcessor.java @@ -46,7 +46,7 @@ public ScreenshotEventProcessor( DEBOUNCE_MAX_EXECUTIONS); if (options.isAttachScreenshot()) { - addIntegrationToSdkVersion(getClass()); + addIntegrationToSdkVersion("Screenshot"); } } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/SendCachedEnvelopeIntegration.java b/sentry-android-core/src/main/java/io/sentry/android/core/SendCachedEnvelopeIntegration.java index 9b50f3b6e5..41f4f838bf 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/SendCachedEnvelopeIntegration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/SendCachedEnvelopeIntegration.java @@ -1,5 +1,7 @@ package io.sentry.android.core; +import static io.sentry.util.IntegrationUtils.addIntegrationToSdkVersion; + import io.sentry.DataCategory; import io.sentry.IConnectionStatusProvider; import io.sentry.IScopes; @@ -57,6 +59,7 @@ public void register(@NotNull IScopes scopes, @NotNull SentryOptions options) { options.getLogger().log(SentryLevel.ERROR, "No cache dir path is defined in options."); return; } + addIntegrationToSdkVersion("SendCachedEnvelope"); sendCachedEnvelopes(scopes, this.options); } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/SystemEventsBreadcrumbsIntegration.java b/sentry-android-core/src/main/java/io/sentry/android/core/SystemEventsBreadcrumbsIntegration.java index 00e0dde646..04988c9a98 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/SystemEventsBreadcrumbsIntegration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/SystemEventsBreadcrumbsIntegration.java @@ -134,7 +134,7 @@ private void startSystemEventsReceiver( // registerReceiver can throw SecurityException but it's not documented in the official docs ContextUtils.registerReceiver(context, options, receiver, filter); options.getLogger().log(SentryLevel.DEBUG, "SystemEventsBreadcrumbsIntegration installed."); - addIntegrationToSdkVersion(getClass()); + addIntegrationToSdkVersion("SystemEventsBreadcrumbs"); } catch (Throwable e) { options.setEnableSystemEventBreadcrumbs(false); options diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/TempSensorBreadcrumbsIntegration.java b/sentry-android-core/src/main/java/io/sentry/android/core/TempSensorBreadcrumbsIntegration.java index f835b3670b..1d72fa2239 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/TempSensorBreadcrumbsIntegration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/TempSensorBreadcrumbsIntegration.java @@ -89,7 +89,7 @@ private void startSensorListener(final @NotNull SentryOptions options) { sensorManager.registerListener(this, defaultSensor, SensorManager.SENSOR_DELAY_NORMAL); options.getLogger().log(SentryLevel.DEBUG, "TempSensorBreadcrumbsIntegration installed."); - addIntegrationToSdkVersion(getClass()); + addIntegrationToSdkVersion("TempSensorBreadcrumbs"); } else { options.getLogger().log(SentryLevel.INFO, "TYPE_AMBIENT_TEMPERATURE is not available."); } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/UserInteractionIntegration.java b/sentry-android-core/src/main/java/io/sentry/android/core/UserInteractionIntegration.java index 02a707173a..46275504cb 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/UserInteractionIntegration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/UserInteractionIntegration.java @@ -121,7 +121,7 @@ public void register(@NotNull IScopes scopes, @NotNull SentryOptions options) { if (isAndroidXAvailable) { application.registerActivityLifecycleCallbacks(this); this.options.getLogger().log(SentryLevel.DEBUG, "UserInteractionIntegration installed."); - addIntegrationToSdkVersion(getClass()); + addIntegrationToSdkVersion("UserInteraction"); } else { options .getLogger() diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ViewHierarchyEventProcessor.java b/sentry-android-core/src/main/java/io/sentry/android/core/ViewHierarchyEventProcessor.java index 8b6a1f87aa..c32b05892f 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/ViewHierarchyEventProcessor.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/ViewHierarchyEventProcessor.java @@ -55,7 +55,7 @@ public ViewHierarchyEventProcessor(final @NotNull SentryAndroidOptions options) DEBOUNCE_MAX_EXECUTIONS); if (options.isAttachViewHierarchy()) { - addIntegrationToSdkVersion(getClass()); + addIntegrationToSdkVersion("ViewHierarchy"); } } 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 86f95b4430..b92db7cfc6 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 @@ -350,6 +350,17 @@ class AndroidTransactionProfilerTest { verify(mockExecutorService, never()).submit(any>()) } + @Test + fun `profiling transaction with empty name fallbacks to unknown`() { + val profiler = fixture.getSut(context) + profiler.start() + profiler.bindTransaction(fixture.transaction1) + val profilingTraceData = profiler.onTransactionFinish(fixture.transaction1, null, fixture.options) + assertNotNull(profilingTraceData) + assertEquals("unknown", profilingTraceData.transactionName) + assertEquals("unknown", profilingTraceData.transactions.first().name) + } + @Test fun `profiler does not throw if traces cannot be written to disk`() { File(fixture.options.profilingTracesDirPath!!).setWritable(false) diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/ManifestMetadataReaderTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/ManifestMetadataReaderTest.kt index 4b0d19977d..761b6b3d69 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/ManifestMetadataReaderTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/ManifestMetadataReaderTest.kt @@ -233,6 +233,31 @@ class ManifestMetadataReaderTest { assertEquals(5000.toLong(), fixture.options.anrTimeoutIntervalMillis) } + @Test + fun `applyMetadata reads anr attach thread dump to options`() { + // Arrange + val bundle = bundleOf(ManifestMetadataReader.ANR_ATTACH_THREAD_DUMPS to true) + val context = fixture.getContext(metaData = bundle) + + // Act + ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider) + + // Assert + assertEquals(true, fixture.options.isAttachAnrThreadDump) + } + + @Test + fun `applyMetadata reads anr attach thread dump to options and keeps default`() { + // Arrange + val context = fixture.getContext() + + // Act + ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider) + + // Assert + assertEquals(false, fixture.options.isAttachAnrThreadDump) + } + @Test fun `applyMetadata reads activity breadcrumbs to options`() { // Arrange @@ -1352,4 +1377,29 @@ class ManifestMetadataReaderTest { assertTrue(fixture.options.experimental.sessionReplay.maskViewClasses.contains(SentryReplayOptions.IMAGE_VIEW_CLASS_NAME)) assertTrue(fixture.options.experimental.sessionReplay.maskViewClasses.contains(SentryReplayOptions.TEXT_VIEW_CLASS_NAME)) } + + @Test + fun `applyMetadata reads integers even when expecting floats`() { + // Arrange + val expectedSampleRate: Int = 1 + + val bundle = bundleOf( + ManifestMetadataReader.SAMPLE_RATE to expectedSampleRate, + ManifestMetadataReader.TRACES_SAMPLE_RATE to expectedSampleRate, + ManifestMetadataReader.PROFILES_SAMPLE_RATE to expectedSampleRate, + ManifestMetadataReader.REPLAYS_SESSION_SAMPLE_RATE to expectedSampleRate, + ManifestMetadataReader.REPLAYS_ERROR_SAMPLE_RATE to expectedSampleRate + ) + val context = fixture.getContext(metaData = bundle) + + // Act + ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider) + + // Assert + assertEquals(expectedSampleRate.toDouble(), fixture.options.sampleRate) + assertEquals(expectedSampleRate.toDouble(), fixture.options.tracesSampleRate) + assertEquals(expectedSampleRate.toDouble(), fixture.options.profilesSampleRate) + assertEquals(expectedSampleRate.toDouble(), fixture.options.experimental.sessionReplay.sessionSampleRate) + assertEquals(expectedSampleRate.toDouble(), fixture.options.experimental.sessionReplay.onErrorSampleRate) + } } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidTest.kt index cf2369d559..68c564da4e 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidTest.kt @@ -6,6 +6,7 @@ import android.app.ApplicationExitInfo import android.content.Context import android.os.Build import android.os.Bundle +import android.os.Looper import android.os.SystemClock import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 @@ -55,6 +56,7 @@ import org.mockito.kotlin.spy import org.mockito.kotlin.times import org.mockito.kotlin.verify import org.mockito.kotlin.whenever +import org.robolectric.Shadows import org.robolectric.annotation.Config import org.robolectric.shadow.api.Shadow import org.robolectric.shadows.ShadowActivityManager @@ -440,8 +442,10 @@ class SentryAndroidTest { await.withAlias("Failed because of BeforeSend callback above, but we swallow BeforeSend exceptions, hence the timeout") .untilTrue(asserted) + // Execute all posted tasks + Shadows.shadowOf(Looper.getMainLooper()).idle() + // assert that persisted values have changed - options.executorService.close(10000L) // finalizes all enqueued persisting tasks assertEquals( "TestActivity", PersistingScopeObserver.read(options, TRANSACTION_FILENAME, String::class.java) diff --git a/sentry-android-fragment/api/sentry-android-fragment.api b/sentry-android-fragment/api/sentry-android-fragment.api index f2f3334280..044d9cfbf5 100644 --- a/sentry-android-fragment/api/sentry-android-fragment.api +++ b/sentry-android-fragment/api/sentry-android-fragment.api @@ -24,6 +24,7 @@ public final class io/sentry/android/fragment/FragmentLifecycleIntegration : and public final class io/sentry/android/fragment/FragmentLifecycleState : java/lang/Enum { public static final field ATTACHED Lio/sentry/android/fragment/FragmentLifecycleState; public static final field CREATED Lio/sentry/android/fragment/FragmentLifecycleState; + public static final field Companion Lio/sentry/android/fragment/FragmentLifecycleState$Companion; public static final field DESTROYED Lio/sentry/android/fragment/FragmentLifecycleState; public static final field DETACHED Lio/sentry/android/fragment/FragmentLifecycleState; public static final field PAUSED Lio/sentry/android/fragment/FragmentLifecycleState; @@ -37,6 +38,10 @@ public final class io/sentry/android/fragment/FragmentLifecycleState : java/lang public static fun values ()[Lio/sentry/android/fragment/FragmentLifecycleState; } +public final class io/sentry/android/fragment/FragmentLifecycleState$Companion { + public final fun getStates ()Ljava/util/HashSet; +} + public final class io/sentry/android/fragment/SentryFragmentLifecycleCallbacks : androidx/fragment/app/FragmentManager$FragmentLifecycleCallbacks { public static final field Companion Lio/sentry/android/fragment/SentryFragmentLifecycleCallbacks$Companion; public static final field FRAGMENT_LOAD_OP Ljava/lang/String; 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 d2fd393200..f1c44422a5 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 @@ -24,7 +24,7 @@ class FragmentLifecycleIntegration( constructor(application: Application) : this( application = application, - filterFragmentLifecycleBreadcrumbs = FragmentLifecycleState.values().toSet(), + filterFragmentLifecycleBreadcrumbs = FragmentLifecycleState.states, enableAutoFragmentLifecycleTracing = false ) @@ -34,7 +34,7 @@ class FragmentLifecycleIntegration( enableAutoFragmentLifecycleTracing: Boolean ) : this( application = application, - filterFragmentLifecycleBreadcrumbs = FragmentLifecycleState.values().toSet() + filterFragmentLifecycleBreadcrumbs = FragmentLifecycleState.states .takeIf { enableFragmentLifecycleBreadcrumbs } .orEmpty(), enableAutoFragmentLifecycleTracing = enableAutoFragmentLifecycleTracing @@ -49,7 +49,7 @@ class FragmentLifecycleIntegration( application.registerActivityLifecycleCallbacks(this) options.logger.log(DEBUG, "FragmentLifecycleIntegration installed.") - addIntegrationToSdkVersion(javaClass) + addIntegrationToSdkVersion("FragmentLifecycle") SentryIntegrationPackageStorage.getInstance() .addPackage("maven:io.sentry:sentry-android-fragment", BuildConfig.VERSION_NAME) } 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 cdc5ea999d..fd52437d60 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 @@ -11,5 +11,21 @@ enum class FragmentLifecycleState(internal val breadcrumbName: String) { STOPPED("stopped"), VIEW_DESTROYED("view destroyed"), DESTROYED("destroyed"), - DETACHED("detached") + DETACHED("detached"); + + companion object { + val states = HashSet().apply { + add(ATTACHED) + add(SAVE_INSTANCE_STATE) + add(CREATED) + add(VIEW_CREATED) + add(STARTED) + add(RESUMED) + add(PAUSED) + add(STOPPED) + add(VIEW_DESTROYED) + add(DESTROYED) + add(DETACHED) + } + } } 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 ec6a50c692..cf5b14b43c 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 @@ -31,7 +31,7 @@ class SentryFragmentLifecycleCallbacks( enableAutoFragmentLifecycleTracing: Boolean ) : this( scopes = scopes, - filterFragmentLifecycleBreadcrumbs = FragmentLifecycleState.values().toSet() + filterFragmentLifecycleBreadcrumbs = FragmentLifecycleState.states .takeIf { enableFragmentLifecycleBreadcrumbs } .orEmpty(), enableAutoFragmentLifecycleTracing = enableAutoFragmentLifecycleTracing @@ -42,7 +42,7 @@ class SentryFragmentLifecycleCallbacks( enableAutoFragmentLifecycleTracing: Boolean = false ) : this( scopes = ScopesAdapter.getInstance(), - filterFragmentLifecycleBreadcrumbs = FragmentLifecycleState.values().toSet() + filterFragmentLifecycleBreadcrumbs = FragmentLifecycleState.states .takeIf { enableFragmentLifecycleBreadcrumbs } .orEmpty(), enableAutoFragmentLifecycleTracing = enableAutoFragmentLifecycleTracing diff --git a/sentry-android-fragment/src/test/java/io/sentry/android/fragment/FragmentLifecycleStateTest.kt b/sentry-android-fragment/src/test/java/io/sentry/android/fragment/FragmentLifecycleStateTest.kt new file mode 100644 index 0000000000..b5ab1f19d2 --- /dev/null +++ b/sentry-android-fragment/src/test/java/io/sentry/android/fragment/FragmentLifecycleStateTest.kt @@ -0,0 +1,11 @@ +package io.sentry.android.fragment + +import kotlin.test.Test +import kotlin.test.assertEquals + +class FragmentLifecycleStateTest { + @Test + fun `states contains all states`() { + assertEquals(FragmentLifecycleState.states, FragmentLifecycleState.values().toSet()) + } +} diff --git a/sentry-android-fragment/src/test/java/io/sentry/android/fragment/SentryFragmentLifecycleCallbacksTest.kt b/sentry-android-fragment/src/test/java/io/sentry/android/fragment/SentryFragmentLifecycleCallbacksTest.kt index 394d4e0aaf..91f84c5af1 100644 --- a/sentry-android-fragment/src/test/java/io/sentry/android/fragment/SentryFragmentLifecycleCallbacksTest.kt +++ b/sentry-android-fragment/src/test/java/io/sentry/android/fragment/SentryFragmentLifecycleCallbacksTest.kt @@ -40,7 +40,7 @@ class SentryFragmentLifecycleCallbacksTest { val span = mock() fun getSut( - loggedFragmentLifecycleStates: Set = FragmentLifecycleState.values().toSet(), + loggedFragmentLifecycleStates: Set = FragmentLifecycleState.states, enableAutoFragmentLifecycleTracing: Boolean = false, tracesSampleRate: Double? = 1.0, isAdded: Boolean = true diff --git a/sentry-android-integration-tests/sentry-uitest-android-critical/.gitignore b/sentry-android-integration-tests/sentry-uitest-android-critical/.gitignore new file mode 100644 index 0000000000..48fc28dcf5 --- /dev/null +++ b/sentry-android-integration-tests/sentry-uitest-android-critical/.gitignore @@ -0,0 +1,2 @@ +/build +/maestro-logs 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 new file mode 100644 index 0000000000..cebf744a24 --- /dev/null +++ b/sentry-android-integration-tests/sentry-uitest-android-critical/build.gradle.kts @@ -0,0 +1,69 @@ +import io.gitlab.arturbosch.detekt.Detekt + +plugins { + id("com.android.application") + kotlin("android") +} + +android { + compileSdk = Config.Android.compileSdkVersion + namespace = "io.sentry.uitest.android.critical" + + signingConfigs { + getByName("debug") { + // Debug config remains unchanged + } + } + + defaultConfig { + applicationId = "io.sentry.uitest.android.critical" + minSdk = Config.Android.minSdkVersionCompose + targetSdk = Config.Android.targetSdkVersion + versionCode = 1 + versionName = "1.0" + } + + buildTypes { + release { + isMinifyEnabled = false + signingConfig = signingConfigs.getByName("debug") + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + kotlinOptions { + jvmTarget = JavaVersion.VERSION_1_8.toString() + } + buildFeatures { + compose = true + } + composeOptions { + kotlinCompilerExtensionVersion = Config.androidComposeCompilerVersion + } + variantFilter { + if (Config.Android.shouldSkipDebugVariant(buildType.name)) { + ignore = true + } + } +} + +dependencies { + implementation(kotlin(Config.kotlinStdLib, org.jetbrains.kotlin.config.KotlinCompilerVersion.VERSION)) + implementation(Config.Libs.androidxCore) + implementation(Config.Libs.composeActivity) + implementation(Config.Libs.composeFoundation) + implementation(Config.Libs.composeMaterial) + implementation(Config.Libs.constraintLayout) + implementation(projects.sentryAndroidCore) +} + +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/maestro/corruptEnvelope.yaml b/sentry-android-integration-tests/sentry-uitest-android-critical/maestro/corruptEnvelope.yaml new file mode 100644 index 0000000000..dec889731b --- /dev/null +++ b/sentry-android-integration-tests/sentry-uitest-android-critical/maestro/corruptEnvelope.yaml @@ -0,0 +1,11 @@ +appId: io.sentry.uitest.android.critical +--- +- launchApp +- tapOn: "Write Corrupted Envelope" +# The close here ensures the next corrupted envelope +# will be present on the next app launch +- tapOn: "Close SDK" +- tapOn: "Write Corrupted Envelope" +- stopApp +- launchApp +- assertVisible: "Welcome!" diff --git a/sentry-android-integration-tests/sentry-uitest-android-critical/maestro/crash.yaml b/sentry-android-integration-tests/sentry-uitest-android-critical/maestro/crash.yaml new file mode 100644 index 0000000000..f9543f365c --- /dev/null +++ b/sentry-android-integration-tests/sentry-uitest-android-critical/maestro/crash.yaml @@ -0,0 +1,6 @@ +appId: io.sentry.uitest.android.critical +--- +- launchApp +- tapOn: "Crash" +- launchApp +- assertVisible: "Welcome!" diff --git a/sentry-android-integration-tests/sentry-uitest-android-critical/proguard-rules.pro b/sentry-android-integration-tests/sentry-uitest-android-critical/proguard-rules.pro new file mode 100644 index 0000000000..f1b424510d --- /dev/null +++ b/sentry-android-integration-tests/sentry-uitest-android-critical/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/sentry-android-integration-tests/sentry-uitest-android-critical/src/main/AndroidManifest.xml b/sentry-android-integration-tests/sentry-uitest-android-critical/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..0ab5e6052d --- /dev/null +++ b/sentry-android-integration-tests/sentry-uitest-android-critical/src/main/AndroidManifest.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + diff --git a/sentry-android-integration-tests/sentry-uitest-android-critical/src/main/java/io/sentry/uitest/android/critical/MainActivity.kt b/sentry-android-integration-tests/sentry-uitest-android-critical/src/main/java/io/sentry/uitest/android/critical/MainActivity.kt new file mode 100644 index 0000000000..8802f3dca2 --- /dev/null +++ b/sentry-android-integration-tests/sentry-uitest-android-critical/src/main/java/io/sentry/uitest/android/critical/MainActivity.kt @@ -0,0 +1,51 @@ +package io.sentry.uitest.android.critical + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.Column +import androidx.compose.material3.Button +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import io.sentry.Sentry +import java.io.File + +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val outboxPath = Sentry.getCurrentHub().options.outboxPath + ?: throw RuntimeException("Outbox path is not set.") + + setContent { + MaterialTheme { + Surface() { + Column() { + Text(text = "Welcome!") + Button(onClick = { + throw RuntimeException("Crash the test app.") + }) { + Text("Crash") + } + Button(onClick = { + Sentry.close() + }) { + Text("Close SDK") + } + Button(onClick = { + val file = File(outboxPath, "corrupted.envelope") + val corruptedEnvelopeContent = """ + {"event_id":"1990b5bc31904b7395fd07feb72daf1c","sdk":{"name":"sentry.java.android","version":"7.21.0"}} + {"type":"test","length":50} + """.trimIndent() + file.writeText(corruptedEnvelopeContent) + println("Wrote corrupted envelope to: ${file.absolutePath}") + }) { + Text("Write Corrupted Envelope") + } + } + } + } + } + } +} diff --git a/sentry-android-integration-tests/sentry-uitest-android/src/androidTest/java/io/sentry/uitest/android/SdkInitTests.kt b/sentry-android-integration-tests/sentry-uitest-android/src/androidTest/java/io/sentry/uitest/android/SdkInitTests.kt index 6a5707f70e..e9c08f4359 100644 --- a/sentry-android-integration-tests/sentry-uitest-android/src/androidTest/java/io/sentry/uitest/android/SdkInitTests.kt +++ b/sentry-android-integration-tests/sentry-uitest-android/src/androidTest/java/io/sentry/uitest/android/SdkInitTests.kt @@ -127,8 +127,10 @@ class SdkInitTests : BaseUiTest() { Sentry.startTransaction("afterRestart", "emptyTransaction").finish() // We assert for less than 1 second just to account for slow devices in saucelabs or headless emulator - // TODO: Revert back to 1000ms after making scope.close() faster again - assertTrue(restartMs < 2500, "Expected less than 2500 ms for SDK restart. Got $restartMs ms") + assertTrue( + restartMs < 1000, + "Expected less than 1000 ms for SDK restart. Got $restartMs ms" + ) relay.assert { findEnvelope { 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 bc008fa678..1ea9e42c3c 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 @@ -49,7 +49,7 @@ class SentryNavigationListener @JvmOverloads constructor( private var activeTransaction: ITransaction? = null init { - addIntegrationToSdkVersion(javaClass) + addIntegrationToSdkVersion("NavigationListener") SentryIntegrationPackageStorage.getInstance() .addPackage("maven:io.sentry:sentry-android-navigation", BuildConfig.VERSION_NAME) } diff --git a/sentry-android-ndk/build.gradle.kts b/sentry-android-ndk/build.gradle.kts index ccf3a419f0..41d6bbab13 100644 --- a/sentry-android-ndk/build.gradle.kts +++ b/sentry-android-ndk/build.gradle.kts @@ -75,6 +75,6 @@ dependencies { testImplementation(kotlin(Config.kotlinStdLib, KotlinCompilerVersion.VERSION)) testImplementation(Config.TestLibs.kotlinTestJunit) - testImplementation(Config.TestLibs.mockitoKotlin) + testImplementation(projects.sentryTestSupport) } diff --git a/sentry-android-ndk/src/main/java/io/sentry/android/ndk/NdkScopeObserver.java b/sentry-android-ndk/src/main/java/io/sentry/android/ndk/NdkScopeObserver.java index 4a4237ba08..118b1f6851 100644 --- a/sentry-android-ndk/src/main/java/io/sentry/android/ndk/NdkScopeObserver.java +++ b/sentry-android-ndk/src/main/java/io/sentry/android/ndk/NdkScopeObserver.java @@ -33,12 +33,18 @@ public NdkScopeObserver(final @NotNull SentryOptions options) { @Override public void setUser(final @Nullable User user) { try { - if (user == null) { - // remove user if its null - nativeScope.removeUser(); - } else { - nativeScope.setUser(user.getId(), user.getEmail(), user.getIpAddress(), user.getUsername()); - } + options + .getExecutorService() + .submit( + () -> { + if (user == null) { + // remove user if its null + nativeScope.removeUser(); + } else { + nativeScope.setUser( + user.getId(), user.getEmail(), user.getIpAddress(), user.getUsername()); + } + }); } catch (Throwable e) { options.getLogger().log(SentryLevel.ERROR, e, "Scope sync setUser has an error."); } @@ -47,24 +53,36 @@ public void setUser(final @Nullable User user) { @Override public void addBreadcrumb(final @NotNull Breadcrumb crumb) { try { - String level = null; - if (crumb.getLevel() != null) { - level = crumb.getLevel().name().toLowerCase(Locale.ROOT); - } - final String timestamp = DateUtils.getTimestamp(crumb.getTimestamp()); + options + .getExecutorService() + .submit( + () -> { + String level = null; + if (crumb.getLevel() != null) { + level = crumb.getLevel().name().toLowerCase(Locale.ROOT); + } + final String timestamp = DateUtils.getTimestamp(crumb.getTimestamp()); - String data = null; - try { - final Map dataRef = crumb.getData(); - if (!dataRef.isEmpty()) { - data = options.getSerializer().serialize(dataRef); - } - } catch (Throwable e) { - options.getLogger().log(SentryLevel.ERROR, e, "Breadcrumb data is not serializable."); - } + String data = null; + try { + final Map dataRef = crumb.getData(); + if (!dataRef.isEmpty()) { + data = options.getSerializer().serialize(dataRef); + } + } catch (Throwable e) { + options + .getLogger() + .log(SentryLevel.ERROR, e, "Breadcrumb data is not serializable."); + } - nativeScope.addBreadcrumb( - level, crumb.getMessage(), crumb.getCategory(), crumb.getType(), timestamp, data); + nativeScope.addBreadcrumb( + level, + crumb.getMessage(), + crumb.getCategory(), + crumb.getType(), + timestamp, + data); + }); } catch (Throwable e) { options.getLogger().log(SentryLevel.ERROR, e, "Scope sync addBreadcrumb has an error."); } @@ -73,7 +91,7 @@ public void addBreadcrumb(final @NotNull Breadcrumb crumb) { @Override public void setTag(final @NotNull String key, final @NotNull String value) { try { - nativeScope.setTag(key, value); + options.getExecutorService().submit(() -> nativeScope.setTag(key, value)); } catch (Throwable e) { options.getLogger().log(SentryLevel.ERROR, e, "Scope sync setTag(%s) has an error.", key); } @@ -82,7 +100,7 @@ public void setTag(final @NotNull String key, final @NotNull String value) { @Override public void removeTag(final @NotNull String key) { try { - nativeScope.removeTag(key); + options.getExecutorService().submit(() -> nativeScope.removeTag(key)); } catch (Throwable e) { options.getLogger().log(SentryLevel.ERROR, e, "Scope sync removeTag(%s) has an error.", key); } @@ -91,7 +109,7 @@ public void removeTag(final @NotNull String key) { @Override public void setExtra(final @NotNull String key, final @NotNull String value) { try { - nativeScope.setExtra(key, value); + options.getExecutorService().submit(() -> nativeScope.setExtra(key, value)); } catch (Throwable e) { options.getLogger().log(SentryLevel.ERROR, e, "Scope sync setExtra(%s) has an error.", key); } @@ -100,7 +118,7 @@ public void setExtra(final @NotNull String key, final @NotNull String value) { @Override public void removeExtra(final @NotNull String key) { try { - nativeScope.removeExtra(key); + options.getExecutorService().submit(() -> nativeScope.removeExtra(key)); } catch (Throwable e) { options .getLogger() diff --git a/sentry-android-ndk/src/test/java/io/sentry/android/ndk/NdkScopeObserverTest.kt b/sentry-android-ndk/src/test/java/io/sentry/android/ndk/NdkScopeObserverTest.kt index ad523a883e..07e0e2828b 100644 --- a/sentry-android-ndk/src/test/java/io/sentry/android/ndk/NdkScopeObserverTest.kt +++ b/sentry-android-ndk/src/test/java/io/sentry/android/ndk/NdkScopeObserverTest.kt @@ -7,8 +7,13 @@ import io.sentry.SentryLevel import io.sentry.SentryOptions import io.sentry.ndk.INativeScope import io.sentry.protocol.User +import io.sentry.test.DeferredExecutorService +import io.sentry.test.ImmediateExecutorService +import org.mockito.kotlin.any +import org.mockito.kotlin.anyOrNull import org.mockito.kotlin.eq import org.mockito.kotlin.mock +import org.mockito.kotlin.never import org.mockito.kotlin.verify import kotlin.test.Test @@ -18,6 +23,7 @@ class NdkScopeObserverTest { val nativeScope = mock() val options = SentryOptions().apply { setSerializer(JsonSerializer(mock())) + executorService = ImmediateExecutorService() } fun getSut(): NdkScopeObserver { @@ -112,4 +118,36 @@ class NdkScopeObserverTest { eq(data) ) } + + @Test + fun `scope sync utilizes executor service`() { + val executorService = DeferredExecutorService() + fixture.options.executorService = executorService + val sut = fixture.getSut() + + sut.setTag("a", "b") + sut.removeTag("a") + sut.setExtra("a", "b") + sut.removeExtra("a") + sut.setUser(User()) + sut.addBreadcrumb(Breadcrumb()) + + // as long as the executor service is not run, the scope sync is not called + verify(fixture.nativeScope, never()).setTag(any(), any()) + verify(fixture.nativeScope, never()).removeTag(any()) + verify(fixture.nativeScope, never()).setExtra(any(), any()) + verify(fixture.nativeScope, never()).removeExtra(any()) + verify(fixture.nativeScope, never()).setUser(any(), any(), any(), any()) + verify(fixture.nativeScope, never()).addBreadcrumb(any(), any(), any(), any(), any(), any()) + + // when the executor service is run, the scope sync is called + executorService.runAll() + + verify(fixture.nativeScope).setTag(any(), any()) + verify(fixture.nativeScope).removeTag(any()) + verify(fixture.nativeScope).setExtra(any(), any()) + verify(fixture.nativeScope).removeExtra(any()) + verify(fixture.nativeScope).setUser(anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull()) + verify(fixture.nativeScope).addBreadcrumb(anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull()) + } } 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 c95b72088a..d5c666b5b0 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 @@ -12,14 +12,14 @@ import kotlin.LazyThreadSafetyMode.NONE public open class DefaultReplayBreadcrumbConverter : ReplayBreadcrumbConverter { internal companion object { private val snakecasePattern by lazy(NONE) { "_[a-z]".toRegex() } - private val supportedNetworkData = setOf( - "status_code", - "method", - "response_content_length", - "request_content_length", - "http.response_content_length", - "http.request_content_length" - ) + private val supportedNetworkData = HashSet().apply { + add("status_code") + add("method") + add("response_content_length") + add("request_content_length") + add("http.response_content_length") + add("http.request_content_length") + } } private var lastConnectivityState: String? = null 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 a449d3843a..38483e3aac 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 @@ -35,9 +35,9 @@ import io.sentry.transport.ICurrentDateProvider import io.sentry.util.FileUtils import io.sentry.util.HintUtils import io.sentry.util.IntegrationUtils.addIntegrationToSdkVersion +import io.sentry.util.Random import java.io.Closeable import java.io.File -import java.security.SecureRandom import java.util.LinkedList import java.util.concurrent.atomic.AtomicBoolean import kotlin.LazyThreadSafetyMode.NONE @@ -78,7 +78,7 @@ public class ReplayIntegration( private var scopes: IScopes? = null private var recorder: Recorder? = null private var gestureRecorder: GestureRecorder? = null - private val random by lazy { SecureRandom() } + private val random by lazy { Random() } private val rootViewsSpy by lazy(NONE) { RootViewsSpy.install() } // TODO: probably not everything has to be thread-safe here @@ -119,7 +119,7 @@ public class ReplayIntegration( options.logger.log(INFO, "ComponentCallbacks is not available, orientation changes won't be handled by Session replay", e) } - addIntegrationToSdkVersion(javaClass) + addIntegrationToSdkVersion("Replay") SentryIntegrationPackageStorage.getInstance() .addPackage("maven:io.sentry:sentry-android-replay", BuildConfig.VERSION_NAME) 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 4a229f85df..c7aded105a 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 @@ -37,6 +37,7 @@ import java.util.concurrent.Executors import java.util.concurrent.ThreadFactory import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicReference +import kotlin.LazyThreadSafetyMode.NONE import kotlin.math.roundToInt @TargetApi(26) @@ -52,15 +53,19 @@ internal class ScreenshotRecorder( } private var rootView: WeakReference? = null private val pendingViewHierarchy = AtomicReference() - private val maskingPaint = Paint() - private val singlePixelBitmap: Bitmap = Bitmap.createBitmap( - 1, - 1, - Bitmap.Config.ARGB_8888 - ) - private val singlePixelBitmapCanvas: Canvas = Canvas(singlePixelBitmap) - private val prescaledMatrix = Matrix().apply { - preScale(config.scaleFactorX, config.scaleFactorY) + private val maskingPaint by lazy(NONE) { Paint() } + private val singlePixelBitmap: Bitmap by lazy(NONE) { + Bitmap.createBitmap( + 1, + 1, + Bitmap.Config.ARGB_8888 + ) + } + private val singlePixelBitmapCanvas: Canvas by lazy(NONE) { Canvas(singlePixelBitmap) } + private val prescaledMatrix by lazy(NONE) { + Matrix().apply { + preScale(config.scaleFactorX, config.scaleFactorY) + } } private val contentChanged = AtomicBoolean(false) private val isCapturing = AtomicBoolean(true) diff --git a/sentry-android-replay/src/main/java/io/sentry/android/replay/capture/BufferCaptureStrategy.kt b/sentry-android-replay/src/main/java/io/sentry/android/replay/capture/BufferCaptureStrategy.kt index e0c728fd08..9cca35973d 100644 --- a/sentry-android-replay/src/main/java/io/sentry/android/replay/capture/BufferCaptureStrategy.kt +++ b/sentry-android-replay/src/main/java/io/sentry/android/replay/capture/BufferCaptureStrategy.kt @@ -18,8 +18,8 @@ import io.sentry.android.replay.util.submitSafely import io.sentry.protocol.SentryId import io.sentry.transport.ICurrentDateProvider import io.sentry.util.FileUtils +import io.sentry.util.Random import java.io.File -import java.security.SecureRandom import java.util.Date import java.util.concurrent.ScheduledExecutorService @@ -27,7 +27,7 @@ internal class BufferCaptureStrategy( private val options: SentryOptions, private val scopes: IScopes?, private val dateProvider: ICurrentDateProvider, - private val random: SecureRandom, + private val random: Random, executor: ScheduledExecutorService? = null, replayCacheProvider: ((replayId: SentryId, recorderConfig: ScreenshotRecorderConfig) -> ReplayCache)? = null ) : BaseCaptureStrategy(options, scopes, dateProvider, executor = executor, replayCacheProvider = replayCacheProvider) { diff --git a/sentry-android-replay/src/main/java/io/sentry/android/replay/util/Sampling.kt b/sentry-android-replay/src/main/java/io/sentry/android/replay/util/Sampling.kt index 8acb6b00a6..5ec46ea962 100644 --- a/sentry-android-replay/src/main/java/io/sentry/android/replay/util/Sampling.kt +++ b/sentry-android-replay/src/main/java/io/sentry/android/replay/util/Sampling.kt @@ -1,8 +1,8 @@ package io.sentry.android.replay.util -import java.security.SecureRandom +import io.sentry.util.Random -internal fun SecureRandom.sample(rate: Double?): Boolean { +internal fun Random.sample(rate: Double?): Boolean { if (rate != null) { return !(rate < this.nextDouble()) // bad luck } diff --git a/sentry-android-replay/src/main/java/io/sentry/android/replay/viewhierarchy/ComposeViewHierarchyNode.kt b/sentry-android-replay/src/main/java/io/sentry/android/replay/viewhierarchy/ComposeViewHierarchyNode.kt index 888528f769..5640fbc96f 100644 --- a/sentry-android-replay/src/main/java/io/sentry/android/replay/viewhierarchy/ComposeViewHierarchyNode.kt +++ b/sentry-android-replay/src/main/java/io/sentry/android/replay/viewhierarchy/ComposeViewHierarchyNode.kt @@ -8,7 +8,6 @@ import androidx.compose.ui.graphics.isUnspecified import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.layout.LayoutCoordinates import androidx.compose.ui.layout.findRootCoordinates -import androidx.compose.ui.layout.positionInWindow import androidx.compose.ui.node.LayoutNode import androidx.compose.ui.node.Owner import androidx.compose.ui.semantics.SemanticsActions @@ -87,12 +86,13 @@ internal object ComposeViewHierarchyNode { (semantics == null || !semantics.contains(SemanticsProperties.InvisibleToUser)) && visibleRect.height() > 0 && visibleRect.width() > 0 val isEditable = semantics?.contains(SemanticsActions.SetText) == true - val positionInWindow = node.coordinates.positionInWindow() return when { semantics?.contains(SemanticsProperties.Text) == true || isEditable -> { val shouldMask = isVisible && node.shouldMask(isImage = false, options) parent?.setImportantForCaptureToAncestors(true) + // TODO: if we get reports that it's slow, we can drop this, and just mask + // TODO: the whole view instead of per-line val textLayoutResults = mutableListOf() semantics?.getOrNull(SemanticsActions.GetTextLayoutResult) ?.action @@ -108,8 +108,8 @@ internal object ComposeViewHierarchyNode { TextViewHierarchyNode( layout = if (textLayoutResults.isNotEmpty() && !isEditable) ComposeTextLayout(textLayoutResults.first(), hasFillModifier) else null, dominantColor = textColor?.toArgb()?.toOpaque(), - x = positionInWindow.x, - y = positionInWindow.y, + x = visibleRect.left.toFloat(), + y = visibleRect.top.toFloat(), width = node.width, height = node.height, elevation = (parent?.elevation ?: 0f), @@ -128,8 +128,8 @@ internal object ComposeViewHierarchyNode { parent?.setImportantForCaptureToAncestors(true) ImageViewHierarchyNode( - x = positionInWindow.x, - y = positionInWindow.y, + x = visibleRect.left.toFloat(), + y = visibleRect.top.toFloat(), width = node.width, height = node.height, elevation = (parent?.elevation ?: 0f), @@ -147,8 +147,8 @@ internal object ComposeViewHierarchyNode { // TODO: traverse the ViewHierarchyNode here again. For now we can recommend // TODO: using custom modifiers to obscure the entire node if it's sensitive GenericViewHierarchyNode( - x = positionInWindow.x, - y = positionInWindow.y, + x = visibleRect.left.toFloat(), + y = visibleRect.top.toFloat(), width = node.width, height = node.height, elevation = (parent?.elevation ?: 0f), 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 ef05ecb029..03cb37ad3e 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 @@ -239,8 +239,8 @@ sealed class ViewHierarchyNode( private fun Class<*>.isAssignableFrom(set: Set): Boolean { var cls: Class<*>? = this while (cls != null) { - val canonicalName = cls.canonicalName - if (canonicalName != null && set.contains(canonicalName)) { + val canonicalName = cls.name + if (set.contains(canonicalName)) { return true } cls = cls.superclass diff --git a/sentry-android-replay/src/test/java/io/sentry/android/replay/capture/BufferCaptureStrategyTest.kt b/sentry-android-replay/src/test/java/io/sentry/android/replay/capture/BufferCaptureStrategyTest.kt index 840035989f..64f950ce07 100644 --- a/sentry-android-replay/src/test/java/io/sentry/android/replay/capture/BufferCaptureStrategyTest.kt +++ b/sentry-android-replay/src/test/java/io/sentry/android/replay/capture/BufferCaptureStrategyTest.kt @@ -20,6 +20,7 @@ import io.sentry.android.replay.capture.BufferCaptureStrategyTest.Fixture.Compan import io.sentry.protocol.SentryId import io.sentry.transport.CurrentDateProvider import io.sentry.transport.ICurrentDateProvider +import io.sentry.util.Random import org.awaitility.kotlin.await import org.junit.Rule import org.junit.rules.TemporaryFolder @@ -35,7 +36,6 @@ import org.mockito.kotlin.times import org.mockito.kotlin.verify import org.mockito.kotlin.whenever import java.io.File -import java.security.SecureRandom import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFalse @@ -97,7 +97,7 @@ class BufferCaptureStrategyTest { options, scopes, dateProvider, - SecureRandom(), + Random(), mock { doAnswer { invocation -> (invocation.arguments[0] as Runnable).run() 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 334146a218..b0b756fa26 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 @@ -29,7 +29,7 @@ class SentryTimberIntegration( logger.log(SentryLevel.DEBUG, "SentryTimberIntegration installed.") SentryIntegrationPackageStorage.getInstance().addPackage("maven:io.sentry:sentry-android-timber", VERSION_NAME) - addIntegrationToSdkVersion(javaClass) + addIntegrationToSdkVersion("Timber") } override fun close() { 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 fe5a6a4762..dd5fdd3980 100644 --- a/sentry-apollo/src/main/java/io/sentry/apollo/SentryApolloInterceptor.kt +++ b/sentry-apollo/src/main/java/io/sentry/apollo/SentryApolloInterceptor.kt @@ -40,7 +40,7 @@ class SentryApolloInterceptor( constructor(beforeSpan: BeforeSpanCallback) : this(ScopesAdapter.getInstance(), beforeSpan) init { - addIntegrationToSdkVersion(javaClass) + addIntegrationToSdkVersion("Apollo") SentryIntegrationPackageStorage.getInstance().addPackage("maven:io.sentry:sentry-apollo", BuildConfig.VERSION_NAME) } diff --git a/sentry-okhttp/src/main/java/io/sentry/okhttp/SentryOkHttpInterceptor.kt b/sentry-okhttp/src/main/java/io/sentry/okhttp/SentryOkHttpInterceptor.kt index bd6061da5f..ade89df494 100644 --- a/sentry-okhttp/src/main/java/io/sentry/okhttp/SentryOkHttpInterceptor.kt +++ b/sentry-okhttp/src/main/java/io/sentry/okhttp/SentryOkHttpInterceptor.kt @@ -33,7 +33,7 @@ import java.io.IOException * @param scopes The [IScopes], internal and only used for testing. * @param beforeSpan The [ISpan] can be customized or dropped with the [BeforeSpanCallback]. * @param captureFailedRequests The SDK will only capture HTTP Client errors if it is enabled, - * Defaults to false. + * Defaults to true. * @param failedRequestStatusCodes The SDK will only capture HTTP Client errors if the HTTP Response * status code is within the defined ranges. * @param failedRequestTargets The SDK will only capture HTTP Client errors if the HTTP Request URL @@ -54,7 +54,7 @@ public open class SentryOkHttpInterceptor( public constructor(beforeSpan: BeforeSpanCallback) : this(ScopesAdapter.getInstance(), beforeSpan) init { - addIntegrationToSdkVersion(javaClass) + addIntegrationToSdkVersion("OkHttp") SentryIntegrationPackageStorage.getInstance() .addPackage("maven:io.sentry:sentry-okhttp", BuildConfig.VERSION_NAME) } diff --git a/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml b/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml index 84474b889c..ac85411017 100644 --- a/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml +++ b/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml @@ -110,7 +110,7 @@ - + @@ -159,7 +159,7 @@ - + diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 293ea6cb8c..1bbe484f7b 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -6006,7 +6006,6 @@ public final class io/sentry/util/InitUtil { public final class io/sentry/util/IntegrationUtils { public fun ()V - public static fun addIntegrationToSdkVersion (Ljava/lang/Class;)V public static fun addIntegrationToSdkVersion (Ljava/lang/String;)V } @@ -6020,6 +6019,7 @@ public final class io/sentry/util/JsonSerializationUtils { public final class io/sentry/util/LazyEvaluator { public fun (Lio/sentry/util/LazyEvaluator$Evaluator;)V public fun getValue ()Ljava/lang/Object; + public fun resetValue ()V public fun setValue (Ljava/lang/Object;)V } @@ -6136,6 +6136,19 @@ public final class io/sentry/util/PropagationTargetsUtils { public static fun contain (Ljava/util/List;Ljava/net/URI;)Z } +public final class io/sentry/util/Random : java/io/Serializable { + public fun ()V + public fun (J)V + public fun nextBoolean ()Z + public fun nextBytes ([B)V + public fun nextDouble ()D + public fun nextFloat ()F + public fun nextInt ()I + public fun nextInt (I)I + public fun nextLong ()J + public fun setSeed (J)V +} + public final class io/sentry/util/SampleRateUtils { public fun ()V public static fun isValidProfilesSampleRate (Ljava/lang/Double;)Z @@ -6144,6 +6157,11 @@ public final class io/sentry/util/SampleRateUtils { public static fun isValidTracesSampleRate (Ljava/lang/Double;Z)Z } +public final class io/sentry/util/SentryRandom { + public fun ()V + public static fun current ()Lio/sentry/util/Random; +} + public final class io/sentry/util/SpanUtils { public fun ()V public static fun ignoredSpanOriginsForOpenTelemetry ()Ljava/util/List; diff --git a/sentry/src/main/java/io/sentry/Baggage.java b/sentry/src/main/java/io/sentry/Baggage.java index 7facf5afb9..954cda7a97 100644 --- a/sentry/src/main/java/io/sentry/Baggage.java +++ b/sentry/src/main/java/io/sentry/Baggage.java @@ -133,7 +133,7 @@ public static Baggage fromEvent( final Baggage baggage = new Baggage(options.getLogger()); final SpanContext trace = event.getContexts().getTrace(); baggage.setTraceId(trace != null ? trace.getTraceId().toString() : null); - baggage.setPublicKey(new Dsn(options.getDsn()).getPublicKey()); + baggage.setPublicKey(options.getParsedDsn().getPublicKey()); baggage.setRelease(event.getRelease()); baggage.setEnvironment(event.getEnvironment()); baggage.setTransaction(event.getTransaction()); @@ -383,7 +383,7 @@ public void setValuesFromTransaction( final @Nullable String transactionName, final @Nullable TransactionNameSource transactionNameSource) { setTraceId(traceId.toString()); - setPublicKey(new Dsn(sentryOptions.getDsn()).getPublicKey()); + setPublicKey(sentryOptions.getParsedDsn().getPublicKey()); setRelease(sentryOptions.getRelease()); setEnvironment(sentryOptions.getEnvironment()); setTransaction(isHighQualityTransactionName(transactionNameSource) ? transactionName : null); @@ -400,7 +400,7 @@ public void setValuesFromScope( final @NotNull PropagationContext propagationContext = scope.getPropagationContext(); final @NotNull SentryId replayId = scope.getReplayId(); setTraceId(propagationContext.getTraceId().toString()); - setPublicKey(new Dsn(options.getDsn()).getPublicKey()); + setPublicKey(options.getParsedDsn().getPublicKey()); setRelease(options.getRelease()); setEnvironment(options.getEnvironment()); if (!SentryId.EMPTY_ID.equals(replayId)) { diff --git a/sentry/src/main/java/io/sentry/DsnUtil.java b/sentry/src/main/java/io/sentry/DsnUtil.java index 3c48e4a43e..6cc0dc360b 100644 --- a/sentry/src/main/java/io/sentry/DsnUtil.java +++ b/sentry/src/main/java/io/sentry/DsnUtil.java @@ -23,7 +23,7 @@ public static boolean urlContainsDsnHost(@Nullable SentryOptions options, @Nulla return false; } - final @NotNull Dsn dsn = new Dsn(dsnString); + final @NotNull Dsn dsn = options.getParsedDsn(); final @NotNull URI sentryUri = dsn.getSentryUri(); final @Nullable String dsnHost = sentryUri.getHost(); diff --git a/sentry/src/main/java/io/sentry/ProfilingTraceData.java b/sentry/src/main/java/io/sentry/ProfilingTraceData.java index 17332b5931..3998735917 100644 --- a/sentry/src/main/java/io/sentry/ProfilingTraceData.java +++ b/sentry/src/main/java/io/sentry/ProfilingTraceData.java @@ -144,7 +144,7 @@ public ProfilingTraceData( // Transaction info this.transactions = transactions; - this.transactionName = transactionName; + this.transactionName = transactionName.isEmpty() ? "unknown" : transactionName; this.durationNs = durationNanos; // App info diff --git a/sentry/src/main/java/io/sentry/ProfilingTransactionData.java b/sentry/src/main/java/io/sentry/ProfilingTransactionData.java index 045b859f05..84d149ef15 100644 --- a/sentry/src/main/java/io/sentry/ProfilingTransactionData.java +++ b/sentry/src/main/java/io/sentry/ProfilingTransactionData.java @@ -29,7 +29,7 @@ public ProfilingTransactionData( @NotNull ITransaction transaction, @NotNull Long startNs, @NotNull Long startCpuMs) { this.id = transaction.getEventId().toString(); this.traceId = transaction.getSpanContext().getTraceId().toString(); - this.name = transaction.getName(); + this.name = transaction.getName().isEmpty() ? "unknown" : transaction.getName(); this.relativeStartNs = startNs; this.relativeStartCpuMs = startCpuMs; } diff --git a/sentry/src/main/java/io/sentry/RequestDetailsResolver.java b/sentry/src/main/java/io/sentry/RequestDetailsResolver.java index b4474893a2..6083c69e99 100644 --- a/sentry/src/main/java/io/sentry/RequestDetailsResolver.java +++ b/sentry/src/main/java/io/sentry/RequestDetailsResolver.java @@ -21,7 +21,7 @@ public RequestDetailsResolver(final @NotNull SentryOptions options) { @NotNull RequestDetails resolve() { - final Dsn dsn = new Dsn(options.getDsn()); + final Dsn dsn = options.getParsedDsn(); final URI sentryUri = dsn.getSentryUri(); final String envelopeUrl = sentryUri.resolve(sentryUri.getPath() + "/envelope/").toString(); diff --git a/sentry/src/main/java/io/sentry/SendCachedEnvelopeFireAndForgetIntegration.java b/sentry/src/main/java/io/sentry/SendCachedEnvelopeFireAndForgetIntegration.java index 6f58d426dc..37ba75783a 100644 --- a/sentry/src/main/java/io/sentry/SendCachedEnvelopeFireAndForgetIntegration.java +++ b/sentry/src/main/java/io/sentry/SendCachedEnvelopeFireAndForgetIntegration.java @@ -81,7 +81,7 @@ public void register(final @NotNull IScopes scopes, final @NotNull SentryOptions options .getLogger() .log(SentryLevel.DEBUG, "SendCachedEventFireAndForgetIntegration installed."); - addIntegrationToSdkVersion(getClass()); + addIntegrationToSdkVersion("SendCachedEnvelopeFireAndForget"); sendCachedEnvelopes(scopes, options); } diff --git a/sentry/src/main/java/io/sentry/Sentry.java b/sentry/src/main/java/io/sentry/Sentry.java index 3c4e139548..7a35720e2d 100644 --- a/sentry/src/main/java/io/sentry/Sentry.java +++ b/sentry/src/main/java/io/sentry/Sentry.java @@ -264,7 +264,11 @@ public static void init(final @NotNull SentryOptions options) { * @param options options the SentryOptions * @param globalHubMode the globalHubMode */ - @SuppressWarnings("deprecation") + @SuppressWarnings({ + "deprecation", + "Convert2MethodRef", + "FutureReturnValueIgnored" + }) // older AGP versions do not support method references private static void init(final @NotNull SentryOptions options, final boolean globalHubMode) { try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { if (!options.getClass().getName().equals("io.sentry.android.core.SentryAndroidOptions") @@ -296,6 +300,18 @@ private static void init(final @NotNull SentryOptions options, final boolean glo "Sentry has been already initialized. Previous configuration will be overwritten."); } + // load lazy fields of the options in a separate thread + try { + options.getExecutorService().submit(() -> options.loadLazyFields()); + } catch (RejectedExecutionException e) { + options + .getLogger() + .log( + SentryLevel.DEBUG, + "Failed to call the executor. Lazy fields will not be loaded. Did you call Sentry.close()?", + e); + } + final IScopes scopes = getCurrentScopes(); scopes.close(true); @@ -458,8 +474,8 @@ private static boolean preInitConfigurations(final @NotNull SentryOptions option "DSN is required. Use empty string or set enabled to false in SentryOptions to disable SDK."); } - @SuppressWarnings("unused") - final Dsn parsedDsn = new Dsn(dsn); + // This creates the DSN object and performs some checks + options.getParsedDsn(); return true; } diff --git a/sentry/src/main/java/io/sentry/SentryClient.java b/sentry/src/main/java/io/sentry/SentryClient.java index 6715990fee..fdeb8b9b6b 100644 --- a/sentry/src/main/java/io/sentry/SentryClient.java +++ b/sentry/src/main/java/io/sentry/SentryClient.java @@ -14,10 +14,11 @@ import io.sentry.util.CheckInUtils; import io.sentry.util.HintUtils; import io.sentry.util.Objects; +import io.sentry.util.Random; +import io.sentry.util.SentryRandom; import io.sentry.util.TracingUtils; import java.io.Closeable; import java.io.IOException; -import java.security.SecureRandom; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -37,7 +38,6 @@ public final class SentryClient implements ISentryClient { private final @NotNull SentryOptions options; private final @NotNull ITransport transport; - private final @Nullable SecureRandom random; private final @NotNull SortBreadcrumbsByDate sortBreadcrumbsByDate = new SortBreadcrumbsByDate(); @Override @@ -57,8 +57,6 @@ public boolean isEnabled() { final RequestDetailsResolver requestDetailsResolver = new RequestDetailsResolver(options); transport = transportFactory.create(options, requestDetailsResolver.resolve()); - - this.random = options.getSampleRate() == null ? null : new SecureRandom(); } private boolean shouldApplyScopeData( @@ -1169,6 +1167,7 @@ public boolean isHealthy() { } private boolean sample() { + final @Nullable Random random = options.getSampleRate() == null ? null : SentryRandom.current(); // https://docs.sentry.io/development/sdk-dev/features/#event-sampling if (options.getSampleRate() != null && random != null) { final double sampling = options.getSampleRate(); diff --git a/sentry/src/main/java/io/sentry/SentryOptions.java b/sentry/src/main/java/io/sentry/SentryOptions.java index 127d87401d..c02765fd54 100644 --- a/sentry/src/main/java/io/sentry/SentryOptions.java +++ b/sentry/src/main/java/io/sentry/SentryOptions.java @@ -83,6 +83,9 @@ public class SentryOptions { */ private @Nullable String dsn; + /** Parsed DSN to avoid parsing it every time. */ + private final @NotNull LazyEvaluator parsedDsn = new LazyEvaluator<>(() -> new Dsn(dsn)); + /** dsnHash is used as a subfolder of cacheDirPath to isolate events when rotating DSNs */ private @Nullable String dsnHash; @@ -538,7 +541,7 @@ public void addIntegration(@NotNull Integration integration) { } /** - * Returns the DSN + * Returns the DSN. * * @return the DSN or null if not set */ @@ -546,6 +549,17 @@ public void addIntegration(@NotNull Integration integration) { return dsn; } + /** + * Evaluates and parses the DSN. May throw an exception if the DSN is invalid. + * + * @return the parsed DSN or throws if dsn is invalid + */ + @ApiStatus.Internal + @NotNull + Dsn getParsedDsn() throws IllegalArgumentException { + return parsedDsn.getValue(); + } + /** * Sets the DSN * @@ -553,6 +567,7 @@ public void addIntegration(@NotNull Integration integration) { */ public void setDsn(final @Nullable String dsn) { this.dsn = dsn; + this.parsedDsn.resetValue(); dsnHash = StringUtils.calculateStringHash(this.dsn, logger); } @@ -2394,6 +2409,17 @@ public void setGlobalHubMode(final @Nullable Boolean globalHubMode) { return globalHubMode; } + /** + * Load the lazy fields. Useful to load in the background, so that results are already cached. DO + * NOT CALL THIS METHOD ON THE MAIN THREAD. + */ + void loadLazyFields() { + getSerializer(); + getParsedDsn(); + getEnvelopeReader(); + getDateProvider(); + } + /** The BeforeSend callback */ public interface BeforeSendCallback { diff --git a/sentry/src/main/java/io/sentry/ShutdownHookIntegration.java b/sentry/src/main/java/io/sentry/ShutdownHookIntegration.java index e553d0a9f5..3d5bccc3d7 100644 --- a/sentry/src/main/java/io/sentry/ShutdownHookIntegration.java +++ b/sentry/src/main/java/io/sentry/ShutdownHookIntegration.java @@ -37,7 +37,7 @@ public void register(final @NotNull IScopes scopes, final @NotNull SentryOptions () -> { runtime.addShutdownHook(thread); options.getLogger().log(SentryLevel.DEBUG, "ShutdownHookIntegration installed."); - addIntegrationToSdkVersion(getClass()); + addIntegrationToSdkVersion("ShutdownHook"); }); } else { options.getLogger().log(SentryLevel.INFO, "enableShutdownHook is disabled."); diff --git a/sentry/src/main/java/io/sentry/TracesSampler.java b/sentry/src/main/java/io/sentry/TracesSampler.java index 9b215c96c2..3ce28ab745 100644 --- a/sentry/src/main/java/io/sentry/TracesSampler.java +++ b/sentry/src/main/java/io/sentry/TracesSampler.java @@ -1,7 +1,8 @@ package io.sentry; import io.sentry.util.Objects; -import java.security.SecureRandom; +import io.sentry.util.Random; +import io.sentry.util.SentryRandom; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -10,14 +11,14 @@ @ApiStatus.Internal public final class TracesSampler { private final @NotNull SentryOptions options; - private final @NotNull SecureRandom random; + private final @Nullable Random random; public TracesSampler(final @NotNull SentryOptions options) { - this(Objects.requireNonNull(options, "options are required"), new SecureRandom()); + this(Objects.requireNonNull(options, "options are required"), null); } @TestOnly - TracesSampler(final @NotNull SentryOptions options, final @NotNull SecureRandom random) { + TracesSampler(final @NotNull SentryOptions options, final @Nullable Random random) { this.options = options; this.random = random; } @@ -85,6 +86,13 @@ public TracesSamplingDecision sample(final @NotNull SamplingContext samplingCont } private boolean sample(final @NotNull Double aDouble) { - return !(aDouble < random.nextDouble()); + return !(aDouble < getRandom().nextDouble()); + } + + private Random getRandom() { + if (random == null) { + return SentryRandom.current(); + } + return random; } } diff --git a/sentry/src/main/java/io/sentry/UncaughtExceptionHandlerIntegration.java b/sentry/src/main/java/io/sentry/UncaughtExceptionHandlerIntegration.java index a30639a79c..1cf4c151b0 100644 --- a/sentry/src/main/java/io/sentry/UncaughtExceptionHandlerIntegration.java +++ b/sentry/src/main/java/io/sentry/UncaughtExceptionHandlerIntegration.java @@ -90,7 +90,7 @@ public final void register(final @NotNull IScopes scopes, final @NotNull SentryO this.options .getLogger() .log(SentryLevel.DEBUG, "UncaughtExceptionHandlerIntegration installed."); - addIntegrationToSdkVersion(getClass()); + addIntegrationToSdkVersion("UncaughtExceptionHandler"); } } diff --git a/sentry/src/main/java/io/sentry/cache/PersistingScopeObserver.java b/sentry/src/main/java/io/sentry/cache/PersistingScopeObserver.java index 7c186cf99d..908e2c66e4 100644 --- a/sentry/src/main/java/io/sentry/cache/PersistingScopeObserver.java +++ b/sentry/src/main/java/io/sentry/cache/PersistingScopeObserver.java @@ -133,6 +133,12 @@ public void setReplayId(@NotNull SentryId replayId) { @SuppressWarnings("FutureReturnValueIgnored") private void serializeToDisk(final @NotNull Runnable task) { + if (Thread.currentThread().getName().contains("SentryExecutor")) { + // we're already on the sentry executor thread, so we can just execute it directly + task.run(); + return; + } + try { options .getExecutorService() diff --git a/sentry/src/main/java/io/sentry/util/IntegrationUtils.java b/sentry/src/main/java/io/sentry/util/IntegrationUtils.java index 6d504c1451..4edbb7aedd 100644 --- a/sentry/src/main/java/io/sentry/util/IntegrationUtils.java +++ b/sentry/src/main/java/io/sentry/util/IntegrationUtils.java @@ -6,16 +6,6 @@ @ApiStatus.Internal public final class IntegrationUtils { - public static void addIntegrationToSdkVersion(final @NotNull Class clazz) { - final String name = - clazz - .getSimpleName() - .replace("Sentry", "") - .replace("Integration", "") - .replace("Interceptor", "") - .replace("EventProcessor", ""); - addIntegrationToSdkVersion(name); - } public static void addIntegrationToSdkVersion(final @NotNull String name) { SentryIntegrationPackageStorage.getInstance().addIntegration(name); diff --git a/sentry/src/main/java/io/sentry/util/LazyEvaluator.java b/sentry/src/main/java/io/sentry/util/LazyEvaluator.java index 0f8653c411..82c2b866d2 100644 --- a/sentry/src/main/java/io/sentry/util/LazyEvaluator.java +++ b/sentry/src/main/java/io/sentry/util/LazyEvaluator.java @@ -27,7 +27,8 @@ public LazyEvaluator(final @NotNull Evaluator evaluator) { } /** - * Executes the evaluator function and caches its result, so that it's called only once. + * Executes the evaluator function and caches its result, so that it's called only once, unless + * resetValue is called. * * @return The result of the evaluator function. */ @@ -50,6 +51,16 @@ public void setValue(final @Nullable T value) { } } + /** + * Resets the internal value and forces the evaluator function to be called the next time + * getValue() is called. + */ + public void resetValue() { + try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { + this.value = null; + } + } + public interface Evaluator { @NotNull T evaluate(); diff --git a/sentry/src/main/java/io/sentry/util/Random.java b/sentry/src/main/java/io/sentry/util/Random.java new file mode 100644 index 0000000000..cbd81824df --- /dev/null +++ b/sentry/src/main/java/io/sentry/util/Random.java @@ -0,0 +1,466 @@ +/* + * Copyright (c) 1995, 2013, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package io.sentry.util; + +import java.util.concurrent.atomic.AtomicLong; +import org.jetbrains.annotations.ApiStatus; + +/** + * A simplified version of {@link java.util.Random} that we use for sampling, which is much faster + * than {@link java.security.SecureRandom}. This is necessary so that some security tools do not + * flag our Random usage as potentially insecure. + */ +@ApiStatus.Internal +public final class Random implements java.io.Serializable { + /** use serialVersionUID from JDK 1.1 for interoperability */ + private static final long serialVersionUID = 3905348978240129619L; + + /** + * The internal state associated with this pseudorandom number generator. (The specs for the + * methods in this class describe the ongoing computation of this value.) + */ + private final AtomicLong seed; + + private static final long multiplier = 0x5DEECE66DL; + private static final long addend = 0xBL; + private static final long mask = (1L << 48) - 1; + + private static final double DOUBLE_UNIT = 0x1.0p-53; // 1.0 / (1L << 53) + + // IllegalArgumentException messages + static final String BadBound = "bound must be positive"; + + /** + * Creates a new random number generator. This constructor sets the seed of the random number + * generator to a value very likely to be distinct from any other invocation of this constructor. + */ + public Random() { + this(seedUniquifier() ^ System.nanoTime()); + } + + private static long seedUniquifier() { + // L'Ecuyer, "Tables of Linear Congruential Generators of + // Different Sizes and Good Lattice Structure", 1999 + for (; ; ) { + long current = seedUniquifier.get(); + long next = current * 1181783497276652981L; + if (seedUniquifier.compareAndSet(current, next)) return next; + } + } + + private static final AtomicLong seedUniquifier = new AtomicLong(8682522807148012L); + + /** + * Creates a new random number generator using a single {@code long} seed. The seed is the initial + * value of the internal state of the pseudorandom number generator which is maintained by method + * {@link #next}. + * + *

The invocation {@code new Random(seed)} is equivalent to: + * + *

{@code
+   * Random rnd = new Random();
+   * rnd.setSeed(seed);
+   * }
+ * + * @param seed the initial seed + * @see #setSeed(long) + */ + public Random(long seed) { + if (getClass() == Random.class) this.seed = new AtomicLong(initialScramble(seed)); + else { + // subclass might have overriden setSeed + this.seed = new AtomicLong(); + setSeed(seed); + } + } + + private static long initialScramble(long seed) { + return (seed ^ multiplier) & mask; + } + + /** + * Sets the seed of this random number generator using a single {@code long} seed. The general + * contract of {@code setSeed} is that it alters the state of this random number generator object + * so as to be in exactly the same state as if it had just been created with the argument {@code + * seed} as a seed. The method {@code setSeed} is implemented by class {@code Random} by + * atomically updating the seed to + * + *
{@code (seed ^ 0x5DEECE66DL) & ((1L << 48) - 1)}
+ * + *

The implementation of {@code setSeed} by class {@code Random} happens to use only 48 bits of + * the given seed. In general, however, an overriding method may use all 64 bits of the {@code + * long} argument as a seed value. + * + * @param seed the initial seed + */ + public synchronized void setSeed(long seed) { + this.seed.set(initialScramble(seed)); + } + + /** + * Generates the next pseudorandom number. Subclasses should override this, as this is used by all + * other methods. + * + *

The general contract of {@code next} is that it returns an {@code int} value and if the + * argument {@code bits} is between {@code 1} and {@code 32} (inclusive), then that many low-order + * bits of the returned value will be (approximately) independently chosen bit values, each of + * which is (approximately) equally likely to be {@code 0} or {@code 1}. The method {@code next} + * is implemented by class {@code Random} by atomically updating the seed to + * + *

{@code (seed * 0x5DEECE66DL + 0xBL) & ((1L << 48) - 1)}
+ * + * and returning + * + *
{@code (int)(seed >>> (48 - bits))}.
+ * + * This is a linear congruential pseudorandom number generator, as defined by D. H. Lehmer and + * described by Donald E. Knuth in The Art of Computer Programming, Volume 2: + * Seminumerical Algorithms, section 3.2.1. + * + * @param bits random bits + * @return the next pseudorandom value from this random number generator's sequence + * @since 1.1 + */ + private int next(int bits) { + long oldseed, nextseed; + AtomicLong seed = this.seed; + do { + oldseed = seed.get(); + nextseed = (oldseed * multiplier + addend) & mask; + } while (!seed.compareAndSet(oldseed, nextseed)); + return (int) (nextseed >>> (48 - bits)); + } + + /** + * Generates random bytes and places them into a user-supplied byte array. The number of random + * bytes produced is equal to the length of the byte array. + * + *

The method {@code nextBytes} is implemented by class {@code Random} as if by: + * + *

{@code
+   * public void nextBytes(byte[] bytes) {
+   *   for (int i = 0; i < bytes.length; )
+   *     for (int rnd = nextInt(), n = Math.min(bytes.length - i, 4);
+   *          n-- > 0; rnd >>= 8)
+   *       bytes[i++] = (byte)rnd;
+   * }
+   * }
+ * + * @param bytes the byte array to fill with random bytes + * @throws NullPointerException if the byte array is null + * @since 1.1 + */ + public void nextBytes(byte[] bytes) { + for (int i = 0, len = bytes.length; i < len; ) + for (int rnd = nextInt(), n = Math.min(len - i, Integer.SIZE / Byte.SIZE); + n-- > 0; + rnd >>= Byte.SIZE) bytes[i++] = (byte) rnd; + } + + /** + * The form of nextLong used by LongStream Spliterators. If origin is greater than bound, acts as + * unbounded form of nextLong, else as bounded form. + * + * @param origin the least value, unless greater than bound + * @param bound the upper bound (exclusive), must not equal origin + * @return a pseudorandom value + */ + final long internalNextLong(long origin, long bound) { + long r = nextLong(); + if (origin < bound) { + long n = bound - origin, m = n - 1; + if ((n & m) == 0L) // power of two + r = (r & m) + origin; + else if (n > 0L) { // reject over-represented candidates + for (long u = r >>> 1; // ensure nonnegative + u + m - (r = u % n) < 0L; // rejection check + u = nextLong() >>> 1) // retry + ; + r += origin; + } else { // range not representable as long + while (r < origin || r >= bound) r = nextLong(); + } + } + return r; + } + + /** + * The form of nextInt used by IntStream Spliterators. For the unbounded case: uses nextInt(). For + * the bounded case with representable range: uses nextInt(int bound) For the bounded case with + * unrepresentable range: uses nextInt() + * + * @param origin the least value, unless greater than bound + * @param bound the upper bound (exclusive), must not equal origin + * @return a pseudorandom value + */ + final int internalNextInt(int origin, int bound) { + if (origin < bound) { + int n = bound - origin; + if (n > 0) { + return nextInt(n) + origin; + } else { // range not representable as int + int r; + do { + r = nextInt(); + } while (r < origin || r >= bound); + return r; + } + } else { + return nextInt(); + } + } + + /** + * The form of nextDouble used by DoubleStream Spliterators. + * + * @param origin the least value, unless greater than bound + * @param bound the upper bound (exclusive), must not equal origin + * @return a pseudorandom value + */ + final double internalNextDouble(double origin, double bound) { + double r = nextDouble(); + if (origin < bound) { + r = r * (bound - origin) + origin; + if (r >= bound) // correct for rounding + r = Double.longBitsToDouble(Double.doubleToLongBits(bound) - 1); + } + return r; + } + + /** + * Returns the next pseudorandom, uniformly distributed {@code int} value from this random number + * generator's sequence. The general contract of {@code nextInt} is that one {@code int} value is + * pseudorandomly generated and returned. All 232 possible {@code int} values are + * produced with (approximately) equal probability. + * + *

The method {@code nextInt} is implemented by class {@code Random} as if by: + * + *

{@code
+   * public int nextInt() {
+   *   return next(32);
+   * }
+   * }
+ * + * @return the next pseudorandom, uniformly distributed {@code int} value from this random number + * generator's sequence + */ + public int nextInt() { + return next(32); + } + + /** + * Returns a pseudorandom, uniformly distributed {@code int} value between 0 (inclusive) and the + * specified value (exclusive), drawn from this random number generator's sequence. The general + * contract of {@code nextInt} is that one {@code int} value in the specified range is + * pseudorandomly generated and returned. All {@code bound} possible {@code int} values are + * produced with (approximately) equal probability. The method {@code nextInt(int bound)} is + * implemented by class {@code Random} as if by: + * + *
{@code
+   * public int nextInt(int bound) {
+   *   if (bound <= 0)
+   *     throw new IllegalArgumentException("bound must be positive");
+   *
+   *   if ((bound & -bound) == bound)  // i.e., bound is a power of 2
+   *     return (int)((bound * (long)next(31)) >> 31);
+   *
+   *   int bits, val;
+   *   do {
+   *       bits = next(31);
+   *       val = bits % bound;
+   *   } while (bits - val + (bound-1) < 0);
+   *   return val;
+   * }
+   * }
+ * + *

The hedge "approximately" is used in the foregoing description only because the next method + * is only approximately an unbiased source of independently chosen bits. If it were a perfect + * source of randomly chosen bits, then the algorithm shown would choose {@code int} values from + * the stated range with perfect uniformity. + * + *

The algorithm is slightly tricky. It rejects values that would result in an uneven + * distribution (due to the fact that 2^31 is not divisible by n). The probability of a value + * being rejected depends on n. The worst case is n=2^30+1, for which the probability of a reject + * is 1/2, and the expected number of iterations before the loop terminates is 2. + * + *

The algorithm treats the case where n is a power of two specially: it returns the correct + * number of high-order bits from the underlying pseudo-random number generator. In the absence of + * special treatment, the correct number of low-order bits would be returned. Linear + * congruential pseudo-random number generators such as the one implemented by this class are + * known to have short periods in the sequence of values of their low-order bits. Thus, this + * special case greatly increases the length of the sequence of values returned by successive + * calls to this method if n is a small power of two. + * + * @param bound the upper bound (exclusive). Must be positive. + * @return the next pseudorandom, uniformly distributed {@code int} value between zero (inclusive) + * and {@code bound} (exclusive) from this random number generator's sequence + * @throws IllegalArgumentException if bound is not positive + * @since 1.2 + */ + public int nextInt(int bound) { + if (bound <= 0) throw new IllegalArgumentException(BadBound); + + int r = next(31); + int m = bound - 1; + if ((bound & m) == 0) // i.e., bound is a power of 2 + r = (int) ((bound * (long) r) >> 31); + else { + for (int u = r; u - (r = u % bound) + m < 0; u = next(31)) + ; + } + return r; + } + + /** + * Returns the next pseudorandom, uniformly distributed {@code long} value from this random number + * generator's sequence. The general contract of {@code nextLong} is that one {@code long} value + * is pseudorandomly generated and returned. + * + *

The method {@code nextLong} is implemented by class {@code Random} as if by: + * + *

{@code
+   * public long nextLong() {
+   *   return ((long)next(32) << 32) + next(32);
+   * }
+   * }
+ * + * Because class {@code Random} uses a seed with only 48 bits, this algorithm will not return all + * possible {@code long} values. + * + * @return the next pseudorandom, uniformly distributed {@code long} value from this random number + * generator's sequence + */ + @SuppressWarnings("UnnecessaryParentheses") + public long nextLong() { + // it's okay that the bottom word remains signed. + return ((long) (next(32)) << 32) + next(32); + } + + /** + * Returns the next pseudorandom, uniformly distributed {@code boolean} value from this random + * number generator's sequence. The general contract of {@code nextBoolean} is that one {@code + * boolean} value is pseudorandomly generated and returned. The values {@code true} and {@code + * false} are produced with (approximately) equal probability. + * + *

The method {@code nextBoolean} is implemented by class {@code Random} as if by: + * + *

{@code
+   * public boolean nextBoolean() {
+   *   return next(1) != 0;
+   * }
+   * }
+ * + * @return the next pseudorandom, uniformly distributed {@code boolean} value from this random + * number generator's sequence + * @since 1.2 + */ + public boolean nextBoolean() { + return next(1) != 0; + } + + /** + * Returns the next pseudorandom, uniformly distributed {@code float} value between {@code 0.0} + * and {@code 1.0} from this random number generator's sequence. + * + *

The general contract of {@code nextFloat} is that one {@code float} value, chosen + * (approximately) uniformly from the range {@code 0.0f} (inclusive) to {@code 1.0f} (exclusive), + * is pseudorandomly generated and returned. All 224 possible {@code float} values of + * the form m x 2-24, where m is a positive integer less than + * 224, are produced with (approximately) equal probability. + * + *

The method {@code nextFloat} is implemented by class {@code Random} as if by: + * + *

{@code
+   * public float nextFloat() {
+   *   return next(24) / ((float)(1 << 24));
+   * }
+   * }
+ * + *

The hedge "approximately" is used in the foregoing description only because the next method + * is only approximately an unbiased source of independently chosen bits. If it were a perfect + * source of randomly chosen bits, then the algorithm shown would choose {@code float} values from + * the stated range with perfect uniformity. + * + *

[In early versions of Java, the result was incorrectly calculated as: + * + *

{@code
+   * return next(30) / ((float)(1 << 30));
+   * }
+ * + * This might seem to be equivalent, if not better, but in fact it introduced a slight + * nonuniformity because of the bias in the rounding of floating-point numbers: it was slightly + * more likely that the low-order bit of the significand would be 0 than that it would be 1.] + * + * @return the next pseudorandom, uniformly distributed {@code float} value between {@code 0.0} + * and {@code 1.0} from this random number generator's sequence + */ + public float nextFloat() { + return next(24) / ((float) (1 << 24)); + } + + /** + * Returns the next pseudorandom, uniformly distributed {@code double} value between {@code 0.0} + * and {@code 1.0} from this random number generator's sequence. + * + *

The general contract of {@code nextDouble} is that one {@code double} value, chosen + * (approximately) uniformly from the range {@code 0.0d} (inclusive) to {@code 1.0d} (exclusive), + * is pseudorandomly generated and returned. + * + *

The method {@code nextDouble} is implemented by class {@code Random} as if by: + * + *

{@code
+   * public double nextDouble() {
+   *   return (((long)next(26) << 27) + next(27))
+   *     / (double)(1L << 53);
+   * }
+   * }
+ * + *

The hedge "approximately" is used in the foregoing description only because the {@code next} + * method is only approximately an unbiased source of independently chosen bits. If it were a + * perfect source of randomly chosen bits, then the algorithm shown would choose {@code double} + * values from the stated range with perfect uniformity. + * + *

[In early versions of Java, the result was incorrectly calculated as: + * + *

{@code
+   * return (((long)next(27) << 27) + next(27))
+   *   / (double)(1L << 54);
+   * }
+ * + * This might seem to be equivalent, if not better, but in fact it introduced a large + * nonuniformity because of the bias in the rounding of floating-point numbers: it was three times + * as likely that the low-order bit of the significand would be 0 than that it would be 1! This + * nonuniformity probably doesn't matter much in practice, but we strive for perfection.] + * + * @return the next pseudorandom, uniformly distributed {@code double} value between {@code 0.0} + * and {@code 1.0} from this random number generator's sequence + * @see Math#random + */ + @SuppressWarnings("UnnecessaryParentheses") + public double nextDouble() { + return (((long) (next(26)) << 27) + next(27)) * DOUBLE_UNIT; + } +} diff --git a/sentry/src/main/java/io/sentry/util/SentryRandom.java b/sentry/src/main/java/io/sentry/util/SentryRandom.java new file mode 100644 index 0000000000..f6e1d0a974 --- /dev/null +++ b/sentry/src/main/java/io/sentry/util/SentryRandom.java @@ -0,0 +1,37 @@ +package io.sentry.util; + +import org.jetbrains.annotations.NotNull; + +/** + * This SentryRandom is a compromise used for improving performance of the SDK. + * + *

We did some testing where using Random from multiple threads degrades performance + * significantly. We opted for this approach as it wasn't easily possible to vendor + * ThreadLocalRandom since it's using advanced features that can cause java.lang.IllegalAccessError. + */ +public final class SentryRandom { + + private static final @NotNull SentryRandomThreadLocal instance = new SentryRandomThreadLocal(); + + /** + * Returns the current threads instance of {@link Random}. An instance of {@link Random} will be + * created the first time this is invoked on each thread. + * + *

NOTE: Avoid holding a reference to the returned {@link Random} instance as sharing a + * reference across threads (while being thread-safe) will likely degrade performance + * significantly. + * + * @return random + */ + public static @NotNull Random current() { + return instance.get(); + } + + private static class SentryRandomThreadLocal extends ThreadLocal { + + @Override + protected Random initialValue() { + return new Random(); + } + } +} diff --git a/sentry/src/test/java/io/sentry/JsonSerializerTest.kt b/sentry/src/test/java/io/sentry/JsonSerializerTest.kt index 4697c66c83..2a8a8a3443 100644 --- a/sentry/src/test/java/io/sentry/JsonSerializerTest.kt +++ b/sentry/src/test/java/io/sentry/JsonSerializerTest.kt @@ -567,7 +567,7 @@ class JsonSerializerTest { mapOf( "trace_id" to "00000000000000000000000000000000", "relative_cpu_end_ms" to null, - "name" to "", + "name" to "unknown", "relative_start_ns" to 1, "relative_end_ns" to null, "id" to "00000000000000000000000000000000", @@ -576,7 +576,7 @@ class JsonSerializerTest { mapOf( "trace_id" to "00000000000000000000000000000000", "relative_cpu_end_ms" to null, - "name" to "", + "name" to "unknown", "relative_start_ns" to 2, "relative_end_ns" to null, "id" to "00000000000000000000000000000000", diff --git a/sentry/src/test/java/io/sentry/TracesSamplerTest.kt b/sentry/src/test/java/io/sentry/TracesSamplerTest.kt index 33c95a09c7..06eb60aece 100644 --- a/sentry/src/test/java/io/sentry/TracesSamplerTest.kt +++ b/sentry/src/test/java/io/sentry/TracesSamplerTest.kt @@ -1,11 +1,11 @@ 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 java.security.SecureRandom import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFalse @@ -22,7 +22,7 @@ class TracesSamplerTest { profilesSamplerCallback: SentryOptions.ProfilesSamplerCallback? = null, logger: ILogger? = null ): TracesSampler { - val random = mock() + val random = mock() if (randomResult != null) { whenever(random.nextDouble()).thenReturn(randomResult) } diff --git a/sentry/src/test/java/io/sentry/util/LazyEvaluatorTest.kt b/sentry/src/test/java/io/sentry/util/LazyEvaluatorTest.kt index 8f0e3bc0a7..a238205405 100644 --- a/sentry/src/test/java/io/sentry/util/LazyEvaluatorTest.kt +++ b/sentry/src/test/java/io/sentry/util/LazyEvaluatorTest.kt @@ -38,4 +38,18 @@ class LazyEvaluatorTest { assertEquals(1, evaluator.value) assertEquals(1, fixture.count) } + + @Test + fun `evaluates again after resetValue`() { + val evaluator = fixture.getSut() + assertEquals(0, fixture.count) + assertEquals(1, evaluator.value) + assertEquals(1, evaluator.value) + assertEquals(1, fixture.count) + // Evaluate again, only once + evaluator.resetValue() + assertEquals(2, evaluator.value) + assertEquals(2, evaluator.value) + assertEquals(2, fixture.count) + } } diff --git a/sentry/src/test/java/io/sentry/util/SentryRandomTest.kt b/sentry/src/test/java/io/sentry/util/SentryRandomTest.kt new file mode 100644 index 0000000000..c812c6cbdc --- /dev/null +++ b/sentry/src/test/java/io/sentry/util/SentryRandomTest.kt @@ -0,0 +1,45 @@ +package io.sentry.util + +import kotlin.test.Test +import kotlin.test.assertNotSame +import kotlin.test.assertSame + +class SentryRandomTest { + + @Test + fun `thread local creates a new instance per thread but keeps re-using it for the same thread`() { + val mainThreadRandom1 = SentryRandom.current() + val mainThreadRandom2 = SentryRandom.current() + assertSame(mainThreadRandom1, mainThreadRandom2) + + var thread1Random1: Random? = null + var thread1Random2: Random? = null + + val thread1 = Thread() { + thread1Random1 = SentryRandom.current() + thread1Random2 = SentryRandom.current() + } + + var thread2Random1: Random? = null + var thread2Random2: Random? = null + + val thread2 = Thread() { + thread2Random1 = SentryRandom.current() + thread2Random2 = SentryRandom.current() + } + + thread1.start() + thread2.start() + thread1.join() + thread2.join() + + assertSame(thread1Random1, thread1Random2) + assertNotSame(mainThreadRandom1, thread1Random1) + + assertSame(thread2Random1, thread2Random2) + assertNotSame(mainThreadRandom1, thread2Random1) + + val mainThreadRandom3 = SentryRandom.current() + assertSame(mainThreadRandom1, mainThreadRandom3) + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 34584d1def..b10dbc6bc3 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -65,6 +65,7 @@ include( "sentry-samples:sentry-samples-spring-boot-webflux", "sentry-samples:sentry-samples-spring-boot-webflux-jakarta", "sentry-samples:sentry-samples-netflix-dgs", + "sentry-android-integration-tests:sentry-uitest-android-critical", "sentry-android-integration-tests:sentry-uitest-android-benchmark", "sentry-android-integration-tests:sentry-uitest-android", "sentry-android-integration-tests:test-app-plain",