diff --git a/.craft.yml b/.craft.yml index 680caa7d58d..763f85252d5 100644 --- a/.craft.yml +++ b/.craft.yml @@ -24,6 +24,7 @@ targets: maven:io.sentry:sentry-spring: maven:io.sentry:sentry-spring-boot-starter: maven:io.sentry:sentry-servlet: + maven:io.sentry:sentry-servlet-jakarta: maven:io.sentry:sentry-logback: maven:io.sentry:sentry-log4j2: maven:io.sentry:sentry-jul: diff --git a/.github/ISSUE_TEMPLATE/bug_report_java.yml b/.github/ISSUE_TEMPLATE/bug_report_java.yml index ecca4189421..8d56959802f 100644 --- a/.github/ISSUE_TEMPLATE/bug_report_java.yml +++ b/.github/ISSUE_TEMPLATE/bug_report_java.yml @@ -14,6 +14,7 @@ body: - sentry-apollo - sentry-kotlin-extensions - sentry-servlet + - sentry-servlet-jakarta - sentry-spring-boot-starter - sentry-spring - sentry-logback diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b4ef20d3b22..21b5a0dbae6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -8,6 +8,10 @@ on: force: description: Force a release even when there are release-blockers (optional) required: false + merge_target: + description: Target branch to merge into. Uses the default branch as a fallback (optional) + required: false + jobs: release: runs-on: ubuntu-latest @@ -24,3 +28,4 @@ jobs: with: version: ${{ github.event.inputs.version }} force: ${{ github.event.inputs.force }} + merge_target: ${{ github.event.inputs.merge_target }} diff --git a/.gitignore b/.gitignore index 03d9c5ea4bb..72dc1695faf 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ .idea/ .gradle/ build/ +artifacts/ out/ local.properties **.iml @@ -15,3 +16,4 @@ target/ bin/ distributions/ /Sentry/A1F16C4F5D23B2A1D281EE471D6F836BDEA23CB4/ +*.vscode/ diff --git a/.sauce/sentry-uitest-android-benchmark.yml b/.sauce/sentry-uitest-android-benchmark.yml new file mode 100644 index 00000000000..35bce15f84e --- /dev/null +++ b/.sauce/sentry-uitest-android-benchmark.yml @@ -0,0 +1,29 @@ +apiVersion: v1alpha +kind: espresso +sauce: + region: us-west-1 + # Controls how many suites are executed at the same time (sauce test env only). + concurrency: 1 + metadata: + name: Android benchmarks with Espresso + tags: + - benchmarks + - android +espresso: + app: ./sentry-android-integration-tests/sentry-uitest-android-benchmark/build/outputs/apk/release/sentry-uitest-android-benchmark-release.apk + testApp: ./sentry-android-integration-tests/sentry-uitest-android-benchmark/build/outputs/apk/androidTest/release/sentry-uitest-android-benchmark-release-androidTest.apk +suites: + name: "Android Benchmarks" + devices: + - name: "Google Pixel 2" + platformVersion: 11 + - id: Google_Pixel_2_real_us + testOptions: + useTestOrchestrator: true +# Controls what artifacts to fetch when the suite on Sauce Cloud has finished. +artifacts: + download: + when: always + match: + - junit.xml + directory: ./artifacts/ diff --git a/.sauce/sentry-uitest-android-end2end.yml b/.sauce/sentry-uitest-android-end2end.yml new file mode 100644 index 00000000000..62178a4b0dc --- /dev/null +++ b/.sauce/sentry-uitest-android-end2end.yml @@ -0,0 +1,31 @@ +apiVersion: v1alpha +kind: espresso +sauce: + region: us-west-1 + # Controls how many suites are executed at the same time (sauce test env only). + concurrency: 1 + metadata: + name: Android end2end tests with Espresso + tags: + - e2e + - android +espresso: + app: ./sentry-android-integration-tests/sentry-uitest-android/build/outputs/apk/release/sentry-uitest-android-release.apk + testApp: ./sentry-android-integration-tests/sentry-uitest-android/build/outputs/apk/androidTest/release/sentry-uitest-android-release-androidTest.apk +suites: + name: "Android End2end" + emulators: + - name: "Android GoogleApi Emulator" + orientation: portrait + platformVersions: + - "11.0" + - "10.0" + testOptions: + useTestOrchestrator: true +# Controls what artifacts to fetch when the suite on Sauce Cloud has finished. +artifacts: + download: + when: always + match: + - junit.xml + directory: ./artifacts/ diff --git a/CHANGELOG.md b/CHANGELOG.md index bb8c5b2ffa7..6b49bbdf7ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,76 @@ ## Unreleased +### Sentry Self-hosted Compatibility + +- Starting with version `6.0.0` of the `sentry` package, [Sentry's self hosted version >= v21.9.0](https://github.com/getsentry/self-hosted/releases) is required or you have to manually disable sending client reports via the `sendClientReports` option. This only applies to self-hosted Sentry. If you are using [sentry.io](https://sentry.io), no action is needed. + +### Features + +- Allow optimization and obfuscation of the SDK by reducing proguard rules ([#2031](https://github.com/getsentry/sentry-java/pull/2031)) +- Relax TransactionNameProvider ([#1861](https://github.com/getsentry/sentry-java/pull/1861)) +- Use float instead of Date for protocol types for higher precision ([#1737](https://github.com/getsentry/sentry-java/pull/1737)) +- Allow setting SDK info (name & version) in manifest ([#2016](https://github.com/getsentry/sentry-java/pull/2016)) +- Allow setting native Android SDK name during build ([#2035](https://github.com/getsentry/sentry-java/pull/2035)) +- Include application permissions in Android events ([#2018](https://github.com/getsentry/sentry-java/pull/2018)) +- Automatically create transactions for UI events ([#1975](https://github.com/getsentry/sentry-java/pull/1975)) +- Hints are now used via a Hint object and passed into beforeSend and EventProcessor as @NotNull Hint object ([#2045](https://github.com/getsentry/sentry-java/pull/2045)) +- Attachments can be manipulated via hint ([#2046](https://github.com/getsentry/sentry-java/pull/2046)) +- Add sentry-servlet-jakarta module ([#1987](https://github.com/getsentry/sentry-java/pull/1987)) +- Add client reports ([#1982](https://github.com/getsentry/sentry-java/pull/1982)) +- Screenshot is taken when there is an error ([#1967](https://github.com/getsentry/sentry-java/pull/1967)) +- Add Android profiling traces ([#1897](https://github.com/getsentry/sentry-java/pull/1897)) ([#1959](https://github.com/getsentry/sentry-java/pull/1959)) and its tests ([#1949](https://github.com/getsentry/sentry-java/pull/1949)) +- Enable enableScopeSync by default for Android ([#1928](https://github.com/getsentry/sentry-java/pull/1928)) +- Feat: Vendor JSON ([#1554](https://github.com/getsentry/sentry-java/pull/1554)) + - Introduce `JsonSerializable` and `JsonDeserializer` interfaces for manual json + serialization/deserialization. + - Introduce `JsonUnknwon` interface to preserve unknown properties when deserializing/serializing + SDK classes. + - When passing custom objects, for example in `Contexts`, these are supported for serialization: + - `JsonSerializable` + - `Map`, `Collection`, `Array`, `String` and all primitive types. + - Objects with the help of refection. + - `Map`, `Collection`, `Array`, `String` and all primitive types. + - Call `toString()` on objects that have a cyclic reference to a ancestor object. + - Call `toString()` where object graphs exceed max depth. + - Remove `gson` dependency. + - Remove `IUnknownPropertiesConsumer` + +### Fixes + +- Calling Sentry.init and specifying contextTags now has an effect on the Logback SentryAppender ([#2052](https://github.com/getsentry/sentry-java/pull/2052)) +- Calling Sentry.init and specifying contextTags now has an effect on the Log4j SentryAppender ([#2054](https://github.com/getsentry/sentry-java/pull/2054)) +- Calling Sentry.init and specifying contextTags now has an effect on the jul SentryAppender ([#2057](https://github.com/getsentry/sentry-java/pull/2057)) +- Update Spring Boot dependency to 2.6.8 and fix the CVE-2022-22970 ([#2068](https://github.com/getsentry/sentry-java/pull/2068)) +- Sentry can now self heal after a Thread had its currentHub set to a NoOpHub ([#2076](https://github.com/getsentry/sentry-java/pull/2076)) +- No longer close OutputStream that is passed into JsonSerializer ([#2029](https://github.com/getsentry/sentry-java/pull/2029)) +- Fix setting context tags on events captured by Spring ([#2060](https://github.com/getsentry/sentry-java/pull/2060)) +- Isolate cached events with hashed DSN subfolder ([#2038](https://github.com/getsentry/sentry-java/pull/2038)) +- SentryThread.current flag will not be overridden by DefaultAndroidEventProcessor if already set ([#2050](https://github.com/getsentry/sentry-java/pull/2050)) +- Fix serialization of Long inside of Request.data ([#2051](https://github.com/getsentry/sentry-java/pull/2051)) +- Update sentry-native to 0.4.17 ([#2033](https://github.com/getsentry/sentry-java/pull/2033)) +- Update Gradle to 7.4.2 and AGP to 7.2 ([#2042](https://github.com/getsentry/sentry-java/pull/2042)) +- Change order of event filtering mechanisms ([#2001](https://github.com/getsentry/sentry-java/pull/2001)) +- Only send session update for dropped events if state changed ([#2002](https://github.com/getsentry/sentry-java/pull/2002)) +- Android profiling initializes on first profile start ([#2009](https://github.com/getsentry/sentry-java/pull/2009)) +- Profiling rate decreased from 300hz to 100hz ([#1997](https://github.com/getsentry/sentry-java/pull/1997)) +- Allow disabling sending of client reports via Android Manifest and external options ([#2007](https://github.com/getsentry/sentry-java/pull/2007)) +- Ref: Upgrade Spring Boot dependency to 2.5.13 ([#2011](https://github.com/getsentry/sentry-java/pull/2011)) +- Ref: Make options.printUncaughtStackTrace primitive type ([#1995](https://github.com/getsentry/sentry-java/pull/1995)) +- Ref: Remove not needed interface abstractions on Android ([#1953](https://github.com/getsentry/sentry-java/pull/1953)) +- Ref: Make hints Map instead of only Object ([#1929](https://github.com/getsentry/sentry-java/pull/1929)) +- Ref: Simplify DateUtils with ISO8601Utils ([#1837](https://github.com/getsentry/sentry-java/pull/1837)) +- Ref: Remove deprecated and scheduled fields ([#1875](https://github.com/getsentry/sentry-java/pull/1875)) +- Ref: Add shutdownTimeoutMillis in favor of shutdownTimeout ([#1873](https://github.com/getsentry/sentry-java/pull/1873)) +- Ref: Remove Attachment ContentType since the Server infers it ([#1874](https://github.com/getsentry/sentry-java/pull/1874)) +- Ref: Bind external properties to a dedicated class. ([#1750](https://github.com/getsentry/sentry-java/pull/1750)) +- Ref: Debug log serializable objects ([#1795](https://github.com/getsentry/sentry-java/pull/1795)) +- Ref: catch Throwable instead of Exception to suppress internal SDK errors ([#1812](https://github.com/getsentry/sentry-java/pull/1812)) +- `SentryOptions` can merge properties from `ExternalOptions` instead of another instance of `SentryOptions` +- Following boolean properties from `SentryOptions` that allowed `null` values are now not nullable - `debug`, `enableUncaughtExceptionHandler`, `enableDeduplication` +- `SentryOptions` cannot be created anymore using `PropertiesProvider` with `SentryOptions#from` method. Use `ExternalOptions#from` instead and merge created object with `SentryOptions#merge` +- Bump: Kotlin to 1.5 and compatibility to 1.4 for sentry-android-timber ([#1815](https://github.com/getsentry/sentry-java/pull/1815)) + ## 5.7.4 ### Fixes @@ -10,54 +80,72 @@ ## 5.7.3 -* Fix: Sentry Timber integration throws an exception when using args (#1986) +### Fixes + +- Sentry Timber integration throws an exception when using args ([#1986](https://github.com/getsentry/sentry-java/pull/1986)) ## 5.7.2 -* Fix: bring back support for `Timber.tag` ([#1974](https://github.com/getsentry/sentry-java/pull/1974)) +### Fixes + +- Bring back support for `Timber.tag` ([#1974](https://github.com/getsentry/sentry-java/pull/1974)) ## 5.7.1 -* Fix: Sentry Timber integration does not submit msg.formatted breadcrumbs (#1957) -* Fix: ANR WatchDog won't crash on SecurityException ([#1962](https://github.com/getsentry/sentry-java/pull/1962)) +### Fixes + +- Sentry Timber integration does not submit msg.formatted breadcrumbs ([#1957](https://github.com/getsentry/sentry-java/pull/1957)) +- ANR WatchDog won't crash on SecurityException ([#1962](https://github.com/getsentry/sentry-java/pull/1962)) ## 5.7.0 -* Feat: Automatically enable `Timber` and `Fragment` integrations if they are present on the classpath (#1936) +### Features + +- Automatically enable `Timber` and `Fragment` integrations if they are present on the classpath ([#1936](https://github.com/getsentry/sentry-java/pull/1936)) ## 5.6.3 -* Fix: If transaction or span is finished, do not allow to mutate (#1940) -* Fix: Keep used AndroidX classes from obfuscation (Fixes UI breadcrumbs and Slow/Frozen frames) (#1942) +### Fixes + +- If transaction or span is finished, do not allow to mutate ([#1940](https://github.com/getsentry/sentry-java/pull/1940)) +- Keep used AndroidX classes from obfuscation (Fixes UI breadcrumbs and Slow/Frozen frames) ([#1942](https://github.com/getsentry/sentry-java/pull/1942)) ## 5.6.2 -* Ref: Make ActivityFramesTracker public to be used by Hybrid SDKs (#1931) -* Bump: AGP to 7.1.2 (#1930) -* Fix: NPE while adding "response_body_size" breadcrumb, when response body length is unknown (#1908) -* Fix: Do not include stacktrace frames into Timber message (#1898) -* Fix: Potential memory leaks (#1909) +### Fixes + +- Ref: Make ActivityFramesTracker public to be used by Hybrid SDKs ([#1931](https://github.com/getsentry/sentry-java/pull/1931)) +- Bump: AGP to 7.1.2 ([#1930](https://github.com/getsentry/sentry-java/pull/1930)) +- NPE while adding "response_body_size" breadcrumb, when response body length is unknown ([#1908](https://github.com/getsentry/sentry-java/pull/1908)) +- Do not include stacktrace frames into Timber message ([#1898](https://github.com/getsentry/sentry-java/pull/1898)) +- Potential memory leaks ([#1909](https://github.com/getsentry/sentry-java/pull/1909)) Breaking changes: -`Timber.tag` is no longer supported by our [Timber integration](https://docs.sentry.io/platforms/android/configuration/integrations/timber/) and will not appear on Sentry for error events. +`Timber.tag` is no longer supported by our [Timber integration](https://docs.sentry.io/platforms/android/configuration/integrations/timber/) and will not appear on Sentry for error events. Please vote on this [issue](https://github.com/getsentry/sentry-java/issues/1900), if you'd like us to provide support for that. ## 5.6.2-beta.3 -* Ref: Make ActivityFramesTracker public to be used by Hybrid SDKs (#1931) -* Bump: AGP to 7.1.2 (#1930) +### Fixes + +- Ref: Make ActivityFramesTracker public to be used by Hybrid SDKs ([#1931](https://github.com/getsentry/sentry-java/pull/1931)) +- Bump: AGP to 7.1.2 ([#1930](https://github.com/getsentry/sentry-java/pull/1930)) ## 5.6.2-beta.2 -* Fix: NPE while adding "response_body_size" breadcrumb, when response body length is unknown (#1908) +### Fixes + +- NPE while adding "response_body_size" breadcrumb, when response body length is unknown ([#1908](https://github.com/getsentry/sentry-java/pull/1908)) ## 5.6.2-beta.1 -* Fix: Do not include stacktrace frames into Timber message (#1898) -* Fix: Potential memory leaks (#1909) +### Fixes + +- Do not include stacktrace frames into Timber message ([#1898](https://github.com/getsentry/sentry-java/pull/1898)) +- Potential memory leaks ([#1909](https://github.com/getsentry/sentry-java/pull/1909)) Breaking changes: -`Timber.tag` is no longer supported by our [Timber integration](https://docs.sentry.io/platforms/android/configuration/integrations/timber/) and will not appear on Sentry for error events. +`Timber.tag` is no longer supported by our [Timber integration](https://docs.sentry.io/platforms/android/configuration/integrations/timber/) and will not appear on Sentry for error events. Please vote on this [issue](https://github.com/getsentry/sentry-java/issues/1900), if you'd like us to provide support for that. ## 5.6.1 @@ -305,7 +393,7 @@ Breaking changes: ### Fixes -- Make SentryAppender non-final for Log4j2 and Logback ([#1603](https://github.com/getsentry/sentry-java/pull/1603)) +- Make SentryAppender non-final for Log4j2 and Logback ([#1603](https://github.com/getsentry/sentry-java/pull/1603)) - Do not throw IAE when tracing header contain invalid trace id ([#1605](https://github.com/getsentry/sentry-java/pull/1605)) ## 5.1.0-beta.4 @@ -324,7 +412,7 @@ Breaking changes: ### Features -- Support transaction waiting for children to finish. ([#1535](https://github.com/getsentry/sentry-java/pull/1535)) +- Support transaction waiting for children to finish. ([#1535](https://github.com/getsentry/sentry-java/pull/1535)) - Capture logged marker in log4j2 and logback appenders ([#1551](https://github.com/getsentry/sentry-java/pull/1551)) - Allow clearing of attachments in the scope ([#1562](https://github.com/getsentry/sentry-java/pull/1562)) - Set mechanism type in SentryExceptionResolver ([#1556](https://github.com/getsentry/sentry-java/pull/1556)) @@ -344,7 +432,7 @@ Breaking changes: ### Features - Measure app start time ([#1487](https://github.com/getsentry/sentry-java/pull/1487)) -- Automatic breadcrumbs logging for fragment lifecycle ([#1522](https://github.com/getsentry/sentry-java/pull/1522)) +- Automatic breadcrumbs logging for fragment lifecycle ([#1522](https://github.com/getsentry/sentry-java/pull/1522)) ## 5.0.1 @@ -625,7 +713,7 @@ This release brings the Sentry Performance feature to Java SDK, Spring, Spring B - Send user.ip_address = {{auto}} when sendDefaultPii is true ([#1015](https://github.com/getsentry/sentry-java/pull/1015)) - Read tracesSampleRate from AndroidManifest - OutboxSender supports all envelope item types ([#1158](https://github.com/getsentry/sentry-java/pull/1158)) -- Read `uncaught.handler.enabled` property from the external configuration +- Read `uncaught.handler.enabled` property from the external configuration - Resolve servername from the localhost address - Add maxAttachmentSize to SentryOptions ([#1138](https://github.com/getsentry/sentry-java/pull/1138)) - Drop invalid attachments ([#1134](https://github.com/getsentry/sentry-java/pull/1134)) @@ -639,16 +727,16 @@ This release brings the Sentry Performance feature to Java SDK, Spring, Spring B - Ref: Return NoOpTransaction instead of null ([#1126](https://github.com/getsentry/sentry-java/pull/1126)) - Ref: `ITransport` implementations are now responsible for executing request in asynchronous or synchronous way ([#1118](https://github.com/getsentry/sentry-java/pull/1118)) - Ref: Add option to set `TransportFactory` instead of `ITransport` on `SentryOptions` ([#1124](https://github.com/getsentry/sentry-java/pull/1124)) -- Ref: Simplify ITransport creation in ITransportFactory ([#1135](https://github.com/getsentry/sentry-java/pull/1135)) +- Ref: Simplify ITransport creation in ITransportFactory ([#1135](https://github.com/getsentry/sentry-java/pull/1135)) - Fixes and Tests: Session serialization and deserialization - Inheriting sampling decision from parent ([#1100](https://github.com/getsentry/sentry-java/pull/1100)) - Exception only sets a stack trace if there are frames - Initialize Logback after context refreshes ([#1129](https://github.com/getsentry/sentry-java/pull/1129)) - Do not crash when passing null values to @Nullable methods, eg User and Scope - Resolving dashed properties from external configuration -- Consider {{ auto }} as a default ip address ([#1015](https://github.com/getsentry/sentry-java/pull/1015)) +- Consider {{ auto }} as a default ip address ([#1015](https://github.com/getsentry/sentry-java/pull/1015)) - Set release and environment on Transactions ([#1152](https://github.com/getsentry/sentry-java/pull/1152)) -- Do not set transaction on the scope automatically +- Do not set transaction on the scope automatically ## 4.0.0-alpha.2 @@ -733,7 +821,7 @@ This release brings the Sentry Performance feature to Java SDK, Spring, Spring B ### Fixes - ref: Validate event id on user feedback submission - + ## 3.1.1 ### Features @@ -778,7 +866,7 @@ Considerable changes were done, which include a lot of improvements. More are co - Dropped support to `log4j`. - Improved `logback` integration - Capture breadcrumbs for level INFO and higher - - Raises event for ERROR and higher. + - Raises event for ERROR and higher. - Minimum levels are configurable. - Optionally initializes the SDK via appender.xml - Configurable via Spring integration if both are enabled @@ -790,7 +878,7 @@ Considerable changes were done, which include a lot of improvements. More are co ## What’s Changed -- Callback to validate SSL certificate ([#944](https://github.com/getsentry/sentry-java/pull/944)) +- Callback to validate SSL certificate ([#944](https://github.com/getsentry/sentry-java/pull/944)) - Attach stack traces enabled by default ### Android specific @@ -819,7 +907,7 @@ The previous Java releases, are all available in this repository through the tag ## What’s Changed -- feat: ssl support ([#944](https://github.com/getsentry/sentry-java/pull/944)) @ninekaw9 @marandaneto +- feat: ssl support ([#944](https://github.com/getsentry/sentry-java/pull/944)) @ninekaw9 @marandaneto - feat: sync Java to C ([#937](https://github.com/getsentry/sentry-java/pull/937)) @bruno-garcia @marandaneto - feat: Auto-configure Logback appender in Spring Boot integration. ([#938](https://github.com/getsentry/sentry-java/pull/938)) @maciejwalkowiak - feat: Add Servlet integration. ([#935](https://github.com/getsentry/sentry-java/pull/935)) @maciejwalkowiak @@ -1204,7 +1292,7 @@ Release of Sentry's new SDK for Android. ### Features -- Release health @marandaneto @bruno-garcia +- Release health @marandaneto @bruno-garcia - ANR report should have 'was active=yes' on the dashboard ([#299](https://github.com/getsentry/sentry-android/pull/299)) @marandaneto - NDK events apply scoped data ([#322](https://github.com/getsentry/sentry-android/pull/322)) @marandaneto - Add a StdoutTransport ([#310](https://github.com/getsentry/sentry-android/pull/310)) @mike-burns diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c8c4a1368eb..1dff57ad251 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,13 +1,13 @@ # Contributing to sentry-java -We love pull requests from everyone. +We love pull requests from everyone. We suggest opening an issue to discuss bigger changes before investing on a big PR. # Requirements -The project currently requires you run JDK version `1.8.x`. +The project currently requires you run JDK 11. -## Android +## Android This repository is a monorepo which includes Java and Android libraries. If you'd like to contribute to Java and don't have an Android SDK with NDK installed, diff --git a/build.gradle.kts b/build.gradle.kts index 7ac0a493635..278b2a94226 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -56,6 +56,8 @@ apiValidation { "sentry-samples-spring-boot", "sentry-samples-spring-boot-webflux", "sentry-samples-netflix-dgs", + "sentry-uitest-android", + "sentry-uitest-android-benchmark", ) ) } @@ -86,7 +88,7 @@ allprojects { } subprojects { - if (!this.name.contains("sample") && this.name != "sentry-test-support") { + if (!this.name.contains("sample") && !this.name.contains("integration-tests") && this.name != "sentry-test-support") { apply() val sep = File.separator @@ -137,7 +139,7 @@ spotless { target("**/*.java") removeUnusedImports() googleJavaFormat() - targetExclude("**/generated/**") + targetExclude("**/generated/**", "**/vendor/**") } kotlin { @@ -165,7 +167,7 @@ gradle.projectsEvaluated { "https://docs.spring.io/spring-boot/docs/current/api/" ) subprojects - .filter { !it.name.contains("sample") } + .filter { !it.name.contains("sample") && !it.name.contains("integration-tests") } .forEach { proj -> proj.tasks.withType().forEach { javadocTask -> source += javadocTask.source diff --git a/buildSrc/src/main/java/Config.kt b/buildSrc/src/main/java/Config.kt index 9877043284f..ddb6b2e9e11 100644 --- a/buildSrc/src/main/java/Config.kt +++ b/buildSrc/src/main/java/Config.kt @@ -1,15 +1,14 @@ import java.math.BigDecimal object Config { - val kotlinVersion = "1.4.32" + val kotlinVersion = "1.5.31" val kotlinStdLib = "stdlib-jdk8" - val springBootVersion = "2.4.4" - // Spring is currently not compatible with Kotlin 1.4 - val springKotlinCompatibleLanguageVersion = "1.3" + val springBootVersion = "2.6.8" + val kotlinCompatibleLanguageVersion = "1.4" object BuildPlugins { - val androidGradle = "com.android.tools.build:gradle:7.1.2" + val androidGradle = "com.android.tools.build:gradle:7.2.0" val kotlinGradlePlugin = "gradle-plugin" val buildConfig = "com.github.gmazzo.buildconfig" val buildConfigVersion = "3.0.3" @@ -40,15 +39,12 @@ object Config { object Libs { val okHttpVersion = "4.9.2" - val appCompat = "androidx.appcompat:appcompat:1.2.0" + val appCompat = "androidx.appcompat:appcompat:1.3.0" val timber = "com.jakewharton.timber:timber:4.7.1" val okhttpBom = "com.squareup.okhttp3:okhttp-bom:$okHttpVersion" val okhttp = "com.squareup.okhttp3:okhttp" - // only bump gson if https://github.com/google/gson/issues/1597 is fixed - private val gsonVersion = "2.8.5" - val gsonDep = "com.google.code.gson:gson" - val gson = "$gsonDep:$gsonVersion" val leakCanary = "com.squareup.leakcanary:leakcanary-android:2.8.1" + val constraintLayout = "androidx.constraintlayout:constraintlayout:2.1.3" private val lifecycleVersion = "2.2.0" val lifecycleProcess = "androidx.lifecycle:lifecycle-process:$lifecycleVersion" @@ -57,6 +53,7 @@ object Config { val androidxRecylerView = "androidx.recyclerview:recyclerview:1.2.1" val slf4jApi = "org.slf4j:slf4j-api:1.7.30" + val slf4jJdk14 = "org.slf4j:slf4j-jdk14:1.7.30" val logbackVersion = "1.2.9" val logbackClassic = "ch.qos.logback:logback-classic:$logbackVersion" @@ -79,6 +76,7 @@ object Config { val springAop = "org.springframework:spring-aop" val aspectj = "org.aspectj:aspectjweaver" val servletApi = "javax.servlet:javax.servlet-api:3.1.0" + val servletApiJakarta = "jakarta.servlet:jakarta.servlet-api:5.0.0" val apacheHttpClient = "org.apache.httpcomponents.client5:httpclient5:5.0.4" @@ -114,12 +112,19 @@ object Config { } object TestLibs { - private val androidxTestVersion = "1.4.0-rc01" + private val androidxTestVersion = "1.4.0" + private val espressoVersion = "3.4.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 androidxJunit = "androidx.test.ext:junit:1.1.3-rc01" + val androidxTestCoreKtx = "androidx.test:core-ktx:$androidxTestVersion" + val androidxTestRules = "androidx.test:rules:$androidxTestVersion" + val espressoCore = "androidx.test.espresso:espresso-core:$espressoVersion" + val espressoIdlingResource = "androidx.test.espresso:espresso-idling-resource:$espressoVersion" + val androidxTestOrchestrator = "androidx.test:orchestrator:1.4.1" + val androidxJunit = "androidx.test.ext:junit:1.1.3" val androidxCoreKtx = "androidx.core:core-ktx:1.7.0" val robolectric = "org.robolectric:robolectric:4.7.3" val mockitoKotlin = "com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0" diff --git a/gradle.properties b/gradle.properties index c4df3d139ac..c83777dbcff 100644 --- a/gradle.properties +++ b/gradle.properties @@ -7,7 +7,10 @@ org.gradle.parallel=true android.useAndroidX=true # Release information -versionName=5.7.5-SNAPSHOT +versionName=6.0.0-rc.1 + +# Override the SDK name on native crashes on Android +sentryAndroidSdkName=sentry.native.android # disable renderscript, it's enabled by default android.defaults.buildfeatures.renderscript=false diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 7454180f2ae..41d9927a4d4 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 41dfb87909a..aa991fceae6 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/scripts/post-release.sh b/scripts/post-release.sh deleted file mode 100755 index 9b02308edb3..00000000000 --- a/scripts/post-release.sh +++ /dev/null @@ -1,33 +0,0 @@ -#!/bin/bash - -# ./scripts/post-release.sh -# eg ./scripts/post-release.sh "6.0.0-alpha.1" "6.0.0-alpha.2" - -set -euo pipefail - -git checkout main -GRADLE_FILEPATH="gradle.properties" - -# Add a new unreleased entry in the changelog -perl -pi -e 's/# Changelog/# Changelog\n\n## Unreleased/' CHANGELOG.md - -# Increment `versionName` and make it a snapshot -# Incrementing the version name before the release (`bump-version.sh`) sets a -# fixed version until the next release it's made. For testing purposes, it's -# interesting to have a different version name that doesn't match the -# name of the version in production. -# Note that the version must end with a number: `1.2.3-alpha` is a semantic -# version but not compatible with this post-release script. -# and `1.2.3-alpha.0` should be used instead. -VERSION_NAME_PATTERN="versionName" -version="$( awk "/$VERSION_NAME_PATTERN/" $GRADLE_FILEPATH | egrep -o '[0-9].*$' )" # from the first digit until the end -version_digit_to_bump="$( awk "/$VERSION_NAME_PATTERN/" $GRADLE_FILEPATH | egrep -o '[0-9]+$')" -((version_digit_to_bump++)) -# Using `*` instead of `+` for compatibility. The result is the same, -# since the version to be bumped is extracted using `+`. -new_version="$( echo $version | sed "s/[0-9]*$/$version_digit_to_bump/g" )" -perl -pi -e "s/$VERSION_NAME_PATTERN=.*$/$VERSION_NAME_PATTERN=$new_version-SNAPSHOT/g" $GRADLE_FILEPATH - -git add CHANGELOG.md $GRADLE_FILEPATH -git commit -m "Prepare $new_version" -git push diff --git a/sentry-android-core/api/sentry-android-core.api b/sentry-android-core/api/sentry-android-core.api index 7a12eff25c3..f326074de5c 100644 --- a/sentry-android-core/api/sentry-android-core.api +++ b/sentry-android-core/api/sentry-android-core.api @@ -8,7 +8,7 @@ public final class io/sentry/android/core/ActivityFramesTracker { } public final class io/sentry/android/core/ActivityLifecycleIntegration : android/app/Application$ActivityLifecycleCallbacks, io/sentry/Integration, java/io/Closeable { - public fun (Landroid/app/Application;Lio/sentry/android/core/IBuildInfoProvider;Lio/sentry/android/core/ActivityFramesTracker;)V + public fun (Landroid/app/Application;Lio/sentry/android/core/BuildInfoProvider;Lio/sentry/android/core/ActivityFramesTracker;)V public fun close ()V public fun onActivityCreated (Landroid/app/Activity;Landroid/os/Bundle;)V public fun onActivityDestroyed (Landroid/app/Activity;)V @@ -58,10 +58,14 @@ public final class io/sentry/android/core/BuildConfig { public fun ()V } -public final class io/sentry/android/core/BuildInfoProvider : io/sentry/android/core/IBuildInfoProvider { - public fun ()V +public final class io/sentry/android/core/BuildInfoProvider { + public fun (Lio/sentry/ILogger;)V public fun getBuildTags ()Ljava/lang/String; + public fun getManufacturer ()Ljava/lang/String; + public fun getModel ()Ljava/lang/String; public fun getSdkInfoVersion ()I + public fun getVersionRelease ()Ljava/lang/String; + public fun isEmulator ()Ljava/lang/Boolean; } public abstract class io/sentry/android/core/EnvelopeFileObserverIntegration : io/sentry/Integration, java/io/Closeable { @@ -71,11 +75,6 @@ public abstract class io/sentry/android/core/EnvelopeFileObserverIntegration : i public final fun register (Lio/sentry/IHub;Lio/sentry/SentryOptions;)V } -public abstract interface class io/sentry/android/core/IBuildInfoProvider { - public abstract fun getBuildTags ()Ljava/lang/String; - public abstract fun getSdkInfoVersion ()I -} - public abstract interface class io/sentry/android/core/IDebugImagesLoader { public abstract fun clearDebugImages ()V public abstract fun loadDebugImages ()Ljava/util/List; @@ -101,6 +100,19 @@ public final class io/sentry/android/core/PhoneStateBreadcrumbsIntegration : io/ public fun register (Lio/sentry/IHub;Lio/sentry/SentryOptions;)V } +public final class io/sentry/android/core/ScreenshotEventProcessor : android/app/Application$ActivityLifecycleCallbacks, io/sentry/EventProcessor, java/io/Closeable { + public fun (Landroid/app/Application;Lio/sentry/android/core/SentryAndroidOptions;Lio/sentry/android/core/BuildInfoProvider;)V + public fun close ()V + public fun onActivityCreated (Landroid/app/Activity;Landroid/os/Bundle;)V + public fun onActivityDestroyed (Landroid/app/Activity;)V + public fun onActivityPaused (Landroid/app/Activity;)V + public fun onActivityResumed (Landroid/app/Activity;)V + public fun onActivitySaveInstanceState (Landroid/app/Activity;Landroid/os/Bundle;)V + public fun onActivityStarted (Landroid/app/Activity;)V + public fun onActivityStopped (Landroid/app/Activity;)V + public fun process (Lio/sentry/SentryEvent;Lio/sentry/Hint;)Lio/sentry/SentryEvent; +} + public final class io/sentry/android/core/SentryAndroid { public static fun init (Landroid/content/Context;)V public static fun init (Landroid/content/Context;Lio/sentry/ILogger;)V @@ -113,8 +125,10 @@ public final class io/sentry/android/core/SentryAndroidOptions : io/sentry/Sentr public fun enableAllAutoBreadcrumbs (Z)V public fun getAnrTimeoutIntervalMillis ()J public fun getDebugImagesLoader ()Lio/sentry/android/core/IDebugImagesLoader; + public fun getProfilingTracesIntervalMillis ()I public fun isAnrEnabled ()Z public fun isAnrReportInDebug ()Z + public fun isAttachScreenshot ()Z public fun isEnableActivityLifecycleBreadcrumbs ()Z public fun isEnableActivityLifecycleTracingAutoFinish ()Z public fun isEnableAppComponentBreadcrumbs ()Z @@ -122,9 +136,11 @@ public final class io/sentry/android/core/SentryAndroidOptions : io/sentry/Sentr public fun isEnableAutoActivityLifecycleTracing ()Z public fun isEnableSystemEventBreadcrumbs ()Z public fun isEnableUserInteractionBreadcrumbs ()Z + public fun isEnableUserInteractionTracing ()Z public fun setAnrEnabled (Z)V public fun setAnrReportInDebug (Z)V public fun setAnrTimeoutIntervalMillis (J)V + public fun setAttachScreenshot (Z)V public fun setDebugImagesLoader (Lio/sentry/android/core/IDebugImagesLoader;)V public fun setEnableActivityLifecycleBreadcrumbs (Z)V public fun setEnableActivityLifecycleTracingAutoFinish (Z)V @@ -133,6 +149,8 @@ public final class io/sentry/android/core/SentryAndroidOptions : io/sentry/Sentr public fun setEnableAutoActivityLifecycleTracing (Z)V public fun setEnableSystemEventBreadcrumbs (Z)V public fun setEnableUserInteractionBreadcrumbs (Z)V + public fun setEnableUserInteractionTracing (Z)V + public fun setProfilingTracesIntervalMillis (I)V } public final class io/sentry/android/core/SentryInitProvider : android/content/ContentProvider { diff --git a/sentry-android-core/build.gradle.kts b/sentry-android-core/build.gradle.kts index 7937655aad1..73243b80fa9 100644 --- a/sentry-android-core/build.gradle.kts +++ b/sentry-android-core/build.gradle.kts @@ -16,7 +16,7 @@ android { targetSdk = Config.Android.targetSdkVersion minSdk = Config.Android.minSdkVersion - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + testInstrumentationRunner = Config.TestLibs.androidJUnitRunner buildConfigField("String", "SENTRY_ANDROID_SDK_NAME", "\"${Config.Sentry.SENTRY_ANDROID_SDK_NAME}\"") diff --git a/sentry-android-core/proguard-rules.pro b/sentry-android-core/proguard-rules.pro index edda62ecb34..f092eca7e0f 100644 --- a/sentry-android-core/proguard-rules.pro +++ b/sentry-android-core/proguard-rules.pro @@ -1,47 +1,25 @@ +##---------------Begin: proguard configuration for android-core ---------- + ##---------------Begin: proguard configuration for androidx.core ---------- -keep class androidx.core.view.GestureDetectorCompat { (...); } -keep class androidx.core.app.FrameMetricsAggregator { (...); } -keep interface androidx.core.view.ScrollingView { *; } ##---------------End: proguard configuration for androidx.core ---------- -##---------------Begin: proguard configuration for Gson ---------- -# Gson uses generic type information stored in a class file when working with fields. Proguard -# removes such information by default, so configure it to keep all of it. --keepattributes Signature - -# For using GSON @Expose annotation --keepattributes *Annotation* - -# Gson specific classes --dontwarn sun.misc.** --keep class com.google.gson.** { *; } - -# Application classes that will be serialized/deserialized over Gson --keep class io.sentry.** { *; } --keepclassmembers enum io.sentry.** { *; } --keep class io.sentry.android.core.** { *; } - -# Prevent proguard from stripping interface information from TypeAdapter, TypeAdapterFactory, -# JsonSerializer, JsonDeserializer instances (so they can be used in @JsonAdapter) -#-keep class * implements com.google.gson.TypeAdapter --keep class * extends com.google.gson.TypeAdapter --keep class * implements com.google.gson.TypeAdapterFactory --keep class * implements com.google.gson.JsonSerializer --keep class * implements com.google.gson.JsonDeserializer - -# Prevent R8 from leaving Data object members always null --keepclassmembers,allowobfuscation class * { - @com.google.gson.annotations.SerializedName ; -} +##---------------Begin: proguard configuration for androidx.lifecycle ---------- +-keep interface androidx.lifecycle.DefaultLifecycleObserver { *; } +-keep class androidx.lifecycle.ProcessLifecycleOwner { (...); } +##---------------End: proguard configuration for androidx.lifecycle ---------- # don't warn jetbrains annotations -dontwarn org.jetbrains.annotations.** - -# R8: Attribute Signature requires InnerClasses attribute. Check -keepattributes directive. --keepattributes InnerClasses +# don't warn about missing classes (mainly for Guardsquare's proguard). +# We are checking for their presence at runtime +-dontwarn io.sentry.android.timber.SentryTimberIntegration +-dontwarn io.sentry.android.fragment.FragmentLifecycleIntegration # To ensure that stack traces is unambiguous # https://developer.android.com/studio/build/shrink-code#decode-stack-trace -keepattributes LineNumberTable,SourceFile -##---------------End: proguard configuration for Gson ---------- +##---------------End: proguard configuration for android-core ---------- diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ANRWatchDog.java b/sentry-android-core/src/main/java/io/sentry/android/core/ANRWatchDog.java index c32ff921092..16211c1252f 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/ANRWatchDog.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/ANRWatchDog.java @@ -21,7 +21,7 @@ final class ANRWatchDog extends Thread { private final boolean reportInDebug; private final ANRListener anrListener; - private final IHandler uiHandler; + private final MainLooperHandler uiHandler; private final long timeoutIntervalMillis; private final @NotNull ILogger logger; private final AtomicLong tick = new AtomicLong(0); @@ -51,7 +51,7 @@ final class ANRWatchDog extends Thread { boolean reportInDebug, @NotNull ANRListener listener, @NotNull ILogger logger, - @NotNull IHandler uiHandler, + @NotNull MainLooperHandler uiHandler, final @NotNull Context context) { super(); this.reportInDebug = reportInDebug; 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 50c206419bb..86b0c089ed8 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 @@ -1,6 +1,7 @@ package io.sentry.android.core; import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND; +import static io.sentry.TypeCheckHint.ANDROID_ACTIVITY; import android.app.Activity; import android.app.ActivityManager; @@ -10,6 +11,7 @@ import android.os.Bundle; import android.os.Process; import io.sentry.Breadcrumb; +import io.sentry.Hint; import io.sentry.IHub; import io.sentry.ISpan; import io.sentry.ITransaction; @@ -60,7 +62,7 @@ public final class ActivityLifecycleIntegration public ActivityLifecycleIntegration( final @NotNull Application application, - final @NotNull IBuildInfoProvider buildInfoProvider, + final @NotNull BuildInfoProvider buildInfoProvider, final @NotNull ActivityFramesTracker activityFramesTracker) { this.application = Objects.requireNonNull(application, "Application is required"); Objects.requireNonNull(buildInfoProvider, "BuildInfoProvider is required"); @@ -123,7 +125,11 @@ private void addBreadcrumb(final @NotNull Activity activity, final @NotNull Stri breadcrumb.setData("screen", getActivityName(activity)); breadcrumb.setCategory("ui.lifecycle"); breadcrumb.setLevel(SentryLevel.INFO); - hub.addBreadcrumb(breadcrumb); + + final Hint hint = new Hint(); + hint.set(ANDROID_ACTIVITY, activity); + + hub.addBreadcrumb(breadcrumb, hint); } } @@ -210,6 +216,16 @@ void applyScope(final @NotNull Scope scope, final @NotNull ITransaction transact }); } + @VisibleForTesting + void clearScope(final @NotNull Scope scope, final @NotNull ITransaction transaction) { + scope.withTransaction( + scopeTransaction -> { + if (scopeTransaction == transaction) { + scope.clearTransaction(); + } + }); + } + private boolean isRunningTransaction(final @NotNull Activity activity) { return activitiesWithOngoingTransactions.containsKey(activity); } @@ -235,6 +251,14 @@ private void finishTransaction(final @Nullable ITransaction transaction) { status = SpanStatus.OK; } transaction.finish(status); + if (hub != null) { + // make sure to remove the transaction from scope, as it may contain running children, + // therefore `finish` method will not remove it from scope + hub.configureScope( + scope -> { + clearScope(scope, transaction); + }); + } } } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java index 8a032095a24..de292ed6140 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java @@ -12,7 +12,6 @@ import io.sentry.SendFireAndForgetEnvelopeSender; import io.sentry.SendFireAndForgetOutboxSender; import io.sentry.SentryLevel; -import io.sentry.SentryOptions; import io.sentry.android.fragment.FragmentLifecycleIntegration; import io.sentry.android.timber.SentryTimberIntegration; import io.sentry.util.Objects; @@ -27,7 +26,7 @@ /** * Android Options initializer, it reads configurations from AndroidManifest and sets to the - * SentryOptions. It also adds default values for some fields. + * SentryAndroidOptions. It also adds default values for some fields. */ @SuppressWarnings("Convert2MethodRef") // older AGP versions do not support method references final class AndroidOptionsInitializer { @@ -38,7 +37,7 @@ private AndroidOptionsInitializer() {} /** * Init method of the Android Options initializer * - * @param options the SentryOptions + * @param options the SentryAndroidOptions * @param context the Application context */ static void init(final @NotNull SentryAndroidOptions options, final @NotNull Context context) { @@ -51,7 +50,7 @@ static void init(final @NotNull SentryAndroidOptions options, final @NotNull Con /** * Init method of the Android Options initializer * - * @param options the SentryOptions + * @param options the SentryAndroidOptions * @param context the Application context * @param logger the ILogger interface * @param isFragmentAvailable whether the Fragment integration is available on the classpath @@ -63,16 +62,22 @@ static void init( final @NotNull ILogger logger, final boolean isFragmentAvailable, final boolean isTimberAvailable) { - init(options, context, logger, new BuildInfoProvider(), isFragmentAvailable, isTimberAvailable); + init( + options, + context, + logger, + new BuildInfoProvider(logger), + isFragmentAvailable, + isTimberAvailable); } /** * Init method of the Android Options initializer * - * @param options the SentryOptions + * @param options the SentryAndroidOptions * @param context the Application context * @param logger the ILogger interface - * @param buildInfoProvider the IBuildInfoProvider interface + * @param buildInfoProvider the BuildInfoProvider interface * @param isFragmentAvailable whether the Fragment integration is available on the classpath * @param isTimberAvailable whether the Timber integration is available on the classpath */ @@ -80,7 +85,7 @@ static void init( final @NotNull SentryAndroidOptions options, @NotNull Context context, final @NotNull ILogger logger, - final @NotNull IBuildInfoProvider buildInfoProvider, + final @NotNull BuildInfoProvider buildInfoProvider, final boolean isFragmentAvailable, final boolean isTimberAvailable) { init( @@ -96,10 +101,10 @@ static void init( /** * Init method of the Android Options initializer * - * @param options the SentryOptions + * @param options the SentryAndroidOptions * @param context the Application context * @param logger the ILogger interface - * @param buildInfoProvider the IBuildInfoProvider interface + * @param buildInfoProvider the BuildInfoProvider interface * @param loadClass the LoadClass wrapper * @param isFragmentAvailable whether the Fragment integration is available on the classpath * @param isTimberAvailable whether the Timber integration is available on the classpath @@ -108,7 +113,7 @@ static void init( final @NotNull SentryAndroidOptions options, @NotNull Context context, final @NotNull ILogger logger, - final @NotNull IBuildInfoProvider buildInfoProvider, + final @NotNull BuildInfoProvider buildInfoProvider, final @NotNull LoadClass loadClass, final boolean isFragmentAvailable, final boolean isTimberAvailable) { @@ -145,12 +150,14 @@ static void init( options.addEventProcessor(new PerformanceAndroidEventProcessor(options, activityFramesTracker)); options.setTransportGate(new AndroidTransportGate(context, options.getLogger())); + options.setTransactionProfiler( + new AndroidTransactionProfiler(context, options, buildInfoProvider)); } private static void installDefaultIntegrations( final @NotNull Context context, - final @NotNull SentryOptions options, - final @NotNull IBuildInfoProvider buildInfoProvider, + final @NotNull SentryAndroidOptions options, + final @NotNull BuildInfoProvider buildInfoProvider, final @NotNull LoadClass loadClass, final @NotNull ActivityFramesTracker activityFramesTracker, final boolean isFragmentAvailable, @@ -191,6 +198,8 @@ private static void installDefaultIntegrations( if (isFragmentAvailable) { options.addIntegration(new FragmentLifecycleIntegration((Application) context, true, true)); } + options.addEventProcessor( + new ScreenshotEventProcessor((Application) context, options, buildInfoProvider)); } else { options .getLogger() @@ -210,7 +219,7 @@ private static void installDefaultIntegrations( /** * Reads and sets default option values that are Android specific like release and inApp * - * @param options the SentryOptions + * @param options the SentryAndroidOptions * @param context the Android context methods */ private static void readDefaultOptionValues( @@ -285,15 +294,15 @@ private static void readDefaultOptionValues( * Sets the cache dirs like sentry, outbox and sessions * * @param context the Application context - * @param options the SentryOptions + * @param options the SentryAndroidOptions */ private static void initializeCacheDirs( - final @NotNull Context context, final @NotNull SentryOptions options) { + final @NotNull Context context, final @NotNull SentryAndroidOptions options) { final File cacheDir = new File(context.getCacheDir(), "sentry"); options.setCacheDirPath(cacheDir.getAbsolutePath()); } - private static boolean isNdkAvailable(final @NotNull IBuildInfoProvider buildInfoProvider) { + private static boolean isNdkAvailable(final @NotNull BuildInfoProvider buildInfoProvider) { return buildInfoProvider.getSdkInfoVersion() >= Build.VERSION_CODES.JELLY_BEAN; } } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidTransactionProfiler.java b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidTransactionProfiler.java new file mode 100644 index 00000000000..ca5fe6da4be --- /dev/null +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidTransactionProfiler.java @@ -0,0 +1,275 @@ +package io.sentry.android.core; + +import static android.content.Context.ACTIVITY_SERVICE; +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +import android.annotation.SuppressLint; +import android.app.ActivityManager; +import android.content.Context; +import android.content.pm.PackageInfo; +import android.os.Build; +import android.os.Debug; +import android.os.SystemClock; +import io.sentry.ITransaction; +import io.sentry.ITransactionProfiler; +import io.sentry.ProfilingTraceData; +import io.sentry.SentryLevel; +import io.sentry.android.core.internal.util.CpuInfoUtils; +import io.sentry.util.Objects; +import java.io.File; +import java.util.UUID; +import java.util.concurrent.Future; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.TestOnly; + +final class AndroidTransactionProfiler implements ITransactionProfiler { + + /** + * This appears to correspond to the buffer size of the data part of the file, excluding the key + * part. Once the buffer is full, new records are ignored, but the resulting trace file will be + * valid. + * + *

30 second traces can require a buffer of a few MB. 8MB is the default buffer size for + * [Debug.startMethodTracingSampling], but 3 should be enough for most cases. We can adjust this + * in the future if we notice that traces are being truncated in some applications. + */ + private static final int BUFFER_SIZE_BYTES = 3_000_000; + + private static final int PROFILING_TIMEOUT_MILLIS = 30_000; + + private int intervalUs; + private @Nullable File traceFile = null; + private @Nullable File traceFilesDir = null; + private @Nullable Future scheduledFinish = null; + private volatile @Nullable ITransaction activeTransaction = null; + private volatile @Nullable ProfilingTraceData timedOutProfilingData = null; + private final @NotNull Context context; + private final @NotNull SentryAndroidOptions options; + private final @NotNull BuildInfoProvider buildInfoProvider; + private final @Nullable PackageInfo packageInfo; + private long transactionStartNanos = 0; + private boolean isInitialized = false; + + public AndroidTransactionProfiler( + final @NotNull Context context, + final @NotNull SentryAndroidOptions sentryAndroidOptions, + final @NotNull BuildInfoProvider buildInfoProvider) { + this.context = Objects.requireNonNull(context, "The application context is required"); + this.options = Objects.requireNonNull(sentryAndroidOptions, "SentryAndroidOptions is required"); + this.buildInfoProvider = + Objects.requireNonNull(buildInfoProvider, "The BuildInfoProvider is required."); + this.packageInfo = ContextUtils.getPackageInfo(context, options.getLogger()); + } + + private void init() { + // We initialize it only once + if (isInitialized) { + return; + } + isInitialized = true; + final String tracesFilesDirPath = options.getProfilingTracesDirPath(); + if (!options.isProfilingEnabled()) { + options.getLogger().log(SentryLevel.INFO, "Profiling is disabled in options."); + return; + } + if (tracesFilesDirPath == null) { + options + .getLogger() + .log( + SentryLevel.WARNING, + "Disabling profiling because no profiling traces dir path is defined in options."); + return; + } + long intervalMillis = options.getProfilingTracesIntervalMillis(); + if (intervalMillis <= 0) { + options + .getLogger() + .log( + SentryLevel.WARNING, + "Disabling profiling because trace interval is set to %d milliseconds", + intervalMillis); + return; + } + intervalUs = (int) MILLISECONDS.toMicros(intervalMillis); + traceFilesDir = new File(tracesFilesDirPath); + } + + @SuppressLint("NewApi") + @Override + public synchronized void onTransactionStart(@NotNull ITransaction transaction) { + + // Debug.startMethodTracingSampling() is only available since Lollipop + if (buildInfoProvider.getSdkInfoVersion() < Build.VERSION_CODES.LOLLIPOP) return; + + // Let's initialize trace folder and profiling interval + init(); + + // traceFilesDir is null or intervalUs is 0 only if there was a problem in the init, but + // we already logged that + if (traceFilesDir == null || intervalUs == 0 || !traceFilesDir.exists()) { + return; + } + + // If a transaction is currently being profiled, we ignore this call + if (activeTransaction != null) { + options + .getLogger() + .log( + SentryLevel.WARNING, + "Profiling is already active and was started by transaction %s", + activeTransaction.getSpanContext().getTraceId().toString()); + return; + } + + traceFile = new File(traceFilesDir, UUID.randomUUID() + ".trace"); + + if (traceFile.exists()) { + options + .getLogger() + .log(SentryLevel.DEBUG, "Trace file already exists: %s", traceFile.getPath()); + return; + } + activeTransaction = transaction; + + // We stop the trace after 30 seconds, since such a long trace is very probably a trace + // that will never end due to an error + scheduledFinish = + options + .getExecutorService() + .schedule( + () -> timedOutProfilingData = onTransactionFinish(transaction), + PROFILING_TIMEOUT_MILLIS); + + transactionStartNanos = SystemClock.elapsedRealtimeNanos(); + Debug.startMethodTracingSampling(traceFile.getPath(), BUFFER_SIZE_BYTES, intervalUs); + } + + @SuppressLint("NewApi") + @Override + public synchronized @Nullable ProfilingTraceData onTransactionFinish( + @NotNull ITransaction transaction) { + + // onTransactionStart() is only available since Lollipop + // and SystemClock.elapsedRealtimeNanos() since Jelly Bean + if (buildInfoProvider.getSdkInfoVersion() < Build.VERSION_CODES.LOLLIPOP) return null; + + final ITransaction finalActiveTransaction = activeTransaction; + final ProfilingTraceData profilingData = timedOutProfilingData; + + // Profiling finished, but we check if we cached last profiling data due to a timeout + if (finalActiveTransaction == null) { + // If the cached timed out profiling data refers to the transaction that started it we return + // it back, otherwise we would simply lose it + if (profilingData != null) { + // The timed out transaction is finishing + if (profilingData + .getTraceId() + .equals(transaction.getSpanContext().getTraceId().toString())) { + timedOutProfilingData = null; + return profilingData; + } else { + // Another transaction is finishing before the timed out one + options + .getLogger() + .log( + SentryLevel.ERROR, + "Profiling data with id %s exists but doesn't match the closing transaction %s", + profilingData.getTraceId(), + transaction.getSpanContext().getTraceId().toString()); + return null; + } + } + // A transaction is finishing, but profiling didn't start. Maybe it was started by another one + options + .getLogger() + .log( + SentryLevel.INFO, + "Transaction %s finished, but profiling never started for it. Skipping", + transaction.getSpanContext().getTraceId().toString()); + return null; + } + + if (finalActiveTransaction != transaction) { + options + .getLogger() + .log( + SentryLevel.DEBUG, + "Transaction %s finished, but profiling was started by transaction %s. Skipping", + transaction.getSpanContext().getTraceId().toString(), + finalActiveTransaction.getSpanContext().getTraceId().toString()); + return null; + } + + Debug.stopMethodTracing(); + long transactionDurationNanos = SystemClock.elapsedRealtimeNanos() - transactionStartNanos; + + activeTransaction = null; + + if (scheduledFinish != null) { + scheduledFinish.cancel(true); + scheduledFinish = null; + } + + if (traceFile == null) { + options.getLogger().log(SentryLevel.ERROR, "Trace file does not exists"); + return null; + } + + String versionName = ""; + String versionCode = ""; + String totalMem = "0"; + ActivityManager.MemoryInfo memInfo = getMemInfo(); + if (packageInfo != null) { + versionName = ContextUtils.getVersionName(packageInfo); + versionCode = ContextUtils.getVersionCode(packageInfo); + } + if (memInfo != null) { + totalMem = Long.toString(memInfo.totalMem); + } + + // cpu max frequencies are read with a lambda because reading files is involved, so it will be + // done in the background when the trace file is read + return new ProfilingTraceData( + traceFile, + transaction, + Long.toString(transactionDurationNanos), + buildInfoProvider.getSdkInfoVersion(), + () -> CpuInfoUtils.getInstance().readMaxFrequencies(), + buildInfoProvider.getManufacturer(), + buildInfoProvider.getModel(), + buildInfoProvider.getVersionRelease(), + buildInfoProvider.isEmulator(), + totalMem, + options.getProguardUuid(), + versionName, + versionCode, + options.getEnvironment()); + } + + /** + * Get MemoryInfo object representing the memory state of the application. + * + * @return MemoryInfo object representing the memory state of the application + */ + private @Nullable ActivityManager.MemoryInfo getMemInfo() { + try { + ActivityManager actManager = (ActivityManager) context.getSystemService(ACTIVITY_SERVICE); + ActivityManager.MemoryInfo memInfo = new ActivityManager.MemoryInfo(); + if (actManager != null) { + actManager.getMemoryInfo(memInfo); + return memInfo; + } + options.getLogger().log(SentryLevel.INFO, "Error getting MemoryInfo."); + return null; + } catch (Throwable e) { + options.getLogger().log(SentryLevel.ERROR, "Error getting MemoryInfo.", e); + return null; + } + } + + @TestOnly + void setTimedOutProfilingData(@Nullable ProfilingTraceData data) { + this.timedOutProfilingData = data; + } +} 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 6e6538b1eb0..088bf1505a2 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 @@ -1,9 +1,12 @@ package io.sentry.android.core; +import static io.sentry.TypeCheckHint.ANDROID_CONFIGURATION; + import android.content.ComponentCallbacks2; import android.content.Context; import android.content.res.Configuration; import io.sentry.Breadcrumb; +import io.sentry.Hint; import io.sentry.IHub; import io.sentry.Integration; import io.sentry.SentryLevel; @@ -95,7 +98,11 @@ public void onConfigurationChanged(@NotNull Configuration newConfig) { breadcrumb.setCategory("device.orientation"); breadcrumb.setData("position", orientation); breadcrumb.setLevel(SentryLevel.INFO); - hub.addBreadcrumb(breadcrumb); + + final Hint hint = new Hint(); + hint.set(ANDROID_CONFIGURATION, newConfig); + + hub.addBreadcrumb(breadcrumb, hint); } } 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 5d2e7a3cc28..092cbe5b854 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 @@ -19,13 +19,13 @@ public final class AppLifecycleIntegration implements Integration, Closeable { private @Nullable SentryAndroidOptions options; - private final @NotNull IHandler handler; + private final @NotNull MainLooperHandler handler; public AppLifecycleIntegration() { this(new MainLooperHandler()); } - AppLifecycleIntegration(final @NotNull IHandler handler) { + AppLifecycleIntegration(final @NotNull MainLooperHandler handler) { this.handler = handler; } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/BuildInfoProvider.java b/sentry-android-core/src/main/java/io/sentry/android/core/BuildInfoProvider.java index 2b8fdc811f6..b998414a9e2 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/BuildInfoProvider.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/BuildInfoProvider.java @@ -1,25 +1,75 @@ package io.sentry.android.core; import android.os.Build; +import io.sentry.ILogger; +import io.sentry.SentryLevel; +import io.sentry.util.Objects; import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -/** The Android Impl. of IBuildInfoProvider which returns the Build class info. */ +/** The Android Impl. of BuildInfoProvider which returns the Build class info. */ @ApiStatus.Internal -public final class BuildInfoProvider implements IBuildInfoProvider { +public final class BuildInfoProvider { + final @NotNull ILogger logger; + + public BuildInfoProvider(final @NotNull ILogger logger) { + this.logger = Objects.requireNonNull(logger, "The ILogger object is required."); + } /** * Returns the Build.VERSION.SDK_INT * * @return the Build.VERSION.SDK_INT */ - @Override public int getSdkInfoVersion() { return Build.VERSION.SDK_INT; } - @Override public @Nullable String getBuildTags() { return Build.TAGS; } + + public @Nullable String getManufacturer() { + return Build.MANUFACTURER; + } + + public @Nullable String getModel() { + return Build.MODEL; + } + + public @Nullable String getVersionRelease() { + return Build.VERSION.RELEASE; + } + + /** + * Check whether the application is running in an emulator. + * https://github.com/flutter/plugins/blob/master/packages/device_info/android/src/main/java/io/flutter/plugins/deviceinfo/DeviceInfoPlugin.java#L105 + * + * @return true if the application is running in an emulator, false otherwise + */ + public @Nullable Boolean isEmulator() { + try { + return (Build.BRAND.startsWith("generic") && Build.DEVICE.startsWith("generic")) + || Build.FINGERPRINT.startsWith("generic") + || Build.FINGERPRINT.startsWith("unknown") + || Build.HARDWARE.contains("goldfish") + || Build.HARDWARE.contains("ranchu") + || Build.MODEL.contains("google_sdk") + || Build.MODEL.contains("Emulator") + || Build.MODEL.contains("Android SDK built for x86") + || Build.MANUFACTURER.contains("Genymotion") + || Build.PRODUCT.contains("sdk_google") + || Build.PRODUCT.contains("google_sdk") + || Build.PRODUCT.contains("sdk") + || Build.PRODUCT.contains("sdk_x86") + || Build.PRODUCT.contains("vbox86p") + || Build.PRODUCT.contains("emulator") + || Build.PRODUCT.contains("simulator"); + } catch (Throwable e) { + logger.log( + SentryLevel.ERROR, "Error checking whether application is running in an emulator.", e); + return null; + } + } } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ContextUtils.java b/sentry-android-core/src/main/java/io/sentry/android/core/ContextUtils.java index e577576f25c..30d828013be 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/ContextUtils.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/ContextUtils.java @@ -19,8 +19,19 @@ private ContextUtils() {} */ @Nullable static PackageInfo getPackageInfo(final @NotNull Context context, final @NotNull ILogger logger) { + return getPackageInfo(context, 0, logger); + } + + /** + * Return the Application's PackageInfo with the specified flags if possible, or null. + * + * @return the Application's PackageInfo if possible, or null + */ + @Nullable + static PackageInfo getPackageInfo( + final @NotNull Context context, final int flags, final @NotNull ILogger logger) { try { - return context.getPackageManager().getPackageInfo(context.getPackageName(), 0); + return context.getPackageManager().getPackageInfo(context.getPackageName(), flags); } catch (Throwable e) { logger.log(SentryLevel.ERROR, "Error getting package info.", e); return null; @@ -41,6 +52,17 @@ static String getVersionCode(final @NotNull PackageInfo packageInfo) { return getVersionCodeDep(packageInfo); } + /** + * Returns the App's version name based on the PackageInfo + * + * @param packageInfo the PackageInfo class + * @return the versionName + */ + @Nullable + static String getVersionName(final @NotNull PackageInfo packageInfo) { + return packageInfo.versionName; + } + @SuppressWarnings("deprecation") private static @NotNull String getVersionCodeDep(final @NotNull PackageInfo packageInfo) { return Integer.toString(packageInfo.versionCode); diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/DefaultAndroidEventProcessor.java b/sentry-android-core/src/main/java/io/sentry/android/core/DefaultAndroidEventProcessor.java index 3b5539536bf..3575aea7854 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/DefaultAndroidEventProcessor.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/DefaultAndroidEventProcessor.java @@ -1,8 +1,10 @@ package io.sentry.android.core; import static android.content.Context.ACTIVITY_SERVICE; +import static android.content.pm.PackageInfo.REQUESTED_PERMISSION_GRANTED; import static android.os.BatteryManager.EXTRA_TEMPERATURE; +import android.annotation.SuppressLint; import android.app.ActivityManager; import android.content.Context; import android.content.Intent; @@ -20,6 +22,7 @@ import android.util.DisplayMetrics; import io.sentry.DateUtils; import io.sentry.EventProcessor; +import io.sentry.Hint; import io.sentry.ILogger; import io.sentry.SentryBaseEvent; import io.sentry.SentryEvent; @@ -34,7 +37,7 @@ import io.sentry.protocol.SentryThread; import io.sentry.protocol.SentryTransaction; import io.sentry.protocol.User; -import io.sentry.util.ApplyScopeUtils; +import io.sentry.util.HintUtils; import io.sentry.util.Objects; import java.io.BufferedReader; import java.io.File; @@ -64,7 +67,7 @@ final class DefaultAndroidEventProcessor implements EventProcessor { @TestOnly final Future> contextData; - private final @NotNull IBuildInfoProvider buildInfoProvider; + private final @NotNull BuildInfoProvider buildInfoProvider; private final @NotNull RootChecker rootChecker; private final @NotNull ILogger logger; @@ -72,14 +75,14 @@ final class DefaultAndroidEventProcessor implements EventProcessor { public DefaultAndroidEventProcessor( final @NotNull Context context, final @NotNull ILogger logger, - final @NotNull IBuildInfoProvider buildInfoProvider) { + final @NotNull BuildInfoProvider buildInfoProvider) { this(context, logger, buildInfoProvider, new RootChecker(context, buildInfoProvider, logger)); } DefaultAndroidEventProcessor( final @NotNull Context context, final @NotNull ILogger logger, - final @NotNull IBuildInfoProvider buildInfoProvider, + final @NotNull BuildInfoProvider buildInfoProvider, final @NotNull RootChecker rootChecker) { this.context = Objects.requireNonNull(context, "The application context is required."); this.logger = Objects.requireNonNull(logger, "The Logger is required."); @@ -105,7 +108,7 @@ public DefaultAndroidEventProcessor( } // its not IO, but it has been cached in the old version as well - map.put(EMULATOR, isEmulator()); + map.put(EMULATOR, buildInfoProvider.isEmulator()); final Map sideLoadedInfo = getSideLoadedInfo(); if (sideLoadedInfo != null) { @@ -116,8 +119,7 @@ public DefaultAndroidEventProcessor( } @Override - public @NotNull SentryEvent process( - final @NotNull SentryEvent event, final @Nullable Object hint) { + public @NotNull SentryEvent process(final @NotNull SentryEvent event, final @NotNull Hint hint) { final boolean applyScopeData = shouldApplyScopeData(event, hint); if (applyScopeData) { // we only set memory data if it's not a hard crash, when it's a hard crash the event is @@ -143,8 +145,8 @@ private void setCommons( } private boolean shouldApplyScopeData( - final @NotNull SentryBaseEvent event, final @Nullable Object hint) { - if (ApplyScopeUtils.shouldApplyScopeData(hint)) { + final @NotNull SentryBaseEvent event, final @NotNull Hint hint) { + if (HintUtils.shouldApplyScopeData(hint)) { return true; } else { logger.log( @@ -209,13 +211,16 @@ private void processNonCachedEvent(final @NotNull SentryBaseEvent event) { private void setThreads(final @NotNull SentryEvent event) { if (event.getThreads() != null) { for (SentryThread thread : event.getThreads()) { - thread.setCurrent(MainThreadChecker.isMainThread(thread)); + if (thread.isCurrent() == null) { + thread.setCurrent(MainThreadChecker.isMainThread(thread)); + } } } } private void setPackageInfo(final @NotNull SentryBaseEvent event, final @NotNull App app) { - final PackageInfo packageInfo = ContextUtils.getPackageInfo(context, logger); + final PackageInfo packageInfo = + ContextUtils.getPackageInfo(context, PackageManager.GET_PERMISSIONS, logger); if (packageInfo != null) { String versionCode = ContextUtils.getVersionCode(packageInfo); @@ -523,37 +528,6 @@ private TimeZone getTimeZone() { return deviceOrientation; } - /** - * Check whether the application is running in an emulator. - * https://github.com/flutter/plugins/blob/master/packages/device_info/android/src/main/java/io/flutter/plugins/deviceinfo/DeviceInfoPlugin.java#L105 - * - * @return true if the application is running in an emulator, false otherwise - */ - private @Nullable Boolean isEmulator() { - try { - return (Build.BRAND.startsWith("generic") && Build.DEVICE.startsWith("generic")) - || Build.FINGERPRINT.startsWith("generic") - || Build.FINGERPRINT.startsWith("unknown") - || Build.HARDWARE.contains("goldfish") - || Build.HARDWARE.contains("ranchu") - || Build.MODEL.contains("google_sdk") - || Build.MODEL.contains("Emulator") - || Build.MODEL.contains("Android SDK built for x86") - || Build.MANUFACTURER.contains("Genymotion") - || Build.PRODUCT.contains("sdk_google") - || Build.PRODUCT.contains("google_sdk") - || Build.PRODUCT.contains("sdk") - || Build.PRODUCT.contains("sdk_x86") - || Build.PRODUCT.contains("vbox86p") - || Build.PRODUCT.contains("emulator") - || Build.PRODUCT.contains("simulator"); - } catch (Throwable e) { - logger.log( - SentryLevel.ERROR, "Error checking whether application is running in an emulator.", e); - return null; - } - } - /** * Get the total amount of internal storage, in bytes. * @@ -758,10 +732,33 @@ private boolean isExternalStorageMounted() { return os; } + @SuppressLint("NewApi") // we perform an if-check for that, but lint fails to recognize private void setAppPackageInfo(final @NotNull App app, final @NotNull PackageInfo packageInfo) { app.setAppIdentifier(packageInfo.packageName); app.setAppVersion(packageInfo.versionName); app.setAppBuild(ContextUtils.getVersionCode(packageInfo)); + + if (buildInfoProvider.getSdkInfoVersion() >= Build.VERSION_CODES.JELLY_BEAN) { + final Map permissions = new HashMap<>(); + final String[] requestedPermissions = packageInfo.requestedPermissions; + final int[] requestedPermissionsFlags = packageInfo.requestedPermissionsFlags; + + if (requestedPermissions != null + && requestedPermissions.length > 0 + && requestedPermissionsFlags != null + && requestedPermissionsFlags.length > 0) { + for (int i = 0; i < requestedPermissions.length; i++) { + String permission = requestedPermissions[i]; + permission = permission.substring(permission.lastIndexOf('.') + 1); + + final boolean granted = + (requestedPermissionsFlags[i] & REQUESTED_PERMISSION_GRANTED) + == REQUESTED_PERMISSION_GRANTED; + permissions.put(permission, granted ? "granted" : "not_granted"); + } + } + app.setPermissions(permissions); + } } /** @@ -888,7 +885,7 @@ private void setSideLoadedInfo(final @NotNull SentryBaseEvent event) { @Override public @NotNull SentryTransaction process( - final @NotNull SentryTransaction transaction, final @Nullable Object hint) { + final @NotNull SentryTransaction transaction, final @NotNull Hint hint) { final boolean applyScopeData = shouldApplyScopeData(transaction, hint); if (applyScopeData) { diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/EnvelopeFileObserver.java b/sentry-android-core/src/main/java/io/sentry/android/core/EnvelopeFileObserver.java index 2715af6f1a0..4a3dc7a1786 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/EnvelopeFileObserver.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/EnvelopeFileObserver.java @@ -3,6 +3,7 @@ import static io.sentry.SentryLevel.ERROR; import android.os.FileObserver; +import io.sentry.Hint; import io.sentry.IEnvelopeSender; import io.sentry.ILogger; import io.sentry.SentryLevel; @@ -12,6 +13,7 @@ import io.sentry.hints.Resettable; import io.sentry.hints.Retryable; import io.sentry.hints.SubmissionResult; +import io.sentry.util.HintUtils; import io.sentry.util.Objects; import java.io.File; import java.util.concurrent.CountDownLatch; @@ -55,7 +57,10 @@ public void onEvent(int eventType, @Nullable String relativePath) { // TODO: Only some event types should be pass through? - final CachedEnvelopeHint hint = new CachedEnvelopeHint(flushTimeoutMillis, logger); + final CachedEnvelopeHint cachedHint = new CachedEnvelopeHint(flushTimeoutMillis, logger); + + final Hint hint = HintUtils.createWithTypeCheckHint(cachedHint); + envelopeSender.processEnvelopeFile(this.rootPath + File.separator + relativePath, hint); } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/IBuildInfoProvider.java b/sentry-android-core/src/main/java/io/sentry/android/core/IBuildInfoProvider.java deleted file mode 100644 index e23c4db4b89..00000000000 --- a/sentry-android-core/src/main/java/io/sentry/android/core/IBuildInfoProvider.java +++ /dev/null @@ -1,22 +0,0 @@ -package io.sentry.android.core; - -import org.jetbrains.annotations.Nullable; - -/** To make SDK info classes testable */ -public interface IBuildInfoProvider { - - /** - * Returns the SDK version of the given SDK - * - * @return the SDK Version - */ - int getSdkInfoVersion(); - - /** - * Returns the Build tags of the given SDK - * - * @return the Build tags - */ - @Nullable - String getBuildTags(); -} diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/IHandler.java b/sentry-android-core/src/main/java/io/sentry/android/core/IHandler.java deleted file mode 100644 index 6c6b5f997af..00000000000 --- a/sentry-android-core/src/main/java/io/sentry/android/core/IHandler.java +++ /dev/null @@ -1,12 +0,0 @@ -package io.sentry.android.core; - -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.TestOnly; - -@TestOnly -interface IHandler { - void post(@NotNull Runnable runnable); - - @NotNull - Thread getThread(); -} diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/MainLooperHandler.java b/sentry-android-core/src/main/java/io/sentry/android/core/MainLooperHandler.java index 713a541fc42..46fb6d0fc4b 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/MainLooperHandler.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/MainLooperHandler.java @@ -4,7 +4,7 @@ import android.os.Looper; import org.jetbrains.annotations.NotNull; -final class MainLooperHandler implements IHandler { +final class MainLooperHandler { private final @NotNull Handler handler; MainLooperHandler() { @@ -15,12 +15,10 @@ final class MainLooperHandler implements IHandler { handler = new Handler(looper); } - @Override public void post(final @NotNull Runnable runnable) { handler.post(runnable); } - @Override public @NotNull Thread getThread() { return handler.getLooper().getThread(); } 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 d3f78841958..f88d0999ff4 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 @@ -6,6 +6,7 @@ import android.os.Bundle; import io.sentry.ILogger; import io.sentry.SentryLevel; +import io.sentry.protocol.SdkVersion; import io.sentry.util.Objects; import java.util.Arrays; import java.util.List; @@ -31,6 +32,8 @@ final class ManifestMetadataReader { static final String NDK_SCOPE_SYNC_ENABLE = "io.sentry.ndk.scope-sync.enable"; static final String RELEASE = "io.sentry.release"; static final String ENVIRONMENT = "io.sentry.environment"; + static final String SDK_NAME = "io.sentry.sdk.name"; + static final String SDK_VERSION = "io.sentry.sdk.version"; // TODO: remove on 6.x in favor of SESSION_AUTO_TRACKING_ENABLE static final String SESSION_TRACKING_ENABLE = "io.sentry.session-tracking.enable"; @@ -54,6 +57,9 @@ final class ManifestMetadataReader { static final String TRACES_ACTIVITY_ENABLE = "io.sentry.traces.activity.enable"; static final String TRACES_ACTIVITY_AUTO_FINISH_ENABLE = "io.sentry.traces.activity.auto-finish.enable"; + static final String TRACES_UI_ENABLE = "io.sentry.traces.user-interaction.enable"; + + static final String TRACES_PROFILING_ENABLE = "io.sentry.traces.profiling.enable"; @ApiStatus.Experimental static final String TRACE_SAMPLING = "io.sentry.traces.trace-sampling"; @@ -61,6 +67,10 @@ final class ManifestMetadataReader { static final String ATTACH_THREADS = "io.sentry.attach-threads"; static final String PROGUARD_UUID = "io.sentry.proguard-uuid"; + static final String IDLE_TIMEOUT = "io.sentry.traces.idle-timeout"; + + static final String ATTACH_SCREENSHOT = "io.sentry.attach-screenshot"; + static final String CLIENT_REPORTS_ENABLE = "io.sentry.send-client-reports"; /** ManifestMetadataReader ctor */ private ManifestMetadataReader() {} @@ -194,6 +204,12 @@ static void applyMetadata( options.setAttachThreads( readBool(metadata, logger, ATTACH_THREADS, options.isAttachThreads())); + options.setAttachScreenshot( + readBool(metadata, logger, ATTACH_SCREENSHOT, options.isAttachScreenshot())); + + options.setSendClientReports( + readBool(metadata, logger, CLIENT_REPORTS_ENABLE, options.isSendClientReports())); + if (options.getTracesSampleRate() == null) { final Double tracesSampleRate = readDouble(metadata, logger, TRACES_SAMPLE_RATE); if (tracesSampleRate != -1) { @@ -218,6 +234,17 @@ static void applyMetadata( TRACES_ACTIVITY_AUTO_FINISH_ENABLE, options.isEnableActivityLifecycleTracingAutoFinish())); + options.setProfilingEnabled( + readBool(metadata, logger, TRACES_PROFILING_ENABLE, options.isProfilingEnabled())); + + options.setEnableUserInteractionTracing( + readBool(metadata, logger, TRACES_UI_ENABLE, options.isEnableUserInteractionTracing())); + + final long idleTimeout = readLong(metadata, logger, IDLE_TIMEOUT, -1); + if (idleTimeout != -1) { + options.setIdleTimeout(idleTimeout); + } + final List tracingOrigins = readList(metadata, logger, TRACING_ORIGINS); if (tracingOrigins != null) { for (final String tracingOrigin : tracingOrigins) { @@ -227,6 +254,15 @@ static void applyMetadata( options.setProguardUuid( readString(metadata, logger, PROGUARD_UUID, options.getProguardUuid())); + + SdkVersion sdkInfo = options.getSdkVersion(); + if (sdkInfo == null) { + // Is already set by the Options constructor, let's use an empty default otherwise. + sdkInfo = new SdkVersion("", ""); + } + sdkInfo.setName(readStringNotNull(metadata, logger, SDK_NAME, sdkInfo.getName())); + sdkInfo.setVersion(readStringNotNull(metadata, logger, SDK_VERSION, sdkInfo.getVersion())); + options.setSdkVersion(sdkInfo); } options @@ -260,6 +296,16 @@ private static boolean readBool( return value; } + private static @NotNull String readStringNotNull( + final @NotNull Bundle metadata, + final @NotNull ILogger logger, + final @NotNull String key, + final @NotNull String defaultValue) { + final String value = metadata.getString(key, defaultValue); + logger.log(SentryLevel.DEBUG, "%s read: %s", key, 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); 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 c15dde50644..929d2dacb31 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 @@ -40,9 +40,9 @@ public final void register(final @NotNull IHub hub, final @NotNull SentryOptions // up by another integration (EnvelopeFileObserverIntegration). if (enabled && sentryNdkClass != null) { final String cachedDir = this.options.getCacheDirPath(); - if (cachedDir == null || cachedDir.isEmpty()) { + if (cachedDir == null) { this.options.getLogger().log(SentryLevel.ERROR, "No cache dir path is defined in options."); - this.options.setEnableNdk(false); + disableNdkIntegration(this.options); return; } @@ -54,19 +54,24 @@ public final void register(final @NotNull IHub hub, final @NotNull SentryOptions this.options.getLogger().log(SentryLevel.DEBUG, "NdkIntegration installed."); } catch (NoSuchMethodException e) { - this.options.setEnableNdk(false); + disableNdkIntegration(this.options); this.options .getLogger() .log(SentryLevel.ERROR, "Failed to invoke the SentryNdk.init method.", e); } catch (Throwable e) { - this.options.setEnableNdk(false); + disableNdkIntegration(this.options); this.options.getLogger().log(SentryLevel.ERROR, "Failed to initialize SentryNdk.", e); } } else { - this.options.setEnableNdk(false); + disableNdkIntegration(this.options); } } + private void disableNdkIntegration(final @NotNull SentryOptions options) { + options.setEnableNdk(false); + options.setEnableScopeSync(false); + } + @TestOnly @Nullable Class getSentryNdkClass() { @@ -88,7 +93,7 @@ public void close() throws IOException { } catch (Throwable e) { options.getLogger().log(SentryLevel.ERROR, "Failed to close SentryNdk.", e); } finally { - this.options.setEnableNdk(false); + disableNdkIntegration(this.options); } } } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/PerformanceAndroidEventProcessor.java b/sentry-android-core/src/main/java/io/sentry/android/core/PerformanceAndroidEventProcessor.java index 6bd7651ff9f..9c09d24f87d 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/PerformanceAndroidEventProcessor.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/PerformanceAndroidEventProcessor.java @@ -5,6 +5,7 @@ import static io.sentry.android.core.ActivityLifecycleIntegration.UI_LOAD_OP; import io.sentry.EventProcessor; +import io.sentry.Hint; import io.sentry.SentryEvent; import io.sentry.SpanContext; import io.sentry.protocol.MeasurementValue; @@ -42,7 +43,7 @@ final class PerformanceAndroidEventProcessor implements EventProcessor { */ @Override @Nullable - public SentryEvent process(@NotNull SentryEvent event, @Nullable Object hint) { + public SentryEvent process(@NotNull SentryEvent event, @NotNull Hint hint) { // that's only necessary because on newer versions of Unity, if not overriding this method, it's // throwing 'java.lang.AbstractMethodError: abstract method' and the reason is probably // compilation mismatch. @@ -52,7 +53,7 @@ public SentryEvent process(@NotNull SentryEvent event, @Nullable Object hint) { @SuppressWarnings("NullAway") @Override public synchronized @NotNull SentryTransaction process( - @NotNull SentryTransaction transaction, @Nullable Object hint) { + @NotNull SentryTransaction transaction, @NotNull Hint hint) { if (!options.isTracingEnabled()) { return transaction; 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 new file mode 100644 index 00000000000..cf7edb46912 --- /dev/null +++ b/sentry-android-core/src/main/java/io/sentry/android/core/ScreenshotEventProcessor.java @@ -0,0 +1,185 @@ +package io.sentry.android.core; + +import static io.sentry.TypeCheckHint.ANDROID_ACTIVITY; + +import android.annotation.SuppressLint; +import android.app.Activity; +import android.app.Application; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.os.Build; +import android.os.Bundle; +import android.view.View; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import io.sentry.Attachment; +import io.sentry.EventProcessor; +import io.sentry.Hint; +import io.sentry.SentryEvent; +import io.sentry.SentryLevel; +import io.sentry.util.Objects; +import java.io.ByteArrayOutputStream; +import java.io.Closeable; +import java.io.IOException; +import java.lang.ref.WeakReference; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +/** + * ScreenshotEventProcessor responsible for taking a screenshot of the screen when an error is + * captured. + */ +@ApiStatus.Internal +public final class ScreenshotEventProcessor + implements EventProcessor, Application.ActivityLifecycleCallbacks, Closeable { + + private final @NotNull Application application; + private final @NotNull SentryAndroidOptions options; + private @Nullable WeakReference currentActivity; + final @NotNull BuildInfoProvider buildInfoProvider; + + public ScreenshotEventProcessor( + final @NotNull Application application, + final @NotNull SentryAndroidOptions options, + final @NotNull BuildInfoProvider buildInfoProvider) { + this.application = Objects.requireNonNull(application, "Application is required"); + this.options = Objects.requireNonNull(options, "SentryAndroidOptions is required"); + this.buildInfoProvider = + Objects.requireNonNull(buildInfoProvider, "BuildInfoProvider is required"); + + if (this.options.isAttachScreenshot()) { + application.registerActivityLifecycleCallbacks(this); + + this.options + .getLogger() + .log( + SentryLevel.DEBUG, + "attachScreenshot is enabled, ScreenshotEventProcessor is installed."); + } else { + this.options + .getLogger() + .log( + SentryLevel.DEBUG, + "attachScreenshot is disabled, ScreenshotEventProcessor isn't installed."); + } + } + + @SuppressWarnings("NullAway") + @Override + public @NotNull SentryEvent process(final @NotNull SentryEvent event, @NotNull Hint hint) { + if (options.isAttachScreenshot() && event.isErrored() && currentActivity != null) { + final Activity activity = currentActivity.get(); + if (isActivityValid(activity) + && activity.getWindow() != null + && activity.getWindow().getDecorView() != null + && activity.getWindow().getDecorView().getRootView() != null) { + final View view = activity.getWindow().getDecorView().getRootView(); + + if (view.getWidth() > 0 && view.getHeight() > 0) { + try { + // ARGB_8888 -> This configuration is very flexible and offers the best quality + final Bitmap bitmap = + Bitmap.createBitmap(view.getWidth(), view.getHeight(), Bitmap.Config.ARGB_8888); + + final Canvas canvas = new Canvas(bitmap); + view.draw(canvas); + + final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + + // 0 meaning compress for small size, 100 meaning compress for max quality. + // Some formats, like PNG which is lossless, will ignore the quality setting. + bitmap.compress(Bitmap.CompressFormat.PNG, 0, byteArrayOutputStream); + + if (byteArrayOutputStream.size() > 0) { + // screenshot png is around ~100-150 kb + hint.setScreenshot(Attachment.fromScreenshot(byteArrayOutputStream.toByteArray())); + hint.set(ANDROID_ACTIVITY, activity); + } else { + this.options + .getLogger() + .log(SentryLevel.DEBUG, "Screenshot is 0 bytes, not attaching the image."); + } + } catch (Throwable e) { + this.options.getLogger().log(SentryLevel.ERROR, "Taking screenshot failed.", e); + } + } else { + this.options + .getLogger() + .log(SentryLevel.DEBUG, "View's width and height is zeroed, not taking screenshot."); + } + } else { + this.options + .getLogger() + .log(SentryLevel.DEBUG, "Activity isn't valid, not taking screenshot."); + } + } + + return event; + } + + @Override + public void onActivityCreated(@NonNull Activity activity, @Nullable Bundle savedInstanceState) { + setCurrentActivity(activity); + } + + @Override + public void onActivityStarted(@NonNull Activity activity) { + setCurrentActivity(activity); + } + + @Override + public void onActivityResumed(@NonNull Activity activity) { + setCurrentActivity(activity); + } + + @Override + public void onActivityPaused(@NonNull Activity activity) { + cleanCurrentActivity(activity); + } + + @Override + public void onActivityStopped(@NonNull Activity activity) { + cleanCurrentActivity(activity); + } + + @Override + public void onActivitySaveInstanceState(@NonNull Activity activity, @NonNull Bundle outState) {} + + @Override + public void onActivityDestroyed(@NonNull Activity activity) { + cleanCurrentActivity(activity); + } + + @Override + public void close() throws IOException { + if (options.isAttachScreenshot()) { + application.unregisterActivityLifecycleCallbacks(this); + currentActivity = null; + } + } + + private void cleanCurrentActivity(@NonNull Activity activity) { + if (currentActivity != null && currentActivity.get() == activity) { + currentActivity = null; + } + } + + private void setCurrentActivity(@NonNull Activity activity) { + if (currentActivity != null && currentActivity.get() == activity) { + return; + } + currentActivity = new WeakReference<>(activity); + } + + @SuppressLint("NewApi") + private boolean isActivityValid(@Nullable Activity activity) { + if (activity == null) { + return false; + } + if (buildInfoProvider.getSdkInfoVersion() >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + return !activity.isFinishing() && !activity.isDestroyed(); + } else { + return !activity.isFinishing(); + } + } +} diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroidOptions.java b/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroidOptions.java index 5ad11b9a62f..5373c26d703 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroidOptions.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroidOptions.java @@ -84,13 +84,25 @@ public final class SentryAndroidOptions extends SentryOptions { */ private boolean enableActivityLifecycleTracingAutoFinish = true; + /** Interval for profiling traces in milliseconds. Defaults to 100 times per second */ + private int profilingTracesIntervalMillis = 1_000 / 100; + + /** Enables the Auto instrumentation for user interaction tracing. */ + private boolean enableUserInteractionTracing = false; + /** Interface that loads the debug images list */ private @NotNull IDebugImagesLoader debugImagesLoader = NoOpDebugImagesLoader.getInstance(); + /** Enables or disables the attach screenshot feature when an error happened. */ + private boolean attachScreenshot; + public SentryAndroidOptions() { setSentryClientName(BuildConfig.SENTRY_ANDROID_SDK_NAME + "/" + BuildConfig.VERSION_NAME); setSdkVersion(createSdkVersion()); setAttachServerName(false); + + // enable scope sync for Android by default + setEnableScopeSync(true); } private @NotNull SdkVersion createSdkVersion() { @@ -213,6 +225,24 @@ public void enableAllAutoBreadcrumbs(boolean enable) { enableUserInteractionBreadcrumbs = enable; } + /** + * Returns the interval for profiling traces in milliseconds. + * + * @return the interval for profiling traces in milliseconds. + */ + public int getProfilingTracesIntervalMillis() { + return profilingTracesIntervalMillis; + } + + /** + * Sets the interval for profiling traces in milliseconds. + * + * @param profilingTracesIntervalMillis - the interval for profiling traces in milliseconds. + */ + public void setProfilingTracesIntervalMillis(final int profilingTracesIntervalMillis) { + this.profilingTracesIntervalMillis = profilingTracesIntervalMillis; + } + /** * Returns the Debug image loader * @@ -248,4 +278,20 @@ public void setEnableActivityLifecycleTracingAutoFinish( boolean enableActivityLifecycleTracingAutoFinish) { this.enableActivityLifecycleTracingAutoFinish = enableActivityLifecycleTracingAutoFinish; } + + public boolean isAttachScreenshot() { + return attachScreenshot; + } + + public void setAttachScreenshot(boolean attachScreenshot) { + this.attachScreenshot = attachScreenshot; + } + + public boolean isEnableUserInteractionTracing() { + return enableUserInteractionTracing; + } + + public void setEnableUserInteractionTracing(boolean enableUserInteractionTracing) { + this.enableUserInteractionTracing = enableUserInteractionTracing; + } } 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 21c0d0e1922..b4ee214fcce 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 @@ -30,6 +30,7 @@ import static android.content.Intent.ACTION_SHUTDOWN; import static android.content.Intent.ACTION_TIMEZONE_CHANGED; import static android.content.Intent.ACTION_TIME_CHANGED; +import static io.sentry.TypeCheckHint.ANDROID_INTENT; import android.content.BroadcastReceiver; import android.content.Context; @@ -37,6 +38,7 @@ import android.content.IntentFilter; import android.os.Bundle; import io.sentry.Breadcrumb; +import io.sentry.Hint; import io.sentry.IHub; import io.sentry.ILogger; import io.sentry.Integration; @@ -213,7 +215,11 @@ public void onReceive(Context context, Intent intent) { breadcrumb.setData("extras", newExtras); } breadcrumb.setLevel(SentryLevel.INFO); - hub.addBreadcrumb(breadcrumb); + + final Hint hint = new Hint(); + hint.set(ANDROID_INTENT, intent); + + hub.addBreadcrumb(breadcrumb, hint); } } } 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 94373d9bcc1..49014c1fecd 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 @@ -1,6 +1,7 @@ package io.sentry.android.core; import static android.content.Context.SENSOR_SERVICE; +import static io.sentry.TypeCheckHint.ANDROID_SENSOR_EVENT; import android.content.Context; import android.hardware.Sensor; @@ -8,6 +9,7 @@ import android.hardware.SensorEventListener; import android.hardware.SensorManager; import io.sentry.Breadcrumb; +import io.sentry.Hint; import io.sentry.IHub; import io.sentry.Integration; import io.sentry.SentryLevel; @@ -103,7 +105,11 @@ public void onSensorChanged(final @NotNull SensorEvent event) { breadcrumb.setData("timestamp", event.timestamp); breadcrumb.setLevel(SentryLevel.INFO); breadcrumb.setData("degree", event.values[0]); // Celsius - hub.addBreadcrumb(breadcrumb); + + final Hint hint = new Hint(); + hint.set(ANDROID_SENSOR_EVENT, event); + + hub.addBreadcrumb(breadcrumb, hint); } } 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 4f917f86eb3..5c6a736e53a 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 @@ -2,7 +2,6 @@ import android.app.Activity; import android.app.Application; -import android.content.Context; import android.os.Bundle; import android.view.Window; import io.sentry.IHub; @@ -15,7 +14,6 @@ import io.sentry.util.Objects; import java.io.Closeable; import java.io.IOException; -import java.lang.ref.WeakReference; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -39,7 +37,8 @@ public UserInteractionIntegration( classLoader.isClassAvailable("androidx.core.view.ScrollingView", options); } - private void startTracking(final @Nullable Window window, final @NotNull Context context) { + private void startTracking(final @NotNull Activity activity) { + final Window window = activity.getWindow(); if (window == null) { if (options != null) { options.getLogger().log(SentryLevel.INFO, "Window was null in startTracking"); @@ -54,13 +53,13 @@ private void startTracking(final @Nullable Window window, final @NotNull Context } final SentryGestureListener gestureListener = - new SentryGestureListener( - new WeakReference<>(window), hub, options, isAndroidXScrollViewAvailable); - window.setCallback(new SentryWindowCallback(delegate, context, gestureListener, options)); + new SentryGestureListener(activity, hub, options, isAndroidXScrollViewAvailable); + window.setCallback(new SentryWindowCallback(delegate, activity, gestureListener, options)); } } - private void stopTracking(final @Nullable Window window) { + private void stopTracking(final @NotNull Activity activity) { + final Window window = activity.getWindow(); if (window == null) { if (options != null) { options.getLogger().log(SentryLevel.INFO, "Window was null in stopTracking"); @@ -70,6 +69,7 @@ private void stopTracking(final @Nullable Window window) { final Window.Callback current = window.getCallback(); if (current instanceof SentryWindowCallback) { + ((SentryWindowCallback) current).stopTracking(); if (((SentryWindowCallback) current).getDelegate() instanceof NoOpWindowCallback) { window.setCallback(null); } else { @@ -86,12 +86,12 @@ public void onActivityStarted(@NotNull Activity activity) {} @Override public void onActivityResumed(@NotNull Activity activity) { - startTracking(activity.getWindow(), activity); + startTracking(activity); } @Override public void onActivityPaused(@NotNull Activity activity) { - stopTracking(activity.getWindow()); + stopTracking(activity); } @Override diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/internal/gestures/SentryGestureListener.java b/sentry-android-core/src/main/java/io/sentry/android/core/internal/gestures/SentryGestureListener.java index 9fa3ecb7a38..d01db42ef9a 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/internal/gestures/SentryGestureListener.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/internal/gestures/SentryGestureListener.java @@ -1,12 +1,21 @@ package io.sentry.android.core.internal.gestures; +import static io.sentry.TypeCheckHint.ANDROID_MOTION_EVENT; +import static io.sentry.TypeCheckHint.ANDROID_VIEW; + +import android.app.Activity; +import android.content.res.Resources; import android.view.GestureDetector; import android.view.MotionEvent; import android.view.View; import android.view.Window; import io.sentry.Breadcrumb; +import io.sentry.Hint; import io.sentry.IHub; +import io.sentry.ITransaction; +import io.sentry.Scope; import io.sentry.SentryLevel; +import io.sentry.SpanStatus; import io.sentry.android.core.SentryAndroidOptions; import java.lang.ref.WeakReference; import java.util.Collections; @@ -14,23 +23,30 @@ import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.VisibleForTesting; @ApiStatus.Internal public final class SentryGestureListener implements GestureDetector.OnGestureListener { - private final @NotNull WeakReference windowRef; + static final String UI_ACTION = "ui.action"; + + private final @NotNull WeakReference activityRef; private final @NotNull IHub hub; private final @NotNull SentryAndroidOptions options; private final boolean isAndroidXAvailable; + private @Nullable WeakReference activeView = null; + private @Nullable ITransaction activeTransaction = null; + private @Nullable String activeEventType = null; + private final ScrollState scrollState = new ScrollState(); public SentryGestureListener( - final @NotNull WeakReference windowRef, + final @NotNull Activity currentActivity, final @NotNull IHub hub, final @NotNull SentryAndroidOptions options, final boolean isAndroidXAvailable) { - this.windowRef = windowRef; + this.activityRef = new WeakReference<>(currentActivity); this.hub = hub; this.options = options; this.isAndroidXAvailable = isAndroidXAvailable; @@ -51,7 +67,12 @@ public void onUp(final @NotNull MotionEvent motionEvent) { } final String direction = scrollState.calculateDirection(motionEvent); - addBreadcrumb(scrollTarget, scrollState.type, Collections.singletonMap("direction", direction)); + addBreadcrumb( + scrollTarget, + scrollState.type, + Collections.singletonMap("direction", direction), + motionEvent); + startTracing(scrollTarget, scrollState.type); scrollState.reset(); } @@ -88,7 +109,8 @@ public boolean onSingleTapUp(final @Nullable MotionEvent motionEvent) { return false; } - addBreadcrumb(target, "click", Collections.emptyMap()); + addBreadcrumb(target, "click", Collections.emptyMap(), motionEvent); + startTracing(target, "click"); return false; } @@ -154,7 +176,8 @@ public void onLongPress(MotionEvent motionEvent) {} private void addBreadcrumb( final @NotNull View target, final @NotNull String eventType, - final @NotNull Map additionalData) { + final @NotNull Map additionalData, + final @NotNull MotionEvent motionEvent) { @NotNull String className; @Nullable String canonicalName = target.getClass().getCanonicalName(); if (canonicalName != null) { @@ -163,13 +186,139 @@ private void addBreadcrumb( className = target.getClass().getSimpleName(); } + final Hint hint = new Hint(); + hint.set(ANDROID_MOTION_EVENT, motionEvent); + hint.set(ANDROID_VIEW, target); + hub.addBreadcrumb( Breadcrumb.userInteraction( - eventType, ViewUtils.getResourceId(target), className, additionalData)); + eventType, ViewUtils.getResourceIdWithFallback(target), className, additionalData), + hint); + } + + private void startTracing(final @NotNull View target, final @NotNull String eventType) { + if (!(options.isTracingEnabled() && options.isEnableUserInteractionTracing())) { + return; + } + + final Activity activity = activityRef.get(); + if (activity == null) { + options.getLogger().log(SentryLevel.DEBUG, "Activity is null, no transaction captured."); + return; + } + + final String viewId; + try { + viewId = ViewUtils.getResourceId(target); + } catch (Resources.NotFoundException e) { + options + .getLogger() + .log( + SentryLevel.DEBUG, + "View id cannot be retrieved from Resources, no transaction captured."); + return; + } + + final View view = (activeView != null) ? activeView.get() : null; + if (activeTransaction != null) { + if (target.equals(view) + && eventType.equals(activeEventType) + && !activeTransaction.isFinished()) { + options + .getLogger() + .log( + SentryLevel.DEBUG, + "The view with id: " + + viewId + + " already has an ongoing transaction assigned. Rescheduling finish"); + + final Long idleTimeout = options.getIdleTimeout(); + if (idleTimeout != null) { + // reschedule the finish task for the idle transaction, so it keeps running for the same + // view + activeTransaction.scheduleFinish(idleTimeout); + } + return; + } else { + // as we allow a single UI transaction running on the bound Scope, we finish the previous + // one, if it's a new view + stopTracing(SpanStatus.OK); + } + } + + // we can only bind to the scope if there's no running transaction + final String name = getActivityName(activity) + "." + viewId; + final String op = UI_ACTION + "." + eventType; + final ITransaction transaction = + hub.startTransaction(name, op, true, options.getIdleTimeout(), true); + + hub.configureScope( + scope -> { + applyScope(scope, transaction); + }); + + activeTransaction = transaction; + activeView = new WeakReference<>(target); + activeEventType = eventType; + } + + void stopTracing(final @NotNull SpanStatus status) { + if (activeTransaction != null) { + activeTransaction.finish(status); + } + hub.configureScope( + scope -> { + clearScope(scope); + }); + activeTransaction = null; + if (activeView != null) { + activeView.clear(); + } + activeEventType = null; + } + + @VisibleForTesting + void clearScope(final @NotNull Scope scope) { + scope.withTransaction( + transaction -> { + if (transaction == activeTransaction) { + scope.clearTransaction(); + } + }); + } + + @VisibleForTesting + void applyScope(final @NotNull Scope scope, final @NotNull ITransaction transaction) { + scope.withTransaction( + scopeTransaction -> { + // we'd not like to overwrite existent transactions bound to the Scope manually + if (scopeTransaction == null) { + scope.setTransaction(transaction); + } else { + options + .getLogger() + .log( + SentryLevel.DEBUG, + "Transaction '%s' won't be bound to the Scope since there's one already in there.", + transaction.getName()); + } + }); + } + + private @NotNull String getActivityName(final @NotNull Activity activity) { + return activity.getClass().getSimpleName(); } private @Nullable View ensureWindowDecorView(final @NotNull String caller) { - final Window window = windowRef.get(); + final Activity activity = activityRef.get(); + if (activity == null) { + options + .getLogger() + .log(SentryLevel.DEBUG, "Activity is null in " + caller + ". No breadcrumb captured."); + return null; + } + + final Window window = activity.getWindow(); if (window == null) { options .getLogger() diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/internal/gestures/SentryWindowCallback.java b/sentry-android-core/src/main/java/io/sentry/android/core/internal/gestures/SentryWindowCallback.java index 81a9401ac09..473a59b5b00 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/internal/gestures/SentryWindowCallback.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/internal/gestures/SentryWindowCallback.java @@ -6,6 +6,7 @@ import androidx.core.view.GestureDetectorCompat; import io.sentry.SentryLevel; import io.sentry.SentryOptions; +import io.sentry.SpanStatus; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -71,6 +72,10 @@ private void handleTouchEvent(final @NotNull MotionEvent motionEvent) { } } + public void stopTracking() { + gestureListener.stopTracing(SpanStatus.CANCELLED); + } + public @NotNull Window.Callback getDelegate() { return delegate; } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/internal/gestures/ViewUtils.java b/sentry-android-core/src/main/java/io/sentry/android/core/internal/gestures/ViewUtils.java index 3c8880a4d8d..a8e84f1be75 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/internal/gestures/ViewUtils.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/internal/gestures/ViewUtils.java @@ -101,17 +101,29 @@ private static boolean isJetpackScrollingView( * @param view - the view that the id is being retrieved for. * @return human-readable view id */ - static String getResourceId(final @NotNull View view) { + static String getResourceIdWithFallback(final @NotNull View view) { final int viewId = view.getId(); - final Resources resources = view.getContext().getResources(); - String resourceId = ""; try { - if (resources != null) { - resourceId = resources.getResourceEntryName(viewId); - } + return getResourceId(view); } catch (Resources.NotFoundException e) { // fall back to hex representation of the id - resourceId = "0x" + Integer.toString(viewId, 16); + return "0x" + Integer.toString(viewId, 16); + } + } + + /** + * Retrieves the human-readable view id based on {@code view.getContext().getResources()}. + * + * @param view - the view whose id is being retrieved + * @return human-readable view id + * @throws Resources.NotFoundException in case the view id was not found + */ + static String getResourceId(final @NotNull View view) throws Resources.NotFoundException { + final int viewId = view.getId(); + final Resources resources = view.getContext().getResources(); + String resourceId = ""; + if (resources != null) { + resourceId = resources.getResourceEntryName(viewId); } return resourceId; } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/ConnectivityChecker.java b/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/ConnectivityChecker.java index 8f4d0f785c3..c98ffc9344c 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/ConnectivityChecker.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/ConnectivityChecker.java @@ -9,7 +9,7 @@ import android.os.Build; import io.sentry.ILogger; import io.sentry.SentryLevel; -import io.sentry.android.core.IBuildInfoProvider; +import io.sentry.android.core.BuildInfoProvider; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -83,7 +83,7 @@ private ConnectivityChecker() {} public static @Nullable String getConnectionType( final @NotNull Context context, final @NotNull ILogger logger, - final @NotNull IBuildInfoProvider buildInfoProvider) { + final @NotNull BuildInfoProvider buildInfoProvider) { final ConnectivityManager connectivityManager = getConnectivityManager(context, logger); if (connectivityManager == null) { return null; diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/CpuInfoUtils.java b/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/CpuInfoUtils.java new file mode 100644 index 00000000000..69c0a557669 --- /dev/null +++ b/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/CpuInfoUtils.java @@ -0,0 +1,77 @@ +package io.sentry.android.core.internal.util; + +import io.sentry.util.FileUtils; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.TestOnly; +import org.jetbrains.annotations.VisibleForTesting; + +@ApiStatus.Internal +public final class CpuInfoUtils { + + private static final CpuInfoUtils instance = new CpuInfoUtils(); + + public static CpuInfoUtils getInstance() { + return instance; + } + + private CpuInfoUtils() {} + + private static final @NotNull String SYSTEM_CPU_PATH = "/sys/devices/system/cpu"; + + @VisibleForTesting + static final @NotNull String CPUINFO_MAX_FREQ_PATH = "cpufreq/cpuinfo_max_freq"; + + /** Cached max frequencies to avoid reading files multiple times */ + private final @NotNull List cpuMaxFrequenciesMhz = new ArrayList<>(); + + /** + * Read the max frequency of each core of the cpu and returns it in Mhz + * + * @return A list with the frequency of each core of the cpu in Mhz + */ + public @NotNull List readMaxFrequencies() { + if (!cpuMaxFrequenciesMhz.isEmpty()) { + return cpuMaxFrequenciesMhz; + } + File[] cpuDirs = new File(getSystemCpuPath()).listFiles(); + if (cpuDirs == null) { + return new ArrayList<>(); + } + + for (File cpuDir : cpuDirs) { + if (!cpuDir.getName().matches("cpu[0-9]+")) continue; + File cpuMaxFreqFile = new File(cpuDir, CPUINFO_MAX_FREQ_PATH); + + if (!cpuMaxFreqFile.exists() || !cpuMaxFreqFile.canRead()) continue; + + long khz; + try { + String content = FileUtils.readText(cpuMaxFreqFile); + if (content == null) continue; + khz = Long.parseLong(content.trim()); + } catch (NumberFormatException e) { + continue; + } catch (IOException e) { + continue; + } + cpuMaxFrequenciesMhz.add((int) (khz / 1000)); + } + return cpuMaxFrequenciesMhz; + } + + @VisibleForTesting + @NotNull + String getSystemCpuPath() { + return SYSTEM_CPU_PATH; + } + + @TestOnly + final void clear() { + cpuMaxFrequenciesMhz.clear(); + } +} diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/RootChecker.java b/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/RootChecker.java index 4bf95cba507..3d8d497bd37 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/RootChecker.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/RootChecker.java @@ -4,7 +4,7 @@ import android.content.pm.PackageManager; import io.sentry.ILogger; import io.sentry.SentryLevel; -import io.sentry.android.core.IBuildInfoProvider; +import io.sentry.android.core.BuildInfoProvider; import io.sentry.util.Objects; import java.io.BufferedReader; import java.io.File; @@ -22,7 +22,7 @@ public final class RootChecker { private static final Charset UTF_8 = Charset.forName("UTF-8"); private final @NotNull Context context; - private final @NotNull IBuildInfoProvider buildInfoProvider; + private final @NotNull BuildInfoProvider buildInfoProvider; private final @NotNull ILogger logger; private final @NotNull String[] rootFiles; @@ -33,7 +33,7 @@ public final class RootChecker { public RootChecker( final @NotNull Context context, - final @NotNull IBuildInfoProvider buildInfoProvider, + final @NotNull BuildInfoProvider buildInfoProvider, final @NotNull ILogger logger) { this( context, @@ -66,7 +66,7 @@ public RootChecker( RootChecker( final @NotNull Context context, - final @NotNull IBuildInfoProvider buildInfoProvider, + final @NotNull BuildInfoProvider buildInfoProvider, final @NotNull ILogger logger, final @NotNull String[] rootFiles, final @NotNull String[] rootPackages, diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/ANRWatchDogTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/ANRWatchDogTest.kt index b316524b1d3..94c5fe52361 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/ANRWatchDogTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/ANRWatchDogTest.kt @@ -21,7 +21,7 @@ class ANRWatchDogTest { @Test fun `when ANR is detected, callback is invoked with threads stacktrace`() { var anr: ApplicationNotResponding? = null - val handler = mock() + val handler = mock() val thread = mock() val expectedState = Thread.State.BLOCKED val stacktrace = StackTraceElement("class", "method", "fileName", 10) @@ -54,7 +54,7 @@ class ANRWatchDogTest { @Test fun `when ANR is not detected, callback is not invoked`() { var anr: ApplicationNotResponding? = null - val handler = mock() + val handler = mock() val thread = mock() var invoked = false whenever(handler.post(any())).then { i -> @@ -84,7 +84,7 @@ class ANRWatchDogTest { @Test fun `when ANR is detected and ActivityManager has ANR process, callback is invoked`() { var anr: ApplicationNotResponding? = null - val handler = mock() + val handler = mock() val thread = mock() val expectedState = Thread.State.BLOCKED val stacktrace = StackTraceElement("class", "method", "fileName", 10) @@ -125,7 +125,7 @@ class ANRWatchDogTest { @Test fun `when ANR is detected and ActivityManager has no ANR process, callback is not invoked`() { var anr: ApplicationNotResponding? = null - val handler = mock() + val handler = mock() val thread = mock() val expectedState = Thread.State.BLOCKED val stacktrace = StackTraceElement("class", "method", "fileName", 10) diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/ActivityLifecycleIntegrationTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/ActivityLifecycleIntegrationTest.kt index 6a5c88ef5aa..d41bf872bc1 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/ActivityLifecycleIntegrationTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/ActivityLifecycleIntegrationTest.kt @@ -20,6 +20,7 @@ import io.sentry.SentryLevel import io.sentry.SentryOptions import io.sentry.SentryTracer import io.sentry.SpanStatus +import io.sentry.TraceState import io.sentry.TransactionContext import io.sentry.TransactionFinishedCallback import java.util.Date @@ -46,11 +47,11 @@ class ActivityLifecycleIntegrationTest { val activityFramesTracker = mock() val transactionFinishedCallback = mock() val transaction = SentryTracer(context, hub, true, transactionFinishedCallback) - val buildInfo = mock() + val buildInfo = mock() fun getSut(apiVersion: Int = 29, importance: Int = RunningAppProcessInfo.IMPORTANCE_FOREGROUND): ActivityLifecycleIntegration { whenever(hub.options).thenReturn(options) - whenever(hub.startTransaction(any(), any(), anyOrNull(), any(), any())).thenReturn(transaction) + whenever(hub.startTransaction(any(), any(), anyOrNull(), any(), any())).thenReturn(transaction) whenever(buildInfo.sdkInfoVersion).thenReturn(apiVersion) whenever(application.getSystemService(any())).thenReturn(am) @@ -160,7 +161,8 @@ class ActivityLifecycleIntegrationTest { assertEquals("navigation", it.type) assertEquals(SentryLevel.INFO, it.level) // cant assert data, its not a public API - } + }, + anyOrNull() ) } @@ -172,7 +174,7 @@ class ActivityLifecycleIntegrationTest { val activity = mock() sut.onActivityCreated(activity, fixture.bundle) - verify(fixture.hub).addBreadcrumb(any()) + verify(fixture.hub).addBreadcrumb(any(), anyOrNull()) } @Test @@ -183,7 +185,7 @@ class ActivityLifecycleIntegrationTest { val activity = mock() sut.onActivityStarted(activity) - verify(fixture.hub).addBreadcrumb(any()) + verify(fixture.hub).addBreadcrumb(any(), anyOrNull()) } @Test @@ -194,7 +196,7 @@ class ActivityLifecycleIntegrationTest { val activity = mock() sut.onActivityResumed(activity) - verify(fixture.hub).addBreadcrumb(any()) + verify(fixture.hub).addBreadcrumb(any(), anyOrNull()) } @Test @@ -205,7 +207,7 @@ class ActivityLifecycleIntegrationTest { val activity = mock() sut.onActivityPaused(activity) - verify(fixture.hub).addBreadcrumb(any()) + verify(fixture.hub).addBreadcrumb(any(), anyOrNull()) } @Test @@ -216,7 +218,7 @@ class ActivityLifecycleIntegrationTest { val activity = mock() sut.onActivityStopped(activity) - verify(fixture.hub).addBreadcrumb(any()) + verify(fixture.hub).addBreadcrumb(any(), anyOrNull()) } @Test @@ -227,7 +229,7 @@ class ActivityLifecycleIntegrationTest { val activity = mock() sut.onActivitySaveInstanceState(activity, fixture.bundle) - verify(fixture.hub).addBreadcrumb(any()) + verify(fixture.hub).addBreadcrumb(any(), anyOrNull()) } @Test @@ -238,7 +240,7 @@ class ActivityLifecycleIntegrationTest { val activity = mock() sut.onActivityDestroyed(activity) - verify(fixture.hub).addBreadcrumb(any()) + verify(fixture.hub).addBreadcrumb(any(), anyOrNull()) } @Test @@ -249,7 +251,7 @@ class ActivityLifecycleIntegrationTest { val activity = mock() sut.onActivityCreated(activity, fixture.bundle) - verify(fixture.hub, never()).startTransaction(any(), any(), anyOrNull(), any(), any()) + verify(fixture.hub, never()).startTransaction(any(), any(), anyOrNull(), any(), any()) } @Test @@ -262,7 +264,7 @@ class ActivityLifecycleIntegrationTest { sut.onActivityCreated(activity, fixture.bundle) sut.onActivityCreated(activity, fixture.bundle) - verify(fixture.hub).startTransaction(any(), any(), anyOrNull(), any(), any()) + verify(fixture.hub).startTransaction(any(), any(), anyOrNull(), any(), any()) } @Test @@ -281,7 +283,7 @@ class ActivityLifecycleIntegrationTest { check { assertEquals("ui.load", it) }, - anyOrNull(), any(), any() + anyOrNull(), any(), any() ) } @@ -312,7 +314,7 @@ class ActivityLifecycleIntegrationTest { check { assertEquals("Activity", it) }, - any(), anyOrNull(), any(), any() + any(), anyOrNull(), any(), any() ) } @@ -370,6 +372,8 @@ class ActivityLifecycleIntegrationTest { check { assertEquals(SpanStatus.OK, it.status) }, + anyOrNull(), + anyOrNull(), anyOrNull() ) } @@ -391,6 +395,8 @@ class ActivityLifecycleIntegrationTest { check { assertEquals(SpanStatus.UNKNOWN_ERROR, it.status) }, + anyOrNull(), + anyOrNull(), anyOrNull() ) } @@ -406,7 +412,7 @@ class ActivityLifecycleIntegrationTest { sut.onActivityCreated(activity, fixture.bundle) sut.onActivityPostResumed(activity) - verify(fixture.hub, never()).captureTransaction(any(), anyOrNull()) + verify(fixture.hub, never()).captureTransaction(any(), anyOrNull(), anyOrNull(), anyOrNull()) } @Test @@ -417,7 +423,7 @@ class ActivityLifecycleIntegrationTest { val activity = mock() sut.onActivityPostResumed(activity) - verify(fixture.hub, never()).captureTransaction(any(), anyOrNull()) + verify(fixture.hub, never()).captureTransaction(any(), anyOrNull(), anyOrNull(), anyOrNull()) } @Test @@ -430,7 +436,7 @@ class ActivityLifecycleIntegrationTest { sut.onActivityCreated(activity, fixture.bundle) sut.onActivityDestroyed(activity) - verify(fixture.hub).captureTransaction(any(), anyOrNull()) + verify(fixture.hub).captureTransaction(any(), anyOrNull(), anyOrNull(), anyOrNull()) } @Test @@ -499,7 +505,7 @@ class ActivityLifecycleIntegrationTest { sut.onActivityCreated(mock(), mock()) sut.onActivityCreated(mock(), fixture.bundle) - verify(fixture.hub).captureTransaction(any(), anyOrNull()) + verify(fixture.hub).captureTransaction(any(), anyOrNull(), anyOrNull(), anyOrNull()) } @Test @@ -512,7 +518,7 @@ class ActivityLifecycleIntegrationTest { sut.onActivityCreated(activity, mock()) sut.onActivityResumed(activity) - verify(fixture.hub, never()).captureTransaction(any(), any()) + verify(fixture.hub, never()).captureTransaction(any(), any(), anyOrNull()) } @Test @@ -526,7 +532,7 @@ class ActivityLifecycleIntegrationTest { val activity = mock() sut.onActivityCreated(activity, mock()) - verify(fixture.hub).startTransaction(any(), any(), anyOrNull(), any(), any()) + verify(fixture.hub).startTransaction(any(), any(), anyOrNull(), any(), any()) } @Test @@ -539,7 +545,7 @@ class ActivityLifecycleIntegrationTest { sut.onActivityCreated(activity, mock()) sut.onActivityResumed(activity) - verify(fixture.hub).captureTransaction(any(), anyOrNull()) + verify(fixture.hub).captureTransaction(any(), anyOrNull(), anyOrNull(), anyOrNull()) } @Test @@ -735,6 +741,28 @@ class ActivityLifecycleIntegrationTest { verify(fixture.hub).startTransaction(any(), any(), eq(nullDate), any(), any()) } + @Test + fun `When transaction is finished, it gets removed from scope`() { + val sut = fixture.getSut() + fixture.options.tracesSampleRate = 1.0 + sut.register(fixture.hub, fixture.options) + + val activity = mock() + sut.onActivityCreated(activity, fixture.bundle) + + whenever(fixture.hub.configureScope(any())).thenAnswer { + val scope = Scope(fixture.options) + + scope.transaction = fixture.transaction + + sut.clearScope(scope, fixture.transaction) + + assertNull(scope.transaction) + } + + sut.onActivityDestroyed(activity) + } + private fun setAppStartTime(date: Date = Date(0)) { // set by SentryPerformanceProvider so forcing it here AppStartState.getInstance().setAppStartTime(0, date) diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt index ff23d95fd42..3f1ff1a586d 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt @@ -76,8 +76,8 @@ class AndroidOptionsInitializerTest { ) } - private fun createBuildInfo(minApi: Int = 16): IBuildInfoProvider { - val buildInfo = mock() + private fun createBuildInfo(minApi: Int = 16): BuildInfoProvider { + val buildInfo = mock() whenever(buildInfo.sdkInfoVersion).thenReturn(minApi) return buildInfo } @@ -132,6 +132,14 @@ class AndroidOptionsInitializerTest { assertNotNull(actual) } + @Test + fun `ScreenshotEventProcessor added to processors list`() { + fixture.initSut() + val actual = + fixture.sentryOptions.eventProcessors.any { it is ScreenshotEventProcessor } + assertNotNull(actual) + } + @Test fun `envelopesDir should be set at initialization`() { fixture.initSut() @@ -143,6 +151,18 @@ class AndroidOptionsInitializerTest { ) } + @Test + fun `profilingTracesDirPath should be set at initialization`() { + fixture.initSut() + + assertTrue( + fixture.sentryOptions.profilingTracesDirPath?.endsWith( + "${File.separator}cache${File.separator}sentry${File.separator}profiling_traces" + )!! + ) + assertFalse(File(fixture.sentryOptions.profilingTracesDirPath!!).exists()) + } + @Test fun `outboxDir should be set at initialization`() { fixture.initSut() @@ -218,6 +238,14 @@ class AndroidOptionsInitializerTest { assertTrue(fixture.sentryOptions.transportGate is AndroidTransportGate) } + @Test + fun `init should set Android transaction profiler`() { + fixture.initSut() + + assertNotNull(fixture.sentryOptions.transactionProfiler) + assertTrue(fixture.sentryOptions.transactionProfiler is AndroidTransactionProfiler) + } + @Test fun `NdkIntegration will load SentryNdk class and add to the integration list`() { fixture.initSutWithClassLoader(classToLoad = SentryNdk::class.java) 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 new file mode 100644 index 00000000000..a074a575cc2 --- /dev/null +++ b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidTransactionProfilerTest.kt @@ -0,0 +1,249 @@ +package io.sentry.android.core + +import android.content.Context +import android.os.Build +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.nhaarman.mockitokotlin2.mock +import com.nhaarman.mockitokotlin2.never +import com.nhaarman.mockitokotlin2.times +import com.nhaarman.mockitokotlin2.verify +import com.nhaarman.mockitokotlin2.whenever +import io.sentry.ILogger +import io.sentry.SentryLevel +import io.sentry.SentryTracer +import io.sentry.TransactionContext +import io.sentry.test.getCtor +import org.junit.runner.RunWith +import java.io.File +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertNull + +@RunWith(AndroidJUnit4::class) +class AndroidTransactionProfilerTest { + private lateinit var context: Context + + private val className = "io.sentry.android.core.AndroidTransactionProfiler" + private val ctorTypes = arrayOf(Context::class.java, SentryAndroidOptions::class.java, BuildInfoProvider::class.java) + private val fixture = Fixture() + + private class Fixture { + private val mockDsn = "http://key@localhost/proj" + val buildInfo = mock { + whenever(it.sdkInfoVersion).thenReturn(Build.VERSION_CODES.LOLLIPOP) + } + val mockLogger = mock() + val options = SentryAndroidOptions().apply { + dsn = mockDsn + isProfilingEnabled = true + isDebug = true + setLogger(mockLogger) + } + val transaction1 = SentryTracer(TransactionContext("", ""), mock()) + val transaction2 = SentryTracer(TransactionContext("", ""), mock()) + + fun getSut(context: Context, buildInfoProvider: BuildInfoProvider = buildInfo): AndroidTransactionProfiler = + AndroidTransactionProfiler(context, options, buildInfoProvider) + } + + @BeforeTest + fun `set up`() { + context = ApplicationProvider.getApplicationContext() + AndroidOptionsInitializer.init(fixture.options, context, fixture.mockLogger, false, false) + // Profiler doesn't start if the folder doesn't exists. + // Usually it's generated when calling Sentry.init, but for tests we can create it manually. + File(fixture.options.profilingTracesDirPath).mkdirs() + } + + @AfterTest + fun clear() { + context.cacheDir.deleteRecursively() + } + + @Test + fun `when null param is provided, invalid argument is thrown`() { + val ctor = className.getCtor(ctorTypes) + + assertFailsWith { + ctor.newInstance(arrayOf(null, mock(), mock())) + } + assertFailsWith { + ctor.newInstance(arrayOf(mock(), null, mock())) + } + assertFailsWith { + ctor.newInstance(arrayOf(mock(), mock(), null)) + } + } + + @Test + fun `profiler profiles current transaction`() { + val profiler = fixture.getSut(context) + profiler.onTransactionStart(fixture.transaction1) + val traceData = profiler.onTransactionFinish(fixture.transaction1) + assertEquals(fixture.transaction1.eventId.toString(), traceData!!.transactionId) + } + + @Test + fun `profiler works only on api 21+`() { + val buildInfo = mock { + whenever(it.sdkInfoVersion).thenReturn(Build.VERSION_CODES.KITKAT) + } + val profiler = fixture.getSut(context, buildInfo) + profiler.onTransactionStart(fixture.transaction1) + val traceData = profiler.onTransactionFinish(fixture.transaction1) + assertNull(traceData) + } + + @Test + fun `profiler on isProfilingEnabled false`() { + fixture.options.apply { + isProfilingEnabled = false + } + val profiler = fixture.getSut(context) + profiler.onTransactionStart(fixture.transaction1) + val traceData = profiler.onTransactionFinish(fixture.transaction1) + assertNull(traceData) + } + + @Test + fun `profiler evaluates isProfiling options only on first transaction profiling`() { + fixture.options.apply { + isProfilingEnabled = false + } + + // We create the profiler, and nothing goes wrong + val profiler = fixture.getSut(context) + verify(fixture.mockLogger, never()).log(SentryLevel.INFO, "Profiling is disabled in options.") + + // Regardless of how many times the profiler is started, the option is evaluated and logged only once + profiler.onTransactionStart(fixture.transaction1) + profiler.onTransactionStart(fixture.transaction1) + verify(fixture.mockLogger, times(1)).log(SentryLevel.INFO, "Profiling is disabled in options.") + } + + @Test + fun `profiler evaluates profilingTracesDirPath options only on first transaction profiling`() { + fixture.options.apply { + cacheDirPath = null + } + + // We create the profiler, and nothing goes wrong + val profiler = fixture.getSut(context) + verify(fixture.mockLogger, never()).log( + SentryLevel.WARNING, + "Disabling profiling because no profiling traces dir path is defined in options." + ) + + // Regardless of how many times the profiler is started, the option is evaluated and logged only once + profiler.onTransactionStart(fixture.transaction1) + profiler.onTransactionStart(fixture.transaction1) + verify(fixture.mockLogger, times(1)).log( + SentryLevel.WARNING, + "Disabling profiling because no profiling traces dir path is defined in options." + ) + } + + @Test + fun `profiler evaluates profilingTracesIntervalMillis options only on first transaction profiling`() { + fixture.options.apply { + profilingTracesIntervalMillis = 0 + } + + // We create the profiler, and nothing goes wrong + val profiler = fixture.getSut(context) + verify(fixture.mockLogger, never()).log( + SentryLevel.WARNING, + "Disabling profiling because trace interval is set to %d milliseconds", + 0L + ) + + // Regardless of how many times the profiler is started, the option is evaluated and logged only once + profiler.onTransactionStart(fixture.transaction1) + profiler.onTransactionStart(fixture.transaction1) + verify(fixture.mockLogger, times(1)).log( + SentryLevel.WARNING, + "Disabling profiling because trace interval is set to %d milliseconds", + 0L + ) + } + + @Test + fun `profiler on tracesDirPath null`() { + fixture.options.apply { + cacheDirPath = null + } + val profiler = fixture.getSut(context) + profiler.onTransactionStart(fixture.transaction1) + val traceData = profiler.onTransactionFinish(fixture.transaction1) + assertNull(traceData) + } + + @Test + fun `profiler on tracesDirPath empty`() { + fixture.options.apply { + cacheDirPath = null + } + val profiler = fixture.getSut(context) + profiler.onTransactionStart(fixture.transaction1) + val traceData = profiler.onTransactionFinish(fixture.transaction1) + assertNull(traceData) + } + + @Test + fun `profiler on profilingTracesIntervalMillis 0`() { + fixture.options.apply { + profilingTracesIntervalMillis = 0 + } + val profiler = fixture.getSut(context) + profiler.onTransactionStart(fixture.transaction1) + val traceData = profiler.onTransactionFinish(fixture.transaction1) + assertNull(traceData) + } + + @Test + fun `onTransactionFinish works only if previously started`() { + val profiler = fixture.getSut(context) + val traceData = profiler.onTransactionFinish(fixture.transaction1) + assertNull(traceData) + } + + @Test + fun `onTransactionFinish returns timedOutData to the timed out transaction once, even after other transactions`() { + val profiler = fixture.getSut(context) + + // Start and finish first transaction profiling + profiler.onTransactionStart(fixture.transaction1) + val traceData = profiler.onTransactionFinish(fixture.transaction1) + + // Set timed out data + profiler.setTimedOutProfilingData(traceData) + + // Start and finish second transaction profiling + profiler.onTransactionStart(fixture.transaction2) + assertEquals(fixture.transaction2.eventId.toString(), profiler.onTransactionFinish(fixture.transaction2)!!.transactionId) + + // First transaction finishes: timed out data is returned + val traceData2 = profiler.onTransactionFinish(fixture.transaction1) + assertEquals(traceData, traceData2) + + // If first transaction is finished again, nothing is returned + assertNull(profiler.onTransactionFinish(fixture.transaction1)) + } + + @Test + fun `profiling stops and returns data only when starting transaction finishes`() { + val profiler = fixture.getSut(context) + profiler.onTransactionStart(fixture.transaction1) + profiler.onTransactionStart(fixture.transaction2) + + var traceData = profiler.onTransactionFinish(fixture.transaction2) + assertNull(traceData) + + traceData = profiler.onTransactionFinish(fixture.transaction1) + assertEquals(fixture.transaction1.eventId.toString(), traceData!!.transactionId) + } +} diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/AppComponentsBreadcrumbsIntegrationTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/AppComponentsBreadcrumbsIntegrationTest.kt index aa6adc2971a..70ee62d905c 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/AppComponentsBreadcrumbsIntegrationTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/AppComponentsBreadcrumbsIntegrationTest.kt @@ -5,6 +5,7 @@ import android.content.Context import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.nhaarman.mockitokotlin2.any +import com.nhaarman.mockitokotlin2.anyOrNull import com.nhaarman.mockitokotlin2.check import com.nhaarman.mockitokotlin2.mock import com.nhaarman.mockitokotlin2.never @@ -139,7 +140,8 @@ class AppComponentsBreadcrumbsIntegrationTest { assertEquals("navigation", it.type) assertEquals(SentryLevel.INFO, it.level) // cant assert data, its not a public API - } + }, + anyOrNull() ) } } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/AppLifecycleIntegrationTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/AppLifecycleIntegrationTest.kt index b78772f3205..3a92b335d92 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/AppLifecycleIntegrationTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/AppLifecycleIntegrationTest.kt @@ -16,7 +16,7 @@ class AppLifecycleIntegrationTest { private class Fixture { val hub = mock() - val handler = mock() + val handler = mock() val options = SentryAndroidOptions() fun getSut(): AppLifecycleIntegration { diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/ConnectivityCheckerTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/ConnectivityCheckerTest.kt index 04977d29d02..f5f23d6033e 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/ConnectivityCheckerTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/ConnectivityCheckerTest.kt @@ -28,7 +28,7 @@ class ConnectivityCheckerTest { private lateinit var contextMock: Context private lateinit var connectivityManager: ConnectivityManager private lateinit var networkInfo: NetworkInfo - private lateinit var buildInfo: IBuildInfoProvider + private lateinit var buildInfo: BuildInfoProvider private lateinit var network: Network private lateinit var networkCapabilities: NetworkCapabilities @@ -100,7 +100,7 @@ class ConnectivityCheckerTest { @Test fun `When sdkInfoVersion is not min Marshmallow, return null for getConnectionType`() { - val buildInfo = mock() + val buildInfo = mock() whenever(buildInfo.sdkInfoVersion).thenReturn(Build.VERSION_CODES.ICE_CREAM_SANDWICH) assertNull(ConnectivityChecker.getConnectionType(mock(), mock(), buildInfo)) diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/ContextUtilsUnitTests.kt b/sentry-android-core/src/test/java/io/sentry/android/core/ContextUtilsUnitTests.kt index 6859a39f005..cf8e2883772 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/ContextUtilsUnitTests.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/ContextUtilsUnitTests.kt @@ -4,6 +4,7 @@ import android.content.Context import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.nhaarman.mockitokotlin2.mock +import com.nhaarman.mockitokotlin2.spy import org.junit.runner.RunWith import kotlin.test.BeforeTest import kotlin.test.Test @@ -21,7 +22,7 @@ class ContextUtilsUnitTests { } @Test - fun `Given a valid context, returns a valid PackageIndo`() { + fun `Given a valid context, returns a valid PackageInfo`() { val packageInfo = ContextUtils.getPackageInfo(context, mock()) assertNotNull(packageInfo) } @@ -40,4 +41,15 @@ class ContextUtilsUnitTests { assertNotNull(versionCode) } + + @Test + fun `Given a valid PackageInfo, returns a valid versionName`() { + // VersionName is null during tests, so we mock it the second time + val packageInfo = ContextUtils.getPackageInfo(context, mock())!! + val versionName = ContextUtils.getVersionName(packageInfo) + assertNull(versionName) + val mockedPackageInfo = spy(packageInfo) { it.versionName = "" } + val mockedVersionName = ContextUtils.getVersionName(mockedPackageInfo) + assertNotNull(mockedVersionName) + } } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/DefaultAndroidEventProcessorTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/DefaultAndroidEventProcessorTest.kt index 904a2c6f1ea..095c2e84882 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/DefaultAndroidEventProcessorTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/DefaultAndroidEventProcessorTest.kt @@ -1,6 +1,7 @@ package io.sentry.android.core import android.content.Context +import android.os.Build import android.os.Looper import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 @@ -9,7 +10,9 @@ import com.nhaarman.mockitokotlin2.eq import com.nhaarman.mockitokotlin2.mock import com.nhaarman.mockitokotlin2.never import com.nhaarman.mockitokotlin2.verify +import com.nhaarman.mockitokotlin2.whenever import io.sentry.DiagnosticLogger +import io.sentry.Hint import io.sentry.ILogger import io.sentry.SentryEvent import io.sentry.SentryLevel @@ -26,6 +29,7 @@ import io.sentry.protocol.SentryThread import io.sentry.protocol.SentryTransaction import io.sentry.protocol.User import io.sentry.test.getCtor +import io.sentry.util.HintUtils import org.junit.runner.RunWith import java.util.Locale import kotlin.test.BeforeTest @@ -43,14 +47,15 @@ class DefaultAndroidEventProcessorTest { private lateinit var context: Context private val className = "io.sentry.android.core.DefaultAndroidEventProcessor" - private val ctorTypes = arrayOf(Context::class.java, ILogger::class.java, IBuildInfoProvider::class.java) + private val ctorTypes = + arrayOf(Context::class.java, ILogger::class.java, BuildInfoProvider::class.java) init { Locale.setDefault(Locale.US) } private class Fixture { - val buildInfo = mock() + val buildInfo = mock() val options = SentryOptions().apply { setDebug(true) setLogger(mock()) @@ -97,17 +102,34 @@ class DefaultAndroidEventProcessorTest { fun `when null buildInfo is provided, invalid argument is thrown`() { val ctor = className.getCtor(ctorTypes) - val params = arrayOf(null, null, mock()) + val params = arrayOf(null, null, mock()) assertFailsWith { ctor.newInstance(params) } } @Test fun `When Event and hint is not Cached, data should be applied`() { + whenever(fixture.buildInfo.sdkInfoVersion).thenReturn(Build.VERSION_CODES.M) val sut = fixture.getSut(context) - assertNotNull(sut.process(SentryEvent(), null)) { + assertNotNull(sut.process(SentryEvent(), Hint())) { assertNotNull(it.contexts.app) assertNotNull(it.dist) + + // assert adds permissions as unknown + val permissions = it.contexts.app!!.permissions + assertNotNull(permissions) + } + } + + @Test + fun `when Android version is below JELLY_BEAN, does not add permissions`() { + whenever(fixture.buildInfo.sdkInfoVersion).thenReturn(Build.VERSION_CODES.ICE_CREAM_SANDWICH) + val sut = fixture.getSut(context) + + assertNotNull(sut.process(SentryEvent(), Hint())) { + // assert adds permissions + val unknown = it.contexts.app!!.permissions + assertNull(unknown) } } @@ -115,7 +137,12 @@ class DefaultAndroidEventProcessorTest { fun `When Transaction and hint is not Cached, data should be applied`() { val sut = fixture.getSut(context) - assertNotNull(sut.process(SentryTransaction(fixture.sentryTracer), null)) { + assertNotNull( + sut.process( + SentryTransaction(fixture.sentryTracer), + Hint() + ) + ) { assertNotNull(it.contexts.app) assertNotNull(it.dist) } @@ -132,7 +159,7 @@ class DefaultAndroidEventProcessorTest { threads = mutableListOf(sentryThread) } - assertNotNull(sut.process(event, null)) { + assertNotNull(sut.process(event, Hint())) { assertNotNull(it.threads) { threads -> assertTrue(threads.first().isCurrent == true) } @@ -151,18 +178,39 @@ class DefaultAndroidEventProcessorTest { ) } - assertNotNull(sut.process(event, null)) { + assertNotNull(sut.process(event, Hint())) { assertNotNull(it.threads) { threads -> assertFalse(threads.first().isCurrent == true) } } } + @Test + fun `Current should remain true`() { + val sut = fixture.getSut(context) + + val event = SentryEvent().apply { + threads = mutableListOf( + SentryThread().apply { + id = 10L + isCurrent = true + } + ) + } + + assertNotNull(sut.process(event, Hint())) { + assertNotNull(it.threads) { threads -> + assertTrue(threads.first().isCurrent == true) + } + } + } + @Test fun `When Event and hint is Cached, data should not be applied`() { val sut = fixture.getSut(context) - assertNotNull(sut.process(SentryEvent(), CachedEvent())) { + val hints = HintUtils.createWithTypeCheckHint(CachedEvent()) + assertNotNull(sut.process(SentryEvent(), hints)) { assertNull(it.contexts.app) assertNull(it.debugMeta) assertNull(it.dist) @@ -173,7 +221,8 @@ class DefaultAndroidEventProcessorTest { fun `When Transaction and hint is Cached, data should not be applied`() { val sut = fixture.getSut(context) - assertNotNull(sut.process(SentryTransaction(fixture.sentryTracer), CachedEvent())) { + val hints = HintUtils.createWithTypeCheckHint(CachedEvent()) + assertNotNull(sut.process(SentryTransaction(fixture.sentryTracer), hints)) { assertNull(it.contexts.app) assertNull(it.dist) } @@ -182,8 +231,8 @@ class DefaultAndroidEventProcessorTest { @Test fun `When Event and hint is Cached, userId is applied anyway`() { val sut = fixture.getSut(context) - - assertNotNull(sut.process(SentryEvent(), CachedEvent())) { + val hints = HintUtils.createWithTypeCheckHint(CachedEvent()) + assertNotNull(sut.process(SentryEvent(), hints)) { assertNotNull(it.user) } } @@ -192,7 +241,8 @@ class DefaultAndroidEventProcessorTest { fun `When Transaction and hint is Cached, userId is applied anyway`() { val sut = fixture.getSut(context) - assertNotNull(sut.process(SentryTransaction(fixture.sentryTracer), CachedEvent())) { + val hints = HintUtils.createWithTypeCheckHint(CachedEvent()) + assertNotNull(sut.process(SentryTransaction(fixture.sentryTracer), hints)) { assertNotNull(it.user) } } @@ -208,7 +258,7 @@ class DefaultAndroidEventProcessorTest { setUser(user) } - assertNotNull(sut.process(event, null)) { + assertNotNull(sut.process(event, Hint())) { assertNotNull(it.user) assertSame(user, it.user) } @@ -222,7 +272,7 @@ class DefaultAndroidEventProcessorTest { user = User() } - assertNotNull(sut.process(event, null)) { + assertNotNull(sut.process(event, Hint())) { assertNotNull(it.user) assertNotNull(it.user!!.id) } @@ -245,25 +295,33 @@ class DefaultAndroidEventProcessorTest { fun `Processor won't throw exception`() { val sut = fixture.getSut(context) - sut.process(SentryEvent(), null) + sut.process(SentryEvent(), Hint()) - verify((fixture.options.logger as DiagnosticLogger).logger, never())!!.log(eq(SentryLevel.ERROR), any(), any()) + verify( + (fixture.options.logger as DiagnosticLogger).logger, + never() + )!!.log(eq(SentryLevel.ERROR), any(), any()) } @Test fun `Processor won't throw exception when theres a hint`() { - val processor = DefaultAndroidEventProcessor(context, fixture.options.logger, fixture.buildInfo, mock()) + val processor = + DefaultAndroidEventProcessor(context, fixture.options.logger, fixture.buildInfo, mock()) - processor.process(SentryEvent(), CachedEvent()) + val hints = HintUtils.createWithTypeCheckHint(CachedEvent()) + processor.process(SentryEvent(), hints) - verify((fixture.options.logger as DiagnosticLogger).logger, never())!!.log(eq(SentryLevel.ERROR), any(), any()) + verify( + (fixture.options.logger as DiagnosticLogger).logger, + never() + )!!.log(eq(SentryLevel.ERROR), any(), any()) } @Test fun `When event is processed, sideLoaded info should be set`() { val sut = fixture.getSut(context) - assertNotNull(sut.process(SentryEvent(), null)) { + assertNotNull(sut.process(SentryEvent(), Hint())) { assertNotNull(it.getTag("isSideLoaded")) } } @@ -279,7 +337,7 @@ class DefaultAndroidEventProcessorTest { contexts.setOperatingSystem(osLinux) } - assertNotNull(sut.process(event, null)) { + assertNotNull(sut.process(event, Hint())) { assertSame(osLinux, (it.contexts["os_linux"] as OperatingSystem)) assertEquals("Android", it.contexts.operatingSystem!!.name) } @@ -296,7 +354,7 @@ class DefaultAndroidEventProcessorTest { contexts.setOperatingSystem(osNoName) } - assertNotNull(sut.process(event, null)) { + assertNotNull(sut.process(event, Hint())) { assertSame(osNoName, (it.contexts["os_1"] as OperatingSystem)) assertEquals("Android", it.contexts.operatingSystem!!.name) } @@ -306,7 +364,8 @@ class DefaultAndroidEventProcessorTest { fun `When hint is Cached, memory data should not be applied`() { val sut = fixture.getSut(context) - assertNotNull(sut.process(SentryEvent(), CachedEvent())) { + val hints = HintUtils.createWithTypeCheckHint(CachedEvent()) + assertNotNull(sut.process(SentryEvent(), hints)) { assertNull(it.contexts.device!!.freeMemory) assertNull(it.contexts.device!!.isLowMemory) } @@ -316,7 +375,7 @@ class DefaultAndroidEventProcessorTest { fun `When hint is not Cached, memory data should be applied`() { val sut = fixture.getSut(context) - assertNotNull(sut.process(SentryEvent(), null)) { + assertNotNull(sut.process(SentryEvent(), Hint())) { assertNotNull(it.contexts.device!!.freeMemory) assertNotNull(it.contexts.device!!.isLowMemory) } @@ -326,7 +385,12 @@ class DefaultAndroidEventProcessorTest { fun `Device's context is set on transactions`() { val sut = fixture.getSut(context) - assertNotNull(sut.process(SentryTransaction(fixture.sentryTracer), null)) { + assertNotNull( + sut.process( + SentryTransaction(fixture.sentryTracer), + Hint() + ) + ) { assertNotNull(it.contexts.device) } } @@ -335,7 +399,12 @@ class DefaultAndroidEventProcessorTest { fun `Device's OS is set on transactions`() { val sut = fixture.getSut(context) - assertNotNull(sut.process(SentryTransaction(fixture.sentryTracer), null)) { + assertNotNull( + sut.process( + SentryTransaction(fixture.sentryTracer), + Hint() + ) + ) { assertNotNull(it.contexts.operatingSystem) } } @@ -344,7 +413,12 @@ class DefaultAndroidEventProcessorTest { fun `Transaction do not set device's context that requires heavy work`() { val sut = fixture.getSut(context) - assertNotNull(sut.process(SentryTransaction(fixture.sentryTracer), null)) { + assertNotNull( + sut.process( + SentryTransaction(fixture.sentryTracer), + Hint() + ) + ) { val device = it.contexts.device!! assertNull(device.batteryLevel) assertNull(device.isCharging) @@ -364,7 +438,7 @@ class DefaultAndroidEventProcessorTest { fun `Event sets device's context that requires heavy work`() { val sut = fixture.getSut(context) - assertNotNull(sut.process(SentryEvent(), null)) { + assertNotNull(sut.process(SentryEvent(), Hint())) { val device = it.contexts.device!! assertNotNull(device.freeMemory) assertNotNull(device.isLowMemory) @@ -386,7 +460,7 @@ class DefaultAndroidEventProcessorTest { fun `Event sets language and locale`() { val sut = fixture.getSut(context) - assertNotNull(sut.process(SentryEvent(), null)) { + assertNotNull(sut.process(SentryEvent(), Hint())) { val device = it.contexts.device!! assertEquals("en", device.language) assertEquals("en_US", device.locale) diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/EnvelopeFileObserverTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/EnvelopeFileObserverTest.kt index 1d25cb60800..43338b5f96b 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/EnvelopeFileObserverTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/EnvelopeFileObserverTest.kt @@ -15,6 +15,7 @@ import io.sentry.hints.ApplyScopeData import io.sentry.hints.Resettable import io.sentry.hints.Retryable import io.sentry.hints.SubmissionResult +import io.sentry.util.HintUtils import org.junit.runner.RunWith import java.io.File import kotlin.test.Test @@ -73,7 +74,7 @@ class EnvelopeFileObserverTest { verify(fixture.envelopeSender).processEnvelopeFile( eq(fixture.path + File.separator + fixture.fileName), - check { it is ApplyScopeData } + check { HintUtils.hasType(it, ApplyScopeData::class.java) } ) } @@ -83,7 +84,7 @@ class EnvelopeFileObserverTest { verify(fixture.envelopeSender).processEnvelopeFile( eq(fixture.path + File.separator + fixture.fileName), - check { it is Resettable } + check { HintUtils.hasType(it, Resettable::class.java) } ) } @@ -93,14 +94,14 @@ class EnvelopeFileObserverTest { verify(fixture.envelopeSender).processEnvelopeFile( eq(fixture.path + File.separator + fixture.fileName), - check { - (it as SubmissionResult).setResult(true) - (it as Retryable).isRetry = true + check { hints -> + HintUtils.runIfHasType(hints, SubmissionResult::class.java) { it.setResult(true) } + HintUtils.runIfHasType(hints, Retryable::class.java) { it.isRetry = true } - (it as Resettable).reset() + HintUtils.runIfHasType(hints, Resettable::class.java) { it.reset() } - assertFalse(it.isRetry) - assertFalse(it.isSuccess) + assertFalse((HintUtils.getSentrySdkHint(hints) as Retryable).isRetry) + assertFalse((HintUtils.getSentrySdkHint(hints) as SubmissionResult).isSuccess) } ) } 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 af3e83e0d7e..81f3efb1cd0 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 @@ -548,17 +548,47 @@ class ManifestMetadataReaderTest { assertTrue(fixture.options.isEnableNdk) } + @Test + fun `applyMetadata reads SDK name from metadata`() { + // Arrange + val expectedValue = "custom.sdk" + + val bundle = bundleOf(ManifestMetadataReader.SDK_NAME to expectedValue) + val context = fixture.getContext(metaData = bundle) + + // Act + ManifestMetadataReader.applyMetadata(context, fixture.options) + + // Assert + assertEquals(expectedValue, fixture.options.sdkVersion?.name) + } + + @Test + fun `applyMetadata reads SDK version from metadata`() { + // Arrange + val expectedValue = "1.2.3-alpha.0" + + val bundle = bundleOf(ManifestMetadataReader.SDK_VERSION to expectedValue) + val context = fixture.getContext(metaData = bundle) + + // Act + ManifestMetadataReader.applyMetadata(context, fixture.options) + + // Assert + assertEquals(expectedValue, fixture.options.sdkVersion?.version) + } + @Test fun `applyMetadata reads enableScopeSync to options`() { // Arrange - val bundle = bundleOf(ManifestMetadataReader.NDK_SCOPE_SYNC_ENABLE to true) + val bundle = bundleOf(ManifestMetadataReader.NDK_SCOPE_SYNC_ENABLE to false) val context = fixture.getContext(metaData = bundle) // Act ManifestMetadataReader.applyMetadata(context, fixture.options) // Assert - assertTrue(fixture.options.isEnableScopeSync) + assertFalse(fixture.options.isEnableScopeSync) } @Test @@ -570,7 +600,7 @@ class ManifestMetadataReaderTest { ManifestMetadataReader.applyMetadata(context, fixture.options) // Assert - assertFalse(fixture.options.isEnableScopeSync) + assertTrue(fixture.options.isEnableScopeSync) } @Test @@ -689,6 +719,31 @@ class ManifestMetadataReaderTest { assertFalse(fixture.options.isTraceSampling) } + @Test + fun `applyMetadata reads enableTracesProfiling to options`() { + // Arrange + val bundle = bundleOf(ManifestMetadataReader.TRACES_PROFILING_ENABLE to true) + val context = fixture.getContext(metaData = bundle) + + // Act + ManifestMetadataReader.applyMetadata(context, fixture.options) + + // Assert + assertTrue(fixture.options.isProfilingEnabled) + } + + @Test + fun `applyMetadata reads enableTracesProfiling to options and keeps default`() { + // Arrange + val context = fixture.getContext() + + // Act + ManifestMetadataReader.applyMetadata(context, fixture.options) + + // Assert + assertFalse(fixture.options.isProfilingEnabled) + } + @Test fun `applyMetadata reads tracingOrigins to options`() { // Arrange @@ -763,4 +818,105 @@ class ManifestMetadataReaderTest { // Assert assertTrue(fixture.options.isEnableUserInteractionBreadcrumbs) } + + @Test + fun `applyMetadata reads attach screenshots to options`() { + // Arrange + val bundle = bundleOf(ManifestMetadataReader.ATTACH_SCREENSHOT to true) + val context = fixture.getContext(metaData = bundle) + + // Act + ManifestMetadataReader.applyMetadata(context, fixture.options) + + // Assert + assertTrue(fixture.options.isAttachScreenshot) + } + + @Test + fun `applyMetadata reads attach screenshots and keep default value if not found`() { + // Arrange + val context = fixture.getContext() + + // Act + ManifestMetadataReader.applyMetadata(context, fixture.options) + + // Assert + assertFalse(fixture.options.isAttachScreenshot) + } + + @Test + fun `applyMetadata reads send client reports to options`() { + // Arrange + val bundle = bundleOf(ManifestMetadataReader.CLIENT_REPORTS_ENABLE to false) + val context = fixture.getContext(metaData = bundle) + + // Act + ManifestMetadataReader.applyMetadata(context, fixture.options) + + // Assert + assertFalse(fixture.options.isSendClientReports) + } + + @Test + fun `applyMetadata reads send client reports and keep default value if not found`() { + // Arrange + val context = fixture.getContext() + + // Act + ManifestMetadataReader.applyMetadata(context, fixture.options) + + // Assert + assertTrue(fixture.options.isSendClientReports) + } + + @Test + fun `applyMetadata reads user interaction tracing to options`() { + // Arrange + val bundle = bundleOf(ManifestMetadataReader.TRACES_UI_ENABLE to true) + val context = fixture.getContext(metaData = bundle) + + // Act + ManifestMetadataReader.applyMetadata(context, fixture.options) + + // Assert + assertTrue(fixture.options.isEnableUserInteractionTracing) + } + + @Test + fun `applyMetadata reads user interaction tracing and keep default value if not found`() { + // Arrange + val context = fixture.getContext() + + // Act + ManifestMetadataReader.applyMetadata(context, fixture.options) + + // Assert + assertFalse(fixture.options.isEnableUserInteractionTracing) + } + + @Test + fun `applyMetadata reads idleTimeout from metadata`() { + // Arrange + val expectedIdleTimeout = 1500 + val bundle = bundleOf(ManifestMetadataReader.IDLE_TIMEOUT to expectedIdleTimeout) + val context = fixture.getContext(metaData = bundle) + + // Act + ManifestMetadataReader.applyMetadata(context, fixture.options) + + // Assert + assertEquals(expectedIdleTimeout.toLong(), fixture.options.idleTimeout) + } + + @Test + fun `applyMetadata without specifying idleTimeout, stays default`() { + // Arrange + val context = fixture.getContext() + + // Act + ManifestMetadataReader.applyMetadata(context, fixture.options) + + // Assert + assertEquals(3000L, fixture.options.idleTimeout) + } } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/NdkIntegrationTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/NdkIntegrationTest.kt index 40ce519caff..0e73f8f2161 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/NdkIntegrationTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/NdkIntegrationTest.kt @@ -35,6 +35,7 @@ class NdkIntegrationTest { verify(fixture.logger, never()).log(eq(SentryLevel.ERROR), any(), any()) assertTrue(options.isEnableNdk) + assertTrue(options.isEnableScopeSync) } @Test @@ -46,11 +47,13 @@ class NdkIntegrationTest { integration.register(fixture.hub, options) assertTrue(options.isEnableNdk) + assertTrue(options.isEnableScopeSync) integration.close() verify(fixture.logger, never()).log(eq(SentryLevel.ERROR), any(), any()) assertFalse(options.isEnableNdk) + assertFalse(options.isEnableScopeSync) } @Test @@ -64,6 +67,7 @@ class NdkIntegrationTest { verify(fixture.logger, never()).log(eq(SentryLevel.ERROR), any(), any()) assertFalse(options.isEnableNdk) + assertFalse(options.isEnableScopeSync) } @Test @@ -77,6 +81,7 @@ class NdkIntegrationTest { verify(fixture.logger, never()).log(eq(SentryLevel.ERROR), any(), any()) assertFalse(options.isEnableNdk) + assertFalse(options.isEnableScopeSync) } @Test @@ -90,6 +95,7 @@ class NdkIntegrationTest { verify(fixture.logger).log(eq(SentryLevel.ERROR), any(), any()) assertFalse(options.isEnableNdk) + assertFalse(options.isEnableScopeSync) } @Test @@ -101,11 +107,13 @@ class NdkIntegrationTest { integration.register(fixture.hub, options) assertTrue(options.isEnableNdk) + assertTrue(options.isEnableScopeSync) integration.close() verify(fixture.logger).log(eq(SentryLevel.ERROR), any(), any()) assertFalse(options.isEnableNdk) + assertFalse(options.isEnableScopeSync) } @Test @@ -119,6 +127,7 @@ class NdkIntegrationTest { verify(fixture.logger).log(eq(SentryLevel.ERROR), any(), any()) assertFalse(options.isEnableNdk) + assertFalse(options.isEnableScopeSync) } @Test @@ -132,6 +141,7 @@ class NdkIntegrationTest { verify(fixture.logger).log(eq(SentryLevel.ERROR), any()) assertFalse(options.isEnableNdk) + assertFalse(options.isEnableScopeSync) } @Test @@ -145,12 +155,13 @@ class NdkIntegrationTest { verify(fixture.logger).log(eq(SentryLevel.ERROR), any()) assertFalse(options.isEnableNdk) + assertFalse(options.isEnableScopeSync) } private fun getOptions(enableNdk: Boolean = true, cacheDir: String? = "abc"): SentryAndroidOptions { return SentryAndroidOptions().apply { setLogger(fixture.logger) - setDebug(true) + isDebug = true isEnableNdk = enableNdk cacheDirPath = cacheDir } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/PerformanceAndroidEventProcessorTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/PerformanceAndroidEventProcessorTest.kt index 47d561e5c1f..30a78d411d5 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/PerformanceAndroidEventProcessorTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/PerformanceAndroidEventProcessorTest.kt @@ -3,6 +3,7 @@ package io.sentry.android.core import com.nhaarman.mockitokotlin2.any import com.nhaarman.mockitokotlin2.mock import com.nhaarman.mockitokotlin2.whenever +import io.sentry.Hint import io.sentry.IHub import io.sentry.SentryTracer import io.sentry.TransactionContext @@ -45,7 +46,7 @@ class PerformanceAndroidEventProcessorTest { var tr = getTransaction() setAppStart() - tr = sut.process(tr, null) + tr = sut.process(tr, Hint()) assertTrue(tr.measurements.containsKey("app_start_cold")) } @@ -57,7 +58,7 @@ class PerformanceAndroidEventProcessorTest { var tr = getTransaction("app.start.warm") setAppStart(false) - tr = sut.process(tr, null) + tr = sut.process(tr, Hint()) assertTrue(tr.measurements.containsKey("app_start_warm")) } @@ -69,10 +70,10 @@ class PerformanceAndroidEventProcessorTest { var tr1 = getTransaction() setAppStart(false) - tr1 = sut.process(tr1, null) + tr1 = sut.process(tr1, Hint()) var tr2 = getTransaction() - tr2 = sut.process(tr2, null) + tr2 = sut.process(tr2, Hint()) assertTrue(tr1.measurements.containsKey("app_start_warm")) assertTrue(tr2.measurements.isEmpty()) @@ -84,7 +85,7 @@ class PerformanceAndroidEventProcessorTest { var tr = getTransaction() - tr = sut.process(tr, null) + tr = sut.process(tr, Hint()) assertTrue(tr.measurements.isEmpty()) } @@ -95,7 +96,7 @@ class PerformanceAndroidEventProcessorTest { var tr = getTransaction() - tr = sut.process(tr, null) + tr = sut.process(tr, Hint()) assertTrue(tr.measurements.isEmpty()) } @@ -106,7 +107,7 @@ class PerformanceAndroidEventProcessorTest { var tr = getTransaction("task") - tr = sut.process(tr, null) + tr = sut.process(tr, Hint()) assertTrue(tr.measurements.isEmpty()) } @@ -116,7 +117,7 @@ class PerformanceAndroidEventProcessorTest { val sut = fixture.getSut() var tr = getTransaction("task") - tr = sut.process(tr, null) + tr = sut.process(tr, Hint()) assertTrue(tr.measurements.isEmpty()) } @@ -126,7 +127,7 @@ class PerformanceAndroidEventProcessorTest { val sut = fixture.getSut(null) var tr = getTransaction("task") - tr = sut.process(tr, null) + tr = sut.process(tr, Hint()) assertTrue(tr.measurements.isEmpty()) } @@ -141,7 +142,7 @@ class PerformanceAndroidEventProcessorTest { val metrics = mapOf("frames_total" to MeasurementValue(1f)) whenever(fixture.activityFramesTracker.takeMetrics(any())).thenReturn(metrics) - tr = sut.process(tr, null) + tr = sut.process(tr, Hint()) assertTrue(tr.measurements.containsKey("frames_total")) } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/ScreenshotEventProcessorTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/ScreenshotEventProcessorTest.kt new file mode 100644 index 00000000000..d57c510300b --- /dev/null +++ b/sentry-android-core/src/test/java/io/sentry/android/core/ScreenshotEventProcessorTest.kt @@ -0,0 +1,194 @@ +package io.sentry.android.core + +import android.app.Activity +import android.app.Application +import android.view.View +import android.view.Window +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.nhaarman.mockitokotlin2.any +import com.nhaarman.mockitokotlin2.mock +import com.nhaarman.mockitokotlin2.never +import com.nhaarman.mockitokotlin2.verify +import com.nhaarman.mockitokotlin2.whenever +import io.sentry.Attachment +import io.sentry.Hint +import io.sentry.MainEventProcessor +import io.sentry.SentryEvent +import io.sentry.TypeCheckHint.ANDROID_ACTIVITY +import org.junit.runner.RunWith +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull +import kotlin.test.assertSame +import kotlin.test.assertTrue + +@RunWith(AndroidJUnit4::class) +class ScreenshotEventProcessorTest { + + private class Fixture { + val application = mock() + val buildInfo = mock() + val activity = mock() + val window = mock() + val view = mock() + val rootView = mock() + val options = SentryAndroidOptions().apply { + dsn = "https://key@sentry.io/proj" + } + val mainProcessor = MainEventProcessor(options) + + init { + whenever(rootView.width).thenReturn(1) + whenever(rootView.height).thenReturn(1) + whenever(view.rootView).thenReturn(rootView) + whenever(window.decorView).thenReturn(view) + whenever(activity.window).thenReturn(window) + } + + fun getSut(attachScreenshot: Boolean = false): ScreenshotEventProcessor { + options.isAttachScreenshot = attachScreenshot + + return ScreenshotEventProcessor(application, options, buildInfo) + } + } + + private lateinit var fixture: Fixture + + @BeforeTest + fun `set up`() { + fixture = Fixture() + } + + @Test + fun `when attach screenshot is enabled, registerActivityLifecycleCallbacks`() { + fixture.getSut(true) + + verify(fixture.application).registerActivityLifecycleCallbacks(any()) + } + + @Test + fun `when attach screenshot is disabled, does not registerActivityLifecycleCallbacks`() { + fixture.getSut(false) + + verify(fixture.application, never()).registerActivityLifecycleCallbacks(any()) + } + + @Test + fun `when close is called and attach screenshot is enabled, unregisterActivityLifecycleCallbacks`() { + val sut = fixture.getSut(true) + + sut.close() + + verify(fixture.application).unregisterActivityLifecycleCallbacks(any()) + } + + @Test + fun `when close is called and attach screenshot is disabled, does not unregisterActivityLifecycleCallbacks`() { + val sut = fixture.getSut(false) + + sut.close() + + verify(fixture.application, never()).unregisterActivityLifecycleCallbacks(any()) + } + + @Test + fun `when process is called and attachScreenshot is disabled, does nothing`() { + val sut = fixture.getSut(false) + val hint = Hint() + + sut.onActivityCreated(fixture.activity, null) + + val event = fixture.mainProcessor.process(getEvent(), hint) + sut.process(event, hint) + + assertNull(hint.screenshot) + } + + @Test + fun `when event is not errored, does nothing`() { + val sut = fixture.getSut(true) + val hint = Hint() + + sut.onActivityCreated(fixture.activity, null) + + val event = fixture.mainProcessor.process(SentryEvent(), hint) + sut.process(event, hint) + + assertNull(hint.screenshot) + } + + @Test + fun `when there is not activity, does nothing`() { + val sut = fixture.getSut(true) + val hint = Hint() + + val event = fixture.mainProcessor.process(getEvent(), hint) + sut.process(event, hint) + + assertNull(hint.screenshot) + } + + @Test + fun `when activity is finishing, does nothing`() { + val sut = fixture.getSut(true) + val hint = Hint() + + whenever(fixture.activity.isFinishing).thenReturn(true) + sut.onActivityCreated(fixture.activity, null) + + val event = fixture.mainProcessor.process(getEvent(), hint) + sut.process(event, hint) + + assertNull(hint.screenshot) + } + + @Test + fun `when view is zeroed, does nothing`() { + val sut = fixture.getSut(true) + val hint = Hint() + + whenever(fixture.rootView.width).thenReturn(0) + whenever(fixture.rootView.height).thenReturn(0) + sut.onActivityCreated(fixture.activity, null) + + val event = fixture.mainProcessor.process(getEvent(), hint) + sut.process(event, hint) + + assertNull(hint.screenshot) + } + + @Test + fun `when process is called and attachScreenshot is enabled, add attachment to hints`() { + val sut = fixture.getSut(true) + val hint = Hint() + + sut.onActivityCreated(fixture.activity, null) + + val event = fixture.mainProcessor.process(getEvent(), hint) + sut.process(event, hint) + + val screenshot = hint.screenshot + assertTrue(screenshot is Attachment) + assertEquals("screenshot.png", screenshot.filename) + assertEquals("image/png", screenshot.contentType) + + assertSame(fixture.activity, hint[ANDROID_ACTIVITY]) + } + + @Test + fun `when activity is destroyed, does nothing`() { + val sut = fixture.getSut(true) + val hint = Hint() + + sut.onActivityCreated(fixture.activity, null) + sut.onActivityDestroyed(fixture.activity) + + val event = fixture.mainProcessor.process(getEvent(), hint) + sut.process(event, hint) + + assertNull(hint.screenshot) + } + + private fun getEvent(): SentryEvent = SentryEvent(Throwable("Throwable")) +} diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidOptionsTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidOptionsTest.kt index 581060937ee..7c44407ce06 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidOptionsTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidOptionsTest.kt @@ -1,8 +1,13 @@ package io.sentry.android.core +import io.sentry.ITransaction +import io.sentry.ITransactionProfiler +import io.sentry.NoOpTransactionProfiler +import io.sentry.ProfilingTraceData import io.sentry.protocol.DebugImage import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertFalse import kotlin.test.assertNotNull import kotlin.test.assertTrue @@ -55,8 +60,56 @@ class SentryAndroidOptionsTest { assertNotNull(sentryOptions.debugImagesLoader) } + @Test + fun `init should set NoOpTransactionProfiler`() { + val sentryOptions = SentryAndroidOptions() + assertEquals(NoOpTransactionProfiler.getInstance(), sentryOptions.transactionProfiler) + } + + @Test + fun `set transactionProfiler accepts non null value`() { + val sentryOptions = SentryAndroidOptions().apply { + setTransactionProfiler(CustomTransactionProfiler()) + } + assertNotNull(sentryOptions.transactionProfiler) + } + + @Test + fun `set transactionProfiler to null sets it to noop`() { + val sentryOptions = SentryAndroidOptions().apply { + setTransactionProfiler(null) + } + assertEquals(sentryOptions.transactionProfiler, NoOpTransactionProfiler.getInstance()) + } + + @Test + fun `enable scope sync by default for Android`() { + val sentryOptions = SentryAndroidOptions() + + assertTrue(sentryOptions.isEnableScopeSync) + } + + @Test + fun `attach screenshots disabled by default for Android`() { + val sentryOptions = SentryAndroidOptions() + + assertFalse(sentryOptions.isAttachScreenshot) + } + + @Test + fun `user interaction tracing disabled by default for Android`() { + val sentryOptions = SentryAndroidOptions() + + assertFalse(sentryOptions.isEnableUserInteractionTracing) + } + private class CustomDebugImagesLoader : IDebugImagesLoader { override fun loadDebugImages(): List? = null override fun clearDebugImages() {} } + + private class CustomTransactionProfiler : ITransactionProfiler { + override fun onTransactionStart(transaction: ITransaction) {} + override fun onTransactionFinish(transaction: ITransaction): ProfilingTraceData? = null + } } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/SystemEventsBreadcrumbsIntegrationTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/SystemEventsBreadcrumbsIntegrationTest.kt index f8f060045a3..ca4a34f358b 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/SystemEventsBreadcrumbsIntegrationTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/SystemEventsBreadcrumbsIntegrationTest.kt @@ -3,6 +3,7 @@ package io.sentry.android.core import android.content.Context import android.content.Intent import com.nhaarman.mockitokotlin2.any +import com.nhaarman.mockitokotlin2.anyOrNull import com.nhaarman.mockitokotlin2.check import com.nhaarman.mockitokotlin2.mock import com.nhaarman.mockitokotlin2.never @@ -73,7 +74,7 @@ class SystemEventsBreadcrumbsIntegrationTest { val intent = Intent().apply { action = Intent.ACTION_TIME_CHANGED } - sut.receiver!!.onReceive(any(), intent) + sut.receiver!!.onReceive(fixture.context, intent) verify(fixture.hub).addBreadcrumb( check { @@ -81,7 +82,8 @@ class SystemEventsBreadcrumbsIntegrationTest { assertEquals("system", it.type) assertEquals(SentryLevel.INFO, it.level) // cant assert data, its not a public API - } + }, + anyOrNull() ) } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/UserInteractionIntegrationTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/UserInteractionIntegrationTest.kt index 83c6c0b83bb..667b7df4694 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/UserInteractionIntegrationTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/UserInteractionIntegrationTest.kt @@ -17,8 +17,8 @@ import com.nhaarman.mockitokotlin2.whenever import io.sentry.Hub import io.sentry.android.core.internal.gestures.NoOpWindowCallback import io.sentry.android.core.internal.gestures.SentryWindowCallback -import org.junit.Test import org.junit.runner.RunWith +import kotlin.test.Test import kotlin.test.assertTrue @RunWith(AndroidJUnit4::class) @@ -164,4 +164,14 @@ class UserInteractionIntegrationTest { verify(fixture.window).callback = delegate } + + @Test + fun `stops tracing on activity paused`() { + val callback = mock() + val sut = fixture.getSut(callback) + + sut.onActivityPaused(fixture.activity) + + verify(callback).stopTracking() + } } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/internal/gestures/SentryGestureListenerClickTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/internal/gestures/SentryGestureListenerClickTest.kt index 5bafe1b6cff..c569ddecb91 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/internal/gestures/SentryGestureListenerClickTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/internal/gestures/SentryGestureListenerClickTest.kt @@ -1,5 +1,6 @@ package io.sentry.android.core.internal.gestures +import android.app.Activity import android.content.Context import android.content.res.Resources import android.view.MotionEvent @@ -9,6 +10,7 @@ import android.view.Window import android.widget.CheckBox import android.widget.RadioButton import com.nhaarman.mockitokotlin2.any +import com.nhaarman.mockitokotlin2.anyOrNull import com.nhaarman.mockitokotlin2.check import com.nhaarman.mockitokotlin2.mock import com.nhaarman.mockitokotlin2.never @@ -18,12 +20,12 @@ import io.sentry.Breadcrumb import io.sentry.IHub import io.sentry.SentryLevel.INFO import io.sentry.android.core.SentryAndroidOptions -import org.junit.Test -import java.lang.ref.WeakReference +import kotlin.test.Test import kotlin.test.assertEquals class SentryGestureListenerClickTest { class Fixture { + val activity = mock() val window = mock() val context = mock() val resources = mock() @@ -70,8 +72,9 @@ class SentryGestureListenerClickTest { resources.mockForTarget(this.target, resourceName) whenever(context.resources).thenReturn(resources) whenever(this.target.context).thenReturn(context) + whenever(activity.window).thenReturn(window) return SentryGestureListener( - WeakReference(window), + activity, hub, options, true @@ -113,7 +116,8 @@ class SentryGestureListenerClickTest { assertEquals("test_button", it.data["view.id"]) assertEquals("android.view.View", it.data["view.class"]) assertEquals(INFO, it.level) - } + }, + anyOrNull() ) } @@ -132,7 +136,8 @@ class SentryGestureListenerClickTest { check { assertEquals("radio_button", it.data["view.id"]) assertEquals("android.widget.RadioButton", it.data["view.class"]) - } + }, + anyOrNull() ) } @@ -151,7 +156,8 @@ class SentryGestureListenerClickTest { check { assertEquals("check_box", it.data["view.id"]) assertEquals("android.widget.CheckBox", it.data["view.class"]) - } + }, + anyOrNull() ) } @@ -182,7 +188,8 @@ class SentryGestureListenerClickTest { check { assertEquals(decorView.javaClass.canonicalName, it.data["view.class"]) assertEquals("decor_view", it.data["view.id"]) - } + }, + anyOrNull() ) } @@ -212,7 +219,8 @@ class SentryGestureListenerClickTest { verify(fixture.hub).addBreadcrumb( check { assertEquals(fixture.target.javaClass.simpleName, it.data["view.class"]) - } + }, + anyOrNull() ) } } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/internal/gestures/SentryGestureListenerScrollTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/internal/gestures/SentryGestureListenerScrollTest.kt index 690a0da5e08..03a7832bdd7 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/internal/gestures/SentryGestureListenerScrollTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/internal/gestures/SentryGestureListenerScrollTest.kt @@ -1,5 +1,6 @@ package io.sentry.android.core.internal.gestures +import android.app.Activity import android.content.Context import android.content.res.Resources import android.view.MotionEvent @@ -10,6 +11,7 @@ import android.widget.AbsListView import android.widget.ListAdapter import androidx.core.view.ScrollingView import com.nhaarman.mockitokotlin2.any +import com.nhaarman.mockitokotlin2.anyOrNull import com.nhaarman.mockitokotlin2.check import com.nhaarman.mockitokotlin2.inOrder import com.nhaarman.mockitokotlin2.mock @@ -21,12 +23,12 @@ import io.sentry.Breadcrumb import io.sentry.IHub import io.sentry.SentryLevel.INFO import io.sentry.android.core.SentryAndroidOptions -import org.junit.Test -import java.lang.ref.WeakReference +import kotlin.test.Test import kotlin.test.assertEquals class SentryGestureListenerScrollTest { class Fixture { + val activity = mock() val window = mock() val context = mock() val resources = mock() @@ -63,8 +65,9 @@ class SentryGestureListenerScrollTest { if (direction in directions) { endEvent.mockDirection(firstEvent, direction) } + whenever(activity.window).thenReturn(window) return SentryGestureListener( - WeakReference(window), + activity, hub, options, isAndroidXAvailable @@ -92,7 +95,8 @@ class SentryGestureListenerScrollTest { assertEquals(fixture.target.javaClass.canonicalName, it.data["view.class"]) assertEquals("left", it.data["direction"]) assertEquals(INFO, it.level) - } + }, + anyOrNull() ) } @@ -136,7 +140,8 @@ class SentryGestureListenerScrollTest { assertEquals(fixture.target.javaClass.canonicalName, it.data["view.class"]) assertEquals("down", it.data["direction"]) assertEquals(INFO, it.level) - } + }, + anyOrNull() ) verify(fixture.hub).addBreadcrumb( check { @@ -146,7 +151,8 @@ class SentryGestureListenerScrollTest { assertEquals(fixture.target.javaClass.canonicalName, it.data["view.class"]) assertEquals("up", it.data["direction"]) assertEquals(INFO, it.level) - } + }, + anyOrNull() ) } verifyNoMoreInteractions(fixture.hub) diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/internal/gestures/SentryGestureListenerTracingTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/internal/gestures/SentryGestureListenerTracingTest.kt new file mode 100644 index 00000000000..c68ddb0a816 --- /dev/null +++ b/sentry-android-core/src/test/java/io/sentry/android/core/internal/gestures/SentryGestureListenerTracingTest.kt @@ -0,0 +1,314 @@ +package io.sentry.android.core.internal.gestures + +import android.app.Activity +import android.content.Context +import android.content.res.Resources +import android.view.MotionEvent +import android.view.View +import android.view.ViewGroup +import android.view.Window +import android.widget.AbsListView +import android.widget.ListAdapter +import com.nhaarman.mockitokotlin2.any +import com.nhaarman.mockitokotlin2.anyOrNull +import com.nhaarman.mockitokotlin2.check +import com.nhaarman.mockitokotlin2.clearInvocations +import com.nhaarman.mockitokotlin2.mock +import com.nhaarman.mockitokotlin2.never +import com.nhaarman.mockitokotlin2.verify +import com.nhaarman.mockitokotlin2.whenever +import io.sentry.IHub +import io.sentry.Scope +import io.sentry.SentryTracer +import io.sentry.SpanStatus +import io.sentry.TransactionContext +import io.sentry.android.core.SentryAndroidOptions +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull + +class SentryGestureListenerTracingTest { + class Fixture { + val activity = mock() + val window = mock() + val context = mock() + val resources = mock() + val options = SentryAndroidOptions().apply { + dsn = "https://key@sentry.io/proj" + } + val hub = mock() + val event = mock() + lateinit var target: View + lateinit var transaction: SentryTracer + + internal inline fun getSut( + resourceName: String = "test_button", + hasViewIdInRes: Boolean = true, + tracesSampleRate: Double? = 1.0, + isEnableUserInteractionTracing: Boolean = true, + transaction: SentryTracer = SentryTracer(TransactionContext("name", "op"), hub) + ): SentryGestureListener { + options.tracesSampleRate = tracesSampleRate + options.isEnableUserInteractionTracing = isEnableUserInteractionTracing + + this.transaction = transaction + + target = mockView(event = event, clickable = true) + window.mockDecorView(event = event) { + whenever(it.childCount).thenReturn(1) + whenever(it.getChildAt(0)).thenReturn(target) + } + + if (hasViewIdInRes) { + resources.mockForTarget(target, resourceName) + } else { + whenever(resources.getResourceEntryName(target.id)).thenThrow( + Resources.NotFoundException() + ) + } + whenever(context.resources).thenReturn(resources) + whenever(target.context).thenReturn(context) + + whenever(activity.window).thenReturn(window) + + whenever(hub.startTransaction(any(), any(), any(), anyOrNull(), any())) + .thenReturn(transaction) + whenever(hub.options).thenReturn(options) + return SentryGestureListener( + activity, + hub, + options, + true + ) + } + } + + private val fixture = Fixture() + + @Test + fun `when tracing is disabled, does not start a transaction`() { + val sut = fixture.getSut(tracesSampleRate = null) + + sut.onSingleTapUp(fixture.event) + + verify(fixture.hub, never()).startTransaction( + any(), anyOrNull(), any(), anyOrNull(), any() + ) + } + + @Test + fun `when ui-interaction tracing is disabled, does not start a transaction`() { + val sut = fixture.getSut(isEnableUserInteractionTracing = false) + + sut.onSingleTapUp(fixture.event) + + verify(fixture.hub, never()).startTransaction( + any(), anyOrNull(), any(), anyOrNull(), any() + ) + } + + @Test + fun `when view id cannot be retrieved, does not start a transaction`() { + val sut = fixture.getSut(hasViewIdInRes = false) + + sut.onSingleTapUp(fixture.event) + + verify(fixture.hub, never()).startTransaction( + any(), anyOrNull(), any(), anyOrNull(), any() + ) + } + + @Test + fun `when transaction is created, set transaction to the bound Scope`() { + val sut = fixture.getSut() + + whenever(fixture.hub.configureScope(any())).thenAnswer { + val scope = Scope(fixture.options) + + sut.applyScope(scope, fixture.transaction) + + assertNotNull(scope.transaction) + } + + sut.onSingleTapUp(fixture.event) + } + + @Test + fun `when transaction is created, do not overwrite transaction already bound to the Scope`() { + val sut = fixture.getSut() + + whenever(fixture.hub.configureScope(any())).thenAnswer { + val scope = Scope(fixture.options) + val previousTransaction = SentryTracer(TransactionContext("name", "op"), fixture.hub) + scope.transaction = previousTransaction + + sut.applyScope(scope, fixture.transaction) + + assertEquals(previousTransaction, scope.transaction) + } + + sut.onSingleTapUp(fixture.event) + } + + @Test + fun `stopTracing remove transaction from scope`() { + val sut = fixture.getSut() + val expectedStatus = SpanStatus.CANCELLED + + whenever(fixture.hub.configureScope(any())).thenAnswer { + val scope = Scope(fixture.options) + + sut.applyScope(scope, fixture.transaction) + } + sut.onSingleTapUp(fixture.event) + + whenever(fixture.hub.configureScope(any())).thenAnswer { + val scope = Scope(fixture.options) + + scope.transaction = fixture.transaction + + sut.clearScope(scope) + + assertEquals(expectedStatus, fixture.transaction.status) + assertNull(scope.transaction) + } + sut.stopTracing(expectedStatus) + } + + @Test + fun `captures transaction with activity name + view id as transaction name`() { + val sut = fixture.getSut() + + sut.onSingleTapUp(fixture.event) + + verify(fixture.hub).startTransaction( + check { + assertEquals("Activity.test_button", it) + }, + anyOrNull(), any(), anyOrNull(), any() + ) + } + + @Test + fun `captures transaction with interaction event type as op`() { + val sut = fixture.getSut() + + sut.onSingleTapUp(fixture.event) + + verify(fixture.hub).startTransaction( + any(), check { assertEquals("ui.action.click", it) }, any(), + anyOrNull(), any() + ) + } + + @Test + fun `starts a new transaction when a new view is interacted with`() { + // first view interaction + val sut = fixture.getSut() + + sut.onSingleTapUp(fixture.event) + + verify(fixture.hub).startTransaction( + check { + assertEquals("Activity.test_button", it) + }, + any(), any(), anyOrNull(), any() + ) + + clearInvocations(fixture.hub) + // second view interaction with another view + val newTarget = mockView(event = fixture.event, clickable = true) + val newContext = mock() + val newRes = mock() + newRes.mockForTarget(newTarget, "test_checkbox") + whenever(newContext.resources).thenReturn(newRes) + whenever(newTarget.context).thenReturn(newContext) + fixture.window.mockDecorView(event = fixture.event) { + whenever(it.childCount).thenReturn(1) + whenever(it.getChildAt(0)).thenReturn(newTarget) + } + + whenever(fixture.hub.startTransaction(any(), any(), any(), anyOrNull(), any())) + .thenAnswer { + // verify that the active transaction gets finished when a new one appears + assertEquals(true, fixture.transaction.isFinished) + SentryTracer(TransactionContext("name", "op"), fixture.hub) + } + + sut.onSingleTapUp(fixture.event) + + verify(fixture.hub).startTransaction( + check { + assertEquals("Activity.test_checkbox", it) + }, + any(), any(), anyOrNull(), any() + ) + } + + @Test + fun `starts a new transaction when the same view was interacted with a different event type`() { + val sut = fixture.getSut(resourceName = "test_scroll_view") + + sut.onSingleTapUp(fixture.event) + + verify(fixture.hub).startTransaction( + check { + assertEquals("Activity.test_scroll_view", it) + }, + check { + assertEquals("ui.action.click", it) + }, + any(), anyOrNull(), any() + ) + + clearInvocations(fixture.hub) + + // second view interaction with a different interaction type (scroll) + whenever(fixture.hub.startTransaction(any(), any(), any(), anyOrNull(), any())) + .thenAnswer { + // verify that the active transaction gets finished when a new one appears + assertEquals(true, fixture.transaction.isFinished) + SentryTracer(TransactionContext("name", "op"), fixture.hub) + } + + sut.onScroll(fixture.event, mock(), 10.0f, 0f) + sut.onUp(mock()) + + verify(fixture.hub).startTransaction( + check { + assertEquals("Activity.test_scroll_view", it) + }, + check { + assertEquals("ui.action.scroll", it) + }, + any(), anyOrNull(), any() + ) + } + + @Test + fun `resets the idleTimeout when the same view was clicked and the transaction was still active`() { + // first view interaction + val transaction = mock() + val sut = fixture.getSut(transaction = transaction) + + sut.onSingleTapUp(fixture.event) + + verify(fixture.hub).startTransaction( + check { + assertEquals("Activity.test_button", it) + }, + any(), any(), anyOrNull(), any() + ) + + // second view interaction + sut.onSingleTapUp(fixture.event) + + verify(fixture.transaction).scheduleFinish(anyOrNull()) + } + + internal open class ScrollableListView : AbsListView(mock()) { + override fun getAdapter(): ListAdapter = mock() + override fun setSelection(position: Int) = Unit + } +} diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/internal/gestures/SentryWindowCallbackTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/internal/gestures/SentryWindowCallbackTest.kt index dd8f10b56f5..8115cd88901 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/internal/gestures/SentryWindowCallbackTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/internal/gestures/SentryWindowCallbackTest.kt @@ -12,7 +12,7 @@ import com.nhaarman.mockitokotlin2.verify import com.nhaarman.mockitokotlin2.whenever import io.sentry.android.core.SentryAndroidOptions import io.sentry.android.core.internal.gestures.SentryWindowCallback.MotionEventObtainer -import org.junit.Test +import kotlin.test.Test class SentryWindowCallbackTest { class Fixture { diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/internal/gestures/ViewUtilsTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/internal/gestures/ViewUtilsTest.kt index e7d5fa14891..3a57a4a0572 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/internal/gestures/ViewUtilsTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/internal/gestures/ViewUtilsTest.kt @@ -3,11 +3,14 @@ package io.sentry.android.core.internal.gestures import android.content.Context import android.content.res.Resources import android.view.View +import com.nhaarman.mockitokotlin2.any import com.nhaarman.mockitokotlin2.doReturn +import com.nhaarman.mockitokotlin2.doThrow import com.nhaarman.mockitokotlin2.mock import com.nhaarman.mockitokotlin2.whenever -import org.junit.Test +import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertFailsWith class ViewUtilsTest { @@ -27,7 +30,22 @@ class ViewUtilsTest { } @Test - fun `getResourceId falls back to hexadecimal id when resource not found`() { + fun `getResourceId throws when resource id is not available`() { + val view = mock { + whenever(it.id).doReturn(View.generateViewId()) + + val context = mock() + val resources = mock() + whenever(resources.getResourceEntryName(any())).doThrow(Resources.NotFoundException()) + whenever(context.resources).thenReturn(resources) + whenever(it.context).thenReturn(context) + } + + assertFailsWith { ViewUtils.getResourceId(view) } + } + + @Test + fun `getResourceIdWithFallback falls back to hexadecimal id when resource not found`() { val view = mock { whenever(it.id).doReturn(1234) @@ -38,6 +56,6 @@ class ViewUtilsTest { whenever(it.context).thenReturn(context) } - assertEquals(ViewUtils.getResourceId(view), "0x4d2") + assertEquals(ViewUtils.getResourceIdWithFallback(view), "0x4d2") } } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/internal/util/CpuInfoUtilsTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/internal/util/CpuInfoUtilsTest.kt new file mode 100644 index 00000000000..104f637f635 --- /dev/null +++ b/sentry-android-core/src/test/java/io/sentry/android/core/internal/util/CpuInfoUtilsTest.kt @@ -0,0 +1,86 @@ +package io.sentry.android.core.internal.util + +import com.nhaarman.mockitokotlin2.spy +import com.nhaarman.mockitokotlin2.times +import com.nhaarman.mockitokotlin2.verify +import com.nhaarman.mockitokotlin2.whenever +import org.junit.rules.TemporaryFolder +import java.io.File +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Test + +class CpuInfoUtilsTest { + + private lateinit var cpuDirs: File + private lateinit var ciu: CpuInfoUtils + + private fun populateCpuFiles(values: List) = values.mapIndexed { i, v -> + val cpuMaxFreqFile = File(cpuDirs, "cpu$i${File.separator}${CpuInfoUtils.CPUINFO_MAX_FREQ_PATH}") + cpuMaxFreqFile.parentFile?.mkdirs() + cpuMaxFreqFile.writeText(v) + cpuMaxFreqFile + } + + @BeforeTest + fun `set up`() { + val tmpFolder = TemporaryFolder() + tmpFolder.create() + cpuDirs = tmpFolder.newFolder("test") + ciu = spy(CpuInfoUtils.getInstance()) { + whenever(it.systemCpuPath).thenReturn(cpuDirs.absolutePath) + } + } + + @AfterTest + fun clear() { + cpuDirs.deleteRecursively() + ciu.clear() + } + + @Test + fun `readMaxFrequencies reads Khz and returns Mhz`() { + val expected = listOf(0, 1, 2, 3) + populateCpuFiles(listOf("0", "1000", "2000", "3000")) + // The order given by readFiles() is not guaranteed to be sorted, so we compare in this way + assert(expected.containsAll(ciu.readMaxFrequencies())) + assert(ciu.readMaxFrequencies().containsAll(expected)) + } + + @Test + fun `readMaxFrequencies returns empty list for non existent or invalid files`() { + // Empty list if no cpu file exists + assert(emptyList() == ciu.readMaxFrequencies()) + val files = populateCpuFiles(listOf("1000", "2000", "3000")) + files.forEach { + it.setReadable(false) + } + // Empty list for unreadable files + assert(emptyList() == ciu.readMaxFrequencies()) + } + + @Test + fun `readMaxFrequencies skips invalid values`() { + val expected = listOf(2, 3) + populateCpuFiles(listOf("invalid", "2000", "3000", "another")) + + // The order given by readFiles() is not guaranteed to be sorted, so we compare in this way + assert(expected.containsAll(ciu.readMaxFrequencies())) + assert(ciu.readMaxFrequencies().containsAll(expected)) + } + + @Test + fun `readMaxFrequencies caches values if they are valid`() { + // First call with invalid data + ciu.readMaxFrequencies() + val expected = listOf(0, 1, 2, 3) + populateCpuFiles(listOf("0", "1000", "2000", "3000")) + + // Second and third call with valid data will be read only once + // The order given by readFiles() is not guaranteed to be sorted, so we compare in this way + assert(expected.containsAll(ciu.readMaxFrequencies())) + assert(ciu.readMaxFrequencies().containsAll(expected)) + + verify(ciu, times(2)).systemCpuPath + } +} diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/internal/util/RootCheckerTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/internal/util/RootCheckerTest.kt index f60a2dd2b4a..983cf498483 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/internal/util/RootCheckerTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/internal/util/RootCheckerTest.kt @@ -9,7 +9,7 @@ import com.nhaarman.mockitokotlin2.mock import com.nhaarman.mockitokotlin2.verify import com.nhaarman.mockitokotlin2.whenever import io.sentry.ILogger -import io.sentry.android.core.IBuildInfoProvider +import io.sentry.android.core.BuildInfoProvider import java.io.ByteArrayInputStream import java.io.File import java.io.IOException @@ -24,7 +24,7 @@ class RootCheckerTest { private class Fixture { val context = mock() val logger = mock() - val buildInfoProvider = mock() + val buildInfoProvider = mock() val packageManager = mock() val runtime = mock() diff --git a/sentry-android-fragment/proguard-rules.pro b/sentry-android-fragment/proguard-rules.pro index 9d3dc494b77..a63b9e5add3 100644 --- a/sentry-android-fragment/proguard-rules.pro +++ b/sentry-android-fragment/proguard-rules.pro @@ -1,5 +1,10 @@ --keep class io.sentry.android.fragment.** { *; } +##---------------Begin: proguard configuration for Fragment ---------- + +# The Android SDK checks at runtime if this class is available via Class.forName +-keep class io.sentry.android.fragment.FragmentLifecycleIntegration { (...); } # To ensure that stack traces is unambiguous # https://developer.android.com/studio/build/shrink-code#decode-stack-trace -keepattributes LineNumberTable,SourceFile + +##---------------End: proguard configuration for Fragment ---------- 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 9b620f3f774..dcc1b116a86 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 @@ -7,11 +7,13 @@ import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentManager.FragmentLifecycleCallbacks import io.sentry.Breadcrumb +import io.sentry.Hint import io.sentry.HubAdapter import io.sentry.IHub import io.sentry.ISpan import io.sentry.SentryLevel.INFO import io.sentry.SpanStatus +import io.sentry.TypeCheckHint.ANDROID_FRAGMENT import java.util.WeakHashMap @Suppress("TooManyFunctions") @@ -116,7 +118,11 @@ class SentryFragmentLifecycleCallbacks( category = "ui.fragment.lifecycle" level = INFO } - hub.addBreadcrumb(breadcrumb) + + val hint = Hint() + .also { it.set(ANDROID_FRAGMENT, fragment) } + + hub.addBreadcrumb(breadcrumb, hint) } private fun getFragmentName(fragment: Fragment): String { diff --git a/sentry-android-fragment/src/test/java/io/sentry/android/fragment/FragmentLifecycleIntegrationTest.kt b/sentry-android-fragment/src/test/java/io/sentry/android/fragment/FragmentLifecycleIntegrationTest.kt index 6bf3209e34a..889fc424a34 100644 --- a/sentry-android-fragment/src/test/java/io/sentry/android/fragment/FragmentLifecycleIntegrationTest.kt +++ b/sentry-android-fragment/src/test/java/io/sentry/android/fragment/FragmentLifecycleIntegrationTest.kt @@ -12,7 +12,7 @@ import com.nhaarman.mockitokotlin2.verify import com.nhaarman.mockitokotlin2.whenever import io.sentry.IHub import io.sentry.SentryOptions -import org.junit.Test +import kotlin.test.Test import kotlin.test.assertFalse import kotlin.test.assertTrue 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 b07e5922553..6ece174100a 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 @@ -5,6 +5,7 @@ import android.os.Bundle import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager import com.nhaarman.mockitokotlin2.any +import com.nhaarman.mockitokotlin2.anyOrNull import com.nhaarman.mockitokotlin2.check import com.nhaarman.mockitokotlin2.mock import com.nhaarman.mockitokotlin2.never @@ -19,7 +20,7 @@ import io.sentry.ScopeCallback import io.sentry.SentryLevel.INFO import io.sentry.SentryOptions import io.sentry.SpanStatus -import org.junit.Test +import kotlin.test.Test import kotlin.test.assertEquals class SentryFragmentLifecycleCallbacksTest { @@ -268,7 +269,8 @@ class SentryFragmentLifecycleCallbacksTest { assertEquals(INFO, breadcrumb.level) assertEquals(expectedState, breadcrumb.getData("state")) assertEquals(fixture.fragment.javaClass.simpleName, breadcrumb.getData("screen")) - } + }, + anyOrNull() ) } } diff --git a/sentry-android-integration-tests/sentry-uitest-android-benchmark/.gitignore b/sentry-android-integration-tests/sentry-uitest-android-benchmark/.gitignore new file mode 100644 index 00000000000..796b96d1c40 --- /dev/null +++ b/sentry-android-integration-tests/sentry-uitest-android-benchmark/.gitignore @@ -0,0 +1 @@ +/build diff --git a/sentry-android-integration-tests/sentry-uitest-android-benchmark/benchmark-proguard-rules.pro b/sentry-android-integration-tests/sentry-uitest-android-benchmark/benchmark-proguard-rules.pro new file mode 100644 index 00000000000..8f5e14b25e7 --- /dev/null +++ b/sentry-android-integration-tests/sentry-uitest-android-benchmark/benchmark-proguard-rules.pro @@ -0,0 +1,35 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle.kts. +# +# 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 + +-dontobfuscate +#Shrinking removes annotations and "unused classes" from test apk, so we don't shrink +-dontshrink + +-ignorewarnings + +-keepattributes *Annotation* + +-dontnote junit.framework.** +-dontnote junit.runner.** + +-dontwarn androidx.test.** +-dontwarn org.junit.** diff --git a/sentry-android-integration-tests/sentry-uitest-android-benchmark/build.gradle.kts b/sentry-android-integration-tests/sentry-uitest-android-benchmark/build.gradle.kts new file mode 100644 index 00000000000..0ee897b3b0d --- /dev/null +++ b/sentry-android-integration-tests/sentry-uitest-android-benchmark/build.gradle.kts @@ -0,0 +1,133 @@ +import io.gitlab.arturbosch.detekt.Detekt +import io.gitlab.arturbosch.detekt.extensions.DetektExtension +import net.ltgt.gradle.errorprone.errorprone + +plugins { + id("com.android.application") + kotlin("android") + id(Config.QualityPlugins.errorProne) + id(Config.QualityPlugins.gradleVersions) + id(Config.QualityPlugins.detektPlugin) +} + +android { + compileSdk = Config.Android.compileSdkVersion + + defaultConfig { + applicationId = "io.sentry.uitest.android.benchmark" + minSdk = Config.Android.minSdkVersionNdk + targetSdk = Config.Android.targetSdkVersion + versionCode = 1 + versionName = "1.0.0" + + testInstrumentationRunner = Config.TestLibs.androidJUnitRunner + // Runs each test in its own instance of Instrumentation. This way they are isolated from + // one another and get their own Application instance. + // https://developer.android.com/training/testing/instrumented-tests/androidx-test-libraries/runner#enable-gradle + // This doesn't work on some devices with Android 11+. Clearing package data resets permissions. + // Check the readme for more info. +// testInstrumentationRunnerArguments["clearPackageData"] = "true" + } + + buildFeatures { + // Determines whether to support View Binding. + // Note that the viewBinding.enabled property is now deprecated. + viewBinding = true + } + + testOptions { + execution = "ANDROIDX_TEST_ORCHESTRATOR" + } + + signingConfigs { + getByName("debug") { + storeFile = rootProject.file("debug.keystore") + storePassword = "android" + keyAlias = "androiddebugkey" + keyPassword = "android" + } + } + + testBuildType = System.getProperty("testBuildType", "debug") + + buildTypes { + getByName("debug") { + isDebuggable = false + isMinifyEnabled = true + signingConfig = signingConfigs.getByName("debug") + proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "benchmark-proguard-rules.pro") + } + getByName("release") { + isMinifyEnabled = true + isShrinkResources = true + signingConfig = signingConfigs.getByName("debug") // to be able to run release mode + proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "benchmark-proguard-rules.pro") + } + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_1_8.toString() + } + + lint { + warningsAsErrors = true + checkDependencies = true + + // We run a full lint analysis as build part in CI, so skip vital checks for assemble tasks. + checkReleaseBuilds = false + } + + variantFilter { + if (Config.Android.shouldSkipDebugVariant(buildType.name)) { + ignore = true + } + } +} + +dependencies { + + implementation(kotlin(Config.kotlinStdLib, org.jetbrains.kotlin.config.KotlinCompilerVersion.VERSION)) + + implementation(projects.sentryAndroidIntegrationTests.sentryUitestAndroid) + implementation(projects.sentryAndroid) + implementation(Config.Libs.appCompat) + implementation(Config.Libs.androidxCore) + implementation(Config.Libs.androidxRecylerView) + implementation(Config.Libs.constraintLayout) + implementation(Config.TestLibs.espressoIdlingResource) + + compileOnly(Config.CompileOnly.nopen) + errorprone(Config.CompileOnly.nopenChecker) + errorprone(Config.CompileOnly.errorprone) + errorprone(Config.CompileOnly.errorProneNullAway) + + androidTestImplementation(Config.TestLibs.kotlinTestJunit) + androidTestImplementation(Config.TestLibs.espressoCore) + androidTestImplementation(Config.TestLibs.androidxTestCoreKtx) + androidTestImplementation(Config.TestLibs.androidxRunner) + androidTestImplementation(Config.TestLibs.androidxTestRules) + androidTestImplementation(Config.TestLibs.androidxJunit) + androidTestUtil(Config.TestLibs.androidxTestOrchestrator) +} + +tasks.withType().configureEach { + options.errorprone { + check("NullAway", net.ltgt.gradle.errorprone.CheckSeverity.ERROR) + option("NullAway:AnnotatedPackages", "io.sentry") + option("NullAway:UnannotatedSubPackages", "io.sentry.uitest.android.benchmark.databinding") + } +} + +tasks.withType { + // Target version of the generated JVM bytecode. It is used for type resolution. + jvmTarget = JavaVersion.VERSION_1_8.toString() +} + +configure { + buildUponDefaultConfig = true + allRules = true +} + +kotlin { + explicitApi() +} diff --git a/sentry-android-integration-tests/sentry-uitest-android-benchmark/src/androidTest/java/io/sentry/uitest/android/benchmark/SentryBenchmarkTest.kt b/sentry-android-integration-tests/sentry-uitest-android-benchmark/src/androidTest/java/io/sentry/uitest/android/benchmark/SentryBenchmarkTest.kt new file mode 100644 index 00000000000..013fbccf659 --- /dev/null +++ b/sentry-android-integration-tests/sentry-uitest-android-benchmark/src/androidTest/java/io/sentry/uitest/android/benchmark/SentryBenchmarkTest.kt @@ -0,0 +1,135 @@ +package io.sentry.uitest.android.benchmark + +import android.content.Context +import android.view.Choreographer +import androidx.lifecycle.Lifecycle +import androidx.test.core.app.ApplicationProvider +import androidx.test.core.app.launchActivity +import androidx.test.espresso.Espresso +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.IdlingRegistry +import androidx.test.espresso.action.ViewActions.swipeUp +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.runner.AndroidJUnitRunner +import io.sentry.ITransaction +import io.sentry.Sentry +import io.sentry.SentryOptions +import io.sentry.android.core.SentryAndroid +import io.sentry.uitest.android.benchmark.util.BenchmarkOperation +import org.junit.runner.RunWith +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertTrue + +@RunWith(AndroidJUnit4::class) +class SentryBenchmarkTest { + + private lateinit var runner: AndroidJUnitRunner + private lateinit var context: Context + private lateinit var choreographer: Choreographer + + @BeforeTest + fun setUp() { + runner = InstrumentationRegistry.getInstrumentation() as AndroidJUnitRunner + context = ApplicationProvider.getApplicationContext() + context.cacheDir.deleteRecursively() + IdlingRegistry.getInstance().register(BenchmarkActivity.scrollingIdlingResource) + // Must run on the main thread to get the main thread choreographer. + runner.runOnMainSync { + choreographer = Choreographer.getInstance() + } + } + + @AfterTest + fun cleanup() { + IdlingRegistry.getInstance().unregister(BenchmarkActivity.scrollingIdlingResource) + } + + @Test + fun benchmarkSameOperation() { + + // We compare two operation that are the same. We expect the increases to be negligible, as the results + // should be very similar. + val op1 = BenchmarkOperation(choreographer, getOperation(runner)) + val op2 = BenchmarkOperation(choreographer, getOperation(runner)) + val comparisonResult = BenchmarkOperation.compare(op1, "Op1", op2, "Op2") + + assertTrue(comparisonResult.durationIncrease in -1F..1F) + assertTrue(comparisonResult.cpuTimeIncrease in -1F..1F) + // The fps decrease comparison is skipped, due to approximation: 59.51 and 59.49 fps are considered 60 and 59, + // respectively. Also, if the average fps is 20 or 60, a difference of 1 fps becomes 5% or 1.66% respectively. + assertTrue(comparisonResult.droppedFramesIncrease in -1F..1F) + } + + @Test + fun benchmarkProfiledTransaction() { + + runner.runOnMainSync { + SentryAndroid.init(context) { options: SentryOptions -> + options.dsn = "https://key@uri/1234567" + options.tracesSampleRate = 1.0 + options.isProfilingEnabled = true + } + } + + // We compare the same operation with and without profiled transaction. + // We expect the profiled transaction operation to be slower, but not slower than 5%. + val benchmarkOperationNoTransaction = BenchmarkOperation(choreographer, getOperation(runner)) + val benchmarkOperationProfiled = BenchmarkOperation( + choreographer, + getOperation(runner) { + Sentry.startTransaction("Benchmark", "ProfiledTransaction") + } + ) + val comparisonResult = BenchmarkOperation.compare( + benchmarkOperationNoTransaction, + "NoTransaction", + benchmarkOperationProfiled, + "ProfiledTransaction" + ) + + runner.runOnMainSync { + Sentry.close() + } + + assertTrue(comparisonResult.durationIncrease in 0F..5F) + assertTrue(comparisonResult.cpuTimeIncrease in 0F..5F) + assertTrue(comparisonResult.fpsDecrease in 0F..5F) + assertTrue(comparisonResult.droppedFramesIncrease in 0F..5F) + } + + /** + * Operation that will be compared: it launches [BenchmarkActivity], swipe the list and closes it. + * The [transactionBuilder] is used to create the transaction before the swipes. + */ + private fun getOperation(runner: AndroidJUnitRunner, transactionBuilder: () -> ITransaction? = { null }): () -> Unit = { + var transaction: ITransaction? = null + // Launch the sentry-uitest-android-benchmark activity + val benchmarkScenario = launchActivity() + // Starts a transaction (it can be null, but we still runOnMainSync to make operations as similar as possible) + runner.runOnMainSync { + transaction = transactionBuilder() + } + // Just swipe the list some times: this is the benchmarked operation + swipeList(2) + // We finish the transaction + runner.runOnMainSync { + transaction?.finish() + } + // We swipe a last time to measure how finishing the transaction may affect other operations + swipeList(1) + + benchmarkScenario.moveToState(Lifecycle.State.DESTROYED) + } + + private fun swipeList(times: Int) { + repeat(times) { + Thread.sleep(100) + onView(withId(R.id.benchmark_transaction_list)).perform(swipeUp()) + Espresso.onIdle() + } + } +} diff --git a/sentry-android-integration-tests/sentry-uitest-android-benchmark/src/androidTest/java/io/sentry/uitest/android/benchmark/util/BenchmarkOperation.kt b/sentry-android-integration-tests/sentry-uitest-android-benchmark/src/androidTest/java/io/sentry/uitest/android/benchmark/util/BenchmarkOperation.kt new file mode 100644 index 00000000000..72a71e7cb7e --- /dev/null +++ b/sentry-android-integration-tests/sentry-uitest-android-benchmark/src/androidTest/java/io/sentry/uitest/android/benchmark/util/BenchmarkOperation.kt @@ -0,0 +1,128 @@ +package io.sentry.uitest.android.benchmark.util + +import android.os.Process +import android.os.SystemClock +import android.view.Choreographer +import java.util.concurrent.TimeUnit + +// 60 FPS is the recommended target: https://www.youtube.com/watch?v=CaMTIgxCSqU +private const val FRAME_DURATION_60FPS_NS: Double = 1_000_000_000 / 60.0 + +/** + * Class that allows to benchmark some operations. + * Create two [BenchmarkOperation] objects and compare them using [BenchmarkOperation.compare] to get + * a [BenchmarkResult] with relative measured overheads. + */ +internal class BenchmarkOperation(private val choreographer: Choreographer, private val op: () -> Unit) { + + companion object { + + /** + * Running two operations sequentially (running 10 times the first and then 10 times the second) results in the + * first operation to always be slower, so comparing two different operations on equal terms is not possible. + * This method runs [op1] and [op2] in an alternating sequence. + * When [op1] and [op2] are the same, we get (nearly) identical results, as expected. + * You can adjust [warmupIterations] and [measuredIterations]. The lower they are, the faster the benchmark, + * but accuracy decreases. + */ + fun compare( + op1: BenchmarkOperation, + op1Name: String, + op2: BenchmarkOperation, + op2Name: String, + warmupIterations: Int = 3, + measuredIterations: Int = 15 + ): BenchmarkResult { + // The first operations are the slowest, as the device is still doing things like filling the cache. + repeat(warmupIterations) { + op1.warmup() + op2.warmup() + } + // Now we can measure the operations (in alternating sequence). + repeat(measuredIterations) { + op1.iterate() + op2.iterate() + } + val op1Result = op1.getResult(op1Name) + val op2Result = op2.getResult(op2Name) + + // Let's print the raw results. + println("=====================================") + println(op1Name) + println(op1Result) + println("=====================================") + println(op2Name) + println(op2Result) + println("=====================================") + + return op2Result.compare(op1Result) + } + } + + private var iterations: Int = 0 + private var durationNanos: Long = 0 + private var cpuDurationMillis: Long = 0 + private var frames: Int = 0 + private var droppedFrames: Double = 0.0 + private var lastFrameTimeNanos: Long = 0 + + /** Run the operation without measuring it. */ + private fun warmup() { + op() + isolate() + } + + /** Run the operation and measure it, updating sentry-uitest-android-benchmark data. */ + private fun iterate() { + val startRealtimeNs = SystemClock.elapsedRealtimeNanos() + val startCpuTimeMs = Process.getElapsedCpuTime() + + lastFrameTimeNanos = startRealtimeNs + iterations++ + choreographer.postFrameCallback(frameCallback) + + op() + + choreographer.removeFrameCallback(frameCallback) + cpuDurationMillis += Process.getElapsedCpuTime() - startCpuTimeMs + durationNanos += SystemClock.elapsedRealtimeNanos() - startRealtimeNs + + isolate() + } + + /** Return the [BenchmarkOperationResult] for the operation. */ + private fun getResult(operationName: String): BenchmarkOperationResult = BenchmarkOperationResult( + cpuDurationMillis / iterations, + droppedFrames / iterations, + durationNanos / iterations, + // fps = counted frames per seconds converted into frames per nanoseconds, divided by duration in nanoseconds + // We don't convert the duration into seconds to avoid issues with rounding and possible division by 0 + (frames * TimeUnit.SECONDS.toNanos(1) / durationNanos).toInt(), + operationName + ) + + /** + * Helps ensure that operations don't impact one another. + * Doesn't appear to currently have an impact on the benchmark. + */ + private fun isolate() { + Thread.sleep(500) + Runtime.getRuntime().gc() + Thread.sleep(100) + } + + private val frameCallback = object : Choreographer.FrameCallback { + override fun doFrame(frameTimeNanos: Long) { + frames++ + val timeSinceLastFrameNanos = frameTimeNanos - lastFrameTimeNanos + if (timeSinceLastFrameNanos > FRAME_DURATION_60FPS_NS) { + // Fractions of frames dropped are weighted to improve the accuracy of the results. + // For example, 31ms between frames is much worse than 17ms, even though both + // durations are within the "1 frame dropped" range. + droppedFrames += timeSinceLastFrameNanos / FRAME_DURATION_60FPS_NS - 1 + } + lastFrameTimeNanos = frameTimeNanos + choreographer.postFrameCallback(this) + } + } +} diff --git a/sentry-android-integration-tests/sentry-uitest-android-benchmark/src/androidTest/java/io/sentry/uitest/android/benchmark/util/BenchmarkOperationResult.kt b/sentry-android-integration-tests/sentry-uitest-android-benchmark/src/androidTest/java/io/sentry/uitest/android/benchmark/util/BenchmarkOperationResult.kt new file mode 100644 index 00000000000..47a954c49bc --- /dev/null +++ b/sentry-android-integration-tests/sentry-uitest-android-benchmark/src/androidTest/java/io/sentry/uitest/android/benchmark/util/BenchmarkOperationResult.kt @@ -0,0 +1,100 @@ +package io.sentry.uitest.android.benchmark.util + +import java.util.concurrent.TimeUnit + +/** Stores the results of a [BenchmarkOperation]. */ +internal data class BenchmarkOperationResult( + val avgCpuTimeMillis: Long, + val avgDroppedFrames: Double, + val avgDurationNanos: Long, + val avgFramesPerSecond: Int, + val operationName: String +) { + /** + * Compare two [BenchmarkOperationResult], calculating increases of each parameter in percentage. + */ + fun compare(other: BenchmarkOperationResult): BenchmarkResult { + + // Measure average duration + val durationDiffNanos = avgDurationNanos - other.avgDurationNanos + val durationIncreasePercentage = durationDiffNanos * 100.0 / other.avgDurationNanos + println("[${other.operationName}] Average duration: ${other.avgDurationNanos} ns") + println("[$operationName] Average duration: $avgDurationNanos ns") + if (durationIncreasePercentage > 0) { + println("Duration increase: %.2f%%".format(durationIncreasePercentage)) + } else { + println("No measurable duration increase detected.") + } + + println("--------------------") + + // Measure average cpu time + val cores = Runtime.getRuntime().availableProcessors() + val cpuTimeDiff = (avgCpuTimeMillis - other.avgCpuTimeMillis) / cores + val cpuTimeOverheadPercentage = cpuTimeDiff * 100.0 / other.avgCpuTimeMillis + // Cpu time spent profiling is weighted based on available threads, as profiling runs on 1 thread only. + println("The weighted difference of cpu times is $cpuTimeDiff ms (over $cores available cores).") + println("[${other.operationName}] Cpu time: ${other.avgCpuTimeMillis} ms") + println("[$operationName] Cpu time: $avgCpuTimeMillis ms") + if (cpuTimeOverheadPercentage > 0) { + println("CPU time overhead: %.2f%%".format(cpuTimeOverheadPercentage)) + } else { + println("No measurable CPU time overhead detected.") + } + + println("--------------------") + + // Measure average fps + val fpsDiff = other.avgFramesPerSecond - avgFramesPerSecond + val fpsDecreasePercentage = fpsDiff * 100.0 / other.avgFramesPerSecond + println("[${other.operationName}] Average FPS: ${other.avgFramesPerSecond}") + println("[$operationName] Average FPS: $avgFramesPerSecond") + if (fpsDecreasePercentage > 0) { + println("FPS decrease: %.2f%%".format(fpsDecreasePercentage)) + } else { + println("No measurable FPS decrease detected.") + } + + println("--------------------") + + // Measure average dropped frames + val droppedFramesDiff = avgDroppedFrames - other.avgDroppedFrames + val totalExpectedFrames = TimeUnit.NANOSECONDS.toMillis(other.avgDurationNanos) * 60 / 1000 + val droppedFramesIncreasePercentage = droppedFramesDiff * 100 / (totalExpectedFrames - other.avgDroppedFrames) + println("Dropped frames are calculated based on a target of 60 frames per second ($totalExpectedFrames total frames).") + println("[${other.operationName}] Average dropped frames: ${other.avgDroppedFrames}") + println("[$operationName] Average dropped frames: $avgDroppedFrames") + if (droppedFramesIncreasePercentage > 0) { + println("Frame drop increase: %.2f%%".format(droppedFramesIncreasePercentage)) + } else { + println("No measurable frame drop increase detected.") + } + + return BenchmarkResult( + cpuTimeOverheadPercentage, + droppedFramesIncreasePercentage, + durationIncreasePercentage, + fpsDecreasePercentage + ) + } +} + +internal data class BenchmarkResult( + /** + * Increase of cpu time in percentage. + * It has no direct impact on performance of the app, but it has on battery usage, as the cpu is 'awaken' longer. + */ + val cpuTimeIncrease: Double, + /** + * Increase of dropped frames in percentage.Very important, as it weights dropped frames based on the time + * passed between each frame. This is the metric end users can perceive as 'performance' in app usage. + */ + val droppedFramesIncrease: Double, + /** Increase of duration in percentage. If it's low enough, no end user will ever realize it. */ + val durationIncrease: Double, + /** + * Decrease of fps in percentage. Not really important, as even if fps are the same, the cpu could be + * doing more work in the frame window, and it could be hidden by checking average fps only. + */ + val fpsDecrease: Double +) diff --git a/sentry-android-integration-tests/sentry-uitest-android-benchmark/src/main/AndroidManifest.xml b/sentry-android-integration-tests/sentry-uitest-android-benchmark/src/main/AndroidManifest.xml new file mode 100644 index 00000000000..3c1d9e2de5d --- /dev/null +++ b/sentry-android-integration-tests/sentry-uitest-android-benchmark/src/main/AndroidManifest.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + diff --git a/sentry-android-integration-tests/sentry-uitest-android-benchmark/src/main/java/io/sentry/uitest/android/benchmark/BenchmarkActivity.kt b/sentry-android-integration-tests/sentry-uitest-android-benchmark/src/main/java/io/sentry/uitest/android/benchmark/BenchmarkActivity.kt new file mode 100644 index 00000000000..5a33232bb1f --- /dev/null +++ b/sentry-android-integration-tests/sentry-uitest-android-benchmark/src/main/java/io/sentry/uitest/android/benchmark/BenchmarkActivity.kt @@ -0,0 +1,71 @@ +package io.sentry.uitest.android.benchmark + +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import io.sentry.uitest.android.benchmark.databinding.ActivityBenchmarkBinding +import io.sentry.uitest.android.utils.BooleanIdlingResource +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors + +/** A simple activity with a list of bitmaps. */ +class BenchmarkActivity : AppCompatActivity() { + + companion object { + + /** The activity will set this when scrolling. */ + val scrollingIdlingResource = BooleanIdlingResource("sentry-uitest-android-benchmark-activity") + } + + /** + * Each background thread will run non-stop calculations during the benchmark. + * One such thread seems enough to represent a busy application. + * This number can be increased to mimic busier applications. + */ + private val backgroundThreadPoolSize = 1 + private val executor: ExecutorService = Executors.newFixedThreadPool(backgroundThreadPoolSize) + private var resumed = false + private lateinit var binding: ActivityBenchmarkBinding + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + binding = ActivityBenchmarkBinding.inflate(layoutInflater) + setContentView(binding.root) + + // We show a simple list that changes the idling resource + binding.benchmarkTransactionList.apply { + layoutManager = LinearLayoutManager(this@BenchmarkActivity) + adapter = BenchmarkTransactionListAdapter() + addOnScrollListener(object : RecyclerView.OnScrollListener() { + override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { + super.onScrollStateChanged(recyclerView, newState) + scrollingIdlingResource.setIdle(newState == RecyclerView.SCROLL_STATE_IDLE) + } + }) + } + } + + @Suppress("MagicNumber") + override fun onResume() { + super.onResume() + resumed = true + + // Do operations until the activity is paused. + repeat(backgroundThreadPoolSize) { + executor.execute { + var x = 0 + for (i in 0..1_000_000_000) { + x += i * i + if (!resumed) break + } + } + } + } + + override fun onPause() { + super.onPause() + resumed = false + } +} diff --git a/sentry-android-integration-tests/sentry-uitest-android-benchmark/src/main/java/io/sentry/uitest/android/benchmark/BenchmarkTransactionListAdapter.kt b/sentry-android-integration-tests/sentry-uitest-android-benchmark/src/main/java/io/sentry/uitest/android/benchmark/BenchmarkTransactionListAdapter.kt new file mode 100644 index 00000000000..86eb423c52a --- /dev/null +++ b/sentry-android-integration-tests/sentry-uitest-android-benchmark/src/main/java/io/sentry/uitest/android/benchmark/BenchmarkTransactionListAdapter.kt @@ -0,0 +1,47 @@ +package io.sentry.uitest.android.benchmark + +import android.annotation.SuppressLint +import android.graphics.Bitmap +import android.graphics.Color +import android.view.LayoutInflater +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView +import io.sentry.uitest.android.benchmark.databinding.BenchmarkItemListBinding +import kotlin.random.Random + +/** Simple [RecyclerView.Adapter] that generates a bitmap and a text to show for each item. */ +internal class BenchmarkTransactionListAdapter : RecyclerView.Adapter() { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val binding = BenchmarkItemListBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return ViewHolder(binding) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + holder.imageView.setImageBitmap(generateBitmap()) + + @SuppressLint("SetTextI18n") + holder.textView.text = "Item $position ${"sentry ".repeat(position)}" + } + + @Suppress("MagicNumber") + private fun generateBitmap(): Bitmap { + val bitmapSize = 100 + val colors = (0 until (bitmapSize * bitmapSize)).map { + Color.rgb(Random.nextInt(256), Random.nextInt(256), Random.nextInt(256)) + }.toIntArray() + return Bitmap.createBitmap(colors, bitmapSize, bitmapSize, Bitmap.Config.ARGB_8888) + } + + // Disables view recycling. + override fun getItemViewType(position: Int): Int = position + + override fun getItemCount(): Int = 200 +} + +internal class ViewHolder(binding: BenchmarkItemListBinding) : RecyclerView.ViewHolder(binding.root) { + val imageView: ImageView = binding.benchmarkItemListImage + val textView: TextView = binding.benchmarkItemListText +} diff --git a/sentry-android-integration-tests/sentry-uitest-android-benchmark/src/main/res/layout/activity_benchmark.xml b/sentry-android-integration-tests/sentry-uitest-android-benchmark/src/main/res/layout/activity_benchmark.xml new file mode 100644 index 00000000000..f70a1c2449f --- /dev/null +++ b/sentry-android-integration-tests/sentry-uitest-android-benchmark/src/main/res/layout/activity_benchmark.xml @@ -0,0 +1,29 @@ + + + + + + + + diff --git a/sentry-android-integration-tests/sentry-uitest-android-benchmark/src/main/res/layout/benchmark_item_list.xml b/sentry-android-integration-tests/sentry-uitest-android-benchmark/src/main/res/layout/benchmark_item_list.xml new file mode 100644 index 00000000000..a3bc14e3a78 --- /dev/null +++ b/sentry-android-integration-tests/sentry-uitest-android-benchmark/src/main/res/layout/benchmark_item_list.xml @@ -0,0 +1,31 @@ + + + + + + + + diff --git a/sentry-android-integration-tests/sentry-uitest-android/.gitignore b/sentry-android-integration-tests/sentry-uitest-android/.gitignore new file mode 100644 index 00000000000..796b96d1c40 --- /dev/null +++ b/sentry-android-integration-tests/sentry-uitest-android/.gitignore @@ -0,0 +1 @@ +/build diff --git a/sentry-android-integration-tests/sentry-uitest-android/README.md b/sentry-android-integration-tests/sentry-uitest-android/README.md new file mode 100644 index 00000000000..c11397383d0 --- /dev/null +++ b/sentry-android-integration-tests/sentry-uitest-android/README.md @@ -0,0 +1,39 @@ +Ui tests for Android +=========== +Here will be put all ui tests for Android, running through Google's Espresso. +By default the envelopes sent to relay are caught by a mock server which allows us to check the envelopes sent. + +# How to use + +Simply run `./gradlew connectedCheck` to run all ui tests of all modules (requires a connected device, either physical or an emulator). +_Care: the benchmarks need to run the tests multiple times to get reliable results. This means they can take a long time (several minutes)._ +If you don't care about benchmark tests you can run `./gradlew connectedCheck -x :sentry-android-integration-tests:sentry-uitest-android-benchmark:connectedCheck`. +You can run benchmark tests only with `./gradlew :sentry-android-integration-tests:sentry-uitest-android-benchmark:connectedCheck`. + +# SauceLabs +To run on saucelabs execute following commands (need also `SAUCE_USERNAME` and `SAUCE_ACCESS_KEY` environment variables): +For Benchmarks: +``` +./gradlew :sentry-android-integration-tests:sentry-uitest-android-benchmark:assembleRelease +./gradlew :sentry-android-integration-tests:sentry-uitest-android-benchmark:assembleAndroidTest -DtestBuildType=release +saucectl run -c .sauce/sentry-uitest-android-benchmark.yml +``` +For End 2 End: +``` +./gradlew :sentry-android-integration-tests:sentry-uitest-android:assembleRelease +./gradlew :sentry-android-integration-tests:sentry-uitest-android:assembleAndroidTest -DtestBuildType=release +saucectl run -c .sauce/sentry-uitest-android-end2end.yml +``` + +# Troubleshooting + +There is an issue with Android 11+ (Xiaomi only?). +In order to start an activity from the background (which the test orchestrator does internally), the app needs a special permission, which cannot be granted without user interaction. +To allow it on the device go in Settings -> apps -> manage apps -> Select the app, like `Sentry End2End Tests` -> Other permissions -> `Display pop-up windows while running in the background`. +The path may be different on other devices. +On older versions of Android there is no problem. + +For this reason we cannot use the `testInstrumentationRunnerArguments["clearPackageData"] = "true"` in the build.gradle file, as clearing package data resets permissions. +This flag is used to run each test in its own instance of Instrumentation. This way they are isolated from one another and get their own Application instance. +More on https://developer.android.com/training/testing/instrumented-tests/androidx-test-libraries/runner#enable-gradle + diff --git a/sentry-android-integration-tests/sentry-uitest-android/build.gradle.kts b/sentry-android-integration-tests/sentry-uitest-android/build.gradle.kts new file mode 100644 index 00000000000..75d95d5bde6 --- /dev/null +++ b/sentry-android-integration-tests/sentry-uitest-android/build.gradle.kts @@ -0,0 +1,128 @@ +import io.gitlab.arturbosch.detekt.Detekt +import io.gitlab.arturbosch.detekt.extensions.DetektExtension +import net.ltgt.gradle.errorprone.errorprone + +plugins { + id("com.android.library") + kotlin("android") + id(Config.QualityPlugins.errorProne) + id(Config.QualityPlugins.gradleVersions) + id(Config.QualityPlugins.detektPlugin) +} + +android { + compileSdk = Config.Android.compileSdkVersion + + defaultConfig { + minSdk = Config.Android.minSdkVersionNdk + targetSdk = Config.Android.targetSdkVersion + + testInstrumentationRunner = Config.TestLibs.androidJUnitRunner + // Runs each test in its own instance of Instrumentation. This way they are isolated from + // one another and get their own Application instance. + // https://developer.android.com/training/testing/instrumented-tests/androidx-test-libraries/runner#enable-gradle + // This doesn't work on some devices with Android 11+. Clearing package data resets permissions. + // Check the readme for more info. +// testInstrumentationRunnerArguments["clearPackageData"] = "true" + } + + buildFeatures { + // Determines whether to support View Binding. + // Note that the viewBinding.enabled property is now deprecated. + viewBinding = true + } + + testOptions { + execution = "ANDROIDX_TEST_ORCHESTRATOR" + } + + signingConfigs { + getByName("debug") { + storeFile = rootProject.file("debug.keystore") + storePassword = "android" + keyAlias = "androiddebugkey" + keyPassword = "android" + } + } + + testBuildType = System.getProperty("testBuildType", "debug") + + buildTypes { + getByName("debug") { + isMinifyEnabled = true + signingConfig = signingConfigs.getByName("debug") + proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt")) + } + getByName("release") { + isMinifyEnabled = true + isShrinkResources = false + proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt")) + signingConfig = signingConfigs.getByName("debug") // to be able to run release mode + } + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_1_8.toString() + } + + lint { + warningsAsErrors = true + checkDependencies = true + + // We run a full lint analysis as build part in CI, so skip vital checks for assemble tasks. + checkReleaseBuilds = false + } + + variantFilter { + if (Config.Android.shouldSkipDebugVariant(buildType.name)) { + ignore = true + } + } +} + +dependencies { + + implementation(kotlin(Config.kotlinStdLib, org.jetbrains.kotlin.config.KotlinCompilerVersion.VERSION)) + + implementation(projects.sentryAndroid) + implementation(Config.Libs.appCompat) + implementation(Config.Libs.androidxCore) + implementation(Config.TestLibs.espressoIdlingResource) + + compileOnly(Config.CompileOnly.nopen) + errorprone(Config.CompileOnly.nopenChecker) + errorprone(Config.CompileOnly.errorprone) + errorprone(Config.CompileOnly.errorProneNullAway) + + androidTestImplementation(projects.sentryTestSupport) + androidTestImplementation(Config.TestLibs.kotlinTestJunit) + androidTestImplementation(Config.TestLibs.espressoCore) + androidTestImplementation(Config.TestLibs.androidxRunner) + androidTestImplementation(Config.TestLibs.androidxTestRules) + androidTestImplementation(Config.TestLibs.androidxTestCoreKtx) + androidTestImplementation(Config.TestLibs.mockWebserver) + androidTestImplementation(Config.TestLibs.androidxJunit) + androidTestUtil(Config.TestLibs.androidxTestOrchestrator) +} + +tasks.withType().configureEach { + options.errorprone { + check("NullAway", net.ltgt.gradle.errorprone.CheckSeverity.ERROR) + option("NullAway:AnnotatedPackages", "io.sentry") + option("NullAway:UnannotatedSubPackages", "io.sentry.uitest.android.databinding") + } +} + +tasks.withType { + // Target version of the generated JVM bytecode. It is used for type resolution. + jvmTarget = JavaVersion.VERSION_1_8.toString() +} + +configure { + buildUponDefaultConfig = true + allRules = true +} + +kotlin { + explicitApi() +} diff --git a/sentry-android-integration-tests/sentry-uitest-android/proguard-rules.pro b/sentry-android-integration-tests/sentry-uitest-android/proguard-rules.pro new file mode 100644 index 00000000000..f1b424510da --- /dev/null +++ b/sentry-android-integration-tests/sentry-uitest-android/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/src/androidTest/java/io/sentry/uitest/android/BaseUiTest.kt b/sentry-android-integration-tests/sentry-uitest-android/src/androidTest/java/io/sentry/uitest/android/BaseUiTest.kt new file mode 100644 index 00000000000..581c69b0c87 --- /dev/null +++ b/sentry-android-integration-tests/sentry-uitest-android/src/androidTest/java/io/sentry/uitest/android/BaseUiTest.kt @@ -0,0 +1,78 @@ +package io.sentry.uitest.android + +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import androidx.test.espresso.Espresso +import androidx.test.espresso.IdlingRegistry +import androidx.test.espresso.idling.CountingIdlingResource +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.runner.AndroidJUnitRunner +import io.sentry.Sentry +import io.sentry.SentryOptions +import io.sentry.android.core.SentryAndroid +import io.sentry.uitest.android.mockservers.MockRelay +import kotlin.test.AfterTest +import kotlin.test.BeforeTest + +abstract class BaseUiTest { + + /** Runner of the test. */ + protected lateinit var runner: AndroidJUnitRunner + /** Application context for the current test. */ + protected lateinit var context: Context + /** Mock dsn used to send envelopes to our mock [relay] server. */ + protected lateinit var mockDsn: String + // The mockDsn cannot be changed. If a custom dsn needs to be used, it can be set in the options as usual + private set + /** + * Idling resource that will be checked by the relay server (if [initSentry] param relayWaitForRequests is true). + * This should be increased to match any envelope that will be sent during the test, + * so that they can later be checked. + */ + protected val relayIdlingResource = CountingIdlingResource("relay-requests") + + /** Mock relay server that receives all envelopes sent during the test. */ + protected val relay = MockRelay(false, relayIdlingResource) + + @BeforeTest + fun baseSetUp() { + runner = InstrumentationRegistry.getInstrumentation() as AndroidJUnitRunner + context = ApplicationProvider.getApplicationContext() + context.cacheDir.deleteRecursively() + relay.start() + mockDsn = relay.createMockDsn() + } + + @AfterTest + fun baseFinish() { + IdlingRegistry.getInstance().unregister(relayIdlingResource) + relay.shutdown() + Sentry.close() + } + + /** + * Initializes the Sentry sdk through [SentryAndroid.init] with a default dsn used to catch envelopes with [relay]. + * [relayWaitForRequests] sets whether [relay] should wait for all the envelopes sent when doing assertions. + * If true, [relayIdlingResource] should be increased to match any envelope that will be sent during the test. + * Sentry options can be adjusted as usual through [optionsConfiguration]. + */ + protected fun initSentry( + relayWaitForRequests: Boolean = false, + optionsConfiguration: ((options: SentryOptions) -> Unit)? = null + ) { + relay.waitForRequests = relayWaitForRequests + if (relayWaitForRequests) { + IdlingRegistry.getInstance().register(relayIdlingResource) + } + SentryAndroid.init(context) { + it.dsn = mockDsn + optionsConfiguration?.invoke(it) + } + } +} + +/** Waits until the Sentry SDK is idle. */ +fun waitUntilIdle() { + // We rely on Espresso's idling resources. + Espresso.onIdle() +} diff --git a/sentry-android-integration-tests/sentry-uitest-android/src/androidTest/java/io/sentry/uitest/android/EnvelopeTests.kt b/sentry-android-integration-tests/sentry-uitest-android/src/androidTest/java/io/sentry/uitest/android/EnvelopeTests.kt new file mode 100644 index 00000000000..df4bf05c71a --- /dev/null +++ b/sentry-android-integration-tests/sentry-uitest-android/src/androidTest/java/io/sentry/uitest/android/EnvelopeTests.kt @@ -0,0 +1,59 @@ +package io.sentry.uitest.android + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.sentry.ProfilingTraceData +import io.sentry.Sentry +import io.sentry.SentryEvent +import io.sentry.SentryOptions +import io.sentry.protocol.SentryTransaction +import org.junit.runner.RunWith +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +@RunWith(AndroidJUnit4::class) +class EnvelopeTests : BaseUiTest() { + + @Test + fun checkEnvelopeCaptureMessage() { + + initSentry(true) + relayIdlingResource.increment() + Sentry.captureMessage("Message captured during test") + + relay.assert { + assertEnvelope { + val event: SentryEvent = it.assertItem() + it.assertNoOtherItems() + assertTrue(event.message?.formatted == "Message captured during test") + } + assertNoOtherEnvelopes() + assertNoOtherRequests() + } + } + + @Test + fun checkEnvelopeProfiledTransaction() { + + initSentry(true) { options: SentryOptions -> + options.tracesSampleRate = 1.0 + options.isProfilingEnabled = true + } + relayIdlingResource.increment() + val transaction = Sentry.startTransaction("e2etests", "test1") + + transaction.finish() + relay.assert { + assertEnvelope { + val transactionItem: SentryTransaction = it.assertItem() + val profilingTraceData: ProfilingTraceData = it.assertItem() + it.assertNoOtherItems() + assertTrue(transactionItem.transaction == "e2etests") + assertEquals(profilingTraceData.transactionId, transactionItem.eventId.toString()) + assertTrue(profilingTraceData.transactionName == "e2etests") + } + assertNoOtherEnvelopes() + assertNoOtherRequests() + } + } +} diff --git a/sentry-android-integration-tests/sentry-uitest-android/src/androidTest/java/io/sentry/uitest/android/mockservers/EnvelopeAsserter.kt b/sentry-android-integration-tests/sentry-uitest-android/src/androidTest/java/io/sentry/uitest/android/mockservers/EnvelopeAsserter.kt new file mode 100644 index 00000000000..7273531aeea --- /dev/null +++ b/sentry-android-integration-tests/sentry-uitest-android/src/androidTest/java/io/sentry/uitest/android/mockservers/EnvelopeAsserter.kt @@ -0,0 +1,30 @@ +package io.sentry.uitest.android.mockservers + +import io.sentry.SentryEnvelope +import io.sentry.assertEnvelopeItem +import okhttp3.mockwebserver.MockResponse + +/** + * Class to make assertions on an envelope caught by [MockRelay]. + * It contains the sent envelope and the returned response, too. + */ +class EnvelopeAsserter(val envelope: SentryEnvelope, val response: MockResponse) { + /** List of items to assert. */ + val unassertedItems = envelope.items.toMutableList() + + /** + * Asserts an envelope item of [T] exists and returns the first one. + * The asserted item is then removed from internal list of unasserted items. + */ + inline fun assertItem(): T = assertEnvelopeItem(unassertedItems) { index, item -> + unassertedItems.removeAt(index) + return item + } + + /** Asserts there are no other items in the envelope. */ + fun assertNoOtherItems() { + if (unassertedItems.isNotEmpty()) { + throw AssertionError("There were other items: $unassertedItems") + } + } +} diff --git a/sentry-android-integration-tests/sentry-uitest-android/src/androidTest/java/io/sentry/uitest/android/mockservers/MockRelay.kt b/sentry-android-integration-tests/sentry-uitest-android/src/androidTest/java/io/sentry/uitest/android/mockservers/MockRelay.kt new file mode 100644 index 00000000000..ae28eb8e571 --- /dev/null +++ b/sentry-android-integration-tests/sentry-uitest-android/src/androidTest/java/io/sentry/uitest/android/mockservers/MockRelay.kt @@ -0,0 +1,103 @@ +package io.sentry.uitest.android.mockservers + +import androidx.test.espresso.idling.CountingIdlingResource +import io.sentry.uitest.android.waitUntilIdle +import okhttp3.mockwebserver.Dispatcher +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.MockWebServer +import okhttp3.mockwebserver.RecordedRequest + +/** Mocks a relay server. */ +class MockRelay( + var waitForRequests: Boolean, + private val relayIdlingResource: CountingIdlingResource +) { + + /** Mocks a relay server. */ + private val relay = MockWebServer() + + private val dsnProject = "1234" + private val envelopePath = "/api/$dsnProject/envelope/" + + /** List of unasserted requests sent to the [envelopePath]. */ + private val unassertedEnvelopes = mutableListOf() + + /** List of unasserted requests not contained in [unassertedEnvelopes]. */ + private val unassertedRequests = mutableListOf() + + /** List of responses to return when a request is sent. */ + private val responses = mutableListOf<(RecordedRequest) -> MockResponse?>() + + init { + relay.dispatcher = object : Dispatcher() { + override fun dispatch(request: RecordedRequest): MockResponse { + // We check if there is any custom response previously set to return to this request, + // otherwise we return a successful MockResponse. + val response = responses.asSequence() + .mapNotNull { it(request) } + .firstOrNull() + ?: MockResponse() + + // Based on the path of the request, we populate the right list. + val relayResponse = RelayAsserter.RelayResponse(request, response) + when (request.path) { + envelopePath -> { + unassertedEnvelopes.add(relayResponse) + } + else -> { + unassertedRequests.add(relayResponse) + } + } + + // If we are waiting for requests to be received, we decrement the associated counter. + if (waitForRequests) { + relayIdlingResource.decrement() + } + return response + } + } + } + + /** Creates a dsn that will send request to this [MockRelay]. */ + fun createMockDsn() = "http://key@${relay.hostName}:${relay.port}/$dsnProject" + + /** Starts the mock relay server. */ + fun start() = relay.start() + + /** Shutdown the mock relay server and clear everything. */ + fun shutdown() { + responses.clear() + relay.shutdown() + } + + /** Add a custom response to be returned at the next request received. */ + fun addResponse(response: (RecordedRequest) -> MockResponse?) { + // Responses are added to the beginning of the list so they'll take precedence over + // previously added ones. + responses.add(0, response) + } + + /** Add a custom response to be returned at the next request received, if it satisfies the [filter]. */ + fun addResponse( + filter: (RecordedRequest) -> Boolean, + responseBuilder: ((request: RecordedRequest, response: MockResponse) -> Unit)? = null + ) { + addResponse { request -> + if (filter(request)) { + MockResponse().also { response -> + responseBuilder?.invoke(request, response) + } + } else { + null + } + } + } + + /** Wait to receive all requests (if [waitForRequests] is true) and run the [assertion]. */ + fun assert(assertion: RelayAsserter.() -> Unit) { + if (waitForRequests) { + waitUntilIdle() + } + assertion(RelayAsserter(unassertedEnvelopes, unassertedRequests)) + } +} diff --git a/sentry-android-integration-tests/sentry-uitest-android/src/androidTest/java/io/sentry/uitest/android/mockservers/RelayAsserter.kt b/sentry-android-integration-tests/sentry-uitest-android/src/androidTest/java/io/sentry/uitest/android/mockservers/RelayAsserter.kt new file mode 100644 index 00000000000..0ecb8acfff0 --- /dev/null +++ b/sentry-android-integration-tests/sentry-uitest-android/src/androidTest/java/io/sentry/uitest/android/mockservers/RelayAsserter.kt @@ -0,0 +1,75 @@ +package io.sentry.uitest.android.mockservers + +import io.sentry.EnvelopeReader +import io.sentry.Sentry +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.RecordedRequest +import java.util.zip.GZIPInputStream + +/** Class used to assert requests sent to [MockRelay]. */ +class RelayAsserter( + private val unassertedEnvelopes: MutableList, + private val unassertedRequests: MutableList +) { + + /** + * Asserts an envelope request exists and allows to make other assertions on it and its response. + * The asserted envelope request is then removed from internal list of unasserted envelope. + */ + fun assertRawEnvelope(assertion: ((request: RecordedRequest, response: MockResponse) -> Unit)? = null) { + val relayResponse = unassertedEnvelopes.removeFirstOrNull() + ?: throw AssertionError("No envelope request found") + assertion?.let { + it(relayResponse.request, relayResponse.response) + } + } + + /** + * Asserts a request exists and makes other assertions on it and its response. + * The asserted request is then removed from internal list of unasserted requests. + */ + fun assertRawRequest(assertion: ((request: RecordedRequest, response: MockResponse) -> Unit)? = null) { + val relayResponse = unassertedRequests.removeFirstOrNull() + ?: throw AssertionError("No raw request found") + assertion?.let { + it(relayResponse.request, relayResponse.response) + } + } + + /** + * Asserts a request exists, parses it as an envelope and makes other assertions through a [EnvelopeAsserter]. + * The asserted envelope is then removed from internal list of unasserted envelopes. + */ + fun assertEnvelope(assertion: (asserter: EnvelopeAsserter) -> Unit) { + assertRawEnvelope { request, response -> + // Parse the request to rebuild the original envelope. If it fails we throw an assertion error. + val envelope = EnvelopeReader(Sentry.getCurrentHub().options.serializer) + .read(GZIPInputStream(request.body.inputStream())) + ?: throw AssertionError("Was unable to parse the request as an envelope: $request") + assertion(EnvelopeAsserter(envelope, response)) + } + } + + /** Asserts no other envelopes were sent. */ + fun assertNoOtherEnvelopes() { + if (unassertedEnvelopes.isNotEmpty()) { + throw AssertionError("There were other ${unassertedEnvelopes.size} envelope requests: $unassertedEnvelopes") + } + } + + /** Asserts no other raw requests were sent. */ + fun assertNoOtherRawRequests() { + assertNoOtherEnvelopes() + if (unassertedRequests.isNotEmpty()) { + throw AssertionError("There were other ${unassertedRequests.size} requests: $unassertedRequests") + } + } + + /** Asserts no other requests or envelopes were sent. */ + fun assertNoOtherRequests() { + assertNoOtherEnvelopes() + assertNoOtherRawRequests() + } + + data class RelayResponse(val request: RecordedRequest, val response: MockResponse) +} diff --git a/sentry-android-integration-tests/sentry-uitest-android/src/main/AndroidManifest.xml b/sentry-android-integration-tests/sentry-uitest-android/src/main/AndroidManifest.xml new file mode 100644 index 00000000000..15e849b6f90 --- /dev/null +++ b/sentry-android-integration-tests/sentry-uitest-android/src/main/AndroidManifest.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + diff --git a/sentry-android-integration-tests/sentry-uitest-android/src/main/java/io/sentry/uitest/android/EmptyActivity.kt b/sentry-android-integration-tests/sentry-uitest-android/src/main/java/io/sentry/uitest/android/EmptyActivity.kt new file mode 100644 index 00000000000..b8f7d5f373a --- /dev/null +++ b/sentry-android-integration-tests/sentry-uitest-android/src/main/java/io/sentry/uitest/android/EmptyActivity.kt @@ -0,0 +1,11 @@ +package io.sentry.uitest.android + +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity + +class EmptyActivity : AppCompatActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + } +} diff --git a/sentry-android-integration-tests/sentry-uitest-android/src/main/java/io/sentry/uitest/android/utils/BooleanIdlingResource.kt b/sentry-android-integration-tests/sentry-uitest-android/src/main/java/io/sentry/uitest/android/utils/BooleanIdlingResource.kt new file mode 100644 index 00000000000..86a5b26709a --- /dev/null +++ b/sentry-android-integration-tests/sentry-uitest-android/src/main/java/io/sentry/uitest/android/utils/BooleanIdlingResource.kt @@ -0,0 +1,30 @@ +package io.sentry.uitest.android.utils + +import androidx.test.espresso.IdlingResource +import java.util.concurrent.atomic.AtomicBoolean + +/** Idling resource based on a boolean flag. */ +class BooleanIdlingResource(private val name: String) : IdlingResource { + + private val isIdle = AtomicBoolean(true) + + private val isIdleLock = Object() + + private var callback: IdlingResource.ResourceCallback? = null + + /** Sets whether this resource is currently in idle state. */ + fun setIdle(idling: Boolean) { + if (!isIdle.getAndSet(idling) && idling) { + callback?.onTransitionToIdle() + } + } + + override fun getName(): String = name + + /** Check if this resource is currently in idle state. */ + override fun isIdleNow(): Boolean = synchronized(isIdleLock) { isIdle.get() } + + override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback?) { + this.callback = callback + } +} diff --git a/sentry-android-ndk/build.gradle.kts b/sentry-android-ndk/build.gradle.kts index 84a7b0e568b..152e95084ec 100644 --- a/sentry-android-ndk/build.gradle.kts +++ b/sentry-android-ndk/build.gradle.kts @@ -9,6 +9,7 @@ plugins { } var sentryNativeSrc: String = "sentry-native" +val sentryAndroidSdkName: String by project android { compileSdk = Config.Android.compileSdkVersion @@ -24,12 +25,13 @@ android { targetSdk = Config.Android.targetSdkVersion minSdk = Config.Android.minSdkVersionNdk // NDK requires a higher API level than core. - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + testInstrumentationRunner = Config.TestLibs.androidJUnitRunner externalNativeBuild { cmake { arguments.add(0, "-DANDROID_STL=c++_static") arguments.add(0, "-DSENTRY_NATIVE_SRC=$sentryNativeSrc") + arguments.add(0, "-DSENTRY_SDK_NAME=$sentryAndroidSdkName") } } diff --git a/sentry-android-ndk/proguard-rules.pro b/sentry-android-ndk/proguard-rules.pro index 6a5a62a2b85..fe8390c3797 100644 --- a/sentry-android-ndk/proguard-rules.pro +++ b/sentry-android-ndk/proguard-rules.pro @@ -1,6 +1,10 @@ ##---------------Begin: proguard configuration for NDK ---------- --keep class io.sentry.android.ndk.** { *; } +# The Android SDK checks at runtime if this class is available via Class.forName +-keep class io.sentry.android.ndk.SentryNdk { *; } +# The JNI layer uses this classes through reflection +-keep class io.sentry.android.core.SentryAndroidOptions { *; } +-keep class io.sentry.protocol.DebugImage { *; } # For native methods, see http://proguard.sourceforge.net/manual/examples.html#native -keepclasseswithmembernames,includedescriptorclasses class * { diff --git a/sentry-android-ndk/sentry-native b/sentry-android-ndk/sentry-native index 3436a29d839..5500192dda0 160000 --- a/sentry-android-ndk/sentry-native +++ b/sentry-android-ndk/sentry-native @@ -1 +1 @@ -Subproject commit 3436a29d839aa7437548be940ab62a85ca699635 +Subproject commit 5500192dda05c82468787b2b0637e9c2688b9aed diff --git a/sentry-android-ndk/src/main/java/io/sentry/android/ndk/SentryNdk.java b/sentry-android-ndk/src/main/java/io/sentry/android/ndk/SentryNdk.java index 2a5bffd8ad0..1ddc04c5243 100644 --- a/sentry-android-ndk/src/main/java/io/sentry/android/ndk/SentryNdk.java +++ b/sentry-android-ndk/src/main/java/io/sentry/android/ndk/SentryNdk.java @@ -32,7 +32,11 @@ private SentryNdk() {} public static void init(@NotNull final SentryAndroidOptions options) { SentryNdkUtil.addPackage(options.getSdkVersion()); initSentryNative(options); - options.addScopeObserver(new NdkScopeObserver(options)); + + // only add scope sync observer if the scope sync is enabled. + if (options.isEnableScopeSync()) { + options.addScopeObserver(new NdkScopeObserver(options)); + } options.setDebugImagesLoader(new DebugImagesLoader(options, new NativeModuleListLoader())); } 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 3880143234e..1a01c21bef7 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 @@ -5,7 +5,7 @@ import com.nhaarman.mockitokotlin2.mock import com.nhaarman.mockitokotlin2.verify import io.sentry.Breadcrumb import io.sentry.DateUtils -import io.sentry.GsonSerializer +import io.sentry.JsonSerializer import io.sentry.SentryLevel import io.sentry.SentryOptions import io.sentry.protocol.User @@ -16,7 +16,7 @@ class NdkScopeObserverTest { private class Fixture { val nativeScope = mock() val options = SentryOptions().apply { - setSerializer(GsonSerializer(mock())) + setSerializer(JsonSerializer(mock())) } fun getSut(): NdkScopeObserver { diff --git a/sentry-android-okhttp/build.gradle.kts b/sentry-android-okhttp/build.gradle.kts index a06a1fe8463..53243225084 100644 --- a/sentry-android-okhttp/build.gradle.kts +++ b/sentry-android-okhttp/build.gradle.kts @@ -30,6 +30,7 @@ android { kotlinOptions { jvmTarget = JavaVersion.VERSION_1_8.toString() + kotlinOptions.languageVersion = Config.kotlinCompatibleLanguageVersion } testOptions { diff --git a/sentry-android-okhttp/proguard-rules.pro b/sentry-android-okhttp/proguard-rules.pro index 2c038252f1d..3f9ea4feb27 100644 --- a/sentry-android-okhttp/proguard-rules.pro +++ b/sentry-android-okhttp/proguard-rules.pro @@ -1,23 +1,13 @@ ##---------------Begin: proguard configuration for OkHttp ---------- --keep class io.sentry.android.okhttp.** { *; } - # To ensure that stack traces is unambiguous # https://developer.android.com/studio/build/shrink-code#decode-stack-trace -keepattributes LineNumberTable,SourceFile -# https://square.github.io/okhttp/r8_proguard/ -# JSR 305 annotations are for embedding nullability information. --dontwarn javax.annotation.** - -# A resource is loaded with a relative path so the package of this class must be preserved. --keepnames class okhttp3.internal.publicsuffix.PublicSuffixDatabase - -# Animal Sniffer compileOnly dependency to ensure APIs are compatible with older versions of Java. --dontwarn org.codehaus.mojo.animal_sniffer.* - -# OkHttp platform used only on JVM and when Conscrypt dependency is available. --dontwarn okhttp3.internal.platform.ConscryptPlatform --dontwarn org.conscrypt.ConscryptHostnameVerifier +# https://square.github.io/okhttp/features/r8_proguard/ +# If you use OkHttp as a dependency in an Android project which uses R8 as a default compiler you +# don’t have to do anything. The specific rules are already bundled into the JAR which can +# be interpreted by R8 automatically. +# https://raw.githubusercontent.com/square/okhttp/master/okhttp/src/jvmMain/resources/META-INF/proguard/okhttp3.pro ##---------------End: proguard configuration for OkHttp ---------- diff --git a/sentry-android-okhttp/src/main/java/io/sentry/android/okhttp/SentryOkHttpInterceptor.kt b/sentry-android-okhttp/src/main/java/io/sentry/android/okhttp/SentryOkHttpInterceptor.kt index c9eb52acd73..9228ca0a529 100644 --- a/sentry-android-okhttp/src/main/java/io/sentry/android/okhttp/SentryOkHttpInterceptor.kt +++ b/sentry-android-okhttp/src/main/java/io/sentry/android/okhttp/SentryOkHttpInterceptor.kt @@ -1,11 +1,14 @@ package io.sentry.android.okhttp import io.sentry.Breadcrumb +import io.sentry.Hint import io.sentry.HubAdapter import io.sentry.IHub import io.sentry.ISpan import io.sentry.SpanStatus import io.sentry.TracingOrigins +import io.sentry.TypeCheckHint.OKHTTP_REQUEST +import io.sentry.TypeCheckHint.OKHTTP_RESPONSE import okhttp3.Interceptor import okhttp3.Request import okhttp3.Response @@ -59,10 +62,18 @@ class SentryOkHttpInterceptor( request.body?.contentLength().ifHasValidLength { breadcrumb.setData("request_body_size", it) } - response?.body?.contentLength().ifHasValidLength { - breadcrumb.setData("response_body_size", it) + + val hint = Hint() + .also { it.set(OKHTTP_REQUEST, request) } + response?.let { + it.body?.contentLength().ifHasValidLength { responseBodySize -> + breadcrumb.setData("response_body_size", responseBodySize) + } + + hint[OKHTTP_RESPONSE] = it } - hub.addBreadcrumb(breadcrumb) + + hub.addBreadcrumb(breadcrumb, hint) } } diff --git a/sentry-android-okhttp/src/test/java/io/sentry/android/okhttp/SentryOkHttpInterceptorTest.kt b/sentry-android-okhttp/src/test/java/io/sentry/android/okhttp/SentryOkHttpInterceptorTest.kt index c312b1f486e..65b39b746e8 100644 --- a/sentry-android-okhttp/src/test/java/io/sentry/android/okhttp/SentryOkHttpInterceptorTest.kt +++ b/sentry-android-okhttp/src/test/java/io/sentry/android/okhttp/SentryOkHttpInterceptorTest.kt @@ -2,6 +2,7 @@ package io.sentry.android.okhttp import com.nhaarman.mockitokotlin2.any +import com.nhaarman.mockitokotlin2.anyOrNull import com.nhaarman.mockitokotlin2.check import com.nhaarman.mockitokotlin2.mock import com.nhaarman.mockitokotlin2.verify @@ -162,7 +163,8 @@ class SentryOkHttpInterceptorTest { assertEquals("http", it.type) assertEquals(13L, it.data["response_body_size"]) assertEquals(12L, it.data["request_body_size"]) - } + }, + anyOrNull() ) } @@ -182,7 +184,8 @@ class SentryOkHttpInterceptorTest { verify(fixture.hub).addBreadcrumb( check { assertEquals("http", it.type) - } + }, + anyOrNull() ) } diff --git a/sentry-android-timber/build.gradle.kts b/sentry-android-timber/build.gradle.kts index f8c94ce4472..16649360d2b 100644 --- a/sentry-android-timber/build.gradle.kts +++ b/sentry-android-timber/build.gradle.kts @@ -1,7 +1,6 @@ import io.gitlab.arturbosch.detekt.Detekt import io.gitlab.arturbosch.detekt.extensions.DetektExtension import org.jetbrains.kotlin.config.KotlinCompilerVersion -import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { id("com.android.library") @@ -18,7 +17,7 @@ android { targetSdk = Config.Android.targetSdkVersion minSdk = Config.Android.minSdkVersion - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + testInstrumentationRunner = Config.TestLibs.androidJUnitRunner // for AGP 4.1 buildConfigField("String", "VERSION_NAME", "\"${project.version}\"") @@ -34,6 +33,7 @@ android { kotlinOptions { jvmTarget = JavaVersion.VERSION_1_8.toString() + kotlinOptions.languageVersion = Config.kotlinCompatibleLanguageVersion } testOptions { @@ -69,11 +69,6 @@ kotlin { explicitApi() } -tasks.withType().configureEach { - // Timber uses Kotlin 1.2 - kotlinOptions.languageVersion = "1.2" -} - dependencies { api(projects.sentry) diff --git a/sentry-android-timber/proguard-rules.pro b/sentry-android-timber/proguard-rules.pro index 284387e102b..f3557d3fd5b 100644 --- a/sentry-android-timber/proguard-rules.pro +++ b/sentry-android-timber/proguard-rules.pro @@ -1,6 +1,7 @@ ##---------------Begin: proguard configuration for Timber ---------- --keep class io.sentry.android.timber.** { *; } +# The Android SDK checks at runtime if this class is available via Class.forName +-keep class io.sentry.android.timber.SentryTimberIntegration { (...); } # To ensure that stack traces is unambiguous # https://developer.android.com/studio/build/shrink-code#decode-stack-trace diff --git a/sentry-android-timber/src/main/java/io/sentry/android/timber/SentryTimberTree.kt b/sentry-android-timber/src/main/java/io/sentry/android/timber/SentryTimberTree.kt index 1e76f6a69b4..f3a0f599a98 100644 --- a/sentry-android-timber/src/main/java/io/sentry/android/timber/SentryTimberTree.kt +++ b/sentry-android-timber/src/main/java/io/sentry/android/timber/SentryTimberTree.kt @@ -232,7 +232,7 @@ class SentryTimberTree( val sentryMessage = Message().apply { this.message = message if (!message.isNullOrEmpty() && args.isNotEmpty()) { - this.formatted = message?.format(*args) + this.formatted = message.format(*args) } this.params = args.map { it.toString() } } diff --git a/sentry-apache-http-client-5/api/sentry-apache-http-client-5.api b/sentry-apache-http-client-5/api/sentry-apache-http-client-5.api index b911ea86a2e..7d7479c9fd3 100644 --- a/sentry-apache-http-client-5/api/sentry-apache-http-client-5.api +++ b/sentry-apache-http-client-5/api/sentry-apache-http-client-5.api @@ -2,7 +2,7 @@ public final class io/sentry/transport/apache/ApacheHttpClientTransport : io/sen public fun (Lio/sentry/SentryOptions;Lio/sentry/RequestDetails;Lorg/apache/hc/client5/http/impl/async/CloseableHttpAsyncClient;Lio/sentry/transport/RateLimiter;)V public fun close ()V public fun flush (J)V - public fun send (Lio/sentry/SentryEnvelope;Ljava/lang/Object;)V + public fun send (Lio/sentry/SentryEnvelope;Lio/sentry/Hint;)V } public final class io/sentry/transport/apache/ApacheHttpClientTransportFactory : io/sentry/ITransportFactory { diff --git a/sentry-apache-http-client-5/build.gradle.kts b/sentry-apache-http-client-5/build.gradle.kts index 2b6b67073d7..861f1f7a557 100644 --- a/sentry-apache-http-client-5/build.gradle.kts +++ b/sentry-apache-http-client-5/build.gradle.kts @@ -16,7 +16,7 @@ configure { tasks.withType().configureEach { kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString() - kotlinOptions.languageVersion = Config.springKotlinCompatibleLanguageVersion + kotlinOptions.languageVersion = Config.kotlinCompatibleLanguageVersion } dependencies { diff --git a/sentry-apache-http-client-5/src/main/java/io/sentry/transport/apache/ApacheHttpClientTransport.java b/sentry-apache-http-client-5/src/main/java/io/sentry/transport/apache/ApacheHttpClientTransport.java index a2a52581433..d157d400684 100644 --- a/sentry-apache-http-client-5/src/main/java/io/sentry/transport/apache/ApacheHttpClientTransport.java +++ b/sentry-apache-http-client-5/src/main/java/io/sentry/transport/apache/ApacheHttpClientTransport.java @@ -2,13 +2,17 @@ import static io.sentry.SentryLevel.*; +import io.sentry.Hint; import io.sentry.RequestDetails; import io.sentry.SentryEnvelope; import io.sentry.SentryLevel; import io.sentry.SentryOptions; +import io.sentry.clientreport.DiscardReason; +import io.sentry.hints.Retryable; import io.sentry.transport.ITransport; import io.sentry.transport.RateLimiter; import io.sentry.transport.ReusableCountLatch; +import io.sentry.util.HintUtils; import io.sentry.util.Objects; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -25,7 +29,6 @@ import org.apache.hc.core5.io.CloseMode; import org.apache.hc.core5.util.TimeValue; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; /** * {@link ITransport} implementation that executes request asynchronously in a non-blocking manner @@ -63,74 +66,106 @@ public ApacheHttpClientTransport( @Override @SuppressWarnings("FutureReturnValueIgnored") - public void send(final @NotNull SentryEnvelope envelope, final @Nullable Object hint) + public void send(final @NotNull SentryEnvelope envelope, final @NotNull Hint hint) throws IOException { if (isSchedulingAllowed()) { final SentryEnvelope filteredEnvelope = rateLimiter.filter(envelope, hint); if (filteredEnvelope != null) { - currentlyRunning.increment(); - - try (final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - final GZIPOutputStream gzip = new GZIPOutputStream(outputStream)) { - options.getSerializer().serialize(filteredEnvelope, gzip); - - final SimpleHttpRequest request = - SimpleHttpRequests.post(requestDetails.getUrl().toString()); - request.setBody( - outputStream.toByteArray(), ContentType.create("application/x-sentry-envelope")); - request.setHeader("Content-Encoding", "gzip"); - request.setHeader("Accept", "application/json"); - - for (Map.Entry header : requestDetails.getHeaders().entrySet()) { - request.setHeader(header.getKey(), header.getValue()); - } + final SentryEnvelope envelopeWithClientReport = + options.getClientReportRecorder().attachReportToEnvelope(filteredEnvelope); + + if (envelopeWithClientReport != null) { + currentlyRunning.increment(); + + try (final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + final GZIPOutputStream gzip = new GZIPOutputStream(outputStream)) { + options.getSerializer().serialize(envelopeWithClientReport, gzip); + + final SimpleHttpRequest request = + SimpleHttpRequests.post(requestDetails.getUrl().toString()); + request.setBody( + outputStream.toByteArray(), ContentType.create("application/x-sentry-envelope")); + request.setHeader("Content-Encoding", "gzip"); + request.setHeader("Accept", "application/json"); + + for (Map.Entry header : requestDetails.getHeaders().entrySet()) { + request.setHeader(header.getKey(), header.getValue()); + } + + if (options.getLogger().isEnabled(DEBUG)) { + options + .getLogger() + .log(DEBUG, "Currently running %d requests", currentlyRunning.getCount()); + } + + httpclient.execute( + request, + new FutureCallback() { + @Override + public void completed(SimpleHttpResponse response) { + if (response.getCode() != 200) { + options + .getLogger() + .log(ERROR, "Request failed, API returned %s", response.getCode()); + + if (response.getCode() >= 400 && response.getCode() != 429) { + if (!HintUtils.hasType(hint, Retryable.class)) { + options + .getClientReportRecorder() + .recordLostEnvelope( + DiscardReason.NETWORK_ERROR, envelopeWithClientReport); + } + } + } else { + options.getLogger().log(INFO, "Envelope sent successfully."); + } + final Header retryAfter = response.getFirstHeader("Retry-After"); + final Header rateLimits = response.getFirstHeader("X-Sentry-Rate-Limits"); + rateLimiter.updateRetryAfterLimits( + rateLimits != null ? rateLimits.getValue() : null, + retryAfter != null ? retryAfter.getValue() : null, + response.getCode()); + currentlyRunning.decrement(); + } - if (options.getLogger().isEnabled(DEBUG)) { - options - .getLogger() - .log(DEBUG, "Currently running %d requests", currentlyRunning.getCount()); - } + @Override + public void failed(Exception ex) { + options.getLogger().log(ERROR, "Error while sending an envelope", ex); + if (!HintUtils.hasType(hint, Retryable.class)) { + options + .getClientReportRecorder() + .recordLostEnvelope( + DiscardReason.NETWORK_ERROR, envelopeWithClientReport); + } + currentlyRunning.decrement(); + } - httpclient.execute( - request, - new FutureCallback() { - @Override - public void completed(SimpleHttpResponse response) { - if (response.getCode() != 200) { - options - .getLogger() - .log(ERROR, "Request failed, API returned %s", response.getCode()); - } else { - options.getLogger().log(INFO, "Envelope sent successfully."); + @Override + public void cancelled() { + options.getLogger().log(WARNING, "Request cancelled"); + if (!HintUtils.hasType(hint, Retryable.class)) { + options + .getClientReportRecorder() + .recordLostEnvelope( + DiscardReason.NETWORK_ERROR, envelopeWithClientReport); + } + currentlyRunning.decrement(); } - final Header retryAfter = response.getFirstHeader("Retry-After"); - final Header rateLimits = response.getFirstHeader("X-Sentry-Rate-Limits"); - rateLimiter.updateRetryAfterLimits( - rateLimits != null ? rateLimits.getValue() : null, - retryAfter != null ? retryAfter.getValue() : null, - response.getCode()); - currentlyRunning.decrement(); - } - - @Override - public void failed(Exception ex) { - options.getLogger().log(ERROR, "Error while sending an envelope", ex); - currentlyRunning.decrement(); - } - - @Override - public void cancelled() { - options.getLogger().log(WARNING, "Request cancelled"); - currentlyRunning.decrement(); - } - }); - } catch (Throwable e) { - options.getLogger().log(ERROR, "Error when sending envelope", e); + }); + } catch (Throwable e) { + options.getLogger().log(ERROR, "Error when sending envelope", e); + if (!HintUtils.hasType(hint, Retryable.class)) { + options + .getClientReportRecorder() + .recordLostEnvelope(DiscardReason.NETWORK_ERROR, envelopeWithClientReport); + } + } } } } else { options.getLogger().log(SentryLevel.WARNING, "Submit cancelled"); + options.getClientReportRecorder().recordLostEnvelope(DiscardReason.QUEUE_OVERFLOW, envelope); } } diff --git a/sentry-apache-http-client-5/src/main/java/io/sentry/transport/apache/ApacheHttpClientTransportFactory.java b/sentry-apache-http-client-5/src/main/java/io/sentry/transport/apache/ApacheHttpClientTransportFactory.java index 2e4dd658dff..e6687af845b 100644 --- a/sentry-apache-http-client-5/src/main/java/io/sentry/transport/apache/ApacheHttpClientTransportFactory.java +++ b/sentry-apache-http-client-5/src/main/java/io/sentry/transport/apache/ApacheHttpClientTransportFactory.java @@ -62,7 +62,7 @@ public ApacheHttpClientTransportFactory(final @NotNull TimeValue connectionTimeT .setResponseTimeout(options.getReadTimeoutMillis(), TimeUnit.MILLISECONDS) .build()) .build(); - final RateLimiter rateLimiter = new RateLimiter(options.getLogger()); + final RateLimiter rateLimiter = new RateLimiter(options); return new ApacheHttpClientTransport(options, requestDetails, httpclient, rateLimiter); } diff --git a/sentry-apache-http-client-5/src/test/kotlin/io/sentry/SentryOptionsManipulator.kt b/sentry-apache-http-client-5/src/test/kotlin/io/sentry/SentryOptionsManipulator.kt new file mode 100644 index 00000000000..cdbd27ebb5a --- /dev/null +++ b/sentry-apache-http-client-5/src/test/kotlin/io/sentry/SentryOptionsManipulator.kt @@ -0,0 +1,12 @@ +package io.sentry + +import io.sentry.clientreport.IClientReportRecorder + +class SentryOptionsManipulator { + + companion object { + fun setClientReportRecorder(options: SentryOptions, clientReportRecorder: IClientReportRecorder) { + options.clientReportRecorder = clientReportRecorder + } + } +} diff --git a/sentry-apache-http-client-5/src/test/kotlin/io/sentry/transport/apache/ApacheHttpClientTransportClientReportTest.kt b/sentry-apache-http-client-5/src/test/kotlin/io/sentry/transport/apache/ApacheHttpClientTransportClientReportTest.kt new file mode 100644 index 00000000000..2dc23399eaf --- /dev/null +++ b/sentry-apache-http-client-5/src/test/kotlin/io/sentry/transport/apache/ApacheHttpClientTransportClientReportTest.kt @@ -0,0 +1,225 @@ +package io.sentry.transport.apache + +import com.nhaarman.mockitokotlin2.any +import com.nhaarman.mockitokotlin2.anyOrNull +import com.nhaarman.mockitokotlin2.eq +import com.nhaarman.mockitokotlin2.mock +import com.nhaarman.mockitokotlin2.never +import com.nhaarman.mockitokotlin2.same +import com.nhaarman.mockitokotlin2.spy +import com.nhaarman.mockitokotlin2.times +import com.nhaarman.mockitokotlin2.verify +import com.nhaarman.mockitokotlin2.verifyNoMoreInteractions +import com.nhaarman.mockitokotlin2.whenever +import io.sentry.ILogger +import io.sentry.RequestDetails +import io.sentry.SentryEnvelope +import io.sentry.SentryEvent +import io.sentry.SentryLevel +import io.sentry.SentryOptions +import io.sentry.SentryOptionsManipulator +import io.sentry.clientreport.DiscardReason +import io.sentry.clientreport.IClientReportRecorder +import io.sentry.hints.Retryable +import io.sentry.transport.RateLimiter +import io.sentry.transport.ReusableCountLatch +import io.sentry.util.HintUtils +import org.apache.hc.client5.http.async.methods.SimpleHttpResponse +import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient +import org.apache.hc.core5.concurrent.FutureCallback +import java.util.concurrent.CompletableFuture +import java.util.concurrent.Executors +import kotlin.test.AfterTest +import kotlin.test.Test + +class ApacheHttpClientTransportClientReportTest { + + class Fixture { + val options: SentryOptions + val logger = mock() + val rateLimiter = mock() + val clientReportRecorder = mock() + val requestDetails = RequestDetails("http://key@localhost/proj", mapOf("header-name" to "header-value")) + val client = mock() + val currentlyRunning = spy() + val executorService = Executors.newFixedThreadPool(2) + val envelopeBeforeClientReportAttached: SentryEnvelope + val envelopeAfterClientReportAttached: SentryEnvelope + + init { + whenever(rateLimiter.filter(any(), anyOrNull())).thenAnswer { it.arguments[0] } + options = SentryOptions() + options.setSerializer(mock()) + options.setDiagnosticLevel(SentryLevel.WARNING) + options.setDebug(true) + options.setLogger(logger) + SentryOptionsManipulator.setClientReportRecorder(options, clientReportRecorder) + + envelopeBeforeClientReportAttached = SentryEnvelope.from(options.serializer, SentryEvent(), null) + envelopeAfterClientReportAttached = SentryEnvelope.from(options.serializer, SentryEvent(), null) + whenever(clientReportRecorder.attachReportToEnvelope(same(envelopeBeforeClientReportAttached))) + .thenReturn(envelopeAfterClientReportAttached) + } + + fun getSut(response: SimpleHttpResponse? = null, queueFull: Boolean = false): ApacheHttpClientTransport { + + val transport = ApacheHttpClientTransport(options, requestDetails, client, rateLimiter, currentlyRunning) + + if (response != null) { + whenever(client.execute(any(), any())).thenAnswer { + (it.arguments[1] as FutureCallback).completed(response) + CompletableFuture.completedFuture(response) + } + } + + if (queueFull) { + whenever(currentlyRunning.count).thenReturn(options.maxQueueSize) + } + return transport + } + } + + private val fixture = Fixture() + + @AfterTest + fun `shutdown executor`() { + fixture.executorService.shutdownNow() + } + + @Test + fun `attaches client report to envelope`() { + val sut = fixture.getSut(SimpleHttpResponse(200)) + + sut.send(fixture.envelopeBeforeClientReportAttached) + + verify(fixture.clientReportRecorder, times(1)).attachReportToEnvelope(same(fixture.envelopeBeforeClientReportAttached)) + verify(fixture.clientReportRecorder, never()).recordLostEnvelope(any(), any()) + verifyNoMoreInteractions(fixture.clientReportRecorder) + } + + @Test + fun `records lost envelope when queue is full for non retryable`() { + val sut = fixture.getSut(queueFull = true) + + sut.send(fixture.envelopeBeforeClientReportAttached) + + verify(fixture.clientReportRecorder, never()).attachReportToEnvelope(any()) + verify(fixture.clientReportRecorder, times(1)).recordLostEnvelope(eq(DiscardReason.QUEUE_OVERFLOW), eq(fixture.envelopeBeforeClientReportAttached)) + verifyNoMoreInteractions(fixture.clientReportRecorder) + } + + @Test + fun `records lost envelope when queue is full for retryable`() { + val sut = fixture.getSut(queueFull = true) + + sut.send(fixture.envelopeBeforeClientReportAttached, retryableHint()) + + verify(fixture.clientReportRecorder, never()).attachReportToEnvelope(any()) + verify(fixture.clientReportRecorder, times(1)).recordLostEnvelope(eq(DiscardReason.QUEUE_OVERFLOW), same(fixture.envelopeBeforeClientReportAttached)) + verifyNoMoreInteractions(fixture.clientReportRecorder) + } + + @Test + fun `records lost envelope on 500 error for non retryable`() { + val sut = fixture.getSut(SimpleHttpResponse(500)) + + sut.send(fixture.envelopeBeforeClientReportAttached) + + verify(fixture.clientReportRecorder, times(1)).attachReportToEnvelope(same(fixture.envelopeBeforeClientReportAttached)) + verify(fixture.clientReportRecorder, times(1)).recordLostEnvelope(eq(DiscardReason.NETWORK_ERROR), same(fixture.envelopeAfterClientReportAttached)) + verifyNoMoreInteractions(fixture.clientReportRecorder) + } + + @Test + fun `does not record lost envelope on 500 error for retryable`() { + val sut = fixture.getSut(SimpleHttpResponse(500)) + + sut.send(fixture.envelopeBeforeClientReportAttached, retryableHint()) + + verify(fixture.clientReportRecorder, times(1)).attachReportToEnvelope(same(fixture.envelopeBeforeClientReportAttached)) + verify(fixture.clientReportRecorder, never()).recordLostEnvelope(any(), any()) + verifyNoMoreInteractions(fixture.clientReportRecorder) + } + + @Test + fun `records lost envelope on 400 error for non retryable`() { + val sut = fixture.getSut(SimpleHttpResponse(400)) + + sut.send(fixture.envelopeBeforeClientReportAttached) + + verify(fixture.clientReportRecorder, times(1)).attachReportToEnvelope(same(fixture.envelopeBeforeClientReportAttached)) + verify(fixture.clientReportRecorder, times(1)).recordLostEnvelope(eq(DiscardReason.NETWORK_ERROR), same(fixture.envelopeAfterClientReportAttached)) + verifyNoMoreInteractions(fixture.clientReportRecorder) + } + + @Test + fun `does not record lost envelope on 400 error for retryable`() { + val sut = fixture.getSut(SimpleHttpResponse(400)) + + sut.send(fixture.envelopeBeforeClientReportAttached, retryableHint()) + + verify(fixture.clientReportRecorder, times(1)).attachReportToEnvelope(same(fixture.envelopeBeforeClientReportAttached)) + verify(fixture.clientReportRecorder, never()).recordLostEnvelope(any(), any()) + verifyNoMoreInteractions(fixture.clientReportRecorder) + } + + @Test + fun `does not record lost envelope on 429 error for non retryable`() { + val sut = fixture.getSut(SimpleHttpResponse(429)) + + sut.send(fixture.envelopeBeforeClientReportAttached) + + verify(fixture.clientReportRecorder, times(1)).attachReportToEnvelope(same(fixture.envelopeBeforeClientReportAttached)) + verify(fixture.clientReportRecorder, never()).recordLostEnvelope(any(), any()) + verifyNoMoreInteractions(fixture.clientReportRecorder) + } + + @Test + fun `does not record lost envelope on 429 error for retryable`() { + val sut = fixture.getSut(SimpleHttpResponse(429)) + + sut.send(fixture.envelopeBeforeClientReportAttached, retryableHint()) + + verify(fixture.clientReportRecorder, times(1)).attachReportToEnvelope(same(fixture.envelopeBeforeClientReportAttached)) + verify(fixture.clientReportRecorder, never()).recordLostEnvelope(any(), any()) + verifyNoMoreInteractions(fixture.clientReportRecorder) + } + + @Test + fun `does not record lost envelope on io exception for retryable`() { + val sut = fixture.getSut() + whenever(fixture.client.execute(any(), any())).thenThrow(RuntimeException("thrown on purpose")) + + sut.send(fixture.envelopeBeforeClientReportAttached, retryableHint()) + + verify(fixture.clientReportRecorder, times(1)).attachReportToEnvelope(same(fixture.envelopeBeforeClientReportAttached)) + verify(fixture.clientReportRecorder, never()).recordLostEnvelope(any(), any()) + verifyNoMoreInteractions(fixture.clientReportRecorder) + } + + @Test + fun `records lost envelope on io exception for non retryable`() { + val sut = fixture.getSut() + whenever(fixture.client.execute(any(), any())).thenThrow(RuntimeException("thrown on purpose")) + + sut.send(fixture.envelopeBeforeClientReportAttached) + + verify(fixture.clientReportRecorder, times(1)).attachReportToEnvelope(same(fixture.envelopeBeforeClientReportAttached)) + verify(fixture.clientReportRecorder, times(1)).recordLostEnvelope(eq(DiscardReason.NETWORK_ERROR), same(fixture.envelopeAfterClientReportAttached)) + verifyNoMoreInteractions(fixture.clientReportRecorder) + } + + private fun retryableHint() = HintUtils.createWithTypeCheckHint(TestRetryable()) +} + +class TestRetryable : Retryable { + private var retry = false + + override fun setRetry(retry: Boolean) { + this.retry = retry + } + + override fun isRetry(): Boolean { + return this.retry + } +} diff --git a/sentry-apache-http-client-5/src/test/kotlin/io/sentry/transport/apache/ApacheHttpClientTransportTest.kt b/sentry-apache-http-client-5/src/test/kotlin/io/sentry/transport/apache/ApacheHttpClientTransportTest.kt index 7fb8981a4b4..be846c1cf01 100644 --- a/sentry-apache-http-client-5/src/test/kotlin/io/sentry/transport/apache/ApacheHttpClientTransportTest.kt +++ b/sentry-apache-http-client-5/src/test/kotlin/io/sentry/transport/apache/ApacheHttpClientTransportTest.kt @@ -15,6 +15,8 @@ import io.sentry.SentryEnvelope import io.sentry.SentryEvent import io.sentry.SentryLevel import io.sentry.SentryOptions +import io.sentry.SentryOptionsManipulator +import io.sentry.clientreport.NoOpClientReportRecorder import io.sentry.transport.RateLimiter import io.sentry.transport.ReusableCountLatch import org.apache.hc.client5.http.async.methods.SimpleHttpResponse @@ -33,6 +35,7 @@ class ApacheHttpClientTransportTest { val options: SentryOptions val logger = mock() val rateLimiter = mock() + val clientReportRecorder = NoOpClientReportRecorder() val requestDetails = RequestDetails("http://key@localhost/proj", mapOf("header-name" to "header-value")) val client = mock() val currentlyRunning = spy() @@ -45,6 +48,7 @@ class ApacheHttpClientTransportTest { options.setDiagnosticLevel(SentryLevel.WARNING) options.setDebug(true) options.setLogger(logger) + SentryOptionsManipulator.setClientReportRecorder(options, clientReportRecorder) } fun getSut(response: SimpleHttpResponse? = null, queueFull: Boolean = false): ApacheHttpClientTransport { diff --git a/sentry-apollo/build.gradle.kts b/sentry-apollo/build.gradle.kts index f936ed717e7..516bd7bd1db 100644 --- a/sentry-apollo/build.gradle.kts +++ b/sentry-apollo/build.gradle.kts @@ -17,6 +17,7 @@ configure { tasks.withType().configureEach { kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString() + kotlinOptions.languageVersion = Config.kotlinCompatibleLanguageVersion } dependencies { 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 c515da01546..c286a945787 100644 --- a/sentry-apollo/src/main/java/io/sentry/apollo/SentryApolloInterceptor.kt +++ b/sentry-apollo/src/main/java/io/sentry/apollo/SentryApolloInterceptor.kt @@ -12,11 +12,14 @@ import com.apollographql.apollo.interceptor.ApolloInterceptor.InterceptorRequest import com.apollographql.apollo.interceptor.ApolloInterceptor.InterceptorResponse import com.apollographql.apollo.interceptor.ApolloInterceptorChain import io.sentry.Breadcrumb +import io.sentry.Hint import io.sentry.HubAdapter import io.sentry.IHub import io.sentry.ISpan import io.sentry.SentryLevel import io.sentry.SpanStatus +import io.sentry.TypeCheckHint.APOLLO_REQUEST +import io.sentry.TypeCheckHint.APOLLO_RESPONSE import java.util.concurrent.Executor class SentryApolloInterceptor( @@ -112,7 +115,12 @@ class SentryApolloInterceptor( httpResponse.body()?.contentLength().ifHasValidLength { contentLength -> breadcrumb.setData("response_body_size", contentLength) } - hub.addBreadcrumb(breadcrumb) + + val hint = Hint().also { + it.set(APOLLO_REQUEST, httpRequest) + it.set(APOLLO_RESPONSE, httpResponse) + } + hub.addBreadcrumb(breadcrumb, hint) } } } diff --git a/sentry-apollo/src/test/java/io/sentry/apollo/SentryApolloInterceptorTest.kt b/sentry-apollo/src/test/java/io/sentry/apollo/SentryApolloInterceptorTest.kt index 645f9bba307..2aa745050b6 100644 --- a/sentry-apollo/src/test/java/io/sentry/apollo/SentryApolloInterceptorTest.kt +++ b/sentry-apollo/src/test/java/io/sentry/apollo/SentryApolloInterceptorTest.kt @@ -15,6 +15,7 @@ import io.sentry.SentryOptions import io.sentry.SentryTraceHeader import io.sentry.SentryTracer import io.sentry.SpanStatus +import io.sentry.TraceState import io.sentry.TransactionContext import io.sentry.protocol.SentryTransaction import kotlinx.coroutines.launch @@ -84,6 +85,8 @@ class SentryApolloInterceptorTest { assertTransactionDetails(it) assertEquals(SpanStatus.OK, it.spans.first().status) }, + anyOrNull(), + anyOrNull(), anyOrNull() ) } @@ -97,6 +100,8 @@ class SentryApolloInterceptorTest { assertTransactionDetails(it) assertEquals(SpanStatus.PERMISSION_DENIED, it.spans.first().status) }, + anyOrNull(), + anyOrNull(), anyOrNull() ) } @@ -110,6 +115,8 @@ class SentryApolloInterceptorTest { assertTransactionDetails(it) assertEquals(SpanStatus.INTERNAL_ERROR, it.spans.first().status) }, + anyOrNull(), + anyOrNull(), anyOrNull() ) } @@ -144,6 +151,8 @@ class SentryApolloInterceptorTest { val httpClientSpan = it.spans.first() assertEquals("overwritten description", httpClientSpan.description) }, + anyOrNull(), + anyOrNull(), anyOrNull() ) } @@ -158,6 +167,8 @@ class SentryApolloInterceptorTest { check { assertEquals(1, it.spans.size) }, + anyOrNull(), + anyOrNull(), anyOrNull() ) } @@ -170,7 +181,8 @@ class SentryApolloInterceptorTest { assertEquals("http", it.type) assertEquals(280L, it.data["response_body_size"]) assertEquals(193L, it.data["request_body_size"]) - } + }, + anyOrNull() ) } diff --git a/sentry-graphql/build.gradle.kts b/sentry-graphql/build.gradle.kts index 1dd30060328..c9fb4a60048 100644 --- a/sentry-graphql/build.gradle.kts +++ b/sentry-graphql/build.gradle.kts @@ -16,7 +16,7 @@ configure { tasks.withType().configureEach { kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString() - kotlinOptions.languageVersion = Config.springKotlinCompatibleLanguageVersion + kotlinOptions.languageVersion = Config.kotlinCompatibleLanguageVersion } dependencies { diff --git a/sentry-graphql/src/main/java/io/sentry/graphql/SentryDataFetcherExceptionHandler.java b/sentry-graphql/src/main/java/io/sentry/graphql/SentryDataFetcherExceptionHandler.java index b07fbebae3a..5101b687de6 100644 --- a/sentry-graphql/src/main/java/io/sentry/graphql/SentryDataFetcherExceptionHandler.java +++ b/sentry-graphql/src/main/java/io/sentry/graphql/SentryDataFetcherExceptionHandler.java @@ -1,8 +1,11 @@ package io.sentry.graphql; +import static io.sentry.TypeCheckHint.GRAPHQL_HANDLER_PARAMETERS; + import graphql.execution.DataFetcherExceptionHandler; import graphql.execution.DataFetcherExceptionHandlerParameters; import graphql.execution.DataFetcherExceptionHandlerResult; +import io.sentry.Hint; import io.sentry.HubAdapter; import io.sentry.IHub; import io.sentry.util.Objects; @@ -30,7 +33,10 @@ public SentryDataFetcherExceptionHandler(final @NotNull DataFetcherExceptionHand @SuppressWarnings("deprecation") public DataFetcherExceptionHandlerResult onException( final @NotNull DataFetcherExceptionHandlerParameters handlerParameters) { - hub.captureException(handlerParameters.getException(), handlerParameters); + final Hint hint = new Hint(); + hint.set(GRAPHQL_HANDLER_PARAMETERS, handlerParameters); + + hub.captureException(handlerParameters.getException(), hint); return delegate.onException(handlerParameters); } } diff --git a/sentry-graphql/src/test/kotlin/io/sentry/graphql/SentryDataFetcherExceptionHandlerTest.kt b/sentry-graphql/src/test/kotlin/io/sentry/graphql/SentryDataFetcherExceptionHandlerTest.kt index cc1db48e522..ca98debaeb8 100644 --- a/sentry-graphql/src/test/kotlin/io/sentry/graphql/SentryDataFetcherExceptionHandlerTest.kt +++ b/sentry-graphql/src/test/kotlin/io/sentry/graphql/SentryDataFetcherExceptionHandlerTest.kt @@ -1,5 +1,7 @@ package io.sentry.graphql +import com.nhaarman.mockitokotlin2.anyOrNull +import com.nhaarman.mockitokotlin2.eq import com.nhaarman.mockitokotlin2.mock import com.nhaarman.mockitokotlin2.verify import graphql.execution.DataFetcherExceptionHandler @@ -19,7 +21,7 @@ class SentryDataFetcherExceptionHandlerTest { val parameters = DataFetcherExceptionHandlerParameters.newExceptionParameters().exception(exception).build() handler.onException(parameters) - verify(hub).captureException(exception, parameters) + verify(hub).captureException(eq(exception), anyOrNull()) verify(delegate).onException(parameters) } } diff --git a/sentry-jdbc/src/test/kotlin/io/sentry/jdbc/SentryJdbcEventListenerTest.kt b/sentry-jdbc/src/test/kotlin/io/sentry/jdbc/SentryJdbcEventListenerTest.kt index c679a34e0dd..bd4dd986620 100644 --- a/sentry-jdbc/src/test/kotlin/io/sentry/jdbc/SentryJdbcEventListenerTest.kt +++ b/sentry-jdbc/src/test/kotlin/io/sentry/jdbc/SentryJdbcEventListenerTest.kt @@ -32,7 +32,7 @@ class SentryJdbcEventListenerTest { actualDataSource.connection.use { it.prepareStatement("CREATE TABLE foo (id int unique)").execute() } - existingRow?.let { row -> + existingRow?.let { _ -> actualDataSource.connection.use { val statement = it.prepareStatement("INSERT INTO foo VALUES (?)") statement.setInt(1, existingRow) diff --git a/sentry-jul/src/main/java/io/sentry/jul/SentryHandler.java b/sentry-jul/src/main/java/io/sentry/jul/SentryHandler.java index 2796dd39c5b..1a8cdc22e4d 100644 --- a/sentry-jul/src/main/java/io/sentry/jul/SentryHandler.java +++ b/sentry-jul/src/main/java/io/sentry/jul/SentryHandler.java @@ -1,7 +1,12 @@ package io.sentry.jul; +import static io.sentry.TypeCheckHint.JUL_LOG_RECORD; +import static io.sentry.TypeCheckHint.SENTRY_SYNTHETIC_EXCEPTION; + import com.jakewharton.nopen.annotation.Open; import io.sentry.Breadcrumb; +import io.sentry.Hint; +import io.sentry.HubAdapter; import io.sentry.Sentry; import io.sentry.SentryEvent; import io.sentry.SentryLevel; @@ -75,10 +80,16 @@ public void publish(final @NotNull LogRecord record) { } try { if (record.getLevel().intValue() >= minimumEventLevel.intValue()) { - Sentry.captureEvent(createEvent(record)); + final Hint hint = new Hint(); + hint.set(SENTRY_SYNTHETIC_EXCEPTION, record); + + Sentry.captureEvent(createEvent(record), hint); } if (record.getLevel().intValue() >= minimumBreadcrumbLevel.intValue()) { - Sentry.addBreadcrumb(createBreadcrumb(record)); + final Hint hint = new Hint(); + hint.set(JUL_LOG_RECORD, record); + + Sentry.addBreadcrumb(createBreadcrumb(record), hint); } } catch (RuntimeException e) { reportError( @@ -190,7 +201,23 @@ SentryEvent createEvent(final @NotNull LogRecord record) { mdcProperties = CollectionUtils.filterMapEntries(mdcProperties, entry -> entry.getValue() != null); if (!mdcProperties.isEmpty()) { - event.getContexts().put("MDC", mdcProperties); + // get tags from HubAdapter options to allow getting the correct tags if Sentry has been + // initialized somewhere else + final List contextTags = HubAdapter.getInstance().getOptions().getContextTags(); + if (!contextTags.isEmpty()) { + for (final String contextTag : contextTags) { + // if mdc tag is listed in SentryOptions, apply as event tag + if (mdcProperties.containsKey(contextTag)) { + event.setTag(contextTag, mdcProperties.get(contextTag)); + // remove from all tags applied to logging event + mdcProperties.remove(contextTag); + } + } + } + // put the rest of mdc tags in contexts + if (!mdcProperties.isEmpty()) { + event.getContexts().put("MDC", mdcProperties); + } } } event.setExtra(THREAD_ID, record.getThreadID()); diff --git a/sentry-jul/src/test/kotlin/io/sentry/jul/SentryHandlerTest.kt b/sentry-jul/src/test/kotlin/io/sentry/jul/SentryHandlerTest.kt index cee91eae4f9..b365eacd104 100644 --- a/sentry-jul/src/test/kotlin/io/sentry/jul/SentryHandlerTest.kt +++ b/sentry-jul/src/test/kotlin/io/sentry/jul/SentryHandlerTest.kt @@ -24,7 +24,7 @@ import kotlin.test.assertNull import kotlin.test.assertTrue class SentryHandlerTest { - private class Fixture(minimumBreadcrumbLevel: Level? = null, minimumEventLevel: Level? = null, val configureWithLogManager: Boolean = false, val transport: ITransport = mock()) { + private class Fixture(minimumBreadcrumbLevel: Level? = null, minimumEventLevel: Level? = null, val configureWithLogManager: Boolean = false, val transport: ITransport = mock(), contextTags: List? = null) { var logger: Logger var handler: SentryHandler @@ -32,6 +32,7 @@ class SentryHandlerTest { val options = SentryOptions() options.dsn = "http://key@localhost/proj" options.setTransportFactory { _, _ -> transport } + contextTags?.forEach { options.addContextTag(it) } logger = Logger.getLogger("jul.SentryHandlerTest") handler = SentryHandler(options, configureWithLogManager) handler.setMinimumBreadcrumbLevel(minimumBreadcrumbLevel) @@ -312,6 +313,22 @@ class SentryHandlerTest { ) } + @Test + fun `sets tags as Sentry tags from MDC`() { + fixture = Fixture(minimumEventLevel = Level.WARNING, contextTags = listOf("contextTag1")) + MDC.put("key", "value") + MDC.put("contextTag1", "contextTag1Value") + fixture.logger.warning("testing MDC tags") + + verify(fixture.transport).send( + checkEvent { event -> + assertEquals(mapOf("key" to "value"), event.contexts["MDC"]) + assertEquals(mapOf("contextTag1" to "contextTag1Value"), event.tags) + }, + anyOrNull() + ) + } + @Test fun `ignore set tags with null values from MDC`() { fixture = Fixture(minimumEventLevel = Level.WARNING) diff --git a/sentry-kotlin-extensions/build.gradle.kts b/sentry-kotlin-extensions/build.gradle.kts index 69eb737fad9..27bd5273ed9 100644 --- a/sentry-kotlin-extensions/build.gradle.kts +++ b/sentry-kotlin-extensions/build.gradle.kts @@ -18,6 +18,7 @@ configure { tasks.withType().configureEach { kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString() + kotlinOptions.languageVersion = Config.kotlinCompatibleLanguageVersion } dependencies { diff --git a/sentry-log4j2/api/sentry-log4j2.api b/sentry-log4j2/api/sentry-log4j2.api index acf62cf1c3d..d94493083aa 100644 --- a/sentry-log4j2/api/sentry-log4j2.api +++ b/sentry-log4j2/api/sentry-log4j2.api @@ -4,9 +4,9 @@ public final class io/sentry/log4j2/BuildConfig { } public class io/sentry/log4j2/SentryAppender : org/apache/logging/log4j/core/appender/AbstractAppender { - public fun (Ljava/lang/String;Lorg/apache/logging/log4j/core/Filter;Ljava/lang/String;Lorg/apache/logging/log4j/Level;Lorg/apache/logging/log4j/Level;Ljava/lang/Boolean;Lio/sentry/ITransportFactory;Lio/sentry/IHub;)V + public fun (Ljava/lang/String;Lorg/apache/logging/log4j/core/Filter;Ljava/lang/String;Lorg/apache/logging/log4j/Level;Lorg/apache/logging/log4j/Level;Ljava/lang/Boolean;Lio/sentry/ITransportFactory;Lio/sentry/IHub;[Ljava/lang/String;)V public fun append (Lorg/apache/logging/log4j/core/LogEvent;)V - public static fun createAppender (Ljava/lang/String;Lorg/apache/logging/log4j/Level;Lorg/apache/logging/log4j/Level;Ljava/lang/String;Ljava/lang/Boolean;Lorg/apache/logging/log4j/core/Filter;)Lio/sentry/log4j2/SentryAppender; + public static fun createAppender (Ljava/lang/String;Lorg/apache/logging/log4j/Level;Lorg/apache/logging/log4j/Level;Ljava/lang/String;Ljava/lang/Boolean;Lorg/apache/logging/log4j/core/Filter;Ljava/lang/String;)Lio/sentry/log4j2/SentryAppender; protected fun createBreadcrumb (Lorg/apache/logging/log4j/core/LogEvent;)Lio/sentry/Breadcrumb; protected fun createEvent (Lorg/apache/logging/log4j/core/LogEvent;)Lio/sentry/SentryEvent; public fun start ()V diff --git a/sentry-log4j2/src/main/java/io/sentry/log4j2/SentryAppender.java b/sentry-log4j2/src/main/java/io/sentry/log4j2/SentryAppender.java index 1ec436dce18..e35a24708cd 100644 --- a/sentry-log4j2/src/main/java/io/sentry/log4j2/SentryAppender.java +++ b/sentry-log4j2/src/main/java/io/sentry/log4j2/SentryAppender.java @@ -1,8 +1,12 @@ package io.sentry.log4j2; +import static io.sentry.TypeCheckHint.LOG4J_LOG_EVENT; +import static io.sentry.TypeCheckHint.SENTRY_SYNTHETIC_EXCEPTION; + import com.jakewharton.nopen.annotation.Open; import io.sentry.Breadcrumb; import io.sentry.DateUtils; +import io.sentry.Hint; import io.sentry.HubAdapter; import io.sentry.IHub; import io.sentry.ITransportFactory; @@ -42,6 +46,7 @@ public class SentryAppender extends AbstractAppender { private @NotNull Level minimumEventLevel = Level.ERROR; private final @Nullable Boolean debug; private final @NotNull IHub hub; + private final @Nullable List contextTags; public SentryAppender( final @NotNull String name, @@ -51,7 +56,8 @@ public SentryAppender( final @Nullable Level minimumEventLevel, final @Nullable Boolean debug, final @Nullable ITransportFactory transportFactory, - final @NotNull IHub hub) { + final @NotNull IHub hub, + final @Nullable String[] contextTags) { super(name, filter, null, true, null); this.dsn = dsn; if (minimumBreadcrumbLevel != null) { @@ -63,6 +69,7 @@ public SentryAppender( this.debug = debug; this.transportFactory = transportFactory; this.hub = hub; + this.contextTags = contextTags != null ? Arrays.asList(contextTags) : null; } /** @@ -83,7 +90,8 @@ public SentryAppender( @Nullable @PluginAttribute("minimumEventLevel") final Level minimumEventLevel, @Nullable @PluginAttribute("dsn") final String dsn, @Nullable @PluginAttribute("debug") final Boolean debug, - @Nullable @PluginElement("filter") final Filter filter) { + @Nullable @PluginElement("filter") final Filter filter, + @Nullable @PluginAttribute("contextTags") final String contextTags) { if (name == null) { LOGGER.error("No name provided for SentryAppender"); @@ -97,7 +105,8 @@ public SentryAppender( minimumEventLevel, debug, null, - HubAdapter.getInstance()); + HubAdapter.getInstance(), + contextTags != null ? contextTags.split(",") : null); } @Override @@ -108,9 +117,16 @@ public void start() { options -> { options.setEnableExternalConfiguration(true); options.setDsn(dsn); - options.setDebug(debug); + if (debug != null) { + options.setDebug(debug); + } options.setSentryClientName(BuildConfig.SENTRY_LOG4J2_SDK_NAME); options.setSdkVersion(createSdkVersion(options)); + if (contextTags != null) { + for (final String contextTag : contextTags) { + options.addContextTag(contextTag); + } + } Optional.ofNullable(transportFactory).ifPresent(options::setTransportFactory); }); } catch (IllegalArgumentException e) { @@ -123,10 +139,16 @@ public void start() { @Override public void append(final @NotNull LogEvent eventObject) { if (eventObject.getLevel().isMoreSpecificThan(minimumEventLevel)) { - hub.captureEvent(createEvent(eventObject)); + final Hint hint = new Hint(); + hint.set(SENTRY_SYNTHETIC_EXCEPTION, eventObject); + + hub.captureEvent(createEvent(eventObject), hint); } if (eventObject.getLevel().isMoreSpecificThan(minimumBreadcrumbLevel)) { - hub.addBreadcrumb(createBreadcrumb(eventObject)); + final Hint hint = new Hint(); + hint.set(LOG4J_LOG_EVENT, eventObject); + + hub.addBreadcrumb(createBreadcrumb(eventObject), hint); } } @@ -165,7 +187,23 @@ public void append(final @NotNull LogEvent eventObject) { CollectionUtils.filterMapEntries( loggingEvent.getContextData().toMap(), entry -> entry.getValue() != null); if (!contextData.isEmpty()) { - event.getContexts().put("Context Data", contextData); + // get tags from HubAdapter options to allow getting the correct tags if Sentry has been + // initialized somewhere else + final List contextTags = hub.getOptions().getContextTags(); + if (contextTags != null && !contextTags.isEmpty()) { + for (final String contextTag : contextTags) { + // if mdc tag is listed in SentryOptions, apply as event tag + if (contextData.containsKey(contextTag)) { + event.setTag(contextTag, contextData.get(contextTag)); + // remove from all tags applied to logging event + contextData.remove(contextTag); + } + } + } + // put the rest of mdc tags in contexts + if (!contextData.isEmpty()) { + event.getContexts().put("Context Data", contextData); + } } return event; diff --git a/sentry-log4j2/src/test/kotlin/io/sentry/log4j2/SentryAppenderTest.kt b/sentry-log4j2/src/test/kotlin/io/sentry/log4j2/SentryAppenderTest.kt index aee203f1d62..0a1ad7b1090 100644 --- a/sentry-log4j2/src/test/kotlin/io/sentry/log4j2/SentryAppenderTest.kt +++ b/sentry-log4j2/src/test/kotlin/io/sentry/log4j2/SentryAppenderTest.kt @@ -43,13 +43,13 @@ class SentryAppenderTest { whenever(transportFactory.create(any(), any())).thenReturn(transport) } - fun getSut(transportFactory: ITransportFactory? = null, minimumBreadcrumbLevel: Level? = null, minimumEventLevel: Level? = null, debug: Boolean? = null): ExtendedLogger { + fun getSut(transportFactory: ITransportFactory? = null, minimumBreadcrumbLevel: Level? = null, minimumEventLevel: Level? = null, debug: Boolean? = null, contextTags: List? = null): ExtendedLogger { if (transportFactory != null) { this.transportFactory = transportFactory } loggerContext.start() val config: Configuration = loggerContext.configuration - val appender = SentryAppender("sentry", null, "http://key@localhost/proj", minimumBreadcrumbLevel, minimumEventLevel, debug, this.transportFactory, HubAdapter.getInstance()) + val appender = SentryAppender("sentry", null, "http://key@localhost/proj", minimumBreadcrumbLevel, minimumEventLevel, debug, this.transportFactory, HubAdapter.getInstance(), contextTags?.toTypedArray()) config.addAppender(appender) val ref = AppenderRef.createAppenderRef("sentry", null, null) @@ -241,6 +241,22 @@ class SentryAppenderTest { ) } + @Test + fun `sets tags from ThreadContext as Sentry tags`() { + val logger = fixture.getSut(minimumEventLevel = Level.WARN, contextTags = listOf("contextTag1")) + ThreadContext.put("key", "value") + ThreadContext.put("contextTag1", "contextTag1Value") + logger.warn("testing MDC tags") + + verify(fixture.transport).send( + checkEvent { event -> + assertEquals(mapOf("key" to "value"), event.contexts["Context Data"]) + assertEquals(mapOf("contextTag1" to "contextTag1Value"), event.tags) + }, + anyOrNull() + ) + } + @Test fun `ignore set tags with null values from ThreadContext`() { val logger = fixture.getSut(minimumEventLevel = Level.WARN) diff --git a/sentry-logback/src/main/java/io/sentry/logback/SentryAppender.java b/sentry-logback/src/main/java/io/sentry/logback/SentryAppender.java index db6c2086b06..2c8f18e1161 100644 --- a/sentry-logback/src/main/java/io/sentry/logback/SentryAppender.java +++ b/sentry-logback/src/main/java/io/sentry/logback/SentryAppender.java @@ -1,5 +1,8 @@ package io.sentry.logback; +import static io.sentry.TypeCheckHint.LOGBACK_LOGGING_EVENT; +import static io.sentry.TypeCheckHint.SENTRY_SYNTHETIC_EXCEPTION; + import ch.qos.logback.classic.Level; import ch.qos.logback.classic.spi.ILoggingEvent; import ch.qos.logback.classic.spi.ThrowableProxy; @@ -7,6 +10,8 @@ import com.jakewharton.nopen.annotation.Open; import io.sentry.Breadcrumb; import io.sentry.DateUtils; +import io.sentry.Hint; +import io.sentry.HubAdapter; import io.sentry.ITransportFactory; import io.sentry.Sentry; import io.sentry.SentryEvent; @@ -29,6 +34,7 @@ /** Appender for logback in charge of sending the logged events to a Sentry server. */ @Open public class SentryAppender extends UnsynchronizedAppenderBase { + // WARNING: Do not use these options in here, they are only to be used for startup private @NotNull SentryOptions options = new SentryOptions(); private @Nullable ITransportFactory transportFactory; private @NotNull Level minimumBreadcrumbLevel = Level.INFO; @@ -36,6 +42,7 @@ public class SentryAppender extends UnsynchronizedAppenderBase { @Override public void start() { + // NOTE: logback.xml properties will only be applied if the SDK has not yet been initialized if (!Sentry.isEnabled()) { if (options.getDsn() == null || !options.getDsn().endsWith("_IS_UNDEFINED")) { options.setEnableExternalConfiguration(true); @@ -59,10 +66,16 @@ public void start() { @Override protected void append(@NotNull ILoggingEvent eventObject) { if (eventObject.getLevel().isGreaterOrEqual(minimumEventLevel)) { - Sentry.captureEvent(createEvent(eventObject)); + final Hint hint = new Hint(); + hint.set(SENTRY_SYNTHETIC_EXCEPTION, eventObject); + + Sentry.captureEvent(createEvent(eventObject), hint); } if (eventObject.getLevel().isGreaterOrEqual(minimumBreadcrumbLevel)) { - Sentry.addBreadcrumb(createBreadcrumb(eventObject)); + final Hint hint = new Hint(); + hint.set(LOGBACK_LOGGING_EVENT, eventObject); + + Sentry.addBreadcrumb(createBreadcrumb(eventObject), hint); } } @@ -102,7 +115,23 @@ protected void append(@NotNull ILoggingEvent eventObject) { CollectionUtils.filterMapEntries( loggingEvent.getMDCPropertyMap(), entry -> entry.getValue() != null); if (!mdcProperties.isEmpty()) { - event.getContexts().put("MDC", mdcProperties); + // get tags from HubAdapter options to allow getting the correct tags if Sentry has been + // initialized somewhere else + final List contextTags = HubAdapter.getInstance().getOptions().getContextTags(); + if (!contextTags.isEmpty()) { + for (final String contextTag : contextTags) { + // if mdc tag is listed in SentryOptions, apply as event tag + if (mdcProperties.containsKey(contextTag)) { + event.setTag(contextTag, mdcProperties.get(contextTag)); + // remove from all tags applied to logging event + mdcProperties.remove(contextTag); + } + } + } + // put the rest of mdc tags in contexts + if (!mdcProperties.isEmpty()) { + event.getContexts().put("MDC", mdcProperties); + } } return event; diff --git a/sentry-logback/src/test/kotlin/io/sentry/logback/SentryAppenderTest.kt b/sentry-logback/src/test/kotlin/io/sentry/logback/SentryAppenderTest.kt index 4eb90906e80..11c2afba989 100644 --- a/sentry-logback/src/test/kotlin/io/sentry/logback/SentryAppenderTest.kt +++ b/sentry-logback/src/test/kotlin/io/sentry/logback/SentryAppenderTest.kt @@ -31,7 +31,7 @@ import kotlin.test.assertNull import kotlin.test.assertTrue class SentryAppenderTest { - private class Fixture(dsn: String? = "http://key@localhost/proj", minimumBreadcrumbLevel: Level? = null, minimumEventLevel: Level? = null) { + private class Fixture(dsn: String? = "http://key@localhost/proj", minimumBreadcrumbLevel: Level? = null, minimumEventLevel: Level? = null, contextTags: List? = null) { val logger: Logger = LoggerFactory.getLogger(SentryAppenderTest::class.java) val loggerContext = LoggerFactory.getILoggerFactory() as LoggerContext val transportFactory = mock() @@ -43,6 +43,7 @@ class SentryAppenderTest { val appender = SentryAppender() val options = SentryOptions() options.dsn = dsn + contextTags?.forEach { options.addContextTag(it) } appender.setOptions(options) appender.setMinimumBreadcrumbLevel(minimumBreadcrumbLevel) appender.setMinimumEventLevel(minimumEventLevel) @@ -217,6 +218,22 @@ class SentryAppenderTest { ) } + @Test + fun `sets tags as sentry tags from MDC`() { + fixture = Fixture(minimumEventLevel = Level.WARN, contextTags = listOf("contextTag1")) + MDC.put("key", "value") + MDC.put("contextTag1", "contextTag1Value") + fixture.logger.warn("testing MDC tags") + + verify(fixture.transport).send( + checkEvent { event -> + assertEquals(mapOf("key" to "value"), event.contexts["MDC"]) + assertEquals(mapOf("contextTag1" to "contextTag1Value"), event.tags) + }, + anyOrNull() + ) + } + @Test fun `ignore set tags with null values from MDC`() { fixture = Fixture(minimumEventLevel = Level.WARN) diff --git a/sentry-openfeign/src/main/java/io/sentry/openfeign/SentryFeignClient.java b/sentry-openfeign/src/main/java/io/sentry/openfeign/SentryFeignClient.java index c8ea6a3f921..ebc62738280 100644 --- a/sentry-openfeign/src/main/java/io/sentry/openfeign/SentryFeignClient.java +++ b/sentry-openfeign/src/main/java/io/sentry/openfeign/SentryFeignClient.java @@ -1,9 +1,13 @@ package io.sentry.openfeign; +import static io.sentry.TypeCheckHint.OPEN_FEIGN_REQUEST; +import static io.sentry.TypeCheckHint.OPEN_FEIGN_RESPONSE; + import feign.Client; import feign.Request; import feign.Response; import io.sentry.Breadcrumb; +import io.sentry.Hint; import io.sentry.IHub; import io.sentry.ISpan; import io.sentry.SentryTraceHeader; @@ -87,7 +91,14 @@ private void addBreadcrumb(final @NotNull Request request, final @Nullable Respo if (response != null && response.body() != null && response.body().length() != null) { breadcrumb.setData("response_body_size", response.body().length()); } - hub.addBreadcrumb(breadcrumb); + + final Hint hint = new Hint(); + hint.set(OPEN_FEIGN_REQUEST, request); + if (response != null) { + hint.set(OPEN_FEIGN_RESPONSE, response); + } + + hub.addBreadcrumb(breadcrumb, hint); } static final class RequestWrapper { diff --git a/sentry-openfeign/src/test/kotlin/io/sentry/openfeign/SentryFeignClientTest.kt b/sentry-openfeign/src/test/kotlin/io/sentry/openfeign/SentryFeignClientTest.kt index c881625b2ee..ac459c35757 100644 --- a/sentry-openfeign/src/test/kotlin/io/sentry/openfeign/SentryFeignClientTest.kt +++ b/sentry-openfeign/src/test/kotlin/io/sentry/openfeign/SentryFeignClientTest.kt @@ -1,6 +1,7 @@ package io.sentry.openfeign import com.nhaarman.mockitokotlin2.any +import com.nhaarman.mockitokotlin2.anyOrNull import com.nhaarman.mockitokotlin2.check import com.nhaarman.mockitokotlin2.mock import com.nhaarman.mockitokotlin2.verify @@ -134,7 +135,8 @@ class SentryFeignClientTest { assertEquals("http", it.type) assertEquals(13, it.data["response_body_size"]) assertEquals(12, it.data["request_body_size"]) - } + }, + anyOrNull() ) } @@ -147,7 +149,8 @@ class SentryFeignClientTest { assertEquals("http", it.type) assertEquals(0, it.data["response_body_size"]) assertEquals(12, it.data["request_body_size"]) - } + }, + anyOrNull() ) } @@ -165,7 +168,8 @@ class SentryFeignClientTest { verify(fixture.hub).addBreadcrumb( check { assertEquals("http", it.type) - } + }, + anyOrNull() ) } diff --git a/sentry-samples/sentry-samples-android/proguard-rules.pro b/sentry-samples/sentry-samples-android/proguard-rules.pro index f1b424510da..95c4bb7bbca 100644 --- a/sentry-samples/sentry-samples-android/proguard-rules.pro +++ b/sentry-samples/sentry-samples-android/proguard-rules.pro @@ -12,10 +12,24 @@ # public *; #} -# Uncomment this to preserve the line number information for -# debugging stack traces. -#-keepattributes SourceFile,LineNumberTable +# To ensure that stack traces is unambiguous +# https://developer.android.com/studio/build/shrink-code#decode-stack-trace +-keepattributes LineNumberTable,SourceFile -# If you keep the line number information, uncomment this to -# hide the original source file name. -#-renamesourcefileattribute SourceFile + +# For native methods, see http://proguard.sourceforge.net/manual/examples.html#native +-keepclasseswithmembernames,includedescriptorclasses class * { + native ; +} + +# Please add these rules to your existing keep rules in order to suppress warnings. +# This is generated automatically by the Android Gradle plugin. +-dontwarn org.bouncycastle.jsse.BCSSLParameters +-dontwarn org.bouncycastle.jsse.BCSSLSocket +-dontwarn org.bouncycastle.jsse.provider.BouncyCastleJsseProvider +-dontwarn org.conscrypt.Conscrypt$Version +-dontwarn org.conscrypt.Conscrypt +-dontwarn org.conscrypt.ConscryptHostnameVerifier +-dontwarn org.openjsse.javax.net.ssl.SSLParameters +-dontwarn org.openjsse.javax.net.ssl.SSLSocket +-dontwarn org.openjsse.net.ssl.OpenJSSE diff --git a/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml b/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml index 74276baa9d7..ab6ece24c2b 100644 --- a/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml +++ b/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml @@ -10,6 +10,10 @@ + + + + @@ -41,10 +45,14 @@ android:name=".ThirdActivityFragment" android:exported="false" /> - + + @@ -87,12 +95,18 @@ + + + + + + @@ -111,5 +125,8 @@ + + + diff --git a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MainActivity.java b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MainActivity.java index 9a030a8591c..5379ec4faaa 100644 --- a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MainActivity.java +++ b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MainActivity.java @@ -178,6 +178,11 @@ protected void onCreate(Bundle savedInstanceState) { crashCount); }); + binding.openPermissionsActivity.setOnClickListener( + view -> { + startActivity(new Intent(this, PermissionsActivity.class)); + }); + setContentView(binding.getRoot()); } diff --git a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/PermissionsActivity.kt b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/PermissionsActivity.kt new file mode 100644 index 00000000000..dce7af47714 --- /dev/null +++ b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/PermissionsActivity.kt @@ -0,0 +1,41 @@ +package io.sentry.samples.android + +import android.Manifest +import android.os.Bundle +import android.widget.Toast +import androidx.activity.result.contract.ActivityResultContracts.RequestPermission +import androidx.appcompat.app.AppCompatActivity +import io.sentry.samples.android.databinding.ActivityPermissionsBinding + +class PermissionsActivity : AppCompatActivity() { + + private lateinit var binding: ActivityPermissionsBinding + private val requestPermissionLauncher = + registerForActivityResult(RequestPermission()) { isGranted: Boolean -> + if (isGranted) { + Toast.makeText(this, "The permission was granted", Toast.LENGTH_LONG).show() + } else { + Toast.makeText(this, "The permission was denied", Toast.LENGTH_LONG).show() + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + binding = ActivityPermissionsBinding.inflate(layoutInflater) + + binding.cameraPermission.setOnClickListener { + requestPermissionLauncher.launch(Manifest.permission.CAMERA) + } + + binding.writePermission.setOnClickListener { + requestPermissionLauncher.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE) + } + + binding.permissionsCrash.setOnClickListener { + throw RuntimeException("Permissions Activity Exception") + } + + setContentView(binding.root) + } +} diff --git a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/SecondActivity.kt b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/SecondActivity.kt index de9884a5e4e..d7889377560 100644 --- a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/SecondActivity.kt +++ b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/SecondActivity.kt @@ -70,7 +70,7 @@ class SecondActivity : AppCompatActivity() { ?: Sentry.startTransaction("updateRepos", "task") GithubAPI.service.listRepos(binding.editRepo.text.toString()).enqueue(object : Callback> { - override fun onFailure(call: Call>?, t: Throwable) { + override fun onFailure(call: Call>, t: Throwable) { span.finish(SpanStatus.INTERNAL_ERROR) Sentry.captureException(t) diff --git a/sentry-samples/sentry-samples-android/src/main/res/layout/activity_main.xml b/sentry-samples/sentry-samples-android/src/main/res/layout/activity_main.xml index 411054c47f1..3f0549fdf8a 100644 --- a/sentry-samples/sentry-samples-android/src/main/res/layout/activity_main.xml +++ b/sentry-samples/sentry-samples-android/src/main/res/layout/activity_main.xml @@ -105,6 +105,12 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/test_timber_integration"/> + +